includes complete tests for `date.php`pull/1/head
| @ -0,0 +1,2 @@ | |||
| vendor/ | |||
| .DS_Store | |||
| @ -0,0 +1,8 @@ | |||
| language: php | |||
| php: | |||
| - 5.5 | |||
| - 5.6 | |||
| - 7.0 | |||
| - 7.1 | |||
| - nightly | |||
| before_script: composer install | |||
| @ -0,0 +1,30 @@ | |||
| { | |||
| "name": "p3k/utils", | |||
| "type": "library", | |||
| "description": "Some helpful functions used by https://p3k.io projects", | |||
| "license": "MIT", | |||
| "homepage": "https://github.com/aaronpk/p3k-utils", | |||
| "authors": [ | |||
| { | |||
| "name": "Aaron Parecki", | |||
| "homepage": "https://aaronparecki.com" | |||
| } | |||
| ], | |||
| "require": { | |||
| "php": ">=5.5" | |||
| }, | |||
| "require-dev": { | |||
| "phpunit/phpunit": ">=4.8.13" | |||
| }, | |||
| "autoload": { | |||
| "files": [ | |||
| "src/global.php", | |||
| "src/url.php", | |||
| "src/utils.php", | |||
| "src/date.php", | |||
| "src/cache.php" | |||
| ] | |||
| }, | |||
| "autoload-dev": { | |||
| } | |||
| } | |||
| @ -0,0 +1,18 @@ | |||
| <?xml version="1.0"?> | |||
| <phpunit | |||
| bootstrap="tests/bootstrap.php" | |||
| beStrictAboutTestsThatDoNotTestAnything="true"> | |||
| <testsuites> | |||
| <testsuite name="comments"> | |||
| <directory suffix="Test.php">tests</directory> | |||
| </testsuite> | |||
| </testsuites> | |||
| <filter> | |||
| <whitelist processUncoveredFilesFromWhitelist="true"> | |||
| <directory suffix=".php">src</directory> | |||
| </whitelist> | |||
| </filter> | |||
| <logging> | |||
| <log type="coverage-html" target="./coverage"/> | |||
| </logging> | |||
| </phpunit> | |||
| @ -0,0 +1,60 @@ | |||
| <?php | |||
| namespace p3k; | |||
| class Cache { | |||
| private static $redis; | |||
| public static function redis($config=false) { | |||
| if(!isset(self::$redis)) { | |||
| if($config) { | |||
| self::$redis = new \Predis\Client(Config::$redis); | |||
| } else { | |||
| self::$redis = new \Predis\Client('tcp://127.0.0.1:6379'); | |||
| } | |||
| } | |||
| } | |||
| public static function set($key, $value, $exp=600) { | |||
| self::redis(); | |||
| if($exp) { | |||
| self::$redis->setex($key, $exp, json_encode($value)); | |||
| } else { | |||
| self::$redis->set($key, json_encode($value)); | |||
| } | |||
| } | |||
| public static function get($key) { | |||
| self::redis(); | |||
| $data = self::$redis->get($key); | |||
| if($data) { | |||
| return json_decode($data); | |||
| } else { | |||
| return null; | |||
| } | |||
| } | |||
| public static function delete($key) { | |||
| self::redis(); | |||
| return self::$redis->del($key); | |||
| } | |||
| public static function expire($key, $seconds=0) { | |||
| self::redis(); | |||
| if($seconds) | |||
| return self::$redis->expire($key, $seconds); | |||
| else | |||
| return self::$redis->del($key); | |||
| } | |||
| public static function incr($key, $value=1) { | |||
| self::redis(); | |||
| return self::$redis->incrby($key, $value); | |||
| } | |||
| public static function decr($key, $value=1) { | |||
| self::redis(); | |||
| return self::$redis->decrby($key, $value); | |||
| } | |||
| } | |||
| @ -0,0 +1,39 @@ | |||
| <?php | |||
| namespace p3k\date; | |||
| use DateTime, DateTimeZone; | |||
| // $format - one of the php.net/date format strings | |||
| // $date - a string that will be passed to DateTime() | |||
| // $offset - integer timezone offset in seconds | |||
| function format_local($format, $date, $offset) { | |||
| if($offset != 0) | |||
| $tz = new DateTimeZone(($offset < 0 ? '-' : '+') . sprintf('%02d:%02d', abs(floor($offset / 60 / 60)), (($offset / 60) % 60))); | |||
| else | |||
| $tz = new DateTimeZone('UTC'); | |||
| $d = new DateTime($date); | |||
| $d->setTimeZone($tz); | |||
| return $d->format($format); | |||
| } | |||
| function tz_offset_to_seconds($offset) { | |||
| if(preg_match('/([+-])(\d{2}):?(\d{2})/', $offset, $match)) { | |||
| $sign = ($match[1] == '-' ? -1 : 1); | |||
| return (($match[2] * 60 * 60) + ($match[3] * 60)) * $sign; | |||
| } else { | |||
| return 0; | |||
| } | |||
| } | |||
| function tz_seconds_to_offset($seconds) { | |||
| return ($seconds < 0 ? '-' : '+') . sprintf('%02d:%02d', abs($seconds/60/60), ($seconds/60)%60); | |||
| } | |||
| function tz_seconds_to_timezone($seconds) { | |||
| if($seconds != 0) | |||
| $tz = new DateTimeZone(tz_seconds_to_offset($seconds)); | |||
| else | |||
| $tz = new DateTimeZone('UTC'); | |||
| return $tz; | |||
| } | |||
| @ -0,0 +1,2 @@ | |||
| <?php | |||
| date_default_timezone_set('UTC'); | |||
| @ -0,0 +1,136 @@ | |||
| <?php | |||
| namespace p3k\url; | |||
| function display_url($url) { | |||
| # remove scheme | |||
| $url = preg_replace('/^https?:\/\//', '', $url); | |||
| # if the remaining string has no path components but has a trailing slash, remove the trailing slash | |||
| $url = preg_replace('/^([^\/]+)\/$/', '$1', $url); | |||
| return $url; | |||
| } | |||
| function add_query_params_to_url($url, $add_params) { | |||
| $parts = parse_url($url); | |||
| if(array_key_exists('query', $parts) && $parts['query']) { | |||
| parse_str($parts['query'], $params); | |||
| } else { | |||
| $params = []; | |||
| } | |||
| foreach($add_params as $k=>$v) { | |||
| $params[$k] = $v; | |||
| } | |||
| $parts['query'] = http_build_query($params); | |||
| return build_url($parts); | |||
| } | |||
| // Input: Any URL or string like "aaronparecki.com" | |||
| // Output: Normlized URL (default to http if no scheme, force "/" path) | |||
| // or return false if not a valid URL | |||
| function normalize($url) { | |||
| $parts = parse_url($url); | |||
| if(array_key_exists('path', $parts) && $parts['path'] == '') | |||
| return false; | |||
| // parse_url returns just "path" for naked domains | |||
| if(count($parts) == 1 && array_key_exists('path', $parts)) { | |||
| $parts['host'] = $parts['path']; | |||
| unset($parts['path']); | |||
| } | |||
| if(!array_key_exists('scheme', $parts)) | |||
| $parts['scheme'] = 'http'; | |||
| if(!array_key_exists('path', $parts)) | |||
| $parts['path'] = '/'; | |||
| // Invalid scheme | |||
| if(!in_array($parts['scheme'], array('http','https'))) | |||
| return false; | |||
| return build_url($parts); | |||
| } | |||
| // Inverse of parse_url() | |||
| // http://php.net/parse_url | |||
| function build_url($parsed_url) { | |||
| $scheme = isset($parsed_url['scheme']) ? $parsed_url['scheme'] . '://' : ''; | |||
| $host = isset($parsed_url['host']) ? $parsed_url['host'] : ''; | |||
| $port = isset($parsed_url['port']) ? ':' . $parsed_url['port'] : ''; | |||
| $user = isset($parsed_url['user']) ? $parsed_url['user'] : ''; | |||
| $pass = isset($parsed_url['pass']) ? ':' . $parsed_url['pass'] : ''; | |||
| $pass = ($user || $pass) ? "$pass@" : ''; | |||
| $path = isset($parsed_url['path']) ? $parsed_url['path'] : ''; | |||
| $query = isset($parsed_url['query']) ? '?' . $parsed_url['query'] : ''; | |||
| $fragment = isset($parsed_url['fragment']) ? '#' . $parsed_url['fragment'] : ''; | |||
| return "$scheme$user$pass$host$port$path$query$fragment"; | |||
| } | |||
| function parse($url) { | |||
| return parse_url($url); | |||
| } | |||
| function host_matches($a, $b) { | |||
| return parse_url($a, PHP_URL_HOST) == parse_url($b, PHP_URL_HOST); | |||
| } | |||
| function is_url($url) { | |||
| return is_string($url) && preg_match('/^https?:\/\/[a-z0-9\.\-]\/?/', $url); | |||
| } | |||
| function http_header_case($str) { | |||
| $str = str_replace('-', ' ', $str); | |||
| $str = ucwords($str); | |||
| $str = str_replace(' ', '-', $str); | |||
| return $str; | |||
| } | |||
| function is_public_ip($ip) { | |||
| // http://stackoverflow.com/a/30143143 | |||
| //Private ranges... | |||
| //http://www.iana.org/assignments/iana-ipv4-special-registry/ | |||
| $networks = array('10.0.0.0' => '255.0.0.0', //LAN. | |||
| '172.16.0.0' => '255.240.0.0', //LAN. | |||
| '192.168.0.0' => '255.255.0.0', //LAN. | |||
| '127.0.0.0' => '255.0.0.0', //Loopback. | |||
| '169.254.0.0' => '255.255.0.0', //Link-local. | |||
| '100.64.0.0' => '255.192.0.0', //Carrier. | |||
| '192.0.2.0' => '255.255.255.0', //Testing. | |||
| '198.18.0.0' => '255.254.0.0', //Testing. | |||
| '198.51.100.0' => '255.255.255.0', //Testing. | |||
| '203.0.113.0' => '255.255.255.0', //Testing. | |||
| '192.0.0.0' => '255.255.255.0', //Reserved. | |||
| '224.0.0.0' => '224.0.0.0', //Reserved. | |||
| '0.0.0.0' => '255.0.0.0'); //Reserved. | |||
| $ip = @inet_pton($ip); | |||
| if (strlen($ip) !== 4) { return false; } | |||
| //Is the IP in a private range? | |||
| foreach($networks as $network_address => $network_mask) { | |||
| $network_address = inet_pton($network_address); | |||
| $network_mask = inet_pton($network_mask); | |||
| assert(strlen($network_address) === 4); | |||
| assert(strlen($network_mask) === 4); | |||
| if (($ip & $network_mask) === $network_address) | |||
| return false; | |||
| } | |||
| return true; | |||
| } | |||
| function geo_to_latlng($uri) { | |||
| if(preg_match('/geo:([\-\+]?[0-9\.]+),([\-\+]?[0-9\.]+)/', $uri, $match)) { | |||
| return array( | |||
| 'latitude' => (double)$match[1], | |||
| 'longitude' => (double)$match[2], | |||
| ); | |||
| } else { | |||
| return false; | |||
| } | |||
| } | |||
| @ -0,0 +1,179 @@ | |||
| <?php | |||
| namespace p3k; | |||
| function redis() { | |||
| static $client = false; | |||
| if(!$client) | |||
| $client = new Predis\Client(Config::$redis); | |||
| return $client; | |||
| } | |||
| function bs() | |||
| { | |||
| static $pheanstalk; | |||
| if(!isset($pheanstalk)) | |||
| $pheanstalk = new Pheanstalk\Pheanstalk(Config::$beanstalkServer, Config::$beanstalkPort); | |||
| return $pheanstalk; | |||
| } | |||
| function initdb() { | |||
| ORM::configure('mysql:host=' . Config::$db['host'] . ';dbname=' . Config::$db['database']); | |||
| ORM::configure('username', Config::$db['username']); | |||
| ORM::configure('password', Config::$db['password']); | |||
| } | |||
| function e($text) { | |||
| return htmlspecialchars($text); | |||
| } | |||
| function k($a, $k, $default=null) { | |||
| if(is_array($k)) { | |||
| $result = true; | |||
| foreach($k as $key) { | |||
| $result = $result && array_key_exists($key, $a); | |||
| } | |||
| return $result; | |||
| } else { | |||
| if(is_array($a) && array_key_exists($k, $a)) | |||
| return $a[$k]; | |||
| elseif(is_object($a) && property_exists($a, $k)) | |||
| return $a->$k; | |||
| else | |||
| return $default; | |||
| } | |||
| } | |||
| function random_string($len) { | |||
| $charset='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; | |||
| $str = ''; | |||
| $c = strlen($charset)-1; | |||
| for($i=0; $i<$len; $i++) { | |||
| $str .= $charset[mt_rand(0, $c)]; | |||
| } | |||
| return $str; | |||
| } | |||
| // Returns true if $needle is the end of the $haystack | |||
| function str_ends_with($haystack, $needle) { | |||
| if($needle == '' || $haystack == '') return false; | |||
| return strpos(strrev($haystack), strrev($needle)) === 0; | |||
| } | |||
| // Sets up the session. | |||
| // If create is true, the session will be created even if there is no cookie yet. | |||
| // If create is false, the session will only be set up in PHP if they already have a session cookie. | |||
| function session_setup($create=false, $lifetime=2592000) { | |||
| if($create || isset($_COOKIE[session_name()])) { | |||
| session_set_cookie_params($lifetime); | |||
| session_start(); | |||
| } | |||
| } | |||
| function session($key) { | |||
| if(array_key_exists($key, $_SESSION)) | |||
| return $_SESSION[$key]; | |||
| else | |||
| return null; | |||
| } | |||
| function flash($key) { | |||
| if(isset($_SESSION) && isset($_SESSION[$key])) { | |||
| $value = $_SESSION[$key]; | |||
| unset($_SESSION[$key]); | |||
| return $value; | |||
| } | |||
| } | |||
| function html_to_dom_document($html) { | |||
| // Parse the source body as HTML | |||
| $doc = new DOMDocument(); | |||
| libxml_use_internal_errors(true); # suppress parse errors and warnings | |||
| $body = mb_convert_encoding($html, 'HTML-ENTITIES', mb_detect_encoding($html)); | |||
| @$doc->loadHTML($body, LIBXML_NOWARNING|LIBXML_NOERROR); | |||
| libxml_clear_errors(); | |||
| return $doc; | |||
| } | |||
| function xml_to_dom_document($xml) { | |||
| // Parse the source body as XML | |||
| $doc = new DOMDocument(); | |||
| libxml_use_internal_errors(true); # suppress parse errors and warnings | |||
| // $body = mb_convert_encoding($xml, 'HTML-ENTITIES', mb_detect_encoding($xml)); | |||
| $body = $xml; | |||
| $doc->loadXML($body); | |||
| libxml_clear_errors(); | |||
| return $doc; | |||
| } | |||
| // Reads the exif rotation data and actually rotates the photo. | |||
| // Only does anything if the exif library is loaded, otherwise is a noop. | |||
| function correct_photo_rotation($filename) { | |||
| if(class_exists('IMagick')) { | |||
| try { | |||
| $image = new IMagick($filename); | |||
| $orientation = $image->getImageOrientation(); | |||
| switch($orientation) { | |||
| case IMagick::ORIENTATION_BOTTOMRIGHT: | |||
| $image->rotateImage(new ImagickPixel('#00000000'), 180); | |||
| break; | |||
| case IMagick::ORIENTATION_RIGHTTOP: | |||
| $image->rotateImage(new ImagickPixel('#00000000'), 90); | |||
| break; | |||
| case IMagick::ORIENTATION_LEFTBOTTOM: | |||
| $image->rotateImage(new ImagickPixel('#00000000'), -90); | |||
| break; | |||
| } | |||
| $image->setImageOrientation(IMagick::ORIENTATION_TOPLEFT); | |||
| $image->writeImage($filename); | |||
| } catch(Exception $e){} | |||
| } | |||
| } | |||
| /** | |||
| * Converts base 10 to base 60. | |||
| * http://tantek.pbworks.com/NewBase60 | |||
| * @param int $n | |||
| * @return string | |||
| */ | |||
| function b10to60($n) | |||
| { | |||
| $s = ""; | |||
| $m = "0123456789ABCDEFGHJKLMNPQRSTUVWXYZ_abcdefghijkmnopqrstuvwxyz"; | |||
| if ($n==0) | |||
| return 0; | |||
| while ($n>0) | |||
| { | |||
| $d = $n % 60; | |||
| $s = $m[$d] . $s; | |||
| $n = ($n-$d)/60; | |||
| } | |||
| return $s; | |||
| } | |||
| /** | |||
| * Converts base 60 to base 10, with error checking | |||
| * http://tantek.pbworks.com/NewBase60 | |||
| * @param string $s | |||
| * @return int | |||
| */ | |||
| function b60to10($s) | |||
| { | |||
| $n = 0; | |||
| for($i = 0; $i < strlen($s); $i++) // iterate from first to last char of $s | |||
| { | |||
| $c = ord($s[$i]); // put current ASCII of char into $c | |||
| if ($c>=48 && $c<=57) { $c=$c-48; } | |||
| else if ($c>=65 && $c<=72) { $c-=55; } | |||
| else if ($c==73 || $c==108) { $c=1; } // typo capital I, lowercase l to 1 | |||
| else if ($c>=74 && $c<=78) { $c-=56; } | |||
| else if ($c==79) { $c=0; } // error correct typo capital O to 0 | |||
| else if ($c>=80 && $c<=90) { $c-=57; } | |||
| else if ($c==95) { $c=34; } // underscore | |||
| else if ($c>=97 && $c<=107) { $c-=62; } | |||
| else if ($c>=109 && $c<=122) { $c-=63; } | |||
| else { $c = 0; } // treat all other noise as 0 | |||
| $n = (60 * $n) + $c; | |||
| } | |||
| return $n; | |||
| } | |||
| @ -0,0 +1,78 @@ | |||
| <?php | |||
| class DateTest extends PHPUnit_Framework_TestCase { | |||
| public function testFormatLocalPositiveOffset() { | |||
| $local = p3k\date\format_local('c', '2017-05-01T13:30:00+0000', 7200); | |||
| $this->assertEquals('2017-05-01T15:30:00+02:00', $local); | |||
| } | |||
| public function testFormatLocalNegativeOffset() { | |||
| $local = p3k\date\format_local('c', '2017-05-01T13:30:00+0000', -25200); | |||
| $this->assertEquals('2017-05-01T06:30:00-07:00', $local); | |||
| } | |||
| public function testFormatLocalZeroOffset() { | |||
| $local = p3k\date\format_local('c', '2017-05-01T13:30:00+0200', 0); | |||
| $this->assertEquals('2017-05-01T11:30:00+00:00', $local); | |||
| } | |||
| public function testTZSecondsToTimezonePositive() { | |||
| $tz = p3k\date\tz_seconds_to_timezone(7200); | |||
| $this->assertInstanceOf(DateTimeZone::class, $tz); | |||
| $this->assertEquals('+02:00', $tz->getName()); | |||
| } | |||
| public function testTZSecondsToTimezoneNegative() { | |||
| $tz = p3k\date\tz_seconds_to_timezone(-25200); | |||
| $this->assertInstanceOf(DateTimeZone::class, $tz); | |||
| $this->assertEquals('-07:00', $tz->getName()); | |||
| } | |||
| public function testTZSecondsToTimezoneZero() { | |||
| $tz = p3k\date\tz_seconds_to_timezone(0); | |||
| $this->assertInstanceOf(DateTimeZone::class, $tz); | |||
| $this->assertEquals('UTC', $tz->getName()); | |||
| } | |||
| public function testTZOffsetToSecondsPositive() { | |||
| $seconds = p3k\date\tz_offset_to_seconds('+02:00'); | |||
| $this->assertEquals(7200, $seconds); | |||
| $seconds = p3k\date\tz_offset_to_seconds('+0200'); | |||
| $this->assertEquals(7200, $seconds); | |||
| } | |||
| public function testTZOffsetToSecondsNegative() { | |||
| $seconds = p3k\date\tz_offset_to_seconds('-07:00'); | |||
| $this->assertEquals(-25200, $seconds); | |||
| $seconds = p3k\date\tz_offset_to_seconds('-0700'); | |||
| $this->assertEquals(-25200, $seconds); | |||
| } | |||
| public function testTZOffsetToSecondsZero() { | |||
| $seconds = p3k\date\tz_offset_to_seconds('+00:00'); | |||
| $this->assertEquals(0, $seconds); | |||
| $seconds = p3k\date\tz_offset_to_seconds('+0000'); | |||
| $this->assertEquals(0, $seconds); | |||
| } | |||
| public function testTZOffsetToSecondsInvalid() { | |||
| $seconds = p3k\date\tz_offset_to_seconds('foo'); | |||
| $this->assertEquals(0, $seconds); | |||
| } | |||
| public function testTZSecondsToOffsetPositive() { | |||
| $offset = p3k\date\tz_seconds_to_offset(7200); | |||
| $this->assertEquals('+02:00', $offset); | |||
| } | |||
| public function testTZSecondsToOffsetNegative() { | |||
| $offset = p3k\date\tz_seconds_to_offset(-25200); | |||
| $this->assertEquals('-07:00', $offset); | |||
| } | |||
| public function testTZSecondsToOffsetZero() { | |||
| $offset = p3k\date\tz_seconds_to_offset(0); | |||
| $this->assertEquals('+00:00', $offset); | |||
| } | |||
| } | |||
| @ -0,0 +1,2 @@ | |||
| <?php | |||
| require_once __DIR__ . '/../vendor/autoload.php'; | |||