Robert 2 weeks ago
parent 951ac7ee20
commit 434883baa8
  1. 5
      README.md
  2. 102
      src/Framework/Arrays/CommonStuff.php
  3. 262
      src/Framework/Arrays/Countries.php
  4. 166
      src/Framework/Arrays/Mimes.php
  5. 45
      src/Framework/Arrays/Mocking/Address.php
  6. 45
      src/Framework/Arrays/Mocking/Phone.php
  7. 1179
      src/Framework/Arrays/Mocking/RndNames.php
  8. 29
      src/Framework/Arrays/Shortn.php
  9. 185
      src/Framework/Arrays/ZipCodes.php
  10. 267
      src/Framework/Assets.php
  11. 229
      src/Framework/Attributes/Validators/MyValidator.php
  12. 176
      src/Framework/BbCodeParser.php
  13. 32
      src/Framework/Collection.php
  14. 4
      src/Framework/Common.php
  15. 31
      src/Framework/Dollars.php
  16. 18
      src/Framework/Enum/CompressionMethod.php
  17. 22
      src/Framework/Enum/ViewType.php
  18. 53
      src/Framework/GzCompression.php
  19. 229
      src/Framework/Html.php
  20. 435
      src/Framework/HtmlDocument.php
  21. 175
      src/Framework/HtmlParser.php
  22. 18
      src/Framework/Http/App/App.php
  23. 22
      src/Framework/Http/App/AppHandler.php
  24. 16
      src/Framework/Http/Contract/MiddlewareAwareInterface.php
  25. 19
      src/Framework/Http/Contract/MiddlewareInterface.php
  26. 2
      src/Framework/Http/Contract/RouterInterface.php
  27. 15
      src/Framework/Http/Exception/RouteMiddlewareNotFoundException.php
  28. 43
      src/Framework/Http/Kernel.php
  29. 4
      src/Framework/Http/MiddlewareQueueHandler.php
  30. 44
      src/Framework/Http/Routing/RouterMiddlewareHandler.php
  31. 17
      src/Framework/Http/Routing/RoutingHandler.php
  32. 103
      src/Framework/LazyCollection.php
  33. 32
      src/Framework/LazyObject.php
  34. 66
      src/Framework/MemoryUsage.php
  35. 46
      src/Framework/PhpFileCache.php
  36. 106
      src/Framework/RandomEngine.php
  37. 135
      src/Framework/RssFeed.php
  38. 21
      src/Framework/Scalar.php
  39. 94
      src/Framework/SessionManagement.php
  40. 1
      src/Framework/String/StringFacade.php
  41. 66
      src/Framework/TagMatches.php
  42. 48
      src/Framework/TimeZoneSelection.php
  43. 287
      src/Framework/TimeZones.php
  44. 79
      src/Framework/Trait/Database/RunSql.php
  45. 208
      src/Framework/Trait/Database/Validation.php
  46. 156
      src/Framework/Trait/FormValidator.php
  47. 64
      src/Framework/Trait/Macroable.php
  48. 36
      src/Framework/Uuids/Base62.php
  49. 113
      src/Framework/Uuids/UuidV7.php
  50. 121
      src/Framework/Validator.php
  51. 282
      src/Framework/View.php

@ -1,4 +1,5 @@
# IOcornerstone PHP 8.5 Framework
Author Robert Strutts
Copyright (c) 2010-2026 MIT
## Author Robert Strutts
## Copyright (c) 2010-2026 Robert Strutts
## License MIT

@ -0,0 +1,102 @@
<?php
declare(strict_types=1);
namespace IOcornerstone\Framework\Arrays;
class CommonStuff {
public static function getOrdinalNumberSuffix(int $number): string {
$number = abs($number);
if (in_array(($number % 100),array(11,12,13))){
return 'th';
}
switch ($number % 10) {
case 1: return 'st';
case 2: return 'nd';
case 3: return 'rd';
default: return 'th';
}
}
/**
* Fetch Months
* @return array of months
*/
public static function months(): array {
return array(
'1' => 'January',
'2' => 'February',
'3' => 'March',
'4' => 'April',
'5' => 'May',
'6' => 'June',
'7' => 'July',
'8' => 'August',
'9' => 'September',
'10' => 'October',
'11' => 'November',
'12' => 'December'
);
}
/**
* Fetch States in USA
* @return array of states
*/
public static function statesArray(): array {
return array(
'AL' => 'Alabama',
'AK' => 'Alaska',
'AZ' => 'Arizona',
'AR' => 'Arkansas',
'CA' => 'California',
'CO' => 'Colorado',
'CT' => 'Connecticut',
'DE' => 'Delaware',
'DC' => 'District of Columbia',
'FL' => 'Florida',
'GA' => 'Georgia',
'HI' => 'Hawaii',
'ID' => 'Idaho',
'IL' => 'Illinois',
'IN' => 'Indiana',
'IA' => 'Iowa',
'KS' => 'Kansas',
'KY' => 'Kentucky',
'LA' => 'Louisiana',
'ME' => 'Maine',
'MD' => 'Maryland',
'MA' => 'Massachusetts',
'MI' => 'Michigan',
'MN' => 'Minnesota',
'MS' => 'Mississippi',
'MO' => 'Missouri',
'MT' => 'Montana',
'NE' => 'Nebraska',
'NV' => 'Nevada',
'NH' => 'New Hampshire',
'NJ' => 'New Jersey',
'NM' => 'New Mexico',
'NY' => 'New York',
'NC' => 'North Carolina',
'ND' => 'North Dakota',
'OH' => 'Ohio',
'OK' => 'Oklahoma',
'OR' => 'Oregon',
'PA' => 'Pennsylvania',
'RI' => 'Rhode Island',
'SC' => 'South Carolina',
'SD' => 'South Dakota',
'TN' => 'Tennessee',
'TX' => 'Texas',
'UT' => 'Utah',
'VT' => 'Vermont',
'VA' => 'Virginia',
'WA' => 'Washington',
'WV' => 'West Virginia',
'WI' => 'Wisconsin',
'WY' => 'Wyoming',
);
}
}

@ -0,0 +1,262 @@
<?php
declare(strict_types=1);
namespace IOcornerstone\Framework\Arrays;
class Countries {
/**
* Fetch countries
* @return array of countries
*/
public static function countriesArray(): array {
return array(
'AX' => 'Aland Islands',
'AF' => 'Afghanistan',
'AL' => 'Albania',
'DZ' => 'Algeria',
'AS' => 'American Samoa',
'AD' => 'Andorra',
'AO' => 'Angola',
'AI' => 'Anguilla',
'AQ' => 'Antarctica',
'AG' => 'Antigua And Barbuda',
'AR' => 'Argentina',
'AM' => 'Armenia',
'AW' => 'Aruba',
'AU' => 'Australia',
'AT' => 'Austria',
'AZ' => 'Azerbaijan',
'BS' => 'Bahamas',
'BH' => 'Bahrain',
'BD' => 'Bangladesh',
'BB' => 'Barbados',
'BY' => 'Belarus',
'BE' => 'Belgium',
'BZ' => 'Belize',
'BJ' => 'Benin',
'BM' => 'Bermuda',
'BT' => 'Bhutan',
'BO' => 'Bolivia',
'BA' => 'Bosnia And Herzegovina',
'BW' => 'Botswana',
'BV' => 'Bouvet Island',
'BR' => 'Brazil',
'IO' => 'British Indian Ocean Territory',
'BN' => 'Brunei',
'BG' => 'Bulgaria',
'BF' => 'Burkina Faso',
'AR' => 'Burma',
'BI' => 'Burundi',
'KH' => 'Cambodia',
'CM' => 'Cameroon',
'CA' => 'Canada',
'CV' => 'Cape Verde',
'KY' => 'Cayman Islands',
'CF' => 'Central African Republic',
'TD' => 'Chad',
'CL' => 'Chile',
'CN' => 'China',
'CX' => 'Christmas Island',
'CC' => 'Cocos (Keeling) Islands',
'CO' => 'Columbia',
'KM' => 'Comoros',
'CG' => 'Congo',
'CK' => 'Cook Islands',
'CR' => 'Costa Rica',
'CI' => 'Cote D\'Ivorie (Ivory Coast)',
'HR' => 'Croatia (Hrvatska)',
'CU' => 'Cuba',
'CY' => 'Cyprus',
'CZ' => 'Czech Republic static',
'CD' => 'Democratic Republic Of Congo (Zaire)',
'DK' => 'Denmark',
'DJ' => 'Djibouti',
'DM' => 'Dominica',
'DO' => 'Dominican Republic',
'TP' => 'East Timor',
'EC' => 'Ecuador',
'EG' => 'Egypt',
'SV' => 'El Salvador',
'GB' => 'England',
'GQ' => 'Equatorial Guinea',
'ER' => 'Eritrea',
'EE' => 'Estonia',
'ET' => 'Ethiopia',
'EU' => 'European Union',
'FK' => 'Falkland Islands (Malvinas)',
'FO' => 'Faroe Islands',
'FJ' => 'Fiji',
'FI' => 'Finland',
'FR' => 'France',
'FX' => 'France, Metropolitan',
'GF' => 'French Guinea',
'PF' => 'French Polynesia',
'TF' => 'French Southern Territories',
'GA' => 'Gabon',
'GM' => 'Gambia',
'GE' => 'Georgia',
'DE' => 'Germany',
'GH' => 'Ghana',
'GI' => 'Gibraltar',
'GR' => 'Greece',
'GL' => 'Greenland',
'GD' => 'Grenada',
'GP' => 'Guadeloupe',
'GU' => 'Guam',
'GT' => 'Guatemala',
'GN' => 'Guinea',
'GW' => 'Guinea-Bissau',
'GY' => 'Guyana',
'HT' => 'Haiti',
'HM' => 'Heard And McDonald Islands',
'HN' => 'Honduras',
'HK' => 'Hong Kong',
'HU' => 'Hungary',
'IS' => 'Iceland',
'IN' => 'India',
'ID' => 'Indonesia',
'IR' => 'Iran',
'IQ' => 'Iraq',
'IE' => 'Ireland',
'IL' => 'Israel',
'IT' => 'Italy',
'JM' => 'Jamaica',
'JP' => 'Japan',
'JO' => 'Jordan',
'KZ' => 'Kazakhstan',
'KE' => 'Kenya',
'KI' => 'Kiribati',
'KW' => 'Kuwait',
'KG' => 'Kyrgyzstan',
'LA' => 'Laos',
'LV' => 'Latvia',
'LB' => 'Lebanon',
'LS' => 'Lesotho',
'LR' => 'Liberia',
'LY' => 'Libya',
'LI' => 'Liechtenstein',
'LT' => 'Lithuania',
'LU' => 'Luxembourg',
'MO' => 'Macau',
'MK' => 'Macedonia',
'MG' => 'Madagascar',
'MW' => 'Malawi',
'MY' => 'Malaysia',
'MV' => 'Maldives',
'ML' => 'Mali',
'MT' => 'Malta',
'MH' => 'Marshall Islands',
'MQ' => 'Martinique',
'MR' => 'Mauritania',
'MU' => 'Mauritius',
'YT' => 'Mayotte',
'MX' => 'Mexico',
'FM' => 'Micronesia',
'MD' => 'Moldova',
'MC' => 'Monaco',
'MN' => 'Mongolia',
'ME' => 'Montenegro',
'MS' => 'Montserrat',
'MA' => 'Morocco',
'MZ' => 'Mozambique',
'MM' => 'Myanmar (Burma)',
'NA' => 'Namibia',
'NR' => 'Nauru',
'NP' => 'Nepal',
'NL' => 'Netherlands',
'AN' => 'Netherlands Antilles',
'NC' => 'New Caledonia',
'NZ' => 'New Zealand',
'NI' => 'Nicaragua',
'NE' => 'Niger',
'NG' => 'Nigeria',
'NU' => 'Niue',
'NF' => 'Norfolk Island',
'KP' => 'North Korea',
'MP' => 'Northern Mariana Islands',
'NO' => 'Norway',
'OM' => 'Oman',
'PK' => 'Pakistan',
'PW' => 'Palau',
'PS' => 'Palestine',
'PA' => 'Panama',
'PG' => 'Papua New Guinea',
'PY' => 'Paraguay',
'PE' => 'Peru',
'PH' => 'Philippines',
'PN' => 'Pitcairn',
'PL' => 'Poland',
'PT' => 'Portugal',
'PR' => 'Puerto Rico',
'QA' => 'Qatar',
'RE' => 'Reunion',
'RO' => 'Romania',
'RU' => 'Russia',
'RW' => 'Rwanda',
'SH' => 'Saint Helena',
'KN' => 'Saint Kitts And Nevis',
'LC' => 'Saint Lucia',
'PM' => 'Saint Pierre And Miquelon',
'VC' => 'Saint Vincent And The Grenadines',
'SM' => 'San Marino',
'ST' => 'Sao Tome And Principe',
'SA' => 'Saudi Arabia',
'SN' => 'Senegal',
'SC' => 'Seychelles',
'SL' => 'Sierra Leone',
'SG' => 'Singapore',
'SK' => 'Slovakia',
'SI' => 'Slovenia',
'SB' => 'Solomon Islands',
'SO' => 'Somalia',
'ZA' => 'South Africa',
'GS' => 'South Georgia And South Sandwich Islands',
'KR' => 'South Korea',
'ES' => 'Spain',
'LK' => 'Sri Lanka',
'SD' => 'Sudan',
'SR' => 'Suriname',
'SJ' => 'Svalbard And Jan Mayen',
'SZ' => 'Swaziland',
'SE' => 'Sweden',
'CH' => 'Switzerland',
'SY' => 'Syria',
'TW' => 'Taiwan',
'TJ' => 'Tajikistan',
'TZ' => 'Tanzania',
'TH' => 'Thailand',
'TG' => 'Togo',
'TK' => 'Tokelau',
'TO' => 'Tonga',
'TT' => 'Trinidad And Tobago',
'TN' => 'Tunisia',
'TR' => 'Turkey',
'TM' => 'Turkmenistan',
'TC' => 'Turks And Caicos Islands',
'TV' => 'Tuvalu',
'UG' => 'Uganda',
'UA' => 'Ukraine',
'AE' => 'United Arab Emirates',
'UK' => 'United Kingdom',
'US' => 'United States',
'UM' => 'United States Minor Outlying Islands',
'UY' => 'Uruguay',
'UZ' => 'Uzbekistan',
'VU' => 'Vanuatu',
'VA' => 'Vatican City',
'VE' => 'Venezuela',
'VN' => 'Vietnam',
'VG' => 'Virgin Islands (British)',
'VI' => 'Virgin Islands (US)',
'WF' => 'Wallis And Futuna Islands',
'EH' => 'Western Sahara',
'WS' => 'Western Samoa',
'YE' => 'Yemen',
'YU' => 'Yugoslavia',
'ZM' => 'Zambia',
'ZW' => 'Zimbabwe'
);
}
}

@ -0,0 +1,166 @@
<?php
declare(strict_types=1);
namespace IOcornerstone\Framework\Arrays;
class Mimes {
/**
* Fetch Mime Types
* @return array of mime types
*/
public static function mimesArray(): array {
return [
'text' => [
'txt' => 'text/plain',
],
'web' => [
'htm' => 'text/html',
'html' => 'text/html',
'css' => 'text/css',
'json' => 'application/json',
'xml' => 'application/xml'
],
'images' => [
'png' => 'image/png',
'jpe' => 'image/jpeg',
'jpeg' => 'image/jpeg',
'jpg' => 'image/jpeg',
'gif' => 'image/gif',
'bmp' => 'image/bmp',
'ico' => 'image/vnd.microsoft.icon',
'tiff' => 'image/tiff',
'tif' => 'image/tiff',
'svg' => 'image/svg+xml',
'svgz' => 'image/svg+xml',
'webp' => 'image/webp'
],
'archives' => [
'zip' => 'application/zip',
'rar' => 'application/x-rar-compressed',
'cab' => 'application/vnd.ms-cab-compressed',
'gz' => 'application/gzip',
'tgz' => 'application/x-compressed',
'tar' => 'application/x-tar',
'iso' => 'application/x-iso9660-image',
'bz2' => 'application/x-bzip2',
'lz' => 'application/x-lzip',
'xz' => 'application/x-xz',
'7z' => 'application/x-7z-compressed',
],
'audio' => [
'oga' => 'audio/ogg', 'ogg' => 'audio/ogg', 'opus' => 'audio/ogg', 'spx' => 'audio/ogg',
'mp3' => 'audio/mpeg',
'wav' => 'audio/x-wav'
],
'video' => [
'mpeg' => 'video/mpeg', 'mpg' => 'video/mpeg', 'mpe' => 'video/mpeg',
'mp4' => 'video/mp4',
'qt' => 'video/quicktime',
'mov' => 'video/quicktime',
'ogv' => 'video/ogg'
],
'adobe' => [
'pdf' => 'application/pdf',
'psd' => 'image/vnd.adobe.photoshop',
'ai' => 'application/postscript',
'eps' => 'application/postscript',
'ps' => 'application/postscript'
],
'office' => [
'doc' => 'application/msword',
'rtf' => 'application/rtf',
'xls' => 'application/vnd.ms-excel',
'ppt' => 'application/vnd.ms-powerpoint',
'odt' => 'application/vnd.oasis.opendocument.text',
'ods' => 'application/vnd.oasis.opendocument.spreadsheet'
]
];
}
public static function mimesDangerious(): array {
return [
// Programming
'js' => 'application/javascript',
'php' => 'application/x-php-code',
'py' => 'application/x-python-code',
'pyc' => 'application/x-python-code',
'pyo' => 'application/x-python-code',
'torrent' => 'application/x-bittorrent',
// Linux
'deb' => 'application/vnd.debian.binary-package',
'rpm' => 'application/x-redhat-package-manager',
'bash' => 'application/x-shellscript',
'sh' => 'application/x-sh',
'jar' => 'application/java',
'java' => 'application/java',
// Windows
'swf' => 'application/x-shockwave-flash', 'swfl' => 'application/x-shockwave-flash',
'reg' => 'application/registry',
'lnk' => 'application/shortcut',
'inf' => 'application/config',
'scf' => 'application/explorer',
// A Monad script file. Monad was later renamed PowerShell.
'msh' => 'shell', 'msh1' => 'shell', 'msh2' => 'shell', 'mshxml' => 'shell', 'msh1xml' => 'shell', 'msh2xml' => 'shell',
// Power Shell
'ps1' => 'shell', 'ps1xml' => 'shell', 'ps2' => 'shell', 'ps2xml' => 'shell', 'psc1' => 'shell', 'psc2' => 'shell',
'wsc' => 'application/scripting-component', 'wsh' => 'application/scripting-host', 'ws' => 'application/scripting', 'wsf' => 'application/scripting',
'jse' => 'application/javascript-encrypted',
'vbe' => 'application/vbscript-encrypted',
'vb' => 'application/visualbasic',
'vbs' => 'application/visualbasic',
'bat' => 'application/batch',
'cmd' => 'application/batch',
'exe' => 'application/x-msdownload',
'pif' => 'application/executiable',
'application' => 'application/oneclick',
'gadget' => 'vista/desktop',
'msi' => 'application/x-msdownload',
'msp' => 'application/patch',
'com' => 'application/dos',
'scr' => 'application/screen-saver',
'hta' => 'html/application',
'cpl' => 'application/control-panel',
'msc' => 'application/m-console',
// Office 2007 Macros
'docm' => 'macro/msword',
'dotm' => 'macro/template',
'xlsm' => 'macro/excel',
'xltm' => 'macro', 'xlam' => 'macro', 'pptm' => 'macro', 'potm' => 'macro', 'ppam' => 'macro', 'ppsm' => 'macro', 'sldm' => 'marco'
];
}
public static function getMimeType(string $filename, array $allowed = ['all']) {
$ext = strtolower(array_pop(explode('.', $filename)));
$bad = self::mimesDangerious();
if (array_key_exists($ext, $bad)) {
return false;
}
$mime_types = self::mimesArray();
$everyone = in_array('all', $allowed);
foreach ($mime_types as $key => $mime) {
if (($everyone || in_array($key, $allowed) ) && array_key_exists($ext, $mime)) {
return $mime[$ext];
}
}
if ($everyone) {
if (function_exists('finfo_open')) {
$finfo = finfo_open(FILEINFO_MIME);
$mimetype = finfo_file($finfo, $filename);
finfo_close($finfo);
return $mimetype;
} else {
return 'application/octet-stream';
}
}
return false;
}
}

