diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..d1502b0
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+vendor/
+composer.lock
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..d30fd8e
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,27 @@
+sudo: required
+
+language: php
+
+php:
+ - 7.1
+
+before_install:
+ - composer self-update
+
+install:
+ - composer install --no-interaction --prefer-dist --no-scripts --no-progress --no-suggest --optimize-autoloader --classmap-authoritative
+
+script:
+ - ./vendor/bin/phing ci
+
+after_script:
+ - >
+ wget https://github.com/satooshi/php-coveralls/releases/download/v1.0.1/coveralls.phar
+ && php coveralls.phar --verbose --config Tests/.coveralls.yml;
+
+after_failure:
+ - for i in $(find Tests -name \*.actual); do echo "--- $i"; cat $i; echo; echo; done
+
+cache:
+ directories:
+ - $HOME/.composer/cache
diff --git a/Core/CombinedExtension.php b/Core/CombinedExtension.php
new file mode 100644
index 0000000..fefb063
--- /dev/null
+++ b/Core/CombinedExtension.php
@@ -0,0 +1,16 @@
+extensions = $extensions;
+ }
+
+ public function improve(): void {
+ foreach ($this->extensions as $extension)
+ $extension->improve();
+ }
+}
\ No newline at end of file
diff --git a/Core/CspHeader.php b/Core/CspHeader.php
new file mode 100644
index 0000000..868a118
--- /dev/null
+++ b/Core/CspHeader.php
@@ -0,0 +1,43 @@
+directives = $directives;
+ }
+
+ public function nonce(): string {
+ if ($this->nonce === null)
+ $this->nonce = base64_encode(random_bytes(16));
+ return $this->nonce;
+ }
+
+ public function __toString(): string {
+ if (!$this->directives)
+ return '';
+ return 'Content-Security-Policy: ' . $this->withNonce(
+ $this->format($this->directives)
+ );
+ }
+
+ private function format(array $directives): string {
+ return implode(
+ '; ',
+ array_map(
+ function(string $directive, string $constraint): string {
+ return $directive . ' ' . $constraint;
+ },
+ array_keys($directives),
+ $directives
+ )
+ );
+ }
+
+ private function withNonce(string $header): string {
+ return str_replace('nonce', 'nonce-' . $this->nonce(), $header);
+ }
+}
\ No newline at end of file
diff --git a/Core/Extension.php b/Core/Extension.php
new file mode 100644
index 0000000..9baefb7
--- /dev/null
+++ b/Core/Extension.php
@@ -0,0 +1,11 @@
+headers = $headers;
+ }
+
+ public function improve(): void {
+ (new RawHeaderExtension(
+ array_map(
+ function(string $field, string $value): string {
+ return sprintf('%s:%s', $field, $value);
+ },
+ array_keys($this->headers),
+ $this->headers
+ )
+ ))->improve();
+ }
+}
\ No newline at end of file
diff --git a/Core/IniSetExtension.php b/Core/IniSetExtension.php
new file mode 100644
index 0000000..3eddb99
--- /dev/null
+++ b/Core/IniSetExtension.php
@@ -0,0 +1,16 @@
+settings = $settings;
+ }
+
+ public function improve(): void {
+ foreach ($this->settings as $name => $value)
+ ini_set($name, (string) $value);
+ }
+}
\ No newline at end of file
diff --git a/Core/InternationalExtension.php b/Core/InternationalExtension.php
new file mode 100644
index 0000000..4fedd49
--- /dev/null
+++ b/Core/InternationalExtension.php
@@ -0,0 +1,20 @@
+timezone = $timezone;
+ }
+
+ public function improve(): void {
+ mb_internal_encoding('UTF-8');
+ if (@date_default_timezone_set($this->timezone) === false) {
+ throw new \InvalidArgumentException(
+ sprintf('Timezone "%s" is invalid', $this->timezone)
+ );
+ }
+ }
+}
\ No newline at end of file
diff --git a/Core/RawHeaderExtension.php b/Core/RawHeaderExtension.php
new file mode 100644
index 0000000..7f43a99
--- /dev/null
+++ b/Core/RawHeaderExtension.php
@@ -0,0 +1,16 @@
+headers = $headers;
+ }
+
+ public function improve(): void {
+ foreach ($this->headers as $header)
+ header((string) $header);
+ }
+}
\ No newline at end of file
diff --git a/Core/SessionExtension.php b/Core/SessionExtension.php
new file mode 100644
index 0000000..e7a1e54
--- /dev/null
+++ b/Core/SessionExtension.php
@@ -0,0 +1,74 @@
+settings = $settings;
+ $this->break = $break;
+ }
+
+ public function improve(): void {
+ if (session_status() === PHP_SESSION_NONE)
+ session_start($this->native($this->settings));
+ if ($this->elapsed($this->break))
+ session_regenerate_id(true);
+ header(static::raw($this->settings));
+ $_SESSION[self::TIMER] = time();
+ }
+
+ private function elapsed(int $break): bool {
+ return isset($_SESSION[self::TIMER])
+ && (time() - $_SESSION[self::TIMER]) > $break;
+ }
+
+ /**
+ * Just the native and supported php setting
+ * @param array $settings
+ * @return array
+ */
+ private function native(array $settings): array {
+ return array_diff_ukey(
+ $settings,
+ array_flip(self::PROPRIETARIES),
+ 'strcasecmp'
+ );
+ }
+
+ /**
+ * The raw cookie header
+ * @param array $settings
+ * @return string
+ */
+ private static function raw(array $settings): string {
+ $matches = array_intersect_ukey(
+ array_flip(self::PROPRIETARIES),
+ $settings,
+ 'strcasecmp'
+ );
+ $headers = array_combine(
+ array_flip($matches),
+ array_intersect_ukey($settings, $matches, 'strcasecmp')
+ );
+ return sprintf(
+ '%s; %s',
+ current(preg_grep('~^Set-Cookie: ~', headers_list())),
+ implode(
+ ';',
+ array_map(
+ function(string $field, string $value): string {
+ return sprintf('%s=%s', $field, $value);
+ },
+ array_keys($headers),
+ $headers
+ )
+ )
+ );
+ }
+}
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..f81884c
--- /dev/null
+++ b/README.md
@@ -0,0 +1,3 @@
+# Internal
+[![Build Status](https://travis-ci.org/klapuch/Internal.svg?branch=master)](https://travis-ci.org/klapuch/Internal) [![Build status](https://ci.appveyor.com/api/projects/status/80sjyunbbtmdneyp?svg=true)](https://ci.appveyor.com/project/facedown/project) [![Coverage Status](https://coveralls.io/repos/github/klapuch/Internal/badge.svg?branch=master)](https://coveralls.io/github/klapuch/Internal?branch=master)
+
diff --git a/Tests/.coveralls.yml b/Tests/.coveralls.yml
new file mode 100644
index 0000000..c512a3d
--- /dev/null
+++ b/Tests/.coveralls.yml
@@ -0,0 +1,3 @@
+service_name: travis-ci
+coverage_clover: coverage.xml
+json_path: coverage.json
diff --git a/Tests/.gitignore b/Tests/.gitignore
new file mode 100644
index 0000000..45cff74
--- /dev/null
+++ b/Tests/.gitignore
@@ -0,0 +1,2 @@
+*.actual
+*.expected
diff --git a/Tests/Temporary/.gitginore b/Tests/Temporary/.gitginore
new file mode 100644
index 0000000..d6b7ef3
--- /dev/null
+++ b/Tests/Temporary/.gitginore
@@ -0,0 +1,2 @@
+*
+!.gitignore
diff --git a/Tests/Unit/CombinedExtension.phpt b/Tests/Unit/CombinedExtension.phpt
new file mode 100644
index 0000000..4914eef
--- /dev/null
+++ b/Tests/Unit/CombinedExtension.phpt
@@ -0,0 +1,40 @@
+ 7.1
+ */
+namespace Klapuch\Internal\Unit;
+
+use Klapuch\Internal;
+use Tester;
+use Tester\Assert;
+
+require __DIR__ . '/../bootstrap.php';
+
+final class CombinedExtension extends Tester\TestCase {
+ public function testConvertingIntegerToString() {
+ ob_start();
+ (new Internal\CombinedExtension(
+ new class implements Internal\Extension {
+ function improve(): void {
+ echo 'a';
+ }
+ },
+ new class implements Internal\Extension {
+ function improve(): void {
+ echo 'b';
+ }
+ },
+ new class implements Internal\Extension {
+ function improve(): void {
+ echo 'c';
+ }
+ }
+ ))->improve();
+ Assert::same('abc', ob_get_clean());
+ }
+}
+
+
+(new CombinedExtension())->run();
\ No newline at end of file
diff --git a/Tests/Unit/CspHeader.phpt b/Tests/Unit/CspHeader.phpt
new file mode 100644
index 0000000..8d77b47
--- /dev/null
+++ b/Tests/Unit/CspHeader.phpt
@@ -0,0 +1,52 @@
+ 7.1
+ */
+namespace Klapuch\Internal\Unit;
+
+use Klapuch\Internal;
+use Tester;
+use Tester\Assert;
+
+require __DIR__ . '/../bootstrap.php';
+
+final class CspHeader extends Tester\TestCase {
+ public function testNoHeaderOnNoPassedDirectives() {
+ Assert::same('', (string) new Internal\CspHeader([]));
+ }
+
+ /**
+ * @dataProvider directives
+ */
+ public function testFormattedDirectives(string $header, array $directives) {
+ Assert::same(
+ 'Content-Security-Policy: ' . $header,
+ (string) new Internal\CspHeader($directives)
+ );
+ }
+
+ protected function directives(): array {
+ // expected => given
+ return [
+ ["default-src 'self'", ['default-src' => "'self'"]],
+ ["script-src 'self' 'unsafe-inline'", ['script-src' => "'self' 'unsafe-inline'"]],
+ ["default-src 'self'; child-src 'none'", ['default-src' => "'self'", 'child-src' => "'none'"]],
+ ];
+ }
+
+ public function testGeneratingNonceJustOnce() {
+ $csp = new Internal\CspHeader([]);
+ Assert::true(strlen($csp->nonce()) === 24);
+ Assert::same($csp->nonce(), $csp->nonce());
+ }
+
+ public function testPassingNonce() {
+ $csp = new Internal\CspHeader(['sctipt-src' => 'nonce']);
+ Assert::contains('nonce-' . $csp->nonce(), (string) $csp);
+ }
+}
+
+
+(new CspHeader())->run();
\ No newline at end of file
diff --git a/Tests/Unit/HeaderExtension.phpt b/Tests/Unit/HeaderExtension.phpt
new file mode 100644
index 0000000..47f93dd
--- /dev/null
+++ b/Tests/Unit/HeaderExtension.phpt
@@ -0,0 +1,24 @@
+ 7.1
+ */
+namespace Klapuch\Internal\Unit;
+
+use Klapuch\Internal;
+use Tester;
+use Tester\Assert;
+
+require __DIR__ . '/../bootstrap.php';
+
+final class HeaderExtension extends Tester\TestCase {
+ public function testSentHeaders() {
+ (new Internal\HeaderExtension(['Name' => 'Value', 'Foo' => 'bar']))->improve();
+ Assert::contains('Name:Value', headers_list());
+ Assert::contains('Foo:bar', headers_list());
+ }
+}
+
+
+(new HeaderExtension())->run();
\ No newline at end of file
diff --git a/Tests/Unit/IniSetExtension.phpt b/Tests/Unit/IniSetExtension.phpt
new file mode 100644
index 0000000..33704e6
--- /dev/null
+++ b/Tests/Unit/IniSetExtension.phpt
@@ -0,0 +1,24 @@
+ 7.1
+ */
+namespace Klapuch\Internal\Unit;
+
+use Klapuch\Internal;
+use Tester;
+use Tester\Assert;
+
+require __DIR__ . '/../bootstrap.php';
+
+final class IniSetExtension extends Tester\TestCase {
+ public function testConvertingIntegerToString() {
+ Assert::same('1', ini_get('display_errors'));
+ (new Internal\IniSetExtension(['display_errors' => 0]))->improve();
+ Assert::same('0', ini_get('display_errors'));
+ }
+}
+
+
+(new IniSetExtension())->run();
\ No newline at end of file
diff --git a/Tests/Unit/InternationalExtension.phpt b/Tests/Unit/InternationalExtension.phpt
new file mode 100644
index 0000000..f782039
--- /dev/null
+++ b/Tests/Unit/InternationalExtension.phpt
@@ -0,0 +1,31 @@
+ 7.1
+ */
+namespace Klapuch\Internal\Unit;
+
+use Klapuch\Internal;
+use Tester;
+use Tester\Assert;
+
+require __DIR__ . '/../bootstrap.php';
+
+final class InternationalExtension extends Tester\TestCase {
+ public function testSettingAllowedTimezone() {
+ Assert::same('Europe/Prague', date_default_timezone_get());
+ (new Internal\InternationalExtension('Europe/Berlin'))->improve();
+ Assert::same('Europe/Berlin', date_default_timezone_get());
+ }
+
+ /**
+ * @throws \InvalidArgumentException Timezone "Foo" is invalid
+ */
+ public function testThrowingOnUnknownTimezone() {
+ (new Internal\InternationalExtension('Foo'))->improve();
+ }
+}
+
+
+(new InternationalExtension())->run();
\ No newline at end of file
diff --git a/Tests/Unit/RawHeaderExtension.phpt b/Tests/Unit/RawHeaderExtension.phpt
new file mode 100644
index 0000000..5ec3e33
--- /dev/null
+++ b/Tests/Unit/RawHeaderExtension.phpt
@@ -0,0 +1,46 @@
+ 7.1
+ */
+namespace Klapuch\Internal\Unit;
+
+use Klapuch\Internal;
+use Tester;
+use Tester\Assert;
+
+require __DIR__ . '/../bootstrap.php';
+
+final class RawHeaderExtension extends Tester\TestCase {
+ public function testSentHeaders() {
+ (new Internal\RawHeaderExtension(
+ ['Name:Value', 'Foo:bar']
+ ))->improve();
+ Assert::contains('Name:Value', headers_list());
+ Assert::contains('Foo:bar', headers_list());
+ }
+
+ public function testStringConversion() {
+ (new Internal\RawHeaderExtension(
+ [new class {
+ function __toString() {
+ return 'Name:Value';
+ }
+ }]
+ ))->improve();
+ Assert::contains('Name:Value', headers_list());
+ }
+
+ public function testRemovingCaseInsensitiveDuplicates() {
+ (new Internal\RawHeaderExtension(
+ ['Name:Value', 'Foo:bar', 'name:Value']
+ ))->improve();
+ Assert::notContains('Name:Value', headers_list());
+ Assert::contains('name:Value', headers_list());
+ Assert::contains('Foo:bar', headers_list());
+ }
+}
+
+
+(new RawHeaderExtension())->run();
\ No newline at end of file
diff --git a/Tests/Unit/SessionExtension.phpt b/Tests/Unit/SessionExtension.phpt
new file mode 100644
index 0000000..929f6a2
--- /dev/null
+++ b/Tests/Unit/SessionExtension.phpt
@@ -0,0 +1,87 @@
+ 7.1
+ */
+namespace Klapuch\Internal\Unit;
+
+use Klapuch\Internal;
+use Tester;
+use Tester\Assert;
+
+require __DIR__ . '/../bootstrap.php';
+
+final class SessionExtension extends Tester\TestCase {
+ public function testRegeneratingSessionAfterElapse() {
+ $extension = new Internal\SessionExtension([], 1);
+ $extension->improve();
+ $initId = session_id();
+ $_SESSION['_timer'] = time() - 2;
+ $extension->improve();
+ Assert::notSame('', $initId);
+ Assert::notSame($initId, session_id());
+ }
+
+ public function testInstantRegeneration() {
+ $extension = new Internal\SessionExtension([], 0);
+ $extension->improve();
+ $initId = session_id();
+ $_SESSION['_timer'] = time() - 1;
+ $extension->improve();
+ Assert::notSame($initId, session_id());
+ }
+
+ public function testElapseWithOverwhelmingElapse() {
+ $extension = new Internal\SessionExtension([], 1);
+ $extension->improve();
+ $initId = session_id();
+ $_SESSION['_timer'] = time() - 1;
+ $extension->improve();
+ Assert::same($initId, session_id());
+ }
+
+ public function testNoPassedSetting() {
+ $extension = new Internal\SessionExtension([]);
+ $extension->improve();
+ Assert::match('~^Set-Cookie: PHPSESSID=\S+; path=/;$~', headers_list()[4]);
+ }
+
+ public function testPassingSetting() {
+ $extension = new Internal\SessionExtension(['cookie_httponly' => true]);
+ $extension->improve();
+ Assert::contains('HttpOnly', headers_list()[4]);
+ Assert::true(session_get_cookie_params()['httponly']);
+ }
+
+ public function testSettingSameSite() {
+ $extension = new Internal\SessionExtension(['SameSite' => 'strict']);
+ $extension->improve();
+ $cookie = headers_list()[4];
+ Assert::match(
+ '~^Set-Cookie: PHPSESSID=\S+; path=/; SameSite=strict$~',
+ $cookie
+ );
+ }
+
+ public function testSettingSameSiteAsCaseInsensitive() {
+ $extension = new Internal\SessionExtension(['sAmESiTe' => 'strict']);
+ $extension->improve();
+ $cookie = headers_list()[4];
+ Assert::contains('Set-Cookie: ', $cookie);
+ Assert::contains('; SameSite=strict', $cookie);
+ }
+
+ public function testProprietarSettingAfterRegeneration() {
+ $extension = new Internal\SessionExtension(['SameSite' => 'strict'], 1);
+ $extension->improve();
+ $_SESSION['_timer'] = time() - 2;
+ $extension->improve();
+ $cookie = headers_list()[4];
+ Assert::contains('Set-Cookie: ', $cookie);
+ Assert::contains('; SameSite=strict', $cookie);
+ }
+}
+
+
+(new SessionExtension())->run();
\ No newline at end of file
diff --git a/Tests/bootstrap.php b/Tests/bootstrap.php
new file mode 100644
index 0000000..dbdec0a
--- /dev/null
+++ b/Tests/bootstrap.php
@@ -0,0 +1,5 @@
+ appveyor.yml
+ - '%LOCALAPPDATA%\Composer\files -> appveyor.yml'
+
+clone_folder: c:\projects\project
+
+init:
+ - SET PATH=c:\php\71;%PATH%
+ - SET ANSICON=121x90 (121x90)
+
+install:
+ - IF EXIST c:\php\71 (SET PHP=0) ELSE (SET PHP=1)
+ - IF %PHP%==1 mkdir c:\php\71
+ - IF %PHP%==1 cd c:\php\71
+ - IF %PHP%==1 appveyor DownloadFile http://windows.php.net/downloads/releases/archives/php-7.1.0-Win32-VC14-x64.zip
+ - IF %PHP%==1 7z x php-7.1.0-Win32-VC14-x64.zip >nul
+ - IF %PHP%==1 echo extension_dir=ext >> php.ini
+ - IF %PHP%==1 echo extension=php_openssl.dll >> php.ini
+ - IF %PHP%==1 echo extension=php_mbstring.dll >> php.ini
+ - IF %PHP%==1 echo extension=php_xsl.dll >> php.ini
+ - IF %PHP%==1 del /Q *.zip
+
+ - cd c:\projects\project
+ - echo extension_dir=ext >> c:\php\php.ini
+
+ - appveyor DownloadFile https://getcomposer.org/composer.phar
+ - php composer.phar install --no-interaction --prefer-dist --no-scripts --no-progress --no-suggest --optimize-autoloader --classmap-authoritative
+
+test_script:
+ - vendor/bin/tester -o console -s -c c:\php\php.ini -p c:\php\71\php-cgi Tests
diff --git a/build.xml b/build.xml
new file mode 100644
index 0000000..fd6e83b
--- /dev/null
+++ b/build.xml
@@ -0,0 +1,90 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..3896992
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,40 @@
+{
+ "name": "klapuch/internal",
+ "description": "PHP improved internals",
+ "type": "project",
+ "homepage": "https://www.github.com/klapuch/Internal",
+ "authors": [
+ {
+ "name": "Dominik Klapuch",
+ "email": "klapuchdominik@gmail.com",
+ "homepage": "https://www.github.com/klapuch",
+ "role": "Developer"
+ }
+ ],
+ "require": {
+ "php": ">=7.1"
+ },
+ "require-dev": {
+ "nette/tester": "^1.7",
+ "phpstan/phpstan": ">=0.6.0",
+ "jakub-onderka/php-parallel-lint": "^0.9.2",
+ "slevomat/coding-standard": "^2.0.0",
+ "sebastian/phpcpd": "^2.0",
+ "phing/phing": "^2.16",
+ "consistence/coding-standard": "^0.13.0"
+ },
+ "autoload": {
+ "psr-4": {
+ "Klapuch\\Internal\\": [
+ "Core/"
+ ]
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "Klapuch\\Internal\\": [
+ "Tests/"
+ ]
+ }
+ }
+}
diff --git a/phpstan.neon b/phpstan.neon
new file mode 100644
index 0000000..0d1eb8e
--- /dev/null
+++ b/phpstan.neon
@@ -0,0 +1,3 @@
+parameters:
+ ignoreErrors:
+ - '~Function session_start invoked with 1 parameter, 0 required~'
\ No newline at end of file
diff --git a/ruleset.xml b/ruleset.xml
new file mode 100644
index 0000000..c0bf78f
--- /dev/null
+++ b/ruleset.xml
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Tests/Unit
+ Tests/Integration
+ Tests/UI
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Tests/Unit
+ Tests/Integration
+ Tests/UI
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+