PHP Classes

File: src/functions.php

Recommend this page to a friend!
  Packages of Matthew Asham   Binkterm PHP   src/functions.php   Download  
File: src/functions.php
Role: Example script
Content type: text/plain
Description: Example script
Class: Binkterm PHP
Bulletin board system based on the Web
Author: By
Last change:
Date: 5 days ago
Size: 14,701 bytes
 

Contents

Class file image Download
<?php /* * Copright Matthew Asham and BinktermPHP Contributors * * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the * following conditions are met: * * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE * */ /** * Return a shared Logger instance writing to server.log. * * Calling code in route files, static helpers, and classes without a * dedicated logger property should use this rather than error_log(). * The instance is created once per process and reused on subsequent calls. */ function getServerLogger(): \BinktermPHP\Binkp\Logger { static $logger = null; if ($logger === null) { $logger = new \BinktermPHP\Binkp\Logger( \BinktermPHP\Config::getLogPath('server.log'), \BinktermPHP\Binkp\Logger::LEVEL_INFO, false ); } return $logger; } // Helper function to filter kludge lines from message text function filterKludgeLines($messageText) { // First normalize line endings - split on both \r\n, \n, and \r $lines = preg_split('/\r\n|\r|\n/', $messageText); $messageLines = []; foreach ($lines as $line) { $trimmed = trim($line); // Skip empty lines if ($trimmed === '') { continue; } // Skip kludge lines - those that start with specific patterns if (preg_match('/^(INTL|FMPT|TOPT|MSGID|REPLY|PID|TZUTC)\s/', $trimmed) || preg_match('/^Via\s+\d+:\d+\/\d+\s+@\d{8}\.\d{6}\.UTC/', $trimmed) || // Via lines with timestamp strpos($trimmed, "\x01") === 0 || // Traditional kludge lines starting with \x01 strpos($trimmed, 'SEEN-BY:') === 0 || strpos($trimmed, 'PATH:') === 0) { continue; } // Keep all other lines (actual message content, signatures, tearlines) $messageLines[] = $line; } return implode("\n", $messageLines); } /** * Filter kludge lines but preserve empty lines (for Markdown rendering). */ function filterKludgeLinesPreserveEmptyLines($messageText) { $lines = preg_split('/\r\n|\r|\n/', $messageText); $messageLines = []; foreach ($lines as $line) { $trimmed = trim($line); if ($trimmed !== '') { if (preg_match('/^(INTL|FMPT|TOPT|MSGID|REPLY|PID|TZUTC)\s/', $trimmed) || preg_match('/^Via\s+\d+:\d+\/\d+\s+@\d{8}\.\d{6}\.UTC/', $trimmed) || strpos($trimmed, "\x01") === 0 || strpos($trimmed, 'SEEN-BY:') === 0 || strpos($trimmed, 'PATH:') === 0) { continue; } } $messageLines[] = $line; } return implode("\n", $messageLines); } /** * Check if a message contains a Markdown markup kludge. * * Recognises: * ^AMARKUP: Markdown <version> (LSC-001 Draft 2 ? preferred) * ^AMARKDOWN: <version> (legacy backwards-compatibility kludge) * * @param array $message Message array with kludge_lines/bottom_kludges/message_text * @return bool */ function hasMarkdownKludge(array $message): bool { $kludgeText = ''; if (!empty($message['kludge_lines'])) { $kludgeText .= $message['kludge_lines']; } if (!empty($message['bottom_kludges'])) { $kludgeText .= ($kludgeText !== '' ? "\n" : '') . $message['bottom_kludges']; } if ($kludgeText !== '' && ( preg_match('/^\x01MARKUP:\s+Markdown\s+[\d.]+/mi', $kludgeText) || preg_match('/^\x01MARKDOWN:\s*\d+/mi', $kludgeText) )) { return true; } $messageText = $message['message_text'] ?? ''; if ($messageText === '') { return false; } $lines = preg_split('/\r\n|\r|\n/', $messageText); foreach ($lines as $line) { if (preg_match('/^\x01MARKUP:\s+Markdown\s+[\d.]+/i', $line) || preg_match('/^\x01MARKDOWN:\s*\d+/i', $line)) { return true; } } return false; } /** * Get the markup type of a message based on its kludge lines. * * Returns the format string (e.g. 'markdown', 'stylecodes') or null if no * markup kludge is present. Recognises: * ^AMARKUP: <Format> <version> (LSC-001 Draft 2) * ^AMARKDOWN: <version> (legacy ? treated as 'markdown') * * @param array $message Message array with kludge_lines/bottom_kludges/message_text * @return string|null */ function getMessageMarkupType(array $message): ?string { $kludgeText = ''; if (!empty($message['kludge_lines'])) { $kludgeText .= $message['kludge_lines']; } if (!empty($message['bottom_kludges'])) { $kludgeText .= ($kludgeText !== '' ? "\n" : '') . $message['bottom_kludges']; } if ($kludgeText !== '') { if (preg_match('/^\x01MARKUP:\s+([\w]+)\s+[\d.]+/mi', $kludgeText, $m)) { return strtolower($m[1]); } if (preg_match('/^\x01MARKDOWN:\s*\d+/mi', $kludgeText)) { return 'markdown'; } } $messageText = $message['message_text'] ?? ''; if ($messageText !== '') { $lines = preg_split('/\r\n|\r|\n/', $messageText); foreach ($lines as $line) { if (preg_match('/^\x01MARKUP:\s+([\w]+)\s+[\d.]+/i', $line, $m)) { return strtolower($m[1]); } if (preg_match('/^\x01MARKDOWN:\s*\d+/i', $line)) { return 'markdown'; } } } return null; } /** * Generate initials from a person's name for echomail quoting * Examples: "Mark Anderson" -> "MA", "John" -> "JO", "Mary Jane Smith" -> "MS" */ function generateInitials($name) { $name = trim($name); if (empty($name)) { return "??"; } // Split name into parts (remove extra spaces) $parts = array_filter(explode(' ', $name)); if (count($parts) == 1) { // Single name: take first two characters $single = strtoupper($parts[0]); return substr($single, 0, min(2, strlen($single))) ?: "?"; } else { // Multiple parts: take first letter of first and last name $first = strtoupper(substr($parts[0], 0, 1)); $last = strtoupper(substr(end($parts), 0, 1)); return $first . $last; } } /** * Wrap a single quoted line so that the total width stays within $maxWidth. * The prefix (e.g. " MA> ") is prepended to every wrapped segment. * * @param string $prefix The quote prefix including trailing space, e.g. " MA> " * @param string $content The text content (no prefix) * @param int $maxWidth Maximum total line width (default 79) * @return array Array of prefixed lines */ function wrapQuotedLine(string $prefix, string $content, int $maxWidth = 75): array { $available = $maxWidth - strlen($prefix); if ($available <= 0 || strlen($content) <= $available) { return [$prefix . $content]; } $wrapped = wordwrap($content, $available, "\n", true); return array_map(fn($l) => $prefix . $l, explode("\n", $wrapped)); } /** * Quote message text intelligently - only quote original lines, not existing quotes. * Preserves blank lines as bare '>' and wraps all lines to 79 characters. */ function quoteMessageText($messageText, $initials) { $lines = explode("\n", $messageText); $quotedLines = []; foreach ($lines as $line) { $trimmed = trim($line); // Blank lines: pass through as empty lines (no > prefix ? standard FTN practice) if ($trimmed === '') { $quotedLines[] = ''; continue; } // Check if line is already quoted (FSC-0032 style: optional initials + one or more >) // e.g. " RW> text", "RW> text", "> text", " RW>> text" if (preg_match('/^\s*[A-Za-z]{0,2}>/', $trimmed)) { // Bump existing quote depth by inserting an extra > after the initials. // Prepend a space to $trimmed so the leading space is always present. $bumped = preg_replace('/^(\s*[A-Za-z]{0,2})(>+)/', '$1$2>', ' ' . $trimmed); // Split bumped line into prefix (everything up to and including "> ") and content if (preg_match('/^(\s*[A-Za-z]{0,2}>+\s*)(.*)$/', $bumped, $m)) { foreach (wrapQuotedLine($m[1], $m[2]) as $wrapped) { $quotedLines[] = $wrapped; } } else { $quotedLines[] = $bumped; } } else { // Original (unquoted) line ? apply FSC-0032 attribution prefix: " XX> " foreach (wrapQuotedLine(' ' . $initials . '> ', $trimmed) as $wrapped) { $quotedLines[] = $wrapped; } } } return implode("\n", $quotedLines); } /** * Quote a Markdown message by wrapping the entire original body in a blockquote. * This preserves headings, lists, fenced code blocks, and other block structure. * * The attribution line should remain outside the quoted block. * * @param string $messageText Original markdown source * @return string Markdown-safe quoted body */ function quoteMarkdownMessage(string $messageText): string { $normalized = str_replace(["\r\n", "\r"], "\n", $messageText); $lines = explode("\n", $normalized); while (!empty($lines) && trim((string) end($lines)) === '') { array_pop($lines); } if (empty($lines)) { return '> '; } $quotedLines = array_map( static function (string $line): string { return trim($line) === '' ? '>' : '> ' . $line; }, $lines ); return implode("\n", $quotedLines); } /** * Validate if an address is a proper FidoNet address * Returns true if address matches FidoNet format: zone:net/node[.point][@domain] */ function isValidFidonetAddress($address) { // Match FidoNet address pattern: zone:net/node[.point][@domain] return preg_match('/^\d+:\d+\/\d+(?:\.\d+)?(?:@\w+)?$/', trim($address)); } /** * Parse REPLYTO kludge line to extract address and name * Format: "REPLYTO 2:460/256 8421559770" -> ['address' => '2:460/256', 'name' => '8421559770'] * Only returns data if the address is a valid FidoNet address */ function parseReplyToKludge($messageText) { if (empty($messageText)) { return null; } // Normalize line endings and split into lines $lines = preg_split('/\r\n|\r|\n/', $messageText); foreach ($lines as $line) { $trimmed = trim($line); // Look for REPLYTO kludge line (must have \x01 prefix) if (preg_match('/^\x01REPLYTO\s+(.+)$/i', $trimmed, $matches)) { $replyToData = trim($matches[1]); // Parse "address name" or just "address" if (preg_match('/^(\S+)(?:\s+(.+))?$/', $replyToData, $addressMatches)) { $address = trim($addressMatches[1]); $name = isset($addressMatches[2]) ? trim($addressMatches[2]) : null; // Only return if it's a valid FidoNet address if (isValidFidonetAddress($address)) { return [ 'address' => $address, 'name' => $name ]; } } } } return null; } // Helper function to check admin access for BinkP functionality function requireBinkpAdmin() { $auth = new \BinktermPHP\Auth(); $user = $auth->requireAuth(); if (!$user['is_admin']) { $errorCode = 'errors.binkp.admin_required'; $fallback = 'Admin access required'; $translator = new \BinktermPHP\I18n\Translator(); $resolver = new \BinktermPHP\I18n\LocaleResolver($translator); $resolvedLocale = $resolver->resolveLocale((string)($user['locale'] ?? ''), $user); $localized = $translator->translate($errorCode, [], $resolvedLocale, ['errors']); if ($localized === $errorCode) { $localized = $fallback; } http_response_code(403); header('Content-Type: application/json'); echo json_encode([ 'success' => false, 'error_code' => $errorCode, 'error' => $localized ]); exit; } return $user; } /** * Generate TZUTC offset string for FidoNet messages * TZUTC format: negative offsets include "-" (e.g., "-0500"), positive offsets have no sign (e.g., "0800") * * @param string|null $timezone Timezone identifier (e.g., "America/New_York"). If null, uses system timezone from BinkpConfig. * @return string TZUTC offset string (e.g., "0000", "-0500", "0800") */ function generateTzutc($timezone = null) { try { // Use system timezone if not provided if ($timezone === null) { $binkpConfig = \BinktermPHP\Binkp\Config\BinkpConfig::getInstance(); $timezone = $binkpConfig->getSystemTimezone(); } $tz = new \DateTimeZone($timezone); $now = new \DateTime('now', $tz); $offset = $now->getOffset(); $offsetHours = intval($offset / 3600); $offsetMinutes = intval(abs($offset % 3600) / 60); // TZUTC format: negative offsets include "-", positive offsets have no sign if ($offsetHours < 0) { return sprintf('-%02d%02d', abs($offsetHours), $offsetMinutes); } else { return sprintf('%02d%02d', $offsetHours, $offsetMinutes); } } catch (\Exception $e) { // Fallback to UTC if timezone is invalid return '0000'; } }