dbxapp 4.1.3
CMS, Shop, Workflows und modulare Geschäftsanwendungen
Loading...
Searching...
No Matches
dbxMail.class.php
Go to the documentation of this file.
1<?php
2
3use PHPMailer\PHPMailer\Exception as PHPMailerException;
4use PHPMailer\PHPMailer\PHPMailer;
5
6class dbxMail {
7
8 public $errstr = '';
9 public $headers = array();
10 public $textbody = '';
11 public $htmlbody = '';
12 public $attachments = array();
13 public $html_images = array();
14 public $auto_embbed_images = true;
15 public $auto_embed_images = true;
16 public $boundary = '';
17 public $sendmail_path = '';
18 public $subject = '';
19 public $spam_protection = true;
20
21 private $from = '';
22 private $fromname = '';
23
24 public function init() {
25 $this->errstr = '';
26 $this->headers = array();
27 $this->textbody = '';
28 $this->htmlbody = '';
29 $this->attachments = array();
30 $this->html_images = array();
31 $this->auto_embbed_images = true;
32 $this->auto_embed_images = true;
33 $this->boundary = '==Multipart_Boundary_x' . md5((string) microtime(true)) . 'x';
34 $this->sendmail_path = '';
35 $this->subject = '';
36 $this->spam_protection = true;
37 $this->from = '';
38 $this->fromname = '';
39 }
40
41 public function checkEmail($inAddress) {
42 return filter_var((string) $inAddress, FILTER_VALIDATE_EMAIL) !== false;
43 }
44
45 public function get_body() {
46 return $this->textbody . $this->htmlbody;
47 }
48
49 public function get_header() {
50 $retval = '';
51 foreach ($this->headers as $key => $value) {
52 $retval .= $key . ': ' . $value . "\n";
53 }
54 return $retval;
55 }
56
57 public function set_header($name, $value) {
58 $name = trim((string) $name);
59 $value = trim((string) $value);
60
61 if (strcasecmp($name, 'From') === 0) {
62 $from = $this->parse_address($value);
63 $this->from = $from['email'];
64 $this->fromname = $from['name'];
65 }
66
67 $this->headers[$name] = $value;
68 }
69
70 public function set_from($email, $name = '') {
71 $this->from = trim((string) $email);
72 $this->fromname = trim((string) $name);
73 $this->headers['From'] = $this->format_address($this->from, $this->fromname);
74 }
75
76 public function attachfile($file, $disp = 'attachment', $contentType = '', $cid = '') {
77 $file = trim((string) $file);
78
79 if ($file === '' || !is_file($file) || !is_readable($file)) {
80 $this->errstr = 'Attachment nicht lesbar: ' . $file;
81 return 0;
82 }
83
84 $this->attachments[] = array(
85 'path' => $file,
86 'name' => basename($file),
87 'disposition' => $disp ?: 'attachment',
88 'content_type' => $contentType ?: $this->getContentType($file),
89 'cid' => (string) $cid,
90 );
91
92 return 1;
93 }
94
95 public function getContentType($inFileName) {
96 $file = (string) $inFileName;
97
98 if (function_exists('mime_content_type') && is_file($file)) {
99 $type = @mime_content_type($file);
100 if ($type) {
101 return $type;
102 }
103 }
104
105 $extension = strtolower((string) strrchr(basename($file), '.'));
106
107 switch ($extension) {
108 case '.gz': return 'application/gzip';
109 case '.zip': return 'application/zip';
110 case '.tar': return 'application/x-tar';
111 case '.htm':
112 case '.html': return 'text/html';
113 case '.txt': return 'text/plain';
114 case '.jpg':
115 case '.jpeg': return 'image/jpeg';
116 case '.gif': return 'image/gif';
117 case '.png': return 'image/png';
118 case '.pdf': return 'application/pdf';
119 case '.csv': return 'text/csv';
120 case '.json': return 'application/json';
121 default: return 'application/octet-stream';
122 }
123 }
124
125 public function bodytext($text) {
126 if (strlen(trim((string) $text)) > 0) {
127 $this->textbody = (string) $text;
128 }
129 }
130
131 public function bodyhtml($text) {
132 if (strlen(trim((string) $text)) > 0) {
133 $this->htmlbody = (string) $text;
134 }
135 }
136
137 public function htmltext($text) {
138 if (strlen(trim((string) $text)) > 0) {
139 $this->htmlbody .= (string) $text;
140 }
141 }
142
143 public function clear_bodytext() { $this->textbody = ''; }
144 public function clear_htmltext() { $this->htmlbody = ''; }
145 public function get_error() { return $this->errstr; }
146
147 public function send($to = 'root@localhost', $subject = 'Default Subject', $options = array()) {
148 $this->errstr = '';
149 $subject = (string) ($subject !== '' ? $subject : $this->subject);
150
151 $deliveryMode = $this->delivery_mode();
152 if ($deliveryMode !== 'external') {
153 return $this->handle_internal_delivery(
154 $deliveryMode,
155 $to,
156 $subject,
157 is_array($options) ? $options : array()
158 );
159 }
160
161 try {
162 $mail = new PHPMailer(true);
163 $mail->CharSet = 'UTF-8';
164 $mail->Encoding = 'base64';
165
166 $config = $this->mail_config($options);
167 $this->configure_transport($mail, $config);
168 $this->configure_sender($mail, $config, $options);
169
170 $this->add_addresses($mail, $to, 'addAddress');
171 $this->add_addresses($mail, $options['cc'] ?? $this->headers['Cc'] ?? array(), 'addCC');
172 $this->add_addresses($mail, $options['bcc'] ?? $this->headers['Bcc'] ?? array(), 'addBCC');
173 $this->add_addresses($mail, $options['reply_to'] ?? $this->headers['Reply-To'] ?? array(), 'addReplyTo');
174
175 $mail->Subject = $subject;
176 $this->configure_body($mail, $options);
177 $this->configure_headers($mail);
178 $this->configure_attachments($mail);
179
180 $spamReason = $this->spam_guard_reason($mail, $to, $subject, $options);
181 if ($spamReason !== '') {
182 $this->errstr = 'Mail wurde durch den Spam-Schutz blockiert: ' . $spamReason;
183 $this->sys_msg('security', $to, $subject, $this->errstr);
184 return 0;
185 }
186
187 $ok = $mail->send() ? 1 : 0;
188 $this->sys_msg($ok ? 'info' : 'error', $to, $subject, $ok ? 'sent' : $mail->ErrorInfo);
189
190 return $ok;
191 } catch (PHPMailerException $e) {
192 $this->errstr = $e->getMessage();
193 } catch (Throwable $e) {
194 $this->errstr = $e->getMessage();
195 }
196
197 $this->sys_msg('error', $to, $subject, $this->errstr);
198 return 0;
199 }
200
208 public function delivery_mode(): string {
209 $mode = 'internal';
210 if (function_exists('dbx')) {
211 $configured = dbx()->get_config('dbx', 'mail_delivery_mode', 'internal');
212 if (is_scalar($configured)) {
213 $mode = strtolower(trim((string)$configured));
214 }
215 }
216
217 return in_array($mode, array('internal', 'disabled', 'external'), true)
218 ? $mode
219 : 'internal';
220 }
221
228 private function handle_internal_delivery(
229 string $mode,
230 $to,
231 string $subject,
232 array $options
233 ): int {
234 $from = $this->normalize_from($options['from'] ?? null);
235 if ($from['email'] === '' && $this->from !== '') {
236 $from = array('email' => $this->from, 'name' => $this->fromname);
237 }
238
239 $details = array(
240 'delivery' => $mode,
241 'from' => $from['email'],
242 'to' => $this->address_debug($to),
243 'subject' => $subject,
244 );
245 if (function_exists('dbx')) {
246 try {
247 dbx()->sys_msg(
248 'warning',
249 $mode === 'internal' ? 'mail-internal' : 'mail-disabled',
250 '',
251 $mode === 'internal'
252 ? 'Externer E-Mail-Versand ist ausgeschaltet; Ereignis wurde intern angenommen.'
253 : 'E-Mail-Versand ist global deaktiviert.',
254 $details
255 );
256 } catch (Throwable $e) {
257 // Der globale Schalter muss auch funktionieren, bevor dbxSysMsg
258 // während der Erstinstallation provisioniert wurde.
259 }
260 }
261
262 if ($mode === 'internal') {
263 return 1;
264 }
265
266 $this->errstr = 'E-Mail-Versand ist global deaktiviert.';
267 return 0;
268 }
269
270 private function mail_config($options) {
271 $config = array();
272
273 if (function_exists('dbx')) {
274 $cfg = dbx()->get_config('dbx', 'mail');
275 if (is_array($cfg)) {
276 $config = $cfg;
277 }
278
279 $defaultMail = dbx()->get_config('dbx', 'default_mail');
280 if ($defaultMail !== 'undef' && $defaultMail !== '') {
281 $config['default'] = (string) $defaultMail;
282 }
283 }
284
285 if (is_string($options['mail'] ?? null)) {
286 $options['mail_profile'] = $options['mail'];
287 }
288
289 $config = $this->select_mail_profile($config, $options);
290
291 if (is_array($options['mail'] ?? null)) {
292 $config = array_replace_recursive($config, $options['mail']);
293 }
294
295 return $config;
296 }
297
298 private function select_mail_profile(array $config, array $options) {
299 $profiles = $this->mail_profiles_from_config($config);
300 if (!$profiles) {
301 return $config;
302 }
303
304 $profileName = (string) ($options['mail_profile'] ?? $options['profile'] ?? '');
305 $from = $this->normalize_from($options['from'] ?? null);
306
307 if ($profileName === '' && $from['email'] !== '') {
308 $profileName = $this->profile_for_sender($profiles, $from['email']);
309 }
310
311 if ($profileName === '') {
312 $profileName = (string) ($config['default'] ?? '');
313 }
314
315 if ($profileName === '' || !isset($profiles[$profileName]) || !is_array($profiles[$profileName])) {
316 $profileName = (string) ($config['default'] ?? '');
317 }
318
319 if (($profileName === '' || !isset($profiles[$profileName])) && count($profiles)) {
320 $keys = array_keys($profiles);
321 $profileName = (string) $keys[0];
322 }
323
324 $base = $this->mail_base_config($config);
325
326 if ($profileName !== '' && isset($profiles[$profileName]) && is_array($profiles[$profileName])) {
327 return array_replace_recursive($base, $profiles[$profileName], array('profile' => $profileName));
328 }
329
330 return $base;
331 }
332
333 private function mail_profiles_from_config(array $config) {
334 if (is_array($config['profiles'] ?? null)) {
335 return $config['profiles'];
336 }
337
338 $profiles = array();
339 foreach ($config as $key => $value) {
340 if ($key === 'default' || $key === 'profiles') {
341 continue;
342 }
343 if (is_array($value)) {
344 $profiles[(string) $key] = $value;
345 }
346 }
347
348 return $profiles;
349 }
350
351 private function mail_base_config(array $config) {
352 $base = $config;
353 unset($base['profiles'], $base['default']);
354
355 foreach ($base as $key => $value) {
356 if (is_array($value)) {
357 unset($base[$key]);
358 }
359 }
360
361 return $base;
362 }
363
364 private function profile_for_sender(array $profiles, $email) {
365 $domain = strtolower((string) substr(strrchr((string) $email, '@') ?: '', 1));
366 if ($domain === '') {
367 return '';
368 }
369
370 foreach ($profiles as $name => $profile) {
371 if ($this->profile_matches_domain($profile, $domain, false)) {
372 return (string) $name;
373 }
374 }
375
376 foreach ($profiles as $name => $profile) {
377 if ($this->profile_matches_domain($profile, $domain, true)) {
378 return (string) $name;
379 }
380 }
381
382 return '';
383 }
384
385 private function profile_matches_domain($profile, $domain, $allowWildcard) {
386 if (!is_array($profile)) {
387 return false;
388 }
389
390 $domains = $profile['from_domains'] ?? $profile['from_domain'] ?? array();
391 if (is_string($domains)) {
392 $domains = preg_split('/[;,]+/', $domains);
393 }
394
395 foreach ((array) $domains as $allowed) {
396 $allowed = strtolower(trim((string) $allowed));
397 if ($allowed === '') {
398 continue;
399 }
400 if ($allowed === $domain) {
401 return true;
402 }
403 if ($allowWildcard && ($allowed === '*' || (substr($allowed, 0, 2) === '*.' && str_ends_with($domain, substr($allowed, 1))))) {
404 return true;
405 }
406 }
407
408 return false;
409 }
410
411 private function configure_transport(PHPMailer $mail, array $config) {
412 $transport = strtolower((string) ($config['transport'] ?? $config['type'] ?? ''));
413 $host = trim((string) ($config['host'] ?? $config['smtp_host'] ?? ''));
414
415 if ($transport === 'smtp' || $host !== '') {
416 $username = (string) ($config['user'] ?? $config['username'] ?? '');
417 $password = (string) ($config['pass'] ?? $config['password'] ?? '');
418 $auth = (bool) ($config['auth'] ?? $config['smtp_auth'] ?? ($username !== ''));
419
420 if ($auth && ($username === '' || $password === '')) {
421 throw new PHPMailerException('SMTP auth configuration incomplete.');
422 }
423
424 $mail->isSMTP();
425 $mail->Host = $host;
426 $mail->Port = (int) ($config['port'] ?? $config['smtp_port'] ?? 587);
427 $mail->Timeout = max(1, (int) ($config['timeout'] ?? $config['smtp_timeout'] ?? 3));
428 $mail->SMTPAuth = $auth;
429 $mail->Username = $username;
430 $mail->Password = $password;
431
432 $secure = strtolower((string) ($config['secure'] ?? $config['smtp_secure'] ?? ''));
433 if (in_array($secure, array('tls', 'ssl'), true)) {
434 $mail->SMTPSecure = $secure;
435 }
436 return;
437 }
438
439 if ($transport === 'sendmail' || $this->sendmail_path !== '' || !empty($config['sendmail_path'])) {
440 $mail->isSendmail();
441 $path = $this->sendmail_path ?: (string) ($config['sendmail_path'] ?? '');
442 if ($path !== '') {
443 $mail->Sendmail = $path;
444 }
445 return;
446 }
447
448 $mail->isMail();
449 }
450
451 private function configure_sender(PHPMailer $mail, array $config, array $options) {
452 $forceFrom = !empty($config['force_from']);
453 $from = $forceFrom ? array('email' => '', 'name' => '') : $this->normalize_from($options['from'] ?? null);
454
455 if ($from['email'] === '') {
456 $from = $this->normalize_from($this->from ? array('email' => $this->from, 'name' => $this->fromname) : null);
457 }
458
459 if ($from['email'] === '') {
460 $from = $this->normalize_from($config['from'] ?? array(
461 'email' => $config['from_email'] ?? '',
462 'name' => $config['from_name'] ?? '',
463 ));
464 }
465
466 if ($from['email'] === '') {
467 throw new PHPMailerException('Absender fehlt.');
468 }
469
470 $mail->setFrom($from['email'], $from['name']);
471
472 $sender = trim((string) ($config['sender'] ?? $config['return_path'] ?? $config['envelope_from'] ?? ''));
473 if ($sender !== '') {
474 $mail->Sender = $sender;
475 }
476 }
477
478 private function configure_body(PHPMailer $mail, array $options) {
479 $html = (string) ($options['html'] ?? $this->htmlbody);
480 $text = (string) ($options['text'] ?? $this->textbody);
481
482 if ($html !== '') {
483 $mail->isHTML(true);
484 $mail->Body = $this->embed_html_images($mail, $html);
485 $mail->AltBody = $text !== '' ? $text : $this->html_to_text($html);
486 return;
487 }
488
489 $mail->isHTML(false);
490 $mail->Body = $text;
491 }
492
493 private function configure_headers(PHPMailer $mail) {
494 foreach ($this->headers as $key => $value) {
495 if (in_array(strtolower((string) $key), array('from', 'to', 'cc', 'bcc', 'reply-to', 'content-type', 'mime-version'), true)) {
496 continue;
497 }
498 $mail->addCustomHeader((string) $key, (string) $value);
499 }
500 }
501
502 private function configure_attachments(PHPMailer $mail) {
503 foreach ($this->attachments as $attachment) {
504 if (!is_array($attachment)) {
505 continue;
506 }
507
508 $path = (string) ($attachment['path'] ?? '');
509 if ($path === '' || !is_file($path) || !is_readable($path)) {
510 continue;
511 }
512
513 $name = (string) ($attachment['name'] ?? basename($path));
514 $encoding = PHPMailer::ENCODING_BASE64;
515 $type = (string) ($attachment['content_type'] ?? $this->getContentType($path));
516 $disp = (string) ($attachment['disposition'] ?? 'attachment');
517 $cid = (string) ($attachment['cid'] ?? '');
518
519 if ($disp === 'inline' && $cid !== '') {
520 $mail->addEmbeddedImage($path, $cid, $name, $encoding, $type);
521 } else {
522 $mail->addAttachment($path, $name, $encoding, $type, $disp);
523 }
524 }
525 }
526
527 private function embed_html_images(PHPMailer $mail, $html) {
528 if (!$this->auto_embbed_images && !$this->auto_embed_images) {
529 return $html;
530 }
531
532 preg_match_all('/<img[^>]+src=["\']([^"\']+)["\']/i', $html, $matches);
533 $images = array_unique($matches[1] ?? array());
534
535 foreach ($images as $src) {
536 $file = $this->html_image_path($src);
537 if (!$file) {
538 continue;
539 }
540
541 $cid = 'part.' . md5($file);
542 $mail->addEmbeddedImage($file, $cid, basename($file), PHPMailer::ENCODING_BASE64, $this->getContentType($file));
543 $html = str_replace($src, 'cid:' . $cid, $html);
544 $this->html_images[] = $file;
545 }
546
547 return $html;
548 }
549
550 private function html_image_path($src) {
551 $src = trim((string) $src);
552 if ($src === '' || preg_match('/^(https?:|data:|cid:)/i', $src)) {
553 return '';
554 }
555
556 $path = $src;
557 if (!preg_match('/^[A-Za-z]:[\\\\\\/]/', $path) && substr($path, 0, 1) !== '/') {
558 $path = dbx()->get_base_dir() . ltrim($path, '/\\');
559 }
560
561 $path = dbx()->os_path($path);
562 return is_file($path) && is_readable($path) ? $path : '';
563 }
564
565 private function add_addresses(PHPMailer $mail, $addresses, $method) {
566 foreach ($this->normalize_addresses($addresses) as $address) {
567 if ($address['email'] !== '') {
568 $mail->$method($address['email'], $address['name']);
569 }
570 }
571 }
572
573 private function normalize_addresses($addresses) {
574 if ($addresses === null || $addresses === '') {
575 return array();
576 }
577
578 if (is_string($addresses)) {
579 $parts = preg_split('/[;,]+/', $addresses);
580 return array_map(array($this, 'parse_address'), array_filter(array_map('trim', $parts)));
581 }
582
583 if (is_array($addresses)) {
584 if (isset($addresses['email'])) {
585 return array($this->normalize_from($addresses));
586 }
587
588 $out = array();
589 foreach ($addresses as $key => $value) {
590 if (is_string($key) && is_string($value) && filter_var($key, FILTER_VALIDATE_EMAIL)) {
591 $out[] = array('email' => $key, 'name' => $value);
592 } else {
593 $out[] = is_array($value) ? $this->normalize_from($value) : $this->parse_address((string) $value);
594 }
595 }
596 return $out;
597 }
598
599 return array();
600 }
601
602 private function normalize_from($from) {
603 if (is_array($from)) {
604 return array(
605 'email' => trim((string) ($from['email'] ?? $from['mail'] ?? $from[0] ?? '')),
606 'name' => trim((string) ($from['name'] ?? $from[1] ?? '')),
607 );
608 }
609
610 if (is_string($from)) {
611 return $this->parse_address($from);
612 }
613
614 return array('email' => '', 'name' => '');
615 }
616
617 private function parse_address($value) {
618 $value = trim((string) $value);
619 if (preg_match('/^(.*)<([^>]+)>$/', $value, $m)) {
620 return array('email' => trim($m[2]), 'name' => trim(trim($m[1]), '"\' '));
621 }
622 return array('email' => $value, 'name' => '');
623 }
624
625 private function format_address($email, $name = '') {
626 $email = trim((string) $email);
627 $name = trim((string) $name);
628 return $name !== '' ? $name . ' <' . $email . '>' : $email;
629 }
630
631 private function html_to_text($html) {
632 return trim(html_entity_decode(strip_tags((string) $html), ENT_QUOTES | ENT_HTML5, 'UTF-8'));
633 }
634
635 private function sys_msg($status, $to, $subject, $message) {
636 if (!function_exists('dbx')) {
637 return;
638 }
639
640 try {
641 dbx()->sys_msg($status, 'mail', '', (string) $subject, 'to=' . $this->address_debug($to) . ' ' . (string) $message);
642 } catch (Throwable $e) {
643 // Mailversand darf nicht an der Protokollierung scheitern.
644 }
645 }
646
647 private function address_debug($addresses) {
648 $list = array();
649 foreach ($this->normalize_addresses($addresses) as $address) {
650 if ($address['email'] !== '') {
651 $list[] = $address['email'];
652 }
653 }
654 return implode(',', $list);
655 }
656
657 private function spam_guard_reason(PHPMailer $mail, $to, string $subject, array $options): string {
658 if (!$this->spam_protection || !empty($options['skip_spam_guard'])) {
659 return '';
660 }
661
662 return $this->spam_reason_for_text($this->spam_guard_text($mail, $to, $subject, $options));
663 }
664
665 public function spam_reason_for_text(string $text): string {
666 if (!$this->spam_protection) {
667 return '';
668 }
669
670 return $this->spam_content_reason($text);
671 }
672
673 private function spam_guard_text(PHPMailer $mail, $to, string $subject, array $options): string {
674 $parts = array(
675 $subject,
676 $mail->Body,
677 $mail->AltBody,
678 (string) ($options['text'] ?? ''),
679 (string) ($options['html'] ?? ''),
680 $this->address_debug($to),
681 $this->address_debug($options['reply_to'] ?? $this->headers['Reply-To'] ?? array()),
682 );
683
684 foreach ($this->headers as $key => $value) {
685 $parts[] = (string) $key . ': ' . (string) $value;
686 }
687
688 return html_entity_decode(strip_tags(implode("\n", $parts)), ENT_QUOTES | ENT_HTML5, 'UTF-8');
689 }
690
691 private function spam_content_reason(string $text): string {
692 $normalized = strtolower($text);
693 $normalized = preg_replace('/\s+/u', ' ', $normalized) ?: $normalized;
694 $score = 0;
695
696 $hardPatterns = array(
697 '/(?:https?:\/\/)?(?:www\.)?telegra\.ph\//i' => 'telegra.ph link',
698 '/\btransaction[-\s_]*\d{2}-\d{2}-/i' => 'transaction spam code',
699 '/\b(?:transaction|transfer|top\s*up)\b.{0,80}\b(?:get|claim|bonus|payment)\b/i' => 'finance spam phrase',
700 );
701
702 foreach ($hardPatterns as $pattern => $reason) {
703 if (preg_match($pattern, $text)) {
704 return $reason;
705 }
706 }
707
708 if (preg_match('/\b(?:transaction|transfer|top\s*up|crypto|bitcoin|wallet|usdt|profit|investment)\b/i', $text)) {
709 $score += 2;
710 }
711 if (preg_match('/(?:\$|usd|eur)\s*\d{3,}|\d{3,}\s*(?:\$|usd|eur)/i', $text)) {
712 $score += 2;
713 }
714 if (preg_match('/(?:https?:\/\/|www\.|[a-z0-9-]+\.(?:ph|ru|top|xyz|click|link|icu|buzz|cfd|quest)\b)/i', $text)) {
715 $score += 2;
716 }
717 if (preg_match('/\bget\s*(?:-|>|&gt;|to|:)/i', $text)) {
718 $score += 1;
719 }
720 if (preg_match('/[\x{1F300}-\x{1FAFF}]/u', $text)) {
721 $score += 1;
722 }
723
724 $urlCount = preg_match_all('/(?:https?:\/\/|www\.|[a-z0-9-]+\.[a-z]{2,}\/)/i', $text);
725 if ($urlCount >= 2) {
726 $score += 2;
727 }
728
729 return $score >= 5 ? 'spam score ' . $score : '';
730 }
731}
732
733?>
$cfg
spam_reason_for_text(string $text)
bodytext($text)
delivery_mode()
Liefert die globale Versandart.
htmltext($text)
set_header($name, $value)
bodyhtml($text)
set_from($email, $name='')
getContentType($inFileName)
attachfile($file, $disp='attachment', $contentType='', $cid='')
send($to='root @localhost', $subject='Default Subject', $options=array())
checkEmail($inAddress)
sys_msg($status='', $about='', $rid='', $why='', $what='')
Schreibt eine strukturierte Systemmeldung.
Definition dbxApi.php:2047
if($resolved !==$expectedBase . 'files/test/') $config
foreach(array('bootstrapRowColumns', 'setBootstrapColumnLayout', 'addBootstrapColumn', 'dissolveBootstrapColumns',) as $function) $keys
if(!defined( 'IMG_WEBP')) define( 'IMG_WEBP'
DBX schema administration.
$profile
Definition run.php:6