File diff suppressed because one or more lines are too long

@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace IOcornerstone\Arrays\Mocking;
use IOcornerstone\Framework\Common;
class Phone {
const TOLLFREE = [800, 844, 855, 866, 877, 888];
const AREACODES = [201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 212, 213, 214, 215, 216, 217, 218, 219, 220, 224, 225, 226, 228, 229, 231, 234, 236, 239, 240, 242, 246, 248, 249, 250, 251, 252, 253, 254, 256, 260, 262, 264, 267, 268, 269, 270, 272, 276, 281, 284, 289, 301, 302, 303, 304, 305, 306, 307, 308, 309, 310, 312, 313, 314, 315, 316, 317, 318, 319, 320, 321, 323, 325, 330, 331, 334, 336, 337, 339, 340, 343, 345, 346, 347, 351, 352, 360, 361, 364, 365, 380, 385, 386, 401, 402, 403, 404, 405, 406, 407, 408, 409, 410, 412, 413, 414, 415, 416, 417, 418, 419, 423, 424, 425, 430, 431, 432, 434, 435, 437, 438, 440, 441, 442, 443, 450, 458, 469, 470, 473, 475, 478, 479, 480, 484, 501, 502, 503, 504, 505, 506, 507, 508, 509, 510, 512, 513, 514, 515, 516, 517, 518, 519, 520, 530, 531, 534, 539, 540, 541, 548, 551, 559, 561, 562, 563, 567, 570, 571, 573, 574, 575, 579, 580, 581, 585, 586, 587, 601, 602, 603, 604, 605, 606, 607, 608, 609, 610, 612, 613, 614, 615, 616, 617, 618, 619, 620, 623, 626, 628, 629, 630, 631, 636, 639, 641, 646, 647, 649, 650, 651, 657, 660, 661, 662, 664, 667, 669, 670, 671, 678, 681, 682, 684, 701, 702, 703, 704, 705, 706, 707, 708, 709, 712, 713, 714, 715, 716, 717, 718, 719, 720, 721, 724, 725, 727, 731, 732, 734, 737, 740, 743, 747, 754, 757, 758, 760, 762, 763, 765, 767, 769, 770, 772, 773, 774, 775, 778, 779, 780, 781, 782, 784, 785, 786, 787, 801, 802, 803, 804, 805, 806, 807, 808, 809, 810, 812, 813, 814, 815, 816, 817, 818, 819, 825, 828, 829, 830, 831, 832, 843, 845, 847, 848, 849, 850, 854, 856, 857, 858, 859, 860, 862, 863, 864, 865, 867, 868, 869, 870, 872, 873, 876, 878, 901, 902, 903, 904, 905, 906, 907, 908, 909, 910, 912, 913, 914, 915, 916, 917, 918, 919, 920, 925, 928, 929, 930, 931, 934, 936, 937, 938, 939, 940, 941, 947, 949, 951, 952, 954, 956, 959, 970, 971, 972, 973, 978, 979, 980, 984, 985, 989];
private static function rndA(array $a) {
$array_count = Common::getCount($a);
return $a[rand(0, $array_count - 1)];
}
public static function getPhone(int $rows = 100, bool $formatted = true, bool $tollfree = false): array {
$phoneNumbers = [];
for($i =0; $i < $rows; $i++) {
$use_tollfree = ($tollfree && rand(0,9) === 4) ? true : false;
$randomNumber = (string) rand(1000, 9999);
$prefix = (string) rand(111,899);
$toll = "";
if ($use_tollfree) {
$toll = (string) self::rndA(self::TOLLFREE);
if ($formatted) {
$phoneNumbers[] = "1-(" . $toll . ")" . $prefix . "-" . $randomNumber;
} else {
$phoneNumbers[] = "1" . $toll . $prefix . $randomNumber;
}
} else {
$areacode = (string) self::rndA(self::AREACODES);
if ($formatted) {
$phoneNumbers[] = "(" . $areacode . ")" . $prefix . "-". $randomNumber;
} else {
$phoneNumbers[] = $areacode . $prefix . $randomNumber;
}
}
}
return $phoneNumbers;
}
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace IOcornerstone\Framework\Arrays;
final class Shortn {
public static function shortStreetSuffix($text) {
$uspsAbbreviations = [
'AVENUE' => 'Ave',
'BOULEVARD' => 'Blvd',
'DRIVE' => 'Dr',
'LANE' => 'Ln',
'MOUNT' => 'Mt',
'ROAD' => 'Rd',
'STREET' => 'St',
' NORTH ' => ' N. ',
' SOUTH ' => ' S. ',
' EAST ' => ' E. ',
' WEST ' => ' W. '
];
foreach ($uspsAbbreviations as $key => $value) {
$text = str_ireplace($key, $value, $text);
}
return $text;
}
}

@ -0,0 +1,185 @@
<?php
declare(strict_types=1);
namespace IOcornerstone\Framework\Arrays;
class ZipCodes {
public static function getState(string $zipcode): array {
/* Ensure we have exactly 5 characters to parse */
if (strlen($zipcode) !== 5) {
return 'Must pass a 5-digit $zipcode.';
}
$st = '';
$state = '';
/* Code cases alphabetized by state */
if ($zipcode >= 35000 && $zipcode <= 36999) {
$st = 'AL';
$state = 'Alabama';
} else if ($zipcode >= 99500 && $zipcode <= 99999) {
$st = 'AK';
$state = 'Alaska';
} else if ($zipcode >= 85000 && $zipcode <= 86999) {
$st = 'AZ';
$state = 'Arizona';
} else if ($zipcode >= 71600 && $zipcode <= 72999) {
$st = 'AR';
$state = 'Arkansas';
} else if ($zipcode >= 90000 && $zipcode <= 96699) {
$st = 'CA';
$state = 'California';
} else if ($zipcode >= 80000 && $zipcode <= 81999) {
$st = 'CO';
$state = 'Colorado';
} else if (($zipcode >= 6000 && $zipcode <= 6389) || ($zipcode >= 6391 && $zipcode <= 6999)) {
$st = 'CT';
$state = 'Connecticut';
} else if ($zipcode >= 19700 && $zipcode <= 19999) {
$st = 'DE';
$state = 'Delaware';
} else if ($zipcode >= 32000 && $zipcode <= 34999) {
$st = 'FL';
$state = 'Florida';
} else if (($zipcode >= 30000 && $zipcode <= 31999) || ($zipcode >= 39800 && $zipcode <= 39999)) {
$st = 'GA';
$state = 'Georgia';
} else if ($zipcode >= 96700 && $zipcode <= 96999) {
$st = 'HI';
$state = 'Hawaii';
} else if ($zipcode >= 83200 && $zipcode <= 83999) {
$st = 'ID';
$state = 'Idaho';
} else if ($zipcode >= 60000 && $zipcode <= 62999) {
$st = 'IL';
$state = 'Illinois';
} else if ($zipcode >= 46000 && $zipcode <= 47999) {
$st = 'IN';
$state = 'Indiana';
} else if ($zipcode >= 50000 && $zipcode <= 52999) {
$st = 'IA';
$state = 'Iowa';
} else if ($zipcode >= 66000 && $zipcode <= 67999) {
$st = 'KS';
$state = 'Kansas';
} else if ($zipcode >= 40000 && $zipcode <= 42999) {
$st = 'KY';
$state = 'Kentucky';
} else if ($zipcode >= 70000 && $zipcode <= 71599) {
$st = 'LA';
$state = 'Louisiana';
} else if ($zipcode >= 3900 && $zipcode <= 4999) {
$st = 'ME';
$state = 'Maine';
} else if ($zipcode >= 20600 && $zipcode <= 21999) {
$st = 'MD';
$state = 'Maryland';
} else if (($zipcode >= 1000 && $zipcode <= 2799) || ($zipcode == 5501) || ($zipcode == 5544 )) {
$st = 'MA';
$state = 'Massachusetts';
} else if ($zipcode >= 48000 && $zipcode <= 49999) {
$st = 'MI';
$state = 'Michigan';
} else if ($zipcode >= 55000 && $zipcode <= 56899) {
$st = 'MN';
$state = 'Minnesota';
} else if ($zipcode >= 38600 && $zipcode <= 39999) {
$st = 'MS';
$state = 'Mississippi';
} else if ($zipcode >= 63000 && $zipcode <= 65999) {
$st = 'MO';
$state = 'Missouri';
} else if ($zipcode >= 59000 && $zipcode <= 59999) {
$st = 'MT';
$state = 'Montana';
} else if ($zipcode >= 27000 && $zipcode <= 28999) {
$st = 'NC';
$state = 'North Carolina';
} else if ($zipcode >= 58000 && $zipcode <= 58999) {
$st = 'ND';
$state = 'North Dakota';
} else if ($zipcode >= 68000 && $zipcode <= 69999) {
$st = 'NE';
$state = 'Nebraska';
} else if ($zipcode >= 88900 && $zipcode <= 89999) {
$st = 'NV';
$state = 'Nevada';
} else if ($zipcode >= 3000 && $zipcode <= 3899) {
$st = 'NH';
$state = 'New Hampshire';
} else if ($zipcode >= 7000 && $zipcode <= 8999) {
$st = 'NJ';
$state = 'New Jersey';
} else if ($zipcode >= 87000 && $zipcode <= 88499) {
$st = 'NM';
$state = 'New Mexico';
} else if (($zipcode >= 10000 && $zipcode <= 14999) || ($zipcode == 6390) || ($zipcode == 501) || ($zipcode == 544)) {
$st = 'NY';
$state = 'New York';
} else if ($zipcode >= 43000 && $zipcode <= 45999) {
$st = 'OH';
$state = 'Ohio';
} else if (($zipcode >= 73000 && $zipcode <= 73199) || ($zipcode >= 73400 && $zipcode <= 74999)) {
$st = 'OK';
$state = 'Oklahoma';
} else if ($zipcode >= 97000 && $zipcode <= 97999) {
$st = 'OR';
$state = 'Oregon';
} else if ($zipcode >= 15000 && $zipcode <= 19699) {
$st = 'PA';
$state = 'Pennsylvania';
} else if ($zipcode >= 300 && $zipcode <= 999) {
$st = 'PR';
$state = 'Puerto Rico';
} else if ($zipcode >= 2800 && $zipcode <= 2999) {
$st = 'RI';
$state = 'Rhode Island';
} else if ($zipcode >= 29000 && $zipcode <= 29999) {
$st = 'SC';
$state = 'South Carolina';
} else if ($zipcode >= 57000 && $zipcode <= 57999) {
$st = 'SD';
$state = 'South Dakota';
} else if ($zipcode >= 37000 && $zipcode <= 38599) {
$st = 'TN';
$state = 'Tennessee';
} else if (($zipcode >= 75000 && $zipcode <= 79999) || ($zipcode >= 73301 && $zipcode <= 73399) || ($zipcode >= 88500 && $zipcode <= 88599)) {
$st = 'TX';
$state = 'Texas';
} else if ($zipcode >= 84000 && $zipcode <= 84999) {
$st = 'UT';
$state = 'Utah';
} else if ($zipcode >= 5000 && $zipcode <= 5999) {
$st = 'VT';
$state = 'Vermont';
} else if (($zipcode >= 20100 && $zipcode <= 20199) || ($zipcode >= 22000 && $zipcode <= 24699) || ($zipcode == 20598)) {
$st = 'VA';
$state = 'Virgina';
} else if (($zipcode >= 20000 && $zipcode <= 20099) || ($zipcode >= 20200 && $zipcode <= 20599) || ($zipcode >= 56900 && $zipcode <= 56999)) {
$st = 'DC';
$state = 'Washington DC';
} else if ($zipcode >= 98000 && $zipcode <= 99499) {
$st = 'WA';
$state = 'Washington';
} else if ($zipcode >= 24700 && $zipcode <= 26999) {
$st = 'WV';
$state = 'West Virginia';
} else if ($zipcode >= 53000 && $zipcode <= 54999) {
$st = 'WI';
$state = 'Wisconsin';
} else if ($zipcode >= 82000 && $zipcode <= 83199) {
$st = 'WY';
$state = 'Wyoming';
} else {
$st = 'none';
$state = 'none';
return 'No state found matching' . $zipcode;
}
return ['abbr' => $st, 'state' => $state];
}
}

@ -0,0 +1,267 @@
<?php
declare(strict_types = 1);
/**
* @author Robert Strutts
* @copyright (c) 2026, Robert Strutts
* @license MIT
*/
namespace IOcornerstone\Framework;
use IOcornerstone\Framework\{
Security,
Common,
String\StringFacade,
};
class Assets
{
public static function getVer(string $filename): string {
if (self::attemptsRootDir($filename)) {
return "";
}
$safe_file = Security::filterUri($filename);
if (! file_exists($safe_file)) {
return "?ver=false";
}
return "?ver=" . date('Y.m.d_H.i.s', filemtime($safe_file));
}
/**
* Check for / or absolute path not belonging to /var/www or PRJ:ROOT
* @param string|null $path
* @return bool
*/
private static function attemptsRootDir(?string $path): bool {
// up from src and back up to public
$open_base_dir_path = dirname(BaseDir, 1) . '/public/';
if ($path === null || $path === '') {
return false;
}
if (Common::getStringLeft($path, strlen($open_base_dir_path)) == $open_base_dir_path) {
return false;
}
if (Common::getStringLeft($path, 1) == '/') {
return true;
}
return false;
}
public static function getAjaxFiles(string $ajax_folder): string {
if (self::attemptsRootDir($ajax_folder)) {
return "";
}
$safe_folder = Security::filterUri($ajax_folder);
$ret = "var assets_files = [];" . PHP_EOL;
$js = glob($safe_folder . "*.{js,css}", GLOB_BRACE);
foreach($js as $file) {
$ret .= 'assets_files.push({ filename: "' . basename($file) . '", ts: "' . self::getVer($file) . '" });' . PHP_EOL;
}
return $ret;
}
public static function loadallCss(array $files, string $scope='project'): string {
$ret = '';
foreach($files as $file=>$a_media) {
$ret .= self::wrapCss($file, $scope, $a_media);
}
return $ret;
}
public static function loadallJs(array $files, string $scope='project'): string {
$ret = '';
foreach($files as $file=>$a) {
$ret .= self::wrapJs($file, $scope, $a);
}
return $ret;
}
public static function image(string $file, string $scope = '', array $a = array()): string {
$more = '';
if (count($a)) {
foreach ($a as $k => $v) {
$more .= " {$k}=\"{$v}\"";
}
}
$file = self::wrapAsset($file, $scope);
if ($file === false) {
return '';
}
return "<img src=\"{$file}\" {$more}/>\r\n";
}
public static function alert($msg) {
return self::inlineJs('alert("'.$msg.'");');
}
/**
* Wrapper for JS/CSS assets for the page.
* @param string $file or CDN
* @param string $scope (project/framework/cdn)
* @retval boolean|string false is not found, else Asset REF
*/
public static function wrapAsset(string $file, string $scope = 'project') {
$scope = StringFacade::strtolower($scope);
if ($scope === 'cdn') {
$proto = bootstrap\safer_io::get_clean_server_var('SERVER_PROTOCOL');
if ($proto === null) {
$protocol = '://';
} else {
$protocol = strtolower(substr($proto, 0, strpos($proto, '/'))) . '://';
}
return (str_contains($file, '://') === true) ? $file : $protocol . $file;
}
if ($scope === 'project' || $scope === 'app') {
$path = PROJECT_ASSETS_DIR . "/";
$ism = self::isMinified($path, $file);
return ($ism !== false) ? PROJECT_ASSETS_BASE_REF . "/" . $ism : false;
}
if ($scope === 'assets') {
$path = ASSETS_DIR . "/";
$ism = self::isMinified($path, $file);
return ($ism !== false) ? ASSETS_BASE_REF . "/" . $ism : false;
}
return $file;
}
/**
* Fetch Version of file if exists
* @param string $file
* @param string $scope
* @return string|bool
*/
private static function projectPaths(string $file, string $scope = 'project'): string | bool {
$scope = stringFacade::strtolower($scope);
if ($scope === 'cdn') {
return "";
} else if ($scope === 'project' || $scope === 'app') {
$path = PROJECT_ASSETS_DIR . "/";
} else if ($scope === 'assets') {
$path = ASSETS_DIR . "/";
} else {
return "";
}
$check = self::is_minified($path, $file);
return ($check !==false) ? self::get_ver($path . $check) : false;
}
/**
* Wrapper to return the CSS href=file.. stylesheet code for use in page.
* @param string $file - CSS file
* @param string $media - default of all media
* @retval string
*/
public static function wrapCss(string $file, string $scope = 'project', array $a_media = array()): string {
$more = '';
if (count($a_media)) {
foreach ($a_media as $k => $v) {
$more .= " {$k}=\"{$v}\"";
}
} else {
$more .= " media=\"all\"";
}
$ver = self::projectPaths($file, $scope);
$wrap_file = self::wrapAsset($file, $scope);
if ($ver === false || $wrap_file === false) {
return "<!-- CSS: {$file} not Found -->";
}
return "<link rel=\"stylesheet\" href=\"{$wrap_file}{$ver}\" type=\"text/css\"{$more}/>\r\n";
}
/**
* Wrapper to return the JavaScript src=file... code for use with page.
* @param type $file - external JS file.
* @retval string of script src=file
*/
public static function wrapJs(string $file, string $scope = 'project', array $a = array()): string {
$more = '';
if (count($a)) {
foreach ($a as $k => $v) {
$more .= (isset($k) && $k !=0 ) ? " {$k}=\"{$v}\"" : " {$v}";
}
}
$ver = self::projectPaths($file, $scope);
$wrap_file = self::wrapAsset($file, $scope);
if ($ver === false || $wrap_file === false) {
return "<!-- Script: {$file} not Found -->";
}
return "<script src=\"{$wrap_file}{$ver}\"{$more}></script>\r\n";
//return "<script type=\"text/javascript\">js_loader(\"{$file}\");</script>";
}
/**
* Purpose: To do inline JavaScript.
* @param type $code string of code to inline into page.
* @retval type
*/
public static function inlineJs(string $code): string {
return "<script type=\"text/javascript\">\r\n//<![CDATA[\r\n {$code}\r\n //]]> \r\n </script>\r\n";
}
/**
* Purpose: To execute this JavaScript code once JQuery is Ready().
* @param string $code to do once JQuery is Ready
* @retval string wrapped in ready code...
*/
public static function jqueryLoad(string $code): string {
return "\r\n$(function() { \r\n \t {$code} \r\n }); \r\n";
}
public static function isMinified(string $path, string $file) {
if (self::attemptsRootDir($path)) {
return false;
}
$safe_path = Security::filterUri($path);
$safe_file = Security::filterUri($file);
if (str_contains($safe_file, '.auto.js')) {
$production = (isLive());
$safe_file = str_replace('.auto.js', '', $safe_file);
if ( $production && file_exists($safe_path . $safe_file . '.min.js') ) {
return $safe_file . '.min.js';
}
return (file_exists($safe_path . $safe_file . '.js')) ? $safe_file . '.js' : false;
}
if (str_contains($safe_file, '.auto.css')) {
$production = (isLive());
$safe_file = str_replace('.auto.css', '', $safe_file);
if ( $production && file_exists($safe_path . $safe_file . '.min.css') ) {
return $safe_file . '.min.css';
}
return (file_exists($safe_path . $safe_file . '.css')) ? $safe_file . '.css' : false;
}
return ( file_exists($safe_path . $safe_file) ) ? $safe_file : false;
}
/**
* meta redirect when headers are already sent...
* @param string url - site to do redirect on
* @reval none
*/
public static function gotoUrl(string $url): void {
echo '<META http-equiv="refresh" content="0;URL=' . $url . '">';
exit;
}
/**
* Rediect to url and attempt to send via header.
* @param string $url - site to do redirect for
* @retval none - exits once done
*/
public static function redirectUrl(string $url): void {
$url = str_replace(array('&amp;', "\n", "\r"), array('&', '', ''), $url);
if (!headers_sent()) {
header('Location: ' . $url);
} else {
self::goto_url($url);
}
exit;
}
}

@ -0,0 +1,229 @@
<?php
declare(strict_types = 1);
/**
* @author Robert Strutts
* @copyright (c) 2026, Robert Strutts
* @license MIT
*/
namespace IOcornetstone\Framework\Attributes\Validators;
use \Attribute;
enum DEFAULT_VALUE {
case do_error_on_null;
case do_not_error_on_null;
}
#[Attribute(Attribute::TARGET_PROPERTY)]
class Positive {}
#[Attribute(Attribute::TARGET_PROPERTY)]
class Required {}
#[Attribute(Attribute::TARGET_PROPERTY)]
class Email {}
#[Attribute(Attribute::TARGET_PROPERTY)]
class Min {
public function __construct(public int|float $value) {}
}
#[Attribute(Attribute::TARGET_PROPERTY)]
class Max {
public function __construct(public int|float $value) {}
}
#[Attribute(Attribute::TARGET_PROPERTY)]
class GreaterThan {
public function __construct(public int|float $value) {}
}
#[Attribute(Attribute::TARGET_PROPERTY)]
class LessThan {
public function __construct(public int|float $value) {}
}
#[Attribute(Attribute::TARGET_PROPERTY)]
class NumberRange {
public function __construct(
public int|float $min,
public int|float $max
) {}
}
#[Attribute(Attribute::TARGET_PROPERTY)]
class Between {
public function __construct(
public int|float $min,
public int|float $max
) {}
}
#[Attribute(Attribute::TARGET_PROPERTY)]
class Same {
public function __construct(public string $other) {}
}
#[Attribute(Attribute::TARGET_PROPERTY)]
class Alphanumeric {}
#[Attribute(Attribute::TARGET_PROPERTY)]
class Secure {}
#[Attribute(Attribute::TARGET_PROPERTY)]
class ValidEmailDomain {}
#[Attribute(Attribute::TARGET_PROPERTY)]
class ValidDomain {}
class MyValidator {
use \IOcornerstone\Framework\Trait\FormValidator;
private array $errors = [];
private function handleErrors(string $rule_name, string $name, $value, $params = [], array $messages = []) {
// get the message rules
$ruleMessages = array_filter($messages, fn($message) => is_string($message));
// overwrite the default message
$validationErrors = array_merge(self::DEFAULT_VALIDATION_ERRORS, $ruleMessages);
$fn = 'is_' . $rule_name;
$field = $name;
$data[$name] = $value;
$callable = self::class . "::{$fn}";
$string_params = array_map('strval', $params);
if (is_callable($callable)) {
$pass = $callable($data, $field, ...$string_params);
if (!$pass) {
$lookfor = $messages[$field][$rule_name] ?? $validationErrors[$rule_name];
$new_params = array_values($params);
$this->errors[$field] = sprintf(
$lookfor,
$field,
...$params
);
}
}
}
/**
*
* @param object $object
* @param array $messages defined in trait
* @param type $default null avoids an error on no-entry, false gives errors on no-entry
* @return void
*/
public function validate(object $object, array $messages = [], DEFAULT_VALUE $default_value = DEFAULT_VALUE::do_not_error_on_null): void {
$handlers = [
Positive::class => function (string $name, $value, $attr, array $messages) {
$this->handleErrors("positive", $name, $value, messages: $messages);
},
Required::class => function (string $name, $value, $attr, array $messages) {
$this->handleErrors("required", $name, $value, messages: $messages);
},
Email::class => function (string $name, $value, $attr, array $messages) {
$this->handleErrors("email", $name, $value, messages: $messages);
},
GreaterThan::class => function (string $name, $value, $attr, array $messages) {
$params[] = $attr->value;
$this->handleErrors("greaterThan", $name, $value, $params, $messages);
},
LessThan::class => function (string $name, $value, $attr, array $messages) {
$params[] = $attr->value;
$this->handleErrors("lessThan", $name, $value, $params, $messages);
},
Min::class => function (string $name, $value, $attr, array $messages) {
$params[] = $attr->value;
$this->handleErrors("min", $name, $value, $params, $messages);
},
Max::class => function (string $name, $value, $attr, array $messages) {
$params[] = $attr->value;
$this->handleErrors("max", $name, $value, $params, $messages);
},
NumberRange::class => function (string $name, $value, $attr, array $messages) {
$params[] = $attr->min;
$params[] = $attr->max;
$this->handleErrors("numberRange", $name, $value, $params, $messages);
},
Between::class => function (string $name, $value, $attr, array $messages) {
$params[] = $attr->min;
$params[] = $attr->max;
$this->handleErrors("between", $name, $value, $params, $messages);
},
Same::class => function (string $name, $value, $attr, array $messages) {
$params[] = $attr->value;
$this->handleErrors("same", $name, $value, $params, $messages);
},
Alphanumeric::class => function (string $name, $value, $attr, array $messages) {
$this->handleErrors("alphanumeric", $name, $value, messages: $messages);
},
Secure::class => function (string $name, $value, $attr, array $messages) {
$this->handleErrors("secure", $name, $value, messages: $messages);
},
ValidEmailDomain::class => function (string $name, $value, $attr, array $messages) {
$this->handleErrors("validEmailDomain", $name, $value, messages: $messages);
},
ValidDomain::class => function (string $name, $value, $attr, array $messages) {
$this->handleErrors("validDomain", $name, $value, messages: $messages);
},
];
$default = match($default_value) {
DEFAULT_VALUE::do_not_error_on_null => null,
// Fail validation as user requested false on default
DEFAULT_VALUE::do_error_on_null => false,
default => null, // Pass validation as null or value unset
};
$ref = new \ReflectionObject($object);
foreach ($ref->getProperties() as $property) {
$name = $property->getName();
$property->setAccessible(true);
$value = $property->getValue($object) ?? $default;
foreach ($property->getAttributes() as $attribute) {
$class = $attribute->getName();
if (!isset($handlers[$class])) {
continue;
}
$handlers[$class](
$name,
$value,
$attribute->newInstance(),
$messages
);
}
}
}
public function getErrors(): array {
return $this->errors;
}
}

@ -0,0 +1,176 @@
<?php
declare(strict_types = 1);
/**
* @link https://github.com/genert/bbcode/blob/master/src/Parser/BBCodeParser.php
* @license The MIT License (MIT)
* @copyright (c) Genert 2017 - present.
*
* Take BB code 2 HTML, to display output
*/
namespace IOcornerstone\Framework;
final class BbCodeParser
{
protected $parsers = [
'h1' => [
'pattern' => '/\[h1\](.*?)\[\/h1\]/s',
'replace' => '<h1>$1</h1>',
'content' => '$1'
],
'h2' => [
'pattern' => '/\[h2\](.*?)\[\/h2\]/s',
'replace' => '<h2>$1</h2>',
'content' => '$1'
],
'h3' => [
'pattern' => '/\[h3\](.*?)\[\/h3\]/s',
'replace' => '<h3>$1</h3>',
'content' => '$1'
],
'h4' => [
'pattern' => '/\[h4\](.*?)\[\/h4\]/s',
'replace' => '<h4>$1</h4>',
'content' => '$1'
],
'h5' => [
'pattern' => '/\[h5\](.*?)\[\/h5\]/s',
'replace' => '<h5>$1</h5>',
'content' => '$1'
],
'h6' => [
'pattern' => '/\[h6\](.*?)\[\/h6\]/s',
'replace' => '<h6>$1</h6>',
'content' => '$1'
],
'bold' => [
'pattern' => '/\[b\](.*?)\[\/b\]/s',
'replace' => '<b>$1</b>',
'content' => '$1'
],
'italic' => [
'pattern' => '/\[i\](.*?)\[\/i\]/s',
'replace' => '<i>$1</i>',
'content' => '$1'
],
'underline' => [
'pattern' => '/\[u\](.*?)\[\/u\]/s',
'replace' => '<u>$1</u>',
'content' => '$1'
],
'strikethrough' => [
'pattern' => '/\[s\](.*?)\[\/s\]/s',
'replace' => '<s>$1</s>',
'content' => '$1'
],
'quote' => [
'pattern' => '/\[quote\](.*?)\[\/quote\]/s',
'replace' => '<blockquote>$1</blockquote>',
'content' => '$1'
],
'link' => [
'pattern' => '/\[url\](.*?)\[\/url\]/s',
'replace' => '<a href="$1">$1</a>',
'content' => '$1'
],
'namedlink' => [
'pattern' => '/\[url\=(.*?)\](.*?)\[\/url\]/s',
'replace' => '<a href="$1">$2</a>',
'content' => '$2'
],
'image' => [
'pattern' => '/\[img\](.*?)\[\/img\]/s',
'replace' => '<img src="$1">',
'content' => '$1'
],
'orderedlistnumerical' => [
'pattern' => '/\[list=1\](.*?)\[\/list\]/s',
'replace' => '<ol>$1</ol>',
'content' => '$1'
],
'orderedlistalpha' => [
'pattern' => '/\[list=a\](.*?)\[\/list\]/s',
'replace' => '<ol type="a">$1</ol>',
'content' => '$1'
],
'unorderedlist' => [
'pattern' => '/\[list\](.*?)\[\/list\]/s',
'replace' => '<ul>$1</ul>',
'content' => '$1'
],
'listitem' => [
'pattern' => '/\[\*\](.*)/',
'replace' => '<li>$1</li>',
'content' => '$1'
],
'code' => [
'pattern' => '/\[code\](.*?)\[\/code\]/s',
'replace' => '<code>$1</code>',
'content' => '$1'
],
'youtube' => [
'pattern' => '/\[youtube\](.*?)\[\/youtube\]/s',
'replace' => '<iframe width="560" height="315" src="//www.youtube-nocookie.com/embed/$1" frameborder="0" allowfullscreen></iframe>',
'content' => '$1'
],
'sub' => [
'pattern' => '/\[sub\](.*?)\[\/sub\]/s',
'replace' => '<sub>$1</sub>',
'content' => '$1'
],
'sup' => [
'pattern' => '/\[sup\](.*?)\[\/sup\]/s',
'replace' => '<sup>$1</sup>',
'content' => '$1'
],
'small' => [
'pattern' => '/\[small\](.*?)\[\/small\]/s',
'replace' => '<small>$1</small>',
'content' => '$1'
],
'table' => [
'pattern' => '/\[table\](.*?)\[\/table\]/s',
'replace' => '<table>$1</table>',
'content' => '$1',
],
'table-row' => [
'pattern' => '/\[tr\](.*?)\[\/tr\]/s',
'replace' => '<tr>$1</tr>',
'content' => '$1',
],
'table-data' => [
'pattern' => '/\[td\](.*?)\[\/td\]/s',
'replace' => '<td>$1</td>',
'content' => '$1',
],
];
private function searchAndReplace(string $pattern, string $replace, string $source): string {
while (preg_match($pattern, $source)) {
$source = preg_replace($pattern, $replace, $source);
}
return $source;
}
public function stripTags(string $source): string {
foreach ($this->parsers as $name => $parser) {
$source = $this->searchAndReplace($parser['pattern'] . 'i', $parser['content'], $source);
}
return $source;
}
public function parse(string $source, $caseInsensitive = null): string {
$caseInsensitive = $caseInsensitive === self::CASE_INSENSITIVE ? true : false;
foreach ($this->parsers as $name => $parser) {
$pattern = ($caseInsensitive) ? $parser['pattern'] . 'i' : $parser['pattern'];
$source = $this->searchAndReplace($pattern, $parser['replace'], $source);
}
return $source;
}
}

@ -0,0 +1,32 @@
<?php
declare(strict_types = 1);
/**
* @author Robert Strutts
* @copyright (c) 2026, Robert Strutts
* @license MIT
*/
namespace IOcornerstone\Framework;
/**
* @template TItem
*/
final readonly class Collection
{
/**
* @param array<int, TItem> $items
*/
public function __construct(
private array $items
) {
}
/**
* @return array<int, TItem>
*/
public function all(): array {
return $this->items;
}
}

@ -22,7 +22,7 @@ final class Common
unset($sensitive_data);
}
public static function get_count($i): int
public static function getCount($i): int
{
return (is_array($i) || is_object($i)) ? count($i) : 0;
}
@ -86,7 +86,7 @@ final class Common
echo 'It is TRUE!';
} elseif (is_resource($var)) {
echo 'VAR IS a RESOURCE';
} elseif (is_array($var) && self::get_count($var) == 0) {
} elseif (is_array($var) && self::getCount($var) == 0) {
echo 'VAR IS an EMPTY ARRAY!';
} elseif (is_numeric($var)) {
echo 'VAR is a NUMBER = ' . $var;

@ -0,0 +1,31 @@
<?php
declare(strict_types = 1);
/**
* @author Robert Strutts
* @copyright (c) 2026, Robert Strutts
* @license MIT
*/
namespace IOcornerstone\Framework;
class Dollars
{
private function moneyAsFloat(Number $money): float {
// Remove any existing currency symbols and thousands separators
$t = preg_replace('/[^\d.-]/', '', $money);
return (float)$t;
}
public function formatAsUSD(Number $money): string {
return '$' . number_format($this->moneyAsFloat($money), 2, '.', ',');
}
public function intl_USD(Number $money): string|false {
if (class_exists('NumberFormatter')) {
$formatter = new \NumberFormatter('en_US', \NumberFormatter::CURRENCY);
return $formatter->formatCurrency($this->moneyAsFloat($money), 'USD');
}
return false;
}
}

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
/*
* @author Robert Strutts
* @copyright (c) 2026, Robert Strutts
* @license MIT
*/
namespace IOcornerstone\Framework\Enum;
enum CompressionMethod: string
{
case GZIP = 'gzip';
case DEFLATE = 'deflate';
case ZLIB = 'zlib';
}

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
/*
* @author Robert Strutts
* @copyright (c) 2026, Robert Strutts
* @license MIT
*/
namespace IOcornerstone\Framework\Enum;
enum ViewType: string
{
case LIQUID = ".tpl";
case TWIG = ".twig";
case PHP = ".php";
public static function get_file_extension_for(ViewType $type): string {
return $type->value;
}
}

@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
/**
* @author Robert Strutts
* @copyright (c) 2026, Robert Strutts
* @license MIT
*/
namespace IOcornerstone\Framework;
use IOcornerstone\Framework\Enum\CompressionMethod as Method;
final class GzCompression
{
public function __construct(
public Method $method = Method::DEFLATE,
public int $compressionLevel = 4,
public bool $useCompression = true
)
{}
public function compress(string $data): string|false
{
if ($this->useCompression === false) {
return false;
}
$level = ($this->compressionLevel < 10 && $this->compressionLevel > 0) ? $this->compressionLevel : 4;
$c = match ($this->method) {
Method::DEFLATE => gzdeflate($data, $level),
Method::GZIP => gzencode($data, $level),
Method::ZLIB => gzcompress($data, $level),
};
return base64_encode($c);
}
public function decompress(string $compressed_data): string|false
{
if ($this->useCompression === false) {
return false;
}
$d = base64_decode($compressed_data);
return match ($this->method) {
Method::DEFLATE => gzinflate($d),
Method::GZIP => gzdecode($d),
Method::ZLIB => gzuncompress($d),
};
}
}

@ -0,0 +1,229 @@
<?php
declare(strict_types = 1);
/**
* @author Robert Strutts
* @copyright (c) 2026, Robert Strutts
* @license MIT
*/
namespace IOcornerstone\Framework;
final class Html
{
const ECHO = true;
// Escape HTML output
private static function h(string $string): string {
return htmlspecialchars($string, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML5, 'UTF-8');
}
public static function doEcho(?string $input, bool $echo = self::ECHO): ?string {
if ($echo) {
echo $input;
return null;
}
return $input;
}
/**
* Purpose: To output all HTML select (drop down) option values.
* @param array $options array of key=>value
* @param string $default Item to select on.
* @param string $select_by ['value'] for most of your needs.
* @param array $a as optional options
* @param bool $escape XSS prevention...
* @retval string of option values...
*/
public static function doOptions(
array $options,
string $default = '',
string $select_by = 'text',
array $a = array(),
bool $escape = true
): string {
$more = '';
if (count($a)) {
foreach ($a as $k => $v) {
if ($escape) {
$more .= " ". self::h($k) . "=\"" . self::h($v) . "\"";
} else {
$more .= " {$k}=\"{$v}\"";
}
}
}
$values = '';
foreach ($options as $value => $text) {
$compair_to = ($select_by == 'text') ? $text : $value;
$selected = (!empty($default) && $default == $compair_to) ? 'selected' : '';
if ($escape) {
$value = self::h($value);
$text = self::h($text);
}
$values .= "<option value=\"{$value}\" " .
$selected . " " . $more . ">{$text}</option>";
}
return $values;
}
private static function makeHeadersOnTable(array $header_fields, array $options = []) {
$echo = $options['echo'] ?? self::ECHO;
$nl = PHP_EOL;
$default_table_setup = "border='1' cellpadding='1' cellspacing='1'";
$table = $options['table_setup'] ?? $default_table_setup;
$escape = $options['escape'] ?? true;
$th = $options['th'] ?? "";
$th_class = (! empty($th)) ? " class=\"$th\" " : "";
$ret = self::doEcho("{$nl}<table {$table}>{$nl}", $echo);
$ret .= self::doEcho("\t<tr>{$nl}", $echo);
foreach($header_fields as $name=>$header_field) {
$skipping = $options['skip_output'][$name] ?? false;
if ($skipping) continue;
$field = ($escape) ? self::h($header_field) : $header_field;
$ret .= self::doEcho("\t\t<th{$th_class}>{$field}</th>{$nl}", $echo);
}
$ret .= self::doEcho("\t</tr>{$nl}", $echo);
return $ret;
}
/**
* Used by Memory_Usage script.
* Displays a table from input arrays -- fields
* @param array $header_fields - TH
* @param array $fields - TD
* @param bool $escape XSS prevention...
* @retval void
*/
public static function showTable(
array $header_fields,
array $fields,
array $options = []
): ?string {
$echo = $options['echo'] ?? self::ECHO;
$nl = PHP_EOL;
$ret = self::makeHeadersOnTable($header_fields, $options);
$escape = $options['escape'] ?? true;
foreach($fields as $field) {
$ret .= self::doEcho("\t<tr>{$nl}", $echo);
foreach($field as $td) {
$cell = ($escape) ? self::h($td) : $td;
$ret .= self::doEcho("\t\t<td>{$cell}</td>{$nl}", $echo);
}
$ret .= self::doEcho("\t</tr>{$nl}", $echo);
}
$ret .= self::doEcho("</table>{$nl}", $echo);
return $ret;
}
private static function makeCellTableHelperForIO(\Iterator $record, array $options = []): array {
$echo = $options['echo'] ?? self::ECHO;
$escape = $options['escape'] ?? true;
$cell_class = $options['cell_class'] ?? "";
$row_class = $options['row_class'] ?? "";
$cell_class_tag = (! empty($cell_class)) ? " class=\"$cell_class\" " : "";
$row_class_tag = (! empty($row_class)) ? " class=\"$row_class\" " : "";
$nl = PHP_EOL;
$ret = self::doEcho("\t<tr{$row_class_tag}>{$nl}", $echo);
$errors = [];
foreach($record as $html) {
$key = $html['name'] ?? "";
$value = $html['html'];
if (\CodeHydrater\common::get_count($html['errors'])) {
$errors[$key] = $html['errors'][$key];
}
$td = $value ?? "";
if (is_string($td)) {
$cell = ($escape) ? self::h($td) : $td;
} else {
$cell = (string) $td;
}
$ret .= self::doEcho("\t\t<td{$cell_class_tag}>{$cell}</td>{$nl}", $echo);
}
$ret .= self::doEcho("\t</tr>{$nl}", $echo);
return ['errors' => $errors, 'data' => $ret];
}
private static function makeCellTableHelper(array $record, array $options = []): ?string {
$echo = $options['echo'] ?? self::ECHO;
$escape = $options['escape'] ?? true;
$cell_class = $options['cell_class'] ?? "";
$row_class = $options['row_class'] ?? "";
$cell_class_tag = (! empty($cell_class)) ? " class=\"$cell_class\" " : "";
$row_class_tag = (! empty($row_class)) ? " class=\"$row_class\" " : "";
$nl = PHP_EOL;
$ret = self::doEcho("\t<tr{$row_class_tag}>{$nl}", $echo);
foreach($record as $key => $value) {
$skipping = $options['skip_output'][$key] ?? false;
if ($skipping) continue;
if (! is_string($key)) {
continue; // Remove Duplicate records
}
$td = $value ?? "";
if (is_string($td)) {
$cell = ($escape) ? self::h($td) : $td;
} else {
$cell = (string) $td;
}
$ret .= self::doEcho("\t\t<td{$cell_class_tag}>{$cell}</td>{$nl}", $echo);
}
$ret .= self::doEcho("\t</tr>{$nl}", $echo);
return $ret;
}
/**
* Generators use Memory in an Effient way!!!! So use this.
* @param array $header_fields
* @param array $db_field_names
* @param $records, note: \Iterator not always can be used here
* @param bool $escape
* @return void
*/
public static function showTableFrom(
array $header_fields,
$records,
array $options = [],
) {
$echo = $options['echo'] ?? self::ECHO;
$ret = self::makeHeadersOnTable($header_fields, $options);
switch($options['helper']) {
case "single-row":
$ret .= self::makeCellTableHelper($records, $options);
break;
case "io":
$ret = self::makeCellTableHelperForIO($records, $options);
$ret['data'] .= self::doEcho($ret['data'] . "</table>" . PHP_EOL);
return $ret;
default:
foreach($records as $record) {
self::makeCellTableHelper($record, $options);
}
}
$ret .= self::doEcho("</table>" . PHP_EOL, $echo);
return $ret;
}
public static function showErrors(
?array $errors = [],
array $options = []
): ?string {
$echo = $options['echo'] ?? self::ECHO;
$ret = "";
if ($errors) {
$message = "Please correct the following errors: ";
$i = 0;
foreach($errors as $error) {
$i++;
$message .= "<br/>{$i}) " . $error;
}
$ret .= self::doEcho($message, $echo);
}
return $ret;
}
}

@ -0,0 +1,435 @@
<?php
declare(strict_types = 1);
/**
* @author Robert Strutts
* @copyright (c) 2026, Robert Strutts
* @license MIT
*/
namespace IOcornerstone\Framework;
use IOcornerstone\Framework\{
Assets,
Configure,
Common
};
/**
* Description of htmlDocument
*
* @author Robert Strutts
*/
class HtmlDocument
{
private $title = '';
private $author = '';
private $description = '';
private $keywords = '';
private $robots = '';
private $head = '';
private $header = '';
private $body = '';
private $footer = '';
private $jsOnReady = '';
private $styles = '';
private $scripts = '';
private $mainStyles = '';
private $mainScripts = '';
private $activeCrumb = '';
private $breadcrumb = array();
public function __construct() {
$this->title = Configure::get('html', 'title') ?? '';
$this->author = Configure::get('html', 'author') ?? '';
$this->footer = Configure::get('html', 'footer') ?? '';
$this->keywords = Configure::get('html', 'keywords') ?? '';
$this->description = Configure::get('html', 'description') ?? '';
$this->robots = Configure::get('html', 'robots');
$css = Configure::get('html', 'css');
if (Common::getCount($css) > 0) {
foreach($css as $file=>$path) {
if (is_array($file)) continue;
if (is_array($path)) {
if (isset($path['path'])) {
$pathType = $path['path'];
unset($path['path']);
} else {
$pathType = "project";
}
$this->addCss($file, $pathType, $path);
} else {
$this->addCss($file, $path);
}
}
}
$js = Configure::get('html', 'javascript');
if (Common::getCount($js) >0) {
foreach($js as $file=>$path) {
if (is_array($file)) continue;
if (is_array($path)) {
if (isset($path['path'])) {
$pathType = $path['path'];
unset($path['path']);
} else {
$pathType = "project";
}
$this->addJs($file, $pathType, $path);
} else {
$this->addJs($file, $path);
}
}
}
}
public function clearCss(): void {
$this->styles = '';
}
public function clearJs(): void {
$this->scripts = '';
}
/**
* Set both Title and Header for HTML
* @param string $title
*/
public function setTitleAndHeader(string $title): void {
$this->title = $title;
$this->header = $title;
}
/**
* Set Author for HTML
* @param string $title
*/
public function setAuthor(string $author): void {
$this->author = $author;
}
/**
* Set Title for HTML
* @param string $title
*/
public function setTitle(string $title): void {
$this->title = $title;
}
/**
* Set Header for HTML
* @param string $header
*/
public function setHeader(string $header): void {
$this->header = $header;
}
public function setHead(string $head): void {
$this->head = $head;
}
/**
* Set Footer for HTML
* @param string $footer
*/
public function setFooter(string $footer): void {
// $this->add_css('footer.css', 'project');
$this->footer = $footer;
}
/**
* Set Description for HTML
* @param string $description
*/
public function setDescription(string $description): void {
$this->description = $description;
}
/**
* Set Keywords for HTML
* @param string $keywords
*/
public function setKeywords(string $keywords): void {
$this->keywords = $keywords;
}
/**
* Set Robots for HTML
* @param string $robot
*/
public function setRobots(string $robot): void {
$this->robots = $robot;
}
public function setBody(string $body): void {
$this->body = $body;
}
/**
* Set Active BreadCrumb in HTML
* @param string $active
*/
public function setActiveCrumb(string $active): void {
$this->activeCrumb = $active;
}
/**
* Set BreadCrumbs using array (HyperLink => Name of Crumb)
* @param array $crumbs Array(href => name)
*/
public function setBreadcrumbs(array $crumbs): void {
$this->breadcrumb = $crumbs;
}
public function setAssetsFromArray(array $files, string $which, string $scope = 'project'): void {
foreach($files as $file => $a) {
switch($which) {
case 'main_css':
$this->addMainCss($file, $scope, $a);
break;
case 'css':
$this->addCss($file, $scope, $a);
break;
case 'main_js':
$this->addMainJs($file, $scope, $a);
break;
case 'js':
$this->addJs($file, $scope, $a);
break;
}
}
}
private function missingFile(string $file, string $scope, string $kind) {
$failed = strtoupper($kind) . " filename of {$file} - {$scope} Asset Failed to Load!";
$this->addToJavascript("console.log(\"%c {$failed}\", \"color: red\")");
$comment = "<!-- {$failed} -->";
if ($kind === "css") {
$this->styles .= $comment;
} else if ($kind === "main_css") {
$this->mainStyles .= $comment;
} else if ($kind === "js") {
$this->scripts .= $comment;
} else if ($kind === "main_js") {
$this->mainScripts .= $comment;
}
}
/**
* Add CSS stylesheet to HTML under main CSS
* @param string $file
* @param string $scope (project, framework, cdn)
* @return bool was successful
*/
public function addCss(string $file, string $scope = 'project', array $a = array()): bool {
$css = Assets::wrapAsset($file, $scope);
if ($css === false) {
$this->missingFile($file, $scope, "css");
return false;
}
$this->styles .= Assets::wrapCss($file, $scope, $a);
return true;
}
/**
* Add JS JavaScript to HTML under main JS
* @param string $file
* @param string $scope (project, framework, cdn)
* @return bool was successful
*/
public function addJs(string $file, string $scope = 'project', array $a = array()): bool {
$js = Assets::wrapAsset($file, $scope);
if ($js === false) {
$this->js_log($file . " - {$scope} Asset Failed to Load!");
return false;
}
$this->scripts .= Assets::wrapJs($file, $scope, $a);
return true;
}
/**
* Add CSS stylesheet to HTML towards top of HTML for CSS
* @param string $file
* @param string $scope (project, framework, cdn)
* @return bool was successful
*/
public function addMainCss(string $file, string $scope = 'project', array $a = array()): bool {
$css = Assets::wrapAsset($file, $scope);
if ($css === false) {
$this->js_log($file . " - {$scope} Asset Failed to Load!");
return false;
}
$this->mainStyles .= Assets::wrapCss($file, $scope, $a);
return true;
}
/**
* Add JavaScript to HTML towards top of HTML for JS
* @param string $file
* @param string $scope (project, framework, cdn)
* @return bool was successful
*/
public function addMainJs(string $file, string $scope = 'project', array $a = array()): bool {
$js = Assets::wrapAsset($file, $scope);
if ($js === false) {
$this->js_log($file . " - {$scope} Asset Failed to Load!");
return false;
}
$this->mainScripts .= Assets::wrapJs($file, $scope, $a);
return true;
}
/**
* Adds JavaScript code to called after JQuery is ready.
* @param string $code
*/
//public function add_js_onready(string $code): void {
// $this->js_onready .= \px_inline_js(\px_jquery_load($code));
//}
/**
* Log to JavaScript Console under Chrome Browser
* @param string $log
*/
public function jsLog(string $log): void {
$this->addToJavascript("console.log('{$log}');");
}
/**
* Place JavaScript in HTML
* @param string $js
*/
public function addToJavascript(string $js): void {
if (! empty($js)) {
$this->scripts .= Assets::inlineJs($js);
}
}
/**
* Use CSS/JS for Database SSP
public function datatables_code(): void {
$this->addCss('datatables/datatables.min.css', 'cl');
$this->addJs('datatables/datatables_no_jquery.min.js', 'cl');
}
*
*/
/**
* Used by Template file to render HTML Author
* @return string HTML Author
*/
public function getAuthor(): string {
return $this->author;
}
/**
* Used by Template file to render HTML TITLE
* @return string HTML TITLE
*/
public function getTitle(): string {
return $this->title;
}
/**
* Used by Template file to render HTML Header
* @return string HTML Header
*/
public function getHeader(): string {
return $this->header;
}
public function getBody(): string {
return $this->body;
}
/**
* Used by Template file to render HTML Footer
* @return string HTML Footer
*/
public function getFooter(): string {
return $this->footer;
}
/**
* Used by Template file to render HTML Meta data for Description
* @return string HTML Meta Description
*/
public function getDescription(): string {
return $this->description;
}
/**
* Used by Template file to render HTML Meta data for Keywords
* @return string HTML Meta Keywords
*/
public function getKeywords(): string {
return $this->keywords;
}
/**
* Used by Template file to render HTML Meta data for Robots
* @return string HTML Meta Robots
*/
public function getRobots(): ?string {
return $this->robots;
}
/**
* Used by Template file to render HTML CSS
* @return string HTML CSS
*/
public function getStyles(): string {
return $this->styles;
}
/**
* Used by Template file to render HTML JavaScripts
* @return string HTML JS
*/
public function getScripts(): string {
return $this->scripts;
}
/**
* Used by Template file to render HTML main CSS @Top
* @return string HTML CSS
*/
public function getMainStyles(): string {
return $this->mainStyles;
}
/**
* Used by Template file to render HTML main JS @Top
* @return string HTML JavaScript
*/
public function getMainScripts(): string {
return $this->mainScripts;
}
/**
* Used by Template file to render HTML JS after main JS
* @return string HTML JS
*/
public function getJsOnReady(): string {
return $this->jsOnReady;
}
/**
* Used by Template file to render HTML Active BreadCrumb
* @return string HTML Active BreadCrumb
*/
public function getActiveCrumb(): string {
return $this->activeCrumb;
}
/**
* Used by Template file to render HTML BreadCrumbs
* @return string HTML BreadCrumbs
*/
public function getBreadcrumbs(): array {
return $this->breadcrumb;
}
public function getHead(): string {
return $this->head;
}
}

@ -0,0 +1,175 @@
<?php
declare(strict_types = 1);
/**
* @link https://github.com/genert/bbcode/blob/master/src/Parser/HTMLParser.php
* @license The MIT License (MIT)
* @copyright (c) Genert 2017 - present.
*
* Take HTML 2 BB code .. to save into db
*/
namespace IOcornerstone\Framework;
final class HtmlParser
{
protected $parsers = [
'h1' => [
'pattern' => '/<h1>(.*?)<\/h1>/s',
'replace' => '[h1]$1[/h1]',
'content' => '$1'
],
'h2' => [
'pattern' => '/<h2>(.*?)<\/h2>/s',
'replace' => '[h2]$1[/h2]',
'content' => '$1'
],
'h3' => [
'pattern' => '/<h3>(.*?)<\/h3>/s',
'replace' => '[h3]$1[/h3]',
'content' => '$1'
],
'h4' => [
'pattern' => '/<h4>(.*?)<\/h4>/s',
'replace' => '[h4]$1[/h4]',
'content' => '$1'
],
'h5' => [
'pattern' => '/<h5>(.*?)<\/h5>/s',
'replace' => '[h5]$1[/h5]',
'content' => '$1'
],
'h6' => [
'pattern' => '/<h6>(.*?)<\/h6>/s',
'replace' => '[h6]$1[/h6]',
'content' => '$1'
],
'bold' => [
'pattern' => '/<b>(.*?)<\/b>/s',
'replace' => '[b]$1[/b]',
'content' => '$1',
],
'strong' => [
'pattern' => '/<strong>(.*?)<\/strong>/s',
'replace' => '[b]$1[/b]',
'content' => '$1',
],
'italic' => [
'pattern' => '/<i>(.*?)<\/i>/s',
'replace' => '[i]$1[/i]',
'content' => '$1'
],
'em' => [
'pattern' => '/<em>(.*?)<\/em>/s',
'replace' => '[i]$1[/i]',
'content' => '$1'
],
'underline' => [
'pattern' => '/<u>(.*?)<\/u>/s',
'replace' => '[u]$1[/u]',
'content' => '$1',
],
'strikethrough' => [
'pattern' => '/<s>(.*?)<\/s>/s',
'replace' => '[s]$1[/s]',
'content' => '$1',
],
'del' => [
'pattern' => '/<del>(.*?)<\/del>/s',
'replace' => '[s]$1[/s]',
'content' => '$1',
],
'code' => [
'pattern' => '/<code>(.*?)<\/code>/s',
'replace' => '[code]$1[/code]',
'content' => '$1'
],
'orderedlistnumerical' => [
'pattern' => '/<ol>(.*?)<\/ol>/s',
'replace' => '[list=1]$1[/list]',
'content' => '$1'
],
'unorderedlist' => [
'pattern' => '/<ul>(.*?)<\/ul>/s',
'replace' => '[list]$1[/list]',
'content' => '$1'
],
'listitem' => [
'pattern' => '/<li>(.*?)<\/li>/s',
'replace' => '[*]$1',
'content' => '$1'
],
'link' => [
'pattern' => '/<a href="(.*?)">(.*?)<\/a>/s',
'replace' => '[url=$1]$2[/url]',
'content' => '$1'
],
'quote' => [
'pattern' => '/<blockquote>(.*?)<\/blockquote>/s',
'replace' => '[quote]$1[/quote]',
'content' => '$1'
],
'image' => [
'pattern' => '/<img src="(.*?)">/s',
'replace' => '[img]$1[/img]',
'content' => '$1'
],
'youtube' => [
'pattern' => '/<iframe width="560" height="315" src="\/\/www\.youtube\.com\/embed\/(.*?)" frameborder="0" allowfullscreen><\/iframe>/s',
'replace' => '[youtube]$1[/youtube]',
'content' => '$1'
],
'linebreak' => [
'pattern' => '/<br\s*\/?>/',
'replace' => '/\r\n/',
'content' => '',
],
'sub' => [
'pattern' => '/<sub>(.*?)<\/sub>/s',
'replace' => '[sub]$1[/sub]',
'content' => '$1'
],
'sup' => [
'pattern' => '/<sup>(.*?)<\/sup>/s',
'replace' => '[sup]$1[/sup]',
'content' => '$1'
],
'small' => [
'pattern' => '/<small>(.*?)<\/small>/s',
'replace' => '[small]$1[/small]',
'content' => '$1',
],
'table' => [
'pattern' => '/<table>(.*?)<\/table>/s',
'replace' => '[table]$1[/table]',
'content' => '$1',
],
'table-row' => [
'pattern' => '/<tr>(.*?)<\/tr>/s',
'replace' => '[tr]$1[/tr]',
'content' => '$1',
],
'table-data' => [
'pattern' => '/<td>(.*?)<\/td>/s',
'replace' => '[td]$1[/td]',
'content' => '$1',
],
];
private function searchAndReplace(string $pattern, string $replace, string $source): string {
while (preg_match($pattern, $source)) {
$source = preg_replace($pattern, $replace, $source);
}
return $source;
}
public function parse(string $source): string {
foreach ($this->parsers as $name => $parser) {
$source = $this->searchAndReplace($parser['pattern'], $parser['replace'], $source);
}
return $source;
}
}

@ -8,11 +8,12 @@ declare(strict_types=1);
* @license MIT
*/
namespace IOcornerstone\Framework;
namespace IOcornerstone\Framework\Http\App;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use IOcornerstone\Framework\Http\{
Contract\MiddlewareAwareInterface,
Request,
HttpFactory
};
@ -25,14 +26,14 @@ use IOcornerstone\Framework\{
};
use Exception;
class App
class App implements MiddlewareAwareInterface
{
private $file;
private $class;
private $method;
private $params;
private $middleWare = [];
private array $middleWare = [];
private Request $request;
private bool $testing = false;
private string $dirClass = ""; // Testing, CLI, or empty
@ -46,10 +47,17 @@ class App
if ($ret === false) {
return $this->local404();
}
// Make sure $ret is a ResponseInterface
if (!$ret instanceof ResponseInterface) {
// You might need to convert it, for example:
return (new HttpFactory())->createResponse(200, [], $ret);
}
return $ret;
}
public function getMiddleware()
public function getMiddleware(): array
{
return $this->middleWare;
}
@ -98,8 +106,6 @@ class App
$params = $query = $this->request->getUri()->getQuery();
// Now load Route
$is_from_the_controller = true; // TRUE for from Constructor...
return $this->router($route, $the_method, $params);
}

@ -13,7 +13,11 @@ namespace IOcornerstone\Framework\Http\App;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use IOcornerstone\Framework\App;
use IOcornerstone\Framework\Http\{
App\App,
Response,
Stream
};
final class AppHandler implements RequestHandlerInterface
{
@ -21,6 +25,20 @@ final class AppHandler implements RequestHandlerInterface
public function handle(ServerRequestInterface $request): ResponseInterface
{
return $this->app->handle($request);
$result = $this->app->handle($request);
// Convert to ResponseInterface if needed
if ($result instanceof ResponseInterface) {
return $result;
}
// If it returns false (for 404), create a response
if ($result === false) {
return new Response('404 Not Found', 404);
}
$stream = Stream::fromString($result);
// If it returns something else, wrap it
return new Response(body: $result);
}
}

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
/*
* @author Robert Strutts
* @copyright (c) 2026, Robert Strutts
* @license MIT
*/
namespace IOcornerstone\Framework\Http\Contract;
interface MiddlewareAwareInterface
{
public function getMiddleware(): array;
}

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
/*
* @author Robert Strutts
* @copyright (c) 2026, Robert Strutts
* @license MIT
*/
namespace IOcornerstone\Framework\Http\Contract;
interface MiddlewareInterface
{
public function process(
ServerRequestInterface $request,
RequestHandlerInterface $handler
): ResponseInterface;
}

@ -15,6 +15,6 @@ interface RouterInterface
public function delete(string $path, mixed $handler, array $middleware = []): void;
public function options(string $path, mixed $handler, array $middleware = []): void;
public function add(string $method, string $path, callable $handler): void;
public function add(string $method, string $path, callable $handler, array $middleware = []): void;
public function dispatch(ServerRequestInterface $request): array;
}

@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
/**
* @author Robert Strutts
* @copyright (c) 2026, Robert Strutts
* @license MIT
*/
namespace IOcornerstone\Framework\Http\Exception;
class RouteMiddlewareNotFoundException extends \Exception
{
}

@ -12,9 +12,9 @@ namespace IOcornerstone\Framework\Http;
use IOcornerstone\Framework\{
Registry as Reg,
Console,
App
};
use IOcornerstone\Framework\Http\{
App\App,
HttpContainer,
Routing\RoutingHandler,
Routing\Router,
@ -28,6 +28,7 @@ use IOcornerstone\Framework\Middleware\{
};
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
class Kernel {
protected ContainerInterface $psrContainer;
@ -66,17 +67,45 @@ class Kernel {
$router = new Router();
$this->registerRoutes($router);
$fallback = new AppHandler(
$this->psrContainer->get(App::class)
// Get App instance
$app = $this->psrContainer->get(App::class);
// Create AppHandler with middleware support
$appHandler = $this->createAppHandler($app);
// Create routing handler
$routingHandler = new RoutingHandler(
$router,
$this->psrContainer,
$appHandler
);
// Combine global middleware
$globalMiddleware = $this->getGlobalMiddleware();
// Add app-specific middleware if any
$appMiddleware = $app->getMiddleware();
if (!empty($appMiddleware)) {
array_splice($globalMiddleware, -1, 0, $appMiddleware);
}
return new MiddlewareQueueHandler(
[
$globalMiddleware,
$routingHandler
);
}
private function createAppHandler(App $app): RequestHandlerInterface
{
return new AppHandler($app);
}
private function getGlobalMiddleware(): array
{
return [
$this->psrContainer->get(ErrorMiddleware::class),
$this->psrContainer->get(RequestLoggerMiddleware::class),
],
new RoutingHandler($router, $this->psrContainer, $fallback)
);
];
}
private function dispatch($kernel): Response

@ -23,7 +23,9 @@ final class MiddlewareQueueHandler implements RequestHandlerInterface
}
$middleware = array_shift($this->middleware);
if (method_exists($middleware, "process")) {
return $middleware->process($request, $this);
}
throw new \Exception("Middleware, had no process method!");
}
}

@ -0,0 +1,44 @@
<?php
declare(strict_types = 1);
/**
* @author Robert Strutts
* @copyright (c) 2026, Robert Strutts
* @license MIT
*/
namespace IOcornerstone\Framework\Http\Routing;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Container\ContainerInterface;
use IOcornerstone\Framework\Http\Exception\RouteMiddlewareNotFoundException;
class RouterMiddlewareHandler implements RequestHandlerInterface
{
public function __construct(
private array $middleware,
private ContainerInterface $container
) {}
public function handle(ServerRequestInterface $request): ResponseInterface
{
$middleware = array_shift($this->middleware);
// Resolve middleware from container if it's a string
if (is_string($middleware)) {
$middleware = $this->container->get($middleware);
}
if (is_callable($middleware)) {
if (method_exists($middleware, "process")) {
return $middleware->process($request, $this);
}
return $middleware($request, $this);
}
throw new RouteMiddlewareNotFoundException();
}
}

@ -14,7 +14,9 @@ use Psr\Http\Message\ServerRequestInterface;
use Psr\Container\ContainerInterface;
use IOcornerstone\Framework\Http\{
Contract\RouterInterface,
Exception\RouteNotFoundException
Exception\RouteNotFoundException,
Exception\RouteMiddlewareNotFoundException,
CallableHandler
};
final class RoutingHandler implements RequestHandlerInterface
@ -35,9 +37,22 @@ final class RoutingHandler implements RequestHandlerInterface
}
$request = $this->injectRouteAttributes($request, $route['params']);
if (empty($route['middleware'])) {
return $this->invokeHandler($route['handler'], $request);
}
$routeHandler = new RouterMiddlewareHandler(
$route['middleware'],
$this->container
);
try {
return $routeHandler->handle($request);
} catch (RouteMiddlewareNotFoundException $ex) {
return $this->invokeHandler($route['handler'], $request);
}
}
private function injectRouteAttributes(
ServerRequestInterface $request,
array $params

@ -0,0 +1,103 @@
<?php
declare(strict_types = 1);
/**
* @author Robert Strutts
* @copyright (c) 2026, Robert Strutts
* @license MIT
*/
namespace IOcornerstone\Framework;
/**
* @template TKey
* @template TValue
* @implements \IteratorAggregate<TKey, TValue>
*/
class LazyCollection
{
/** @var \Closure|self|array<TKey, TValue>|null */
protected $source;
/**
* @param \Closure(): iterable<TKey, TValue>|self<TKey, TValue>|iterable<TKey, TValue>|null $source
*/
public function __construct($source = null)
{
if ($source instanceof \Closure || $source instanceof self) {
$this->source = $source;
} elseif (is_null($source)) {
$this->source = static::empty();
} else {
$this->source = $this->getArrayableItems($source);
}
}
/**
* @return \Traversable<TKey, TValue>
*/
public function getIterator(): \Traversable
{
return $this->makeIterator($this->source);
}
/**
* @param \Closure(): iterable<TKey, TValue>|self<TKey, TValue>|array<TKey, TValue> $source
* @return \Traversable<TKey, TValue>
*/
protected function makeIterator($source)
{
if ($source instanceof \Closure) {
$source = $source();
}
if (is_array($source)) {
return new \ArrayIterator($source);
}
return $source;
}
/**
* @template TNewKey
* @template TNewValue
* @param \Closure(): iterable<TNewKey, TNewValue>|iterable<TNewKey, TNewValue>|null $source
* @return self<TNewKey, TNewValue>
*/
public static function make($source = null)
{
return new static($source instanceof \Closure ? $source() : $source);
}
/**
* @param callable(TValue, TKey): bool $callback
* @return self<TKey, TValue>
*/
public function each(callable $callback): lazy_collection
{
foreach ($this->getIterator() as $key => $value) {
if ($callback($value, $key) === false) {
break;
}
}
return $this;
}
/**
* @param mixed $items
* @return array<TKey, TValue>
*/
protected function getArrayableItems($items): array
{
if (is_array($items)) {
return $items;
} elseif ($items instanceof \Traversable) {
return iterator_to_array($items);
} elseif (is_object($items) && method_exists($items, 'toArray')) {
return $items->toArray();
}
return (array) $items;
}
}

@ -0,0 +1,32 @@
<?php
declare(strict_types = 1);
/**
* @author Robert Strutts
* @copyright (c) 2026, Robert Strutts
* @license MIT
*/
namespace IOcornerstone\Framework;
class LazyObject
{
public function __construct(public string $className) {}
public function proxy(mixed ...$args): Object {
$className = $this->className;
if (!class_exists($className, true)) {
throw new \InvalidArgumentException("Class {$className} does not exist");
}
$reflector = new \ReflectionClass($className);
if (!$reflector->isInstantiable()) {
throw new \InvalidArgumentException("Class {$className} cannot be instantiated");
}
$initializer = static function () use ($className, $args) {
return new $className(...$args);
};
return $reflector->newLazyProxy($initializer);
}
}

@ -0,0 +1,66 @@
<?php
declare(strict_types = 1);
/**
* @author Robert Strutts
* @copyright (c) 2026, Robert Strutts
* @license MIT
*/
namespace IOcornerstone\Framework;
final class MemoryUsage
{
/**
* Used by get_memory_stats to get appropriate units of bytes
* @param type $size in bytes
* @retval string of unit size for bytes
*/
public static function convertBytes($size) {
if ($size === null) {
return 0;
}
$unit=array('b','kb','mb','gb','tb','pb');
return @round($size/pow(1024,($i=floor(log($size,1024)))),2).' '.$unit[$i];
}
/**
* Display/Returns Current Memory Usage
* @param bool $echo (default of true) - true: displays, false: returns data
*/
public static function getMemoryStats($echo = true) {
if (defined("Memory_Baseline")) {
$mem_baseline = Memory_Baseline;
} else {
$mem_baseline = 0;
}
$check = $_GET['debug'] ?? false;
if ($check || defined('DEBUG') && DEBUG === true) {
$now_mem = memory_get_usage();
$diff_mem = $now_mem - $mem_baseline;
$s_current = self::convertBytes($now_mem);
$s_diff = self::convertBytes($diff_mem);
$s_startup_mem = self::convertBytes($mem_baseline);
$peak = memory_get_peak_usage();
$s_peak = self::convertBytes($peak);
$diff_peak = $peak - $mem_baseline;
$s_diff_peak = self::convertBytes($diff_peak);
$a_headers = array('Memory Item', 'Size');
$a_fields[] = array('On-StartUp', $s_startup_mem);
$a_fields[] = array('Currently', $s_current);
$a_fields[] = array('<b>Total Diff</b>', "<b>{$s_diff}</b>");
$a_fields[] = array('&nbsp;', '&nbsp;');
$a_fields[] = array('PEAK', $s_peak);
$a_fields[] = array('<i>Total Diff PEAK</i>', "<i>{$s_diff_peak}</i>");
$options['escape'] = false;
$options['echo'] = $echo;
return Html::showTable($a_headers, $a_fields, $options);
}
}
}

@ -0,0 +1,46 @@
<?php
declare(strict_types = 1);
/**
* @author Robert Strutts
* @copyright (c) 2026, Robert Strutts
* @license MIT
*/
namespace IOcornerstone\Framework;
use IOcornerstone\Framework\Requires;
/**
* May be used by the Liquid Template Engine
*/
class PhpFileCache
{
protected $cache_path;
public function __construct($path) {
$is_safe_path = Requires::isDangerous($path);
if ($is_safe_path === false) {
throw new \Exception("Bad cache path");
}
$safer_path = Requires::filterDirPath($path);
$this->cache_path = rtrim($safer_path, '/') . '/';
if (!is_dir($this->cache_path)) {
mkdir($this->cache_path, 0775, true);
}
}
public function get($key) {
$file = $this->cache_path . md5($key) . '.cache.php';
if (file_exists($file)) {
return unserialize(file_get_contents($file));
}
return null;
}
public function set($key, $value) {
$file = $this->cache_path . md5($key) . '.cache.php';
file_put_contents($file, serialize($value));
}
}

@ -0,0 +1,106 @@
<?php
declare(strict_types = 1);
/**
* @author Robert Strutts
* @copyright (c) 2026, Robert Strutts
* @license MIT
*/
namespace IOcornerstone\Framework;
use IOcornerstone\Framework\Common;
/**
* random_engine - Provides a high-level API to the randomness
* provided by an Random\Engine OR uses the next Fall Back FN.
*
*/
class RandomEngine
{
private $engine = false;
public function __construct() {
$version = (float) phpversion();
if ($version > 8.1) {
$this->engine = new \Random\Randomizer();
}
}
public function getBytes(int $bytes_length = 16): string {
return ($this->engine) ? $this->engine->getBytes($bytes_length) :
random_bytes($bytes_length);
}
public function getInt(int $min, int $max): int {
if ($this->engine) {
return $this->engine->getInt($min, $max);
}
if (function_exists('random_int')) {
return random_int($min, $max); // secure fallback
} elseif (function_exists('mt_rand')) {
return mt_rand($min, $max); // fast
}
return rand($min, $max); // old
}
// Took from source https://pageconfig.com/post/fixed-length-large-random-numbers-with-php
private function bigRand(int $len = 18 ): int {
$rand = '';
while( !( isset( $rand[$len-1] ) ) ) {
$rand .= mt_rand( );
}
return (int) substr( $rand , 0 , $len );
}
public function getNextBigPostiveInt(): int {
if ($this->engine) {
return $this->engine->nextInt();
}
return $this->bigRand();
}
private function selectFromArray(array $a, int $num ): array {
$array_count = Common::getCount($a) - 1;
if ($array_count < 1) {
return [];
}
$ret = [];
for($i=0; $i<$num; $i++) {
$ret[] = $a[$this->getInt(0, $array_count)];
}
return $ret;
}
/**
* Pick random keys from an Array
*/
public function getArrayKeys(array $a, int $num): array {
if ($this->engine) {
return $this->engine->pickArrayKeys($a, $num);
}
return $this->selectFromArray($a, $num);
}
public function getShuffledArray(array $a): array {
if ($this->engine) {
return $this->engine->shuffleArray($a);
}
shuffle($a);
return $a;
}
public function getShuffledBytes(string $bytes): string {
if ($this->engine) {
return $this->engine->shuffleBytes($bytes);
}
$len = mb_strlen($bytes);
$a = [];
while($len-- > 0) {
$a[] = mb_substr($bytes, $len, 1);
}
shuffle($a);
return join('', $a);
}
}

@ -0,0 +1,135 @@
<?php
declare(strict_types = 1);
/**
* @author Robert Strutts
* @copyright (c) 2026, Robert Strutts
* @license MIT
*/
namespace IOcornerstone\Framework;
class RssFeed
{
public static function generateRssFeed(array $root, array $items) {
// Create the XML document
$rss = new \DOMDocument('1.0', 'UTF-8');
$rss->formatOutput = true;
// Create the RSS root element
$rssElement = $rss->createElement('rss');
$rssElement->setAttribute('version', '2.0');
$rssElement->setAttribute('xmlns:atom', 'http://www.w3.org/2005/Atom');
$rssRoot = $rss->appendChild($rssElement);
// Create the channel element
$channel = $rss->createElement('channel');
$channel = $rssRoot->appendChild($channel);
// Add atom:link for self-reference
$atomLink = $rss->createElement('atom:link');
$rss_feed_link = $root['feed_link'] ?? false;
if ($rss_feed_link === false) {
throw new \Exception("Unknown Feed Link");
}
$atomLink->setAttribute('href', $rss_feed_link);
$atomLink->setAttribute('rel', 'self');
$atomLink->setAttribute('type', 'application/rss+xml');
$channel->appendChild($atomLink);
// Add required channel elements
$title = $root["title"] ?? "News";
$channel->appendChild($rss->createElement('title', $title));
$site_link = $root["site_link"] ?? false;
if ($site_link === false) {
throw new \Exception("Unknown Site Link");
}
$channel->appendChild($rss->createElement('link', $site_link));
$desc = $root["description"] ?? "Latest news from My Website";
$channel->appendChild($rss->createElement('description', $desc));
// Optional channel elements
$lang = $root['lang'] ?? "en-us";
$channel->appendChild($rss->createElement('language', $lang));
$pub_date = $root['pub_date'] ?? date(DATE_RSS);
$channel->appendChild($rss->createElement('pubDate', $pub_date));
$build_date = $root['last_build_date'] ?? date(DATE_RSS);
$channel->appendChild($rss->createElement('lastBuildDate', $build_date));
$channel->appendChild($rss->createElement('generator', 'RSS Generator'));
// Add items to the channel
foreach ($items as $itemData) {
$item = $rss->createElement('item');
$item = $channel->appendChild($item);
$item->appendChild($rss->createElement('title', $itemData['title']));
$item->appendChild($rss->createElement('link', $itemData['link']));
$item->appendChild($rss->createElement('description', $itemData['description']));
$item->appendChild($rss->createElement('pubDate', $itemData['pubDate']));
// GUID should be a permalink
$guid = $rss->createElement('guid', $itemData['guid']);
$guid->setAttribute('isPermaLink', 'true');
$item->appendChild($guid);
// Optional item elements
if (isset($itemData['author'])) {
$item->appendChild($rss->createElement('author', $itemData['author']));
}
}
return $rss;
}
public static function saveToXmlFile(\DOMDocument $rss, string $filename = "feed") {
if (! defined("BaseDir") ) {
throw new \Exception("BaseDir not Set!");
}
$safe_file = BaseDir . "/public/" . preg_replace('/[^A-Za-z0-9]/', '', $filename) . ".xml";
$result = $rss->save($safe_file);
if ($result === false) {
throw new \Exception("Failed to save RSS feed to file");
}
// echo "Saved XML file: " . $safe_file;
return true;
}
public static function outputRss(\DOMDocument $rss): void {
// Set the content type to XML
header('Content-Type: application/rss+xml; charset=utf-8');
echo $rss->saveXML();
}
public static function getRss(\DOMDocument $rss): string {
return $rss->saveXML();
}
}
/**
// Sample items - in a real application, these would come from a database
$items = [
[
'title' => 'First Article',
'link' => 'http://example.com/article1',
'description' => 'This is the description of the first article.',
'pubDate' => date(DATE_RSS, strtotime('-2 days')),
'guid' => 'http://example.com/article1',
'author' => 'author@example.com (John Doe)'
],
[
'title' => 'Second Article',
'link' => 'http://example.com/article2',
'description' => 'This is the description of the second article.',
'pubDate' => date(DATE_RSS, strtotime('-1 day')),
'guid' => 'http://example.com/article2',
'author' => 'author@example.com (Jane Smith)'
]
];
*/

@ -0,0 +1,21 @@
<?php
declare(strict_types = 1);
/**
* @author Robert Strutts
* @copyright (c) 2026, Robert Strutts
* @license MIT
*/
namespace IOcornerstone\Framework;
class Scalar
{
public function intoString(mixed $name): string {
if (is_scalar($name) || (is_object($name) && method_exists($name, '__toString'))) {
return (string)$name;
} else {
throw new \Exception("Cannot convert to string!");
}
}
}

@ -0,0 +1,94 @@
<?php
declare(strict_types = 1);
/**
* @author Robert Strutts
* @copyright (c) 2026, Robert Strutts
* @license MIT
*/
namespace IOcornerstone\Framework;
use IOcornerstone\Framework\{
Configure,
Registry
};
final class SessionManagement
{
public static function start(
array $options = [],
string $type = "",
$enc = false
): void {
if (empty($type)) {
$type = Configure::get('sessions', 'type');
}
if ($enc === false) {
$exists = Registry::get('di')->exists('session_encryption');
if ($exists) {
$enc = Registry::get('di')->get_service('session_encryption');
}
}
if ($type === "none" || $type === "php") {
self::makeSessionStarted();
return;
}
$handler = match($type) {
'redis' => new services\sessions\redis_session_handler($enc, $options),
'files' => new services\sessions\file_session_handler($enc, $options),
default => new services\sessions\cookie_session_handler($enc, $options),
};
session_set_save_handler($handler, true);
self::makeSessionStarted();
}
private static function makeSessionStarted(bool $force_secure = false) {
if ((function_exists('session_status') && session_status() !== PHP_SESSION_ACTIVE) || !session_id()) {
$name = Configure::get('sessions', 'session_name');
if ($name !== null) {
session_name($name);
}
/**
* @todo Fix ForceSecure....to detect mode
*/
if (! headers_sent()) {
$use_secure = 1; // (site_helper::get_use_secure()) ? 1 : 0;
$use_secure = ($force_secure) ? 1 : $use_secure;
session_start([
'cookie_lifetime' => 0, // until browser is closed
'cookie_secure' => $use_secure, // require secure cookies if HTTPS is used
'use_only_cookies' => 1, // should be 1 to prevent URL attacks
'cookie_httponly' => 1, // should be 1 to disable JavaScript access
'cookie_samesite' => 'Strict', // should be Strict to prevent XSS
// So you need it when you do not want to allow a user to pre-define the session ID value. You normally want to prevent that to reduce the attack surface.
'use_strict_mode' => 1, // Note: Enabling session.use_strict_mode is mandatory for general session security. All sites are advised to enable this.
'use_trans_sid' => 0, // should be kept at the default of 0: URL based session management has additional security risks
]);
}
}
}
public static function hasUserRight(string $right): bool {
$rights = (isset($_SESSION['users_rights'])) ? $_SESSION['users_rights'] : false;
if ($rights === false) {
return false;
}
if (! common::is_json($right)) {
return false;
}
$assoc = true; // Use Array format
$a_rights = json_decode($rights, $assoc);
if (in_array($right, $a_rights)) {
return true;
}
return false;
}
public static function getUserId(): int {
$sid = (isset($_SESSION['user_id'])) ? $_SESSION['user_id'] : 0;
return intval($sid);
}
}

@ -8,6 +8,7 @@ declare(strict_types = 1);
* @license MIT
*/
namespace IOcornerstone\Framework\String;
use IOcornerstone\Framework\Exception\BadMethodCallException;
/**

@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
/**
* @author Robert Strutts
* @copyright (c) 2026, Robert Strutts
* @license MIT
*/
namespace IOcornerstone\Framework;
use IOcornerstone\Framework\String as SF;
final class TagMatches
{
const TAGS_TO_CHECK = array('div', 'span', 'form', 'i*', 'a*', 'h1', 'p*');
/**
* Function checks tags to make sure they match.
* Used by view.php
* @param string $page
* @return array [output, alert]
*/
public static function checkTags(string $page): array
{
$alert = '';
$output = '';
$lowercasePage = SF\StringFacade::strtolower($page);
unset($page);
$assets = "/assets/uikit/css/uikit.gradient.min.css";
$ui = '<link rel="stylesheet" href="' . $assets . '/uikit/css/uikit.gradient.min.css" type="text/css" media="all" />';
$ui .= '<div class="uk-alert uk-alert-danger"><b>';
$ui_end = '</b></div>';
foreach (self::TAGS_TO_CHECK as $tagName) {
if (str_contains($tagName, '*')) {
$tagName = str_replace('*', '', $tagName);
$otag = "<{$tagName}>"; // Open Tag
$open = substr_count($lowercasePage, $otag); // Count open tags in page
$otag = "<{$tagName} "; /* Open Tag with space */
$open += substr_count($lowercasePage, $otag); // Count open tags in page
} else {
$otag = "<{$tagName}"; // Open Tag
$open = substr_count($lowercasePage, $otag); // Count open tags in page
}
$ctag = "</{$tagName}>"; // Close Tag
$closed = substr_count($lowercasePage, $ctag); // Count Close tags in page
$totalStillOpen = $open - $closed; // Difference of open vs. closed....
if ($totalStillOpen > 0) {
$msg = "{$totalStillOpen} possibly MISSING closing {$tagName} !!!";
$alert .= "console.log('{$msg}');\r\n";
$output .= (isLive()) ? "<!-- {$msg} -->\r\n" : "{$ui}{$msg}{$ui_end}\r\n";
} elseif ($totalStillOpen < 0) {
$msg = abs($totalStillOpen) . " possibly MISSING opening {$tagName} !!!";
$alert .= "console.log('{$msg}');\r\n";
$output .= (isLive()) ? "<!-- {$msg} -->\r\n" : "{$ui}{$msg}{$ui_end}\r\n";
}
}
return array('output' => $output, 'alert' => $alert);
}
}

@ -0,0 +1,48 @@
<?php
declare(strict_types = 1);
/**
* @folked from https://gist.github.com/Xeoncross/1204255
* @site https://gist.github.com/vrdriver/659562a150ba955063ad849e44dac2cc
*/
namespace IOcornerstone\Framework;
class TimeZoneSelection
{
protected $timezones = array();
public function __construct() {
$regions = array(
'America' => \DateTimeZone::AMERICA,
'Europe' => \DateTimeZone::EUROPE,
'Africa' => \DateTimeZone::AFRICA,
'Antarctica' => \DateTimeZone::ANTARCTICA,
'Aisa' => \DateTimeZone::ASIA,
'Atlantic' => \DateTimeZone::ATLANTIC,
'Indian' => \DateTimeZone::INDIAN,
'Pacific' => \DateTimeZone::PACIFIC
);
foreach ($regions as $name => $mask) {
$zones = \DateTimeZone::listIdentifiers($mask);
foreach ($zones as $timezone) {
$this->timezones[$name][$timezone] = substr($timezone, strlen($name) + 1);
}
}
}
public function view() {
echo '<label>Select Your Timezone</label><br/><select id="timezone">';
foreach ($this->timezones as $region => $list) {
echo '<optgroup label="' . $region . '">' . "\n";
foreach ($list as $timezone => $name) {
echo '<option value="' . $timezone . '">' . $name . '</option>' . "\n";
}
echo '<optgroup>' . "\n";
}
echo '</select>';
}
}

@ -0,0 +1,287 @@
<?php
declare(strict_types=1);
/**
* @author Robert Strutts
* @copyright (c) 2026, Robert Strutts
* @license MIT
*/
namespace IOcornerstone\Framework;
use IOcornerstone\Framework\String as SF;
final class TimeZones
{
/**
* MySQL time conversion!
* @param type $field time
* @param type $time_zone_name, leave blank for users time zone!
* @return type
*/
public static function dbConvertTz($field, $time_zone_name = '')
{
$session_time_zone = (isset($_SESSION['time_zone'])) ? $_SESSION['time_zone'] : 'America/Detroit';
$tz = (!empty($time_zone_name)) ? $time_zone_name : $session_time_zone;
$to_tz = self::currentTimeForTimeZone($tz);
return "CONVERT_TZ({$field}, '+00:00', '{$to_tz}')";
}
/**
* Purpose: To convert time zone to offset time
* @param type $time_zone_name
* @return offset
*/
public static function currentTimeForTimeZone($time_zone_name)
{
$time = new \DateTime('now', new \DateTimeZone($time_zone_name));
return $time->format('P');
}
/**
* Purpose: To make a temporary valid timestamp for a database
*/
public static function expiresInHours(int $hours = 1)
{
$hours = ($hours > 0 && $hours < 10) ? $hours : 1;
$expires = new DateTime('NOW', new \DateTimeZone('UTC'));
$expires->add(new DateInterval("PT0{$hours}H"));
return $expires->format('Y-m-d H:i:s');
}
private static function mdy($userTime, $format)
{
switch (SF\StringFacade::strtolower($format)) {
case 'actions':
return $userTime->format('m-d-Y / h:iA');
case 'report':
return $userTime->format('l jS \of F\, Y\, h:i a');
case 'fancy':
return $userTime->format('l jS \of F Y h:i:s A');
case 'logging':
case 'log':
return $userTime->format('g:i A \o\n l jS F Y');
case 'legal_date':
return $userTime->format('n/j/Y');
case 'date':
return $userTime->format('m/d/Y');
case 'date-time':
return $userTime->format('m/d/Y h:i:s A');
case 'full':
return $userTime->format('m-d-Y h:i:s A');
case 'normal':
return $userTime->format('m/d/Y h:i A');
case 'default':
return $userTime->format('m-d-Y h:i A');
default:
return $userTime->format($format);
}
}
private static function dmy($userTime, $format)
{
switch (SF\StringFacade::strtolower($format)) {
case 'actions':
return $userTime->format('d-m-Y / h:iA');
case 'report':
return $userTime->format('l jS \of F\, Y\, h:i a');
case 'fancy':
return $userTime->format('l jS \of F Y h:i:s A');
case 'logging':
case 'log':
return $userTime->format('g:i A \o\n l jS F Y');
case 'legal_date':
return $userTime->format('j/n/Y');
case 'date':
return $userTime->format('d/m/Y');
case 'date-time':
return $userTime->format('d/m/Y h:i:s A');
case 'full':
return $userTime->format('d-m-Y h:i:s A');
case 'normal':
return $userTime->format('d/m/Y h:i A');
case 'default':
return $userTime->format('d-m-Y h:i A');
default:
return $userTime->format($format);
}
}
private static function ymd($userTime, $format)
{
switch (SF\StringFacade::strtolower($format)) {
case 'actions':
return $userTime->format('Y-m-d / h:iA');
case 'report':
return $userTime->format('l jS \of F\, Y\, h:i a');
case 'fancy':
return $userTime->format('l jS \of F Y h:i:s A');
case 'logging':
case 'log':
return $userTime->format('g:i A \o\n l jS F Y');
case 'legal_date':
return $userTime->format('Y/n/j');
case 'date':
return $userTime->format('Y/m/d');
case 'date-time':
return $userTime->format('Y/m/d h:i:s A');
case 'full':
return $userTime->format('Y-m-d h:i:s A');
case 'normal':
return $userTime->format('Y/m/d h:i A');
case 'default':
return $userTime->format('Y-m-d h:i A');
default:
return $userTime->format($format);
}
}
public static function dtFormat($userTime, $format, $country)
{
switch (SF\StringFacade::strtolower($format)) {
case 'object':
return $userTime;
case 'unix':
return $userTime->format('U');
case 'day':
return $userTime->format('l');
case 'time':
return $userTime->format('h:i A');
case 'military':
return $userTime->format('H:i:s');
case 'atom':
return $userTime->format(DateTime::ATOM);
case 'cookie':
return $userTime->format(DateTime::COOKIE);
case 'iso8601':
case 'iso':
case '8601':
return $userTime->format(DateTime::ISO8601);
case 'rfc822':
return $userTime->format(DateTime::RFC822);
case 'rfc850':
return $userTime->format(DateTime::RFC850);
case 'rfc1036':
return $userTime->format(DateTime::RFC1036);
case 'rfc1123':
return $userTime->format(DateTime::RFC1123);
case 'rfc2822':
return $userTime->format(DateTime::RFC2822);
case 'rfc3339':
return $userTime->format(DateTime::RFC3339);
case 'rss':
return $userTime->format(DateTime::RSS);
case 'w3c':
return $userTime->format(DateTime::W3C);
case 'standard':
case 'computer':
case 'database':
return $userTime->format('Y-m-d H:i:s');
}
switch (SF\StringFacade::strtolower($country)) {
case 'china':
return self::ymd($userTime, $format);
case 'int':
case 'other':
return self::dmy($userTime, $format);
default:
return self::mdy($userTime, $format);
}
}
/**
* Purpose: To convert a database timestamp into the users own Time Zone.
*/
public static function convertTimeZone($options)
{
$format = (isset($options['format'])) ? $options['format'] : 'normal';
$session_time_zone = (isset($_SESSION['time_zone'])) ? $_SESSION['time_zone'] : 'America/Detroit';
$tz = (isset($options['time_zone']) && !empty($options['time_zone'])) ? $options['time_zone'] : $session_time_zone;
$offset = (isset($options['offset'])) ? $options['offset'] : '';
$db_time = (isset($options['time'])) ? $options['time'] : '';
if ($db_time === '0000-00-00 00:00:00') {
return false;
}
$new_time = (empty($db_time)) ? self::getOffset($offset) : self::getOffsetBy($offset, $db_time);
// Convert date("U"); unix timestamps to proper format for DateTime function...
if (substr_count($new_time, ':') == 0) {
$the_time = (empty($new_time) || $new_time == 'now' || $new_time == 'current') ? gmdate("Y-m-d H:i:s") : gmdate("Y-m-d H:i:s", $new_time);
}
$userTime = new \DateTime($the_time, new \DateTimeZone('UTC'));
// Set the users time_zone to their zone
if ($tz !== 'UTC') {
$userTime->setTimezone(new \DateTimeZone($tz));
}
$country = (isset($options['country'])) ? $options['country'] : 'usa';
return self::dtFormat($userTime, $format, $country);
}
public static function humanDate($input_date)
{
if (empty($input_date)) {
return '';
}
$today = self::convertTimeZone(array('format' => 'm/d/Y'));
$date = strtotime($input_date);
if (date('m/d/Y', $date) == $today) {
return date('g:i', $date) . "<span style=\"font-size: 0.65em; vertical-align: text-top; margin-left: 1px; text-transform: lowercase;\">" . date('A', $date) . "</span>";
} elseif (date('Y', $date) == date('Y')) {
return "<span style=\"font-weight: 500;\">" . date('M', $date) . " " . date('j', $date) . "<span style=\"font-size: 0.65em; vertical-align: text-top; margin-left: 1px;\">" . date('S', $date) . "</span></span>";
} else {
return date('m/d/y', $date);
}
}
private static function isValidOffset($offset)
{
if (substr_count($offset, 'second') > 0) {
return true;
} elseif (substr_count($offset, 'minute') > 0) {
return true;
} elseif (substr_count($offset, 'hour') > 0) {
return true;
} elseif (substr_count($offset, 'day') > 0) {
return true;
} elseif (substr_count($offset, 'week') > 0) {
return true;
} elseif (substr_count($offset, 'month') > 0) {
return true;
} elseif (substr_count($offset, 'year') > 0) {
return true;
} elseif (substr_count($offset, 'next') > 0) {
return true;
} elseif (substr_count($offset, 'last') > 0) {
return true;
} else {
return false;
}
}
private static function getOffset($offset)
{
return (self::isValidOffset($offset)) ? strtotime($offset) : $offset;
}
private static function getOffsetBy($offset, $db_time)
{
// strtotime requires a int timestamp
if (substr_count($db_time, ':') > 0) {
$UTC = new \DateTime($db_time, new \DateTimeZone('UTC'));
$db_time = $UTC->format('U');
}
return (self::isValidOffset($offset)) ? strtotime($offset, $db_time) : $db_time;
}
}

@ -0,0 +1,79 @@
<?php
declare(strict_types = 1);
/**
* @author Robert Strutts
* @copyright (c) 2026, Robert Strutts
* @license MIT
*/
namespace IOCornerstone\Framework\Trait\Database;
use IOcornerstone\Framework\Common;
class RunSql
{
public function run($sql, $bind=""): int {
$pdostmt = $this->pdo->prepare(trim($sql));
if (Common::getCount($bind) > 0) {
$exec = $pdostmt->execute($bind);
} else {
$exec = $pdostmt->execute();
}
return $pdostmt->rowCount();
}
public function runSelect($sql) {
$pdostmt = $this->pdo->prepare(trim($sql));
$exec = $pdostmt->execute();
return $pdostmt->fetchAll(\PDO::FETCH_ASSOC);
}
private function describeTable(string $table) {
$driver = $this->pdo->getAttribute(\PDO::ATTR_DRIVER_NAME);
$tickTable = (strrpos($table, "`") === false) ? "`{$table}`" : $table;
if ($driver == 'sqlite') {
$sql = "PRAGMA table_info({$tickTable});";
$key = "name";
} elseif ($driver == 'mysql') {
$sql = "DESCRIBE {$tickTable};";
$key = "Field";
} elseif ($driver == 'pgsql') {
$sql = "SELECT column_name FROM information_schema.columns WHERE table_name = '{$table}';";
$key = "column_name";
} else {
return false;
}
return ['sql'=>$sql, 'key'=>$key];
}
public function getFields(string $table) {
$describe = $this->describeTable($table);
if (false !== ($list = $this->runSelect($describe['sql']))) {
$fields = array();
if (count($list) == 0) {
return array();
}
foreach ($list as $record) {
$fields[] = $record[$describe['key']];
}
return $fields;
}
return array();
}
private function filter(string $table, array $info): array {
$describe = $this->describeTable($table);
if ($describe === false) {
return array_keys($info);
}
if (false !== ($list = $this->runSelect($describe['sql']))) {
$fields = array();
foreach ($list as $record) {
$fields[] = $record[$describe['key']];
}
return array_values(array_intersect($fields, array_keys($info)));
}
return array();
}
}

@ -0,0 +1,208 @@
<?php
declare(strict_types = 1);
/**
* @author Robert Strutts
* @copyright (c) 2026, Robert Strutts
* @license MIT
*/
namespace IOCornerstone\Framework\Trait\Database;
use IOcornerstone\Framework\{
Configure,
Common,
Registry
};
trait Validation
{
/**
* Validate current class members
* @retval bool true valid, false failed tests
*/
public function validateMysql(): bool {
$tbl = (str_contains($this->table, "`")) ? $this->table : "`{$this->table}`";
foreach ($this->members as $field => $value) {
if ($field == $this->primaryKey) {
continue;
}
$validationField = $this->getVaildationMember($field);
if (isset($validationField['native_type']) && isset($validationField['len'])) {
$type = strtoupper($validationField['native_type']);
$len = intval($validationField['len']);
$meta = $validationField;
} else {
$meta = $this->getMySQLMetaField($field, $this->table);
$type = (isset($meta['native_type']) ? $meta['native_type'] : '');
$len = $meta['len'];
}
switch ($type) { //This should be all uppercase input.
case 'SHORT': //Small INT
case 'INT24': //MED INT
case 'LONGLONG': //BIG INT or SERIAL is an alias for BIGINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE.
case 'LONG': // Integers
if (!preg_match('/^[0-9]*$/', $value)) {
$this->do_verr("Failed Validation: NOT a digit {$type} {$field}");
return false;
} // Does not allow decimal numbers!!
if (strlen($value) > $len) {
$this->do_verr("Failed Validation: too long {$type} {$field}");
return false;
}
break;
case 'FLOAT':
if (strlen($value) > $len) {
$this->do_verr("Failed Validation: too long {$type} {$field}");
return false;
}
if (!is_float($value)) {
$this->do_verr("Failed Validation: NOT a float {$type} {$field}");
return false;
}
break;
case 'NEWDECIMAL':
$prec = intval($meta['precision']);
$maxlen = $len - $prec;
if (!str_contains($value, '.')) {
$this->do_verr("Failed Validation: No Decimal Found in field {$field}");
return false;
}
$x = explode('.', $value);
if (strlen($x[0]) >= ($maxlen - 1) || strlen($x[1]) > $prec) {
$this->do_verr("Failed Validation: too long {$type} {$field}");
return false;
}
break;
case 'DOUBLE':
if (strlen($value) > $len) {
$this->do_verr("Failed Validation: too long {$type} {$field}");
return false;
}
if (!is_double($value)) {
$this->do_verr("Failed Validation: NOT a double {$type} {$field}");
return false;
}
break;
case 'BLOB': // Text
if ($len == '4294967295' || $len == '16777215')
continue 2; //Too Big to process, 16777215 MEDIUMTEXT
if (strlen($value) > $len) {
$this->do_verr("Failed Validation: too long {$type} {$field}");
return false;
}
break;
case 'VAR_STRING': // VARCHAR or VARBINARY
case 'STRING': //CHAR or BINARY
if (strlen($value) > $len) {
$this->do_verr("Failed Validation: too long {$type} {$field}");
return false;
}
break;
case 'DATE':
if (!$this->isValidMysqlDate($value)) {
$this->do_verr("Failed Validation: invalid date in {$field}");
return false;
}
break;
case 'TIME':
if (!$this->isValidMysqlTime($value)) {
$this->do_verr("Failed Validation: invalid time in {$field}");
return false;
}
break;
case 'TIMESTAMP':
case 'DATETIME':
if (strlen($value) > $len) {
$this->do_verr("Failed Validation: too long {$type} {$field}");
return false;
}
if (!$this->isValidMysqlTimestamp($value)) {
$this->do_verr("Failed Validation: invalid timestamp in {$field}");
return false;
}
break;
default: //TINYINT, Bit, Bool, or Year is the default for no meta data
//if (!is_Digits($value)) return false; //This fails so its commented out.
if ($len == 3) { // Tiny INT
if (intval($value) > 255) {
$this->do_verr("Failed Validation: too long {$type} {$field}");
return false;
}
if (intval($value) < -127) {
$this->do_verr("Failed Validation: too short {$type} {$field}");
return false;
}
} elseif ($len == 1) { // Bit or Bool
if (intval($value) > 9) {
$this->do_verr("Failed Validation: too long {$type} {$field}");
return false;
}
if (intval($value) < 0) {
$this->do_verr("Failed Validation: too short {$type} {$field}");
return false;
}
}
break;
}
}
return true;
}
public function getMySQLMetaField(string $field, string $table): array {
$query = "SELECT `{$field}` FROM {$table} LIMIT 1";
$pdo_stmt = $this->pdo->prepare($query);
$pdo_stmt->execute();
return $pdo_stmt->getColumnMeta(0);
}
/**
* Check if valid timestamp/datetime
* @param type $Str
* @return type
*/
public function isValidMysqlTimestamp(string $Str): bool {
$Stamp = strtotime($Str);
$Month = date('m', $Stamp);
$Day = date('d', $Stamp);
$Year = date('Y', $Stamp);
return checkdate($Month, $Day, $Year);
}
public function isValidMysqlDate(string $str): bool {
$dateParts = explode('-', $str);
if (Common::getCount($dateParts) != 3)
return false;
if ((strlen($dateParts[0]) != 4) || (!is_numeric($dateParts[0])))
return false;
if ((strlen($dateParts[1]) != 2) || (!is_numeric($dateParts[1])))
return false;
if ((strlen($dateParts[2]) != 2) || (!is_numeric($dateParts[2])))
return false;
if (!checkdate($dateParts[1], $dateParts[2], $dateParts[0]))
return false;
return true;
}
public function isValidMysqlTime(string $str): bool {
return (strtotime($str) === false) ? false : true;
}
/**
* Helper function for validate mysql
* Will echo if not live error message
* If enabled will also log the error.
* @param string $msg error message
*/
private function do_verr(string $msg): void {
$this->error_message = $msg;
$exists = Registry::get('di')->exists('log');
if ($exists && Configure::get('database', 'log_validation_errors') === true) {
$log = Registry::get('di')->getService('log', ['validation_errors']);
$log->write($msg);
}
}
}

@ -0,0 +1,156 @@
<?php
declare(strict_types=1);
/*
* @author Robert Strutts
* @copyright (c) 2026, Robert Strutts
* @license MIT
*/
namespace IOcornerstone\Framework\Trait;
use IOcornerstone\Framework\Common;
trait FormValidator
{
const DEFAULT_VALIDATION_ERRORS = [
'positive' => 'The %s is not a positive number',
'required' => 'Please enter the %s',
'email' => 'The %s is not a valid email address',
'less_than' => 'The %s number must be less than %d',
'greater_than' => 'The %s number must be greater than %d',
'number_range' => 'The %s number must be in range of %d to %d',
'min' => 'The %s must have at least %s characters',
'max' => 'The %s must have at most %s characters',
'between' => 'The %s must have between %d and %d characters',
'same' => 'The %s must match with %s',
'alphanumeric' => 'The %s should have only letters and numbers',
'secure' => 'The %s must have between 8 and 64 characters and contain at least one number, one upper case letter, one lower case letter and one special character',
'valid_email_domain' => 'The %s email address is not active',
'valid_domain' => 'The %s domain name is not active',
];
/**
*
* @param array $data
* @param string $field
* @param mixed return: null avoids an error on no-entry; false gives errors on no-entry; default is the value. DO NOT MESS with this...
*/
private static function checkIfNullOrFalse(array $data, string $field) {
$d = $data[$field] ?? null;
return match($d) {
null => true, // Pass validation as null or value unset
false => false, // Fail validation as user requested false on default
default => $d // Use value
};
}
private static function isPositive(array $data, string $field): bool {
$r = self::checkIfNullOrFalse($data, $field);
if (is_bool($r)) return $r;
return (intval($r) >= 0) ? true : false;
}
private static function isRequired(array $data, string $field): bool {
// Don't use check_if_null_or_false here, it should always fail on false or null!
$r = $data[$field] ?? null;
if ($r === null || $r === false) {
return false; // It's a required field, so fail
}
if (common::get_count($r)) {
return false; // Should not be an array here
}
if (is_string($r)) {
return (trim($r) !== '');
}
if (is_int($r)) {
return true;
}
}
private static function isEmail(array $data, string $field): bool {
$r = self::checkIfNullOrFalse($data, $field);
if (is_bool($r)) return $r;
return (filter_var($r, FILTER_VALIDATE_EMAIL) === false) ? false : true;
}
private static function isMin(array $data, string $field, string $min): bool {
$r = self::checkIfNullOrFalse($data, $field);
if (is_bool($r)) return $r;
return mb_strlen($r) >= intval($min);
}
private static function isMax(array $data, string $field, string $max): bool {
$r = self::checkIfNullOrFalse($data, $field);
if (is_bool($r)) return $r;
return mb_strlen($r) <= intval($max);
}
private static function isGreaterThan(array $data, string $field, string $min): bool {
$r = self::checkIfNullOrFalse($data, $field);
if (is_bool($r)) return $r;
return intval($r) > intval($min);
}
private static function isLessThan(array $data, string $field, string $max): bool {
$r = self::checkIfNullOrFalse($data, $field);
if (is_bool($r)) return $r;
return intval($r) < intval($max);
}
private static function isNumberRange(array $data, string $field, string $min, string $max): bool {
$r = self::checkIfNullOrFalse($data, $field);
if (is_bool($r)) return $r;
$no = intval($r);
return $no >= intval($min) && $no <= intval($max);
}
private static function isBetween(array $data, string $field, string $min, string $max): bool {
$r = self::checkIfNullOrFalse($data, $field);
if (is_bool($r)) return $r;
$len = mb_strlen($r);
return $len >= intval($min) && $len <= intval($max);
}
private static function isSame(array $data, string $field, string $other): bool {
$r = self::checkIfNullOrFalse($data, $field);
if (is_bool($r)) return $r;
if (isset($data[$other])) {
return $r === $data[$other];
}
return false;
}
private static function isAlphanumeric(array $data, string $field): bool {
$r = self::checkIfNullOrFalse($data, $field);
if (is_bool($r)) return $r;
return ctype_alnum($r);
}
private static function isSecure(array $data, string $field): bool {
$r = self::checkIfNullOrFalse($data, $field);
if (is_bool($r)) return $r;
// Is 8 to 64 CHRs
$pattern = "#.*^(?=.{8,64})(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*\W).*$#";
return preg_match($pattern, $r);
}
private static function isValidEmailDomain(array $data, string $field): bool {
$r = self::checkIfNullOrFalse($data, $field);
if (is_bool($r)) return $r;
$domain = ltrim(stristr($r, '@'), '@') . '.';
return checkdnsrr($domain, 'MX');
}
private static function isValidDomain(array $data, string $field): bool {
$r = self::checkIfNullOrFalse($data, $field);
if (is_bool($r)) return $r;
return checkdnsrr($r, 'A')
|| checkdnsrr($r, 'AAAA')
|| checkdnsrr($r, 'CNAME');
}
}

@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
/*
* @author Robert Strutts
* @copyright (c) 2026, Robert Strutts
* @license MIT
*/
namespace IOcornerstone\Framework\Trait;
trait Macroable
{
protected static array $macros = [];
/**
* Register a custom macro.
*/
public static function macro(string $name, mixed $macro): void {
static::$macros[$name] = $macro;
}
/**
* Checks if macro is registered.
*/
public static function hasMacro(string $name): bool {
return isset(static::$macros[$name]);
}
/**
* Dynamically handle calls to the class.
*/
public function __call(string $method, mixed $parameters): mixed {
if (!static::hasMacro($method)) {
throw new \BadMethodCallException("Method {$method} does not exist.");
}
$macro = static::$macros[$method];
if ($macro instanceof \Closure) {
return call_user_func_array($macro->bindTo($this, static::class), $parameters);
}
return call_user_func_array($macro, $parameters);
}
/**
* Dynamically handle static calls to the class.
*/
public static function __callStatic(string $method, mixed $parameters): mixed {
if (!static::hasMacro($method)) {
throw new \BadMethodCallException("Method {$method} does not exist.");
}
$macro = static::$macros[$method];
if ($macro instanceof \Closure) {
return call_user_func_array(\Closure::bind($macro, null, static::class), $parameters);
}
return call_user_func_array($macro, $parameters);
}
}

@ -0,0 +1,36 @@
<?php
declare(strict_types = 1);
/**
* @author Robert Strutts
* @copyright (c) 2026, Robert Strutts
* @license MIT
*/
namespace IOcornerstone\Framework\Uuids;
const BASE62 = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
class Base62
{
public static function encodeId(int $num): string {
if ($num === 0) return BASE62[0];
$base = strlen(BASE62);
$encoded = '';
while ($num > 0) {
$encoded = BASE62[$num % $base] . $encoded;
$num = intdiv($num, $base);
}
return $encoded;
}
public static function decodeId(string $str): int {
$base = strlen(BASE62);
$len = strlen($str);
$num = 0;
for ($i = 0; $i < $len; $i++) {
$num = $num * $base + strpos(BASE62, $str[$i]);
}
return $num;
}
}

@ -0,0 +1,113 @@
<?php
declare(strict_types = 1);
/**
* @author Robert Strutts
* @copyright (c) 2026, Robert Strutts
* @license MIT
*/
namespace IOcornerstone\Framework\Uuids;
class UuidV7
{
public static function base32EncodedUuidV7(): string {
$uuid = self::generateUuidV7();
$uuidHex = str_replace('-', '', $uuid);
return self::base32Encode($uuidHex);
}
public static function generateUuidV7(): string {
$timestamp = (int)(microtime(true) * 1000); // Real Unix milliseconds
$timeHex = str_pad(dechex((int)$timestamp), 12, '0', STR_PAD_LEFT);
$randomHex = bin2hex(random_bytes(10));
return sprintf(
'%s-%s-%s-%s-%s',
substr($timeHex, 0, 8),
substr($timeHex, 8, 4),
'7' . substr($randomHex, 0, 3), // UUIDv7
substr($randomHex, 3, 4),
substr($randomHex, 7, 12)
);
}
/**
* NOTE: the absence of O like Orange and I like Internet, so
* Zero and One are present but not IO...
*/
public static function base32Encode(string $hex): string {
$base32Chars = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
$binary = '';
// Convert hex to binary
foreach (str_split($hex) as $hexChar) {
$binary .= str_pad(base_convert($hexChar, 16, 2), 4, '0', STR_PAD_LEFT);
}
// Split binary string into 5-bit chunks and convert to base32
$base32 = '';
foreach (str_split($binary, 5) as $chunk) {
$base32 .= $base32Chars[bindec(str_pad($chunk, 5, '0', STR_PAD_RIGHT))];
}
return $base32;
}
public static function base32Decode(string $base32): string {
$base32Chars = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
$binary = '';
// Convert each base32 character to 5-bit binary
foreach (str_split(strtoupper($base32)) as $char) {
$pos = strpos($base32Chars, $char);
if ($pos === false) {
throw new InvalidArgumentException("Invalid base32 character: $char");
}
$binary .= str_pad(decbin($pos), 5, '0', STR_PAD_LEFT);
}
// Split binary string into 4-bit chunks and convert to hex
$hex = '';
foreach (str_split($binary, 4) as $chunk) {
// Skip incomplete chunks at the end (padding)
if (strlen($chunk) < 4) break;
$hex .= dechex(bindec($chunk));
}
return $hex;
}
public static function getTimestampFromUuidV7(string $uuid): int {
// Remove hyphens and extract the time parts
$cleaned = str_replace('-', '', $uuid);
// Reconstruct the full 12-character timestamp hex
$timeHex =
substr($cleaned, 0, 8) . // First 8 chars (time_low)
substr($cleaned, 8, 4); // Next 4 chars (time_mid)
// Convert to decimal (custom scaled time)
$customTime = hexdec($timeHex);
// Reverse the scaling: divide by 1000 to get seconds
$unixSeconds = (int)($customTime / 1000);
return $unixSeconds;
}
public static function getFormatedDate(int $timestamp) {
return date('Y-m-d H:i:s', $timestamp);
}
/**
* @example Database SQL:
CREATE TABLE test_table (
id CHAR(36) PRIMARY KEY, -- IMPORTANT PART, to use generateUUIDv7 (UUIDv7)
-- OR CHAR(26) for base32_encoded_UUIDv7 (BASE32 UUIDv7)...
name VARCHAR(255) NOT NULL, -- BLA BLA
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
*/
}

@ -0,0 +1,121 @@
<?php
declare(strict_types = 1);
/**
* Modified from phptutorial.net
* @link https://www.phptutorial.net/php-tutorial/php-validation/
*/
namespace IOcornerstone\Framework;
enum DEFAULT_VALUE {
case do_error_on_null;
case do_not_error_on_null;
}
class Validator
{
use \IOcornerstone\Framework\Trait\FormValidator;
private static function makeArrays(
array $data,
$field,
DEFAULT_VALUE $default_value = DEFAULT_VALUE::do_not_error_on_null
): array {
$dataset = [];
if (isset($data[$field])) {
if (common::get_count($data[$field])) {
foreach($data[$field] as $the_data) {
$dataset[] = $the_data;
}
} else {
$dataset[] = $data[$field];
}
} else {
$default = match($default_value) {
DEFAULT_VALUE::do_not_error_on_null => null,
// Fail validation as user requested false on default
DEFAULT_VALUE::do_error_on_null => false,
default => null, // Pass validation as null or value unset
};
$dataset[] = $default; // How to handle nulls
}
return $dataset;
}
public static function validate(
array $data,
array $fields,
array $messages = [],
DEFAULT_VALUE $default_value = DEFAULT_VALUE::do_not_error_on_null
): array {
// Split the array by a separator, trim each element
// and return the array
$split = fn($str, $separator) => array_map('trim', explode($separator, $str));
// get the message rules
$rule_messages = array_filter($messages, fn($message) => is_string($message));
// overwrite the default message
$validation_errors = array_merge(self::DEFAULT_VALIDATION_ERRORS, $rule_messages);
$errors = [];
foreach ($fields as $field => $option) {
foreach(self::makeArrays($data, $field, $default_value) as $index=>$v) {
$data[$field] = $v; // Force update on arrays
$rules = $split($option, '|');
foreach ($rules as $rule) {
// get rule name params
$params = [];
// if the rule has parameters e.g., min: 1
if (strpos($rule, ':')) {
[$rule_name, $param_str] = $split($rule, ':');
$params = $split($param_str, ',');
} else {
$rule_name = trim($rule);
}
// by convention, the callback should be is_<rule> e.g.,is_required
$fn = 'is_' . $rule_name;
$callable = self::class . "::{$fn}";
if (is_callable($callable)) {
$pass = $callable($data, $field, ...$params);
if (!$pass) {
// get the error message for a specific field and rule if exists
// otherwise get the error message from the $validation_errors
$lookfor = $messages[$field][$rule_name] ?? $validation_errors[$rule_name];
$errors[$field] = sprintf(
$lookfor,
$field,
...$params
);
}
}
}
}
}
return $errors;
}
}
/*
$data = ['email'=>'jim@aol.com'];
$fields = ['email' => 'required | email'];
$errors = Validator::validate($data, $fields, default_value: DEFAULT_VALUE::do_error_on_null);
print_r($errors);
*/
/*
* 'firstname' => 'required | max:255',
'lastname' => 'required | max: 255',
'address' => 'required | min: 10 | max:255',
'zipcode' => 'between: 5,6',
'username' => 'required | alphanumeric | between: 3,255',
'email' => 'required | email | valid_email_domain',
*/

@ -0,0 +1,282 @@
<?php
declare(strict_types=1);
/**
* @author Robert Strutts
* @copyright (c) 2026, Robert Strutts
* @license MIT
*/
namespace IOcornerstone\Framework;
use IOcornerstone\Framework\Enum\ViewType;
use IOcornerstone\Framework\{
Common,
Configure,
Requires,
Registry,
};
final class View
{
public $white_space_control = false;
public $page_output;
private $vars = [];
private $files = [];
private $template = false;
private $use_template_engine = false;
private $use_template_engine_twig = false;
private $use_template_engine_liquid = false;
private $template_type = 'tpl';
protected $tempalte_engine_twig = null;
protected $tempalte_engine_liquid = null;
private function getFile(string $view_file, string $default): string
{
$file = (empty($default)) ? "views/{$view_file}" : "views/{$default}/{$view_file}";
$path = IO_CORNERSTONE_PROJECT;
$vf = $path . $file;
if (Requires::saferFileExists($vf) !== false) {
return $file;
}
return '';
}
/**
* Alias to set_view
*/
public function include(string $file, ViewType $type = ViewType::PHP): void
{
$this->setView($file, $type);
}
/**
* Alias to set_view
*/
public function addView(string $file, ViewType $type = ViewType::PHP): void
{
$this->setView($file, $type);
}
/**
* Check the $_GET['render'] for which folder to use to render the view
* @param string $viewFile
* @return file | false
*/
private function findViewPath(string $viewFile): string|false
{
$found = false;
$defaultPaths = Configure::get('view_mode', 'default_paths');
$get = $_GET['render'];
if (in_array($get, $defaultPaths)) {
if (($key = array_search($get, $defaultPaths)) !== false) {
unset($defaultPaths[$key]); // Remove as we'll make it first later...
}
array_unshift($defaultPaths, $get); // Make First in Array!
}
foreach ($defaultPaths as $default) {
$file = $this->getFile($viewFile, $default);
if (!empty($file)) {
$found = true;
break;
}
}
return ($found) ? $file : false;
}
/**
* Use View File
* @param string $viewFile
* @param string $render_path
* @throws Exception
*/
public function setView(?string $viewFile = null, ViewType $type = ViewType::PHP): void
{
if ($viewFile === null) {
return;
}
$file_ext = ViewType::get_file_extension_for($type);
$file = $this->findViewPath($viewFile . $file_ext);
if ($type == ViewType::TWIG) {
$this->use_template_engine_twig = true;
$this->template_type = str_replace('.', '', $file_ext);
$path = IO_CORNERSTONE_PROJECT;
if ($file !== false) {
$file = str_replace("views/twig", "", $file); // Remove Path, as it is defined in the service file
}
}
if ($type == ViewType::LIQUID) {
$this->use_template_engine_liquid = true;
$this->template_type = str_replace('.', '', $file_ext);
if ($file !== false) {
$file = ltrim(str_replace("views/liquid", "", $file), "/"); // Remove Path, as it is defined in the service file
}
}
if ($file === false) {
echo "No view file exists for: {$viewFile}!";
throw new \Exception("View File does not exist: " . $viewFile);
} else {
$this->files[] = array('file' => $file, 'path' => bootstrap\UseDir::PROJECT, 'file_type' => $file_ext);
}
}
public function clearTemplate(): void
{
$this->template = false;
}
/**
* Use PHP Template with view
* @param string $render_page
* @return bool was found
*/
public function setPhpTemplate(string $render_page): bool
{
if (!empty($render_page)) {
$render_page = str_replace('.php', '', $render_page);
$templ = "/templates/{$render_page}";
if (bootstrap\requires::safer_file_exists(IO_CORNERSTONE_PROJECT . $templ . '.php') !== false) {
$this->template = $templ;
return true;
}
}
$this->template = false;
return false;
}
private function clearOb(int $saved_ob_level): void
{
while (ob_get_level() > $saved_ob_level) {
ob_end_flush();
}
}
private function obStart(): void
{
if (extension_loaded('mbstring')) {
ob_start('mb_output_handler');
} else {
ob_start();
}
}
/**
* Sets a variable in this view with the given name and value
*
* @param mixed $name Name of the variable to set in the view, or an array of key/value pairs where each key is the variable and each value is the value to set.
* @param mixed $value Value of the variable to set in the view.
*/
public function set($name, mixed $value = null): void
{
if (is_array($name)) {
foreach ($name as $var_name => $value) {
$this->vars[$var_name] = $value;
}
} else {
$this->vars[$name] = $value;
}
}
/**
* Alias to fetch with Built-in echo
* @param type $local
* @param string $file
*/
public function render($local, ?string $file = null, ViewType $type = ViewType::PHP)
{
echo $this->fetch($local, $file, $type);
}
/**
* Outputs view
* @param type $local = $this
* @param $file (optional view file)
*/
public function fetch($local, ?string $file = null, ViewType $type = ViewType::PHP): string
{
$page_output = ob_get_clean(); // Get echos before View
$this->obStart();
$saved_ob_level = ob_get_level();
$this->setView($file, $type);
unset($file);
if (!is_object($local)) {
$local = $this; // FALL Back, please use fetch($this);
}
if ($this->use_template_engine_liquid) {
$this->tempalte_engine_liquid = Registry::get('di')->get_service('liquid', [$this->template_type]);
if ($this->white_space_control) {
$this->tempalte_engine->whitespace_control();
}
}
if ($this->use_template_engine_twig) {
$this->tempalte_engine_twig = Registry::get('di')->get_service('twig', [$this->template_type]);
}
if (count($this->files) > 0) {
foreach ($this->files as $view_file) {
if ($view_file['file_type'] == '.php') {
Requires::secureInclude($view_file['file'], UseDir::PROJECT, $local, $this->vars); // Include the PHP file
} else if ($view_file['file_type'] == '.tpl') {
// Liquid uses .tpl by default, so remove that File Ext
$template_file = str_replace('.tpl', '', $view_file['file']);
$this->tempalte_engine_liquid->parse_file($template_file);
$assigns = $this->vars['template_assigns'] ?? [];
$filters = $this->vars['template_filters'] ?? null;
$registers = $this->vars['template_registers'] ?? [];
$assigns['production'] = (isLive());
echo $this->tempalte_engine_liquid->render($assigns, $filters, $registers);
} else if ($view_file['file_type'] == '.twig') {
$fns = $this->vars['twig_functions'] ?? [];
if (Common::getCount($fns)) {
foreach ($fns as $fn_label => $fn_name) {
$this->tempalte_engine_twig->addFunction(new \Twig\TwigFunction($fn_label, $fn_name));
}
}
$twig_data = $this->vars['twig_data'] ?? [];
echo $this->tempalte_engine_twig->render($view_file['file'], $twig_data);
} else {
throw new \Exception("Unable View File Type");
}
}
}
$this->clear_ob($saved_ob_level);
$page_output .= ob_get_clean();
if (Configure::get('CodeHydrater', 'check_HTML_tags') === true) {
$tags = TagMatches::checkTags($page_output);
if (!empty($tags['output'])) {
$page_output .= $tags['output'];
$page_output .= '<script type="text/javascript">' . $tags['alert'] . '</script>';
foreach ($this->files as $bad) {
$page_output .= "<script type=\"text/javascript\">console.log'In view file:{$bad['file']}');\r\n</script>";
}
}
}
if ($this->template !== false && Requires::saferFileExists(IO_CORNERSTONE_PROJECT . $this->template . ".php") !== false) {
$this->obStart();
$saved_ob_level = ob_get_level();
$local->page_output = $page_output;
Requires::secureInclude($this->template, UseDir::PROJECT, $local, $this->vars);
$this->clear_ob($saved_ob_level);
$page_output = ob_get_clean();
}
// Reset Files to zero, as they were outputed....
unset($this->files);
$this->files = [];
return $page_output;
}
}
Loading…
Cancel
Save