First Commit

This commit is contained in:
marco
2026-09-04 14:54:13 -03:00
commit 9c54bd3ac7
4914 changed files with 789004 additions and 0 deletions
+13
View File
@@ -0,0 +1,13 @@
# editorconfig.org
root = true
[*]
charset = utf-8
indent_style = tab
trim_trailing_whitespace = true
end_of_line = lf
insert_final_newline = true
[*.md]
indent_style = space
indent_size = 4
+6
View File
@@ -0,0 +1,6 @@
# IntelliJ
.idea/
# Composer
vendor/
composer.phar
+72
View File
@@ -0,0 +1,72 @@
-- PHP-Auth (https://github.com/delight-im/PHP-Auth)
-- Copyright (c) delight.im (https://www.delight.im/)
-- Licensed under the MIT License (https://opensource.org/licenses/MIT)
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8mb4 */;
CREATE TABLE IF NOT EXISTS `users` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`email` varchar(249) COLLATE utf8mb4_unicode_ci NOT NULL,
`password` varchar(255) CHARACTER SET latin1 COLLATE latin1_general_cs NOT NULL,
`username` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`status` tinyint(2) unsigned NOT NULL DEFAULT '0',
`verified` tinyint(1) unsigned NOT NULL DEFAULT '0',
`resettable` tinyint(1) unsigned NOT NULL DEFAULT '1',
`roles_mask` int(10) unsigned NOT NULL DEFAULT '0',
`registered` int(10) unsigned NOT NULL,
`last_login` int(10) unsigned DEFAULT NULL,
`force_logout` mediumint(7) unsigned NOT NULL DEFAULT '0',
PRIMARY KEY (`id`),
UNIQUE KEY `email` (`email`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `users_confirmations` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`user_id` int(10) unsigned NOT NULL,
`email` varchar(249) COLLATE utf8mb4_unicode_ci NOT NULL,
`selector` varchar(16) CHARACTER SET latin1 COLLATE latin1_general_cs NOT NULL,
`token` varchar(255) CHARACTER SET latin1 COLLATE latin1_general_cs NOT NULL,
`expires` int(10) unsigned NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `selector` (`selector`),
KEY `email_expires` (`email`,`expires`),
KEY `user_id` (`user_id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `users_remembered` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`user` int(10) unsigned NOT NULL,
`selector` varchar(24) CHARACTER SET latin1 COLLATE latin1_general_cs NOT NULL,
`token` varchar(255) CHARACTER SET latin1 COLLATE latin1_general_cs NOT NULL,
`expires` int(10) unsigned NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `selector` (`selector`),
KEY `user` (`user`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `users_resets` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`user` int(10) unsigned NOT NULL,
`selector` varchar(20) CHARACTER SET latin1 COLLATE latin1_general_cs NOT NULL,
`token` varchar(255) CHARACTER SET latin1 COLLATE latin1_general_cs NOT NULL,
`expires` int(10) unsigned NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `selector` (`selector`),
KEY `user_expires` (`user`,`expires`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `users_throttling` (
`bucket` varchar(44) CHARACTER SET latin1 COLLATE latin1_general_cs NOT NULL,
`tokens` float unsigned NOT NULL,
`replenished_at` int(10) unsigned NOT NULL,
`expires_at` int(10) unsigned NOT NULL,
PRIMARY KEY (`bucket`),
KEY `expires_at` (`expires_at`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
+58
View File
@@ -0,0 +1,58 @@
-- PHP-Auth (https://github.com/delight-im/PHP-Auth)
-- Copyright (c) delight.im (https://www.delight.im/)
-- Licensed under the MIT License (https://opensource.org/licenses/MIT)
BEGIN;
CREATE TABLE IF NOT EXISTS "users" (
"id" SERIAL PRIMARY KEY CHECK ("id" >= 0),
"email" VARCHAR(249) UNIQUE NOT NULL,
"password" VARCHAR(255) NOT NULL,
"username" VARCHAR(100) DEFAULT NULL,
"status" SMALLINT NOT NULL DEFAULT '0' CHECK ("status" >= 0),
"verified" SMALLINT NOT NULL DEFAULT '0' CHECK ("verified" >= 0),
"resettable" SMALLINT NOT NULL DEFAULT '1' CHECK ("resettable" >= 0),
"roles_mask" INTEGER NOT NULL DEFAULT '0' CHECK ("roles_mask" >= 0),
"registered" INTEGER NOT NULL CHECK ("registered" >= 0),
"last_login" INTEGER DEFAULT NULL CHECK ("last_login" >= 0),
"force_logout" INTEGER NOT NULL DEFAULT '0' CHECK ("force_logout" >= 0)
);
CREATE TABLE IF NOT EXISTS "users_confirmations" (
"id" SERIAL PRIMARY KEY CHECK ("id" >= 0),
"user_id" INTEGER NOT NULL CHECK ("user_id" >= 0),
"email" VARCHAR(249) NOT NULL,
"selector" VARCHAR(16) UNIQUE NOT NULL,
"token" VARCHAR(255) NOT NULL,
"expires" INTEGER NOT NULL CHECK ("expires" >= 0)
);
CREATE INDEX IF NOT EXISTS "email_expires" ON "users_confirmations" ("email", "expires");
CREATE INDEX IF NOT EXISTS "user_id" ON "users_confirmations" ("user_id");
CREATE TABLE IF NOT EXISTS "users_remembered" (
"id" BIGSERIAL PRIMARY KEY CHECK ("id" >= 0),
"user" INTEGER NOT NULL CHECK ("user" >= 0),
"selector" VARCHAR(24) UNIQUE NOT NULL,
"token" VARCHAR(255) NOT NULL,
"expires" INTEGER NOT NULL CHECK ("expires" >= 0)
);
CREATE INDEX IF NOT EXISTS "user" ON "users_remembered" ("user");
CREATE TABLE IF NOT EXISTS "users_resets" (
"id" BIGSERIAL PRIMARY KEY CHECK ("id" >= 0),
"user" INTEGER NOT NULL CHECK ("user" >= 0),
"selector" VARCHAR(20) UNIQUE NOT NULL,
"token" VARCHAR(255) NOT NULL,
"expires" INTEGER NOT NULL CHECK ("expires" >= 0)
);
CREATE INDEX IF NOT EXISTS "user_expires" ON "users_resets" ("user", "expires");
CREATE TABLE IF NOT EXISTS "users_throttling" (
"bucket" VARCHAR(44) PRIMARY KEY,
"tokens" REAL NOT NULL CHECK ("tokens" >= 0),
"replenished_at" INTEGER NOT NULL CHECK ("replenished_at" >= 0),
"expires_at" INTEGER NOT NULL CHECK ("expires_at" >= 0)
);
CREATE INDEX IF NOT EXISTS "expires_at" ON "users_throttling" ("expires_at");
COMMIT;
+60
View File
@@ -0,0 +1,60 @@
-- PHP-Auth (https://github.com/delight-im/PHP-Auth)
-- Copyright (c) delight.im (https://www.delight.im/)
-- Licensed under the MIT License (https://opensource.org/licenses/MIT)
PRAGMA foreign_keys = OFF;
CREATE TABLE "users" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL CHECK ("id" >= 0),
"email" VARCHAR(249) NOT NULL,
"password" VARCHAR(255) NOT NULL,
"username" VARCHAR(100) DEFAULT NULL,
"status" INTEGER NOT NULL CHECK ("status" >= 0) DEFAULT "0",
"verified" INTEGER NOT NULL CHECK ("verified" >= 0) DEFAULT "0",
"resettable" INTEGER NOT NULL CHECK ("resettable" >= 0) DEFAULT "1",
"roles_mask" INTEGER NOT NULL CHECK ("roles_mask" >= 0) DEFAULT "0",
"registered" INTEGER NOT NULL CHECK ("registered" >= 0),
"last_login" INTEGER CHECK ("last_login" >= 0) DEFAULT NULL,
"force_logout" INTEGER NOT NULL CHECK ("force_logout" >= 0) DEFAULT "0",
CONSTRAINT "email" UNIQUE ("email")
);
CREATE TABLE "users_confirmations" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL CHECK ("id" >= 0),
"user_id" INTEGER NOT NULL CHECK ("user_id" >= 0),
"email" VARCHAR(249) NOT NULL,
"selector" VARCHAR(16) NOT NULL,
"token" VARCHAR(255) NOT NULL,
"expires" INTEGER NOT NULL CHECK ("expires" >= 0),
CONSTRAINT "selector" UNIQUE ("selector")
);
CREATE INDEX "users_confirmations.email_expires" ON "users_confirmations" ("email", "expires");
CREATE INDEX "users_confirmations.user_id" ON "users_confirmations" ("user_id");
CREATE TABLE "users_remembered" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL CHECK ("id" >= 0),
"user" INTEGER NOT NULL CHECK ("user" >= 0),
"selector" VARCHAR(24) NOT NULL,
"token" VARCHAR(255) NOT NULL,
"expires" INTEGER NOT NULL CHECK ("expires" >= 0),
CONSTRAINT "selector" UNIQUE ("selector")
);
CREATE INDEX "users_remembered.user" ON "users_remembered" ("user");
CREATE TABLE "users_resets" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL CHECK ("id" >= 0),
"user" INTEGER NOT NULL CHECK ("user" >= 0),
"selector" VARCHAR(20) NOT NULL,
"token" VARCHAR(255) NOT NULL,
"expires" INTEGER NOT NULL CHECK ("expires" >= 0),
CONSTRAINT "selector" UNIQUE ("selector")
);
CREATE INDEX "users_resets.user_expires" ON "users_resets" ("user", "expires");
CREATE TABLE "users_throttling" (
"bucket" VARCHAR(44) PRIMARY KEY NOT NULL,
"tokens" REAL NOT NULL CHECK ("tokens" >= 0),
"replenished_at" INTEGER NOT NULL CHECK ("replenished_at" >= 0),
"expires_at" INTEGER NOT NULL CHECK ("expires_at" >= 0)
);
CREATE INDEX "users_throttling.expires_at" ON "users_throttling" ("expires_at");
+21
View File
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) delight.im (https://www.delight.im/)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+220
View File
@@ -0,0 +1,220 @@
# Migration
* [General](#general)
* [From `v7.x.x` to `v8.x.x`](#from-v7xx-to-v8xx)
* [From `v6.x.x` to `v7.x.x`](#from-v6xx-to-v7xx)
* [From `v5.x.x` to `v6.x.x`](#from-v5xx-to-v6xx)
* [From `v4.x.x` to `v5.x.x`](#from-v4xx-to-v5xx)
* [From `v3.x.x` to `v4.x.x`](#from-v3xx-to-v4xx)
* [From `v2.x.x` to `v3.x.x`](#from-v2xx-to-v3xx)
* [From `v1.x.x` to `v2.x.x`](#from-v1xx-to-v2xx)
## General
Update your version of this library using Composer and its `composer update` or `composer require` commands [[?]](https://github.com/delight-im/Knowledge/blob/master/Composer%20(PHP).md#how-do-i-update-libraries-or-modules-within-my-application).
## From `v7.x.x` to `v8.x.x`
* The database schema has changed.
* The MySQL database schema has changed. Use the statement below to update your database:
```sql
ALTER TABLE users
ADD COLUMN `force_logout` mediumint(7) unsigned NOT NULL DEFAULT '0' AFTER `last_login`;
```
* The PostgreSQL database schema has changed. Use the statement below to update your database:
```sql
ALTER TABLE users
ADD COLUMN "force_logout" INTEGER NOT NULL DEFAULT '0' CHECK ("force_logout" >= 0);
```
* The SQLite database schema has changed. Use the statement below to update your database:
```sql
ALTER TABLE users
ADD COLUMN "force_logout" INTEGER NOT NULL CHECK ("force_logout" >= 0) DEFAULT "0";
```
* The method `logOutAndDestroySession` has been removed from class `Auth`. Instead, call the two separate methods `logOut` and `destroySession` from class `Auth` one after another for the same effect.
* If you have been using the return values of the methods `confirmEmail` or `confirmEmailAndSignIn` from class `Auth`, these return values have changed. Instead of only returning the new email address (which has just been verified), both methods now return an array with the old email address (if any) at index zero and the new email address (which has just been verified) at index one.
## From `v6.x.x` to `v7.x.x`
* The method `logOutButKeepSession` from class `Auth` is now simply called `logOut`. Therefore, the former method `logout` is now called `logOutAndDestroySession`.
* The second argument of the `Auth` constructor, which was named `$useHttps`, has been removed. If you previously had it set to `true`, make sure to set the value of the `session.cookie_secure` directive to `1` now. You may do so either directly in your [PHP configuration](http://php.net/manual/en/configuration.file.php) (`php.ini`), via the `\ini_set` method or via the `\session_set_cookie_params` method. Otherwise, make sure that directive is set to `0`.
* The third argument of the `Auth` constructor, which was named `$allowCookiesScriptAccess`, has been removed. If you previously had it set to `true`, make sure to set the value of the `session.cookie_httponly` directive to `0` now. You may do so either directly in your [PHP configuration](http://php.net/manual/en/configuration.file.php) (`php.ini`), via the `\ini_set` method or via the `\session_set_cookie_params` method. Otherwise, make sure that directive is set to `1`.
* Only if *both* of the following two conditions are met:
* The directive `session.cookie_domain` is set to an empty value. It may have been set directly in your [PHP configuration](http://php.net/manual/en/configuration.file.php) (`php.ini`), via the `\ini_set` method or via the `\session_set_cookie_params` method. You can check the value of that directive by executing the following statement somewhere in your application:
```php
\var_dump(\ini_get('session.cookie_domain'));
```
* Your application is accessed via a registered or registrable *domain name*, either by yourself during development and testing or by your visitors and users in production. That means your application is *not*, or *not only*, accessed via `localhost` or via an IP address.
Then the domain scope for the [two cookies](README.md#cookies) used by this library has changed. You can handle this change in one of two different ways:
* Restore the old behavior by placing the following statement as early as possible in your application, and before you create the `Auth` instance:
```php
\ini_set('session.cookie_domain', \preg_replace('/^www\./', '', $_SERVER['HTTP_HOST']));
```
You may also evaluate the complete second parameter and put its value directly into your [PHP configuration](http://php.net/manual/en/configuration.file.php) (`php.ini`).
* Use the new domain scope for your application. To do so, you only need to [rename the cookies](README.md#renaming-the-librarys-cookies) used by this library in order to prevent conflicts with old cookies that have been created previously. Renaming the cookies is critically important here. We recommend a versioned name such as `session_v1` for the session cookie.
* Only if *both* of the following two conditions are met:
* The directive `session.cookie_domain` is set to a value that starts with the `www` subdomain. It may have been set directly in your [PHP configuration](http://php.net/manual/en/configuration.file.php) (`php.ini`), via the `\ini_set` method or via the `\session_set_cookie_params` method. You can check the value of that directive by executing the following statement somewhere in your application:
```php
\var_dump(\ini_get('session.cookie_domain'));
```
* Your application is accessed via a registered or registrable *domain name*, either by yourself during development and testing or by your visitors and users in production. That means your application is *not*, or *not only*, accessed via `localhost` or via an IP address.
Then the domain scope for [one of the cookies](README.md#cookies) used by this library has changed. To make your application work correctly with the new scope, [rename the cookies](README.md#renaming-the-librarys-cookies) used by this library in order to prevent conflicts with old cookies that have been created previously. Renaming the cookies is critically important here. We recommend a versioned name such as `session_v1` for the session cookie.
* If the directive `session.cookie_path` is set to an empty value, then the path scope for [one of the cookies](README.md#cookies) used by this library has changed. To make your application work correctly with the new scope, [rename the cookies](README.md#renaming-the-librarys-cookies) used by this library in order to prevent conflicts with old cookies that have been created previously. Renaming the cookies is critically important here. We recommend a versioned name such as `session_v1` for the session cookie.
The directive may have been set directly in your [PHP configuration](http://php.net/manual/en/configuration.file.php) (`php.ini`), via the `\ini_set` method or via the `\session_set_cookie_params` method. You can check the value of that directive by executing the following statement somewhere in your application:
```php
\var_dump(\ini_get('session.cookie_path'));
```
## From `v5.x.x` to `v6.x.x`
* The database schema has changed.
* The MySQL database schema has changed. Use the statements below to update your database:
```sql
ALTER TABLE users
ADD COLUMN roles_mask INT(10) UNSIGNED NOT NULL DEFAULT 0 AFTER verified,
ADD COLUMN resettable TINYINT(1) UNSIGNED NOT NULL DEFAULT 1 AFTER verified;
ALTER TABLE users_confirmations
ADD COLUMN user_id INT(10) UNSIGNED NULL DEFAULT NULL AFTER id;
UPDATE users_confirmations SET user_id = (
SELECT id FROM users WHERE email = users_confirmations.email
) WHERE user_id IS NULL;
ALTER TABLE users_confirmations
CHANGE COLUMN user_id user_id INT(10) UNSIGNED NOT NULL;
ALTER TABLE users_confirmations
ADD INDEX user_id (user_id ASC);
DROP TABLE users_throttling;
CREATE TABLE users_throttling (
bucket varchar(44) CHARACTER SET latin1 COLLATE latin1_general_cs NOT NULL,
tokens float unsigned NOT NULL,
replenished_at int(10) unsigned NOT NULL,
expires_at int(10) unsigned NOT NULL,
PRIMARY KEY (bucket),
KEY expires_at (expires_at)
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
```
* The SQLite database schema has changed. Use the statements below to update your database:
```sql
ALTER TABLE users
ADD COLUMN "roles_mask" INTEGER NOT NULL CHECK ("roles_mask" >= 0) DEFAULT "0",
ADD COLUMN "resettable" INTEGER NOT NULL CHECK ("resettable" >= 0) DEFAULT "1";
ALTER TABLE users_confirmations
ADD COLUMN "user_id" INTEGER CHECK ("user_id" >= 0);
UPDATE users_confirmations SET user_id = (
SELECT id FROM users WHERE email = users_confirmations.email
) WHERE user_id IS NULL;
CREATE INDEX "users_confirmations.user_id" ON "users_confirmations" ("user_id");
DROP TABLE users_throttling;
CREATE TABLE "users_throttling" (
"bucket" VARCHAR(44) PRIMARY KEY NOT NULL,
"tokens" REAL NOT NULL CHECK ("tokens" >= 0),
"replenished_at" INTEGER NOT NULL CHECK ("replenished_at" >= 0),
"expires_at" INTEGER NOT NULL CHECK ("expires_at" >= 0)
);
CREATE INDEX "users_throttling.expires_at" ON "users_throttling" ("expires_at");
```
* The method `setThrottlingOptions` has been removed.
* The method `changePassword` may now throw an additional `\Delight\Auth\TooManyRequestsException` if too many attempts have been made without the correct old password.
* The two methods `confirmEmail` and `confirmEmailAndSignIn` may now throw an additional `\Delight\Auth\UserAlreadyExistsException` if an attempt has been made to change the email address to an address that has become occupied in the meantime.
* The two methods `forgotPassword` and `resetPassword` may now throw an additional `\Delight\Auth\ResetDisabledException` if the user has disabled password resets for their account.
* The `Base64` class is now an external module and has been moved from the namespace `Delight\Auth` to the namespace `Delight\Base64`. The interface and the return values are not compatible with those from previous versions anymore.
## From `v4.x.x` to `v5.x.x`
* The MySQL database schema has changed. Use the statement below to update your database:
```sql
ALTER TABLE `users` ADD COLUMN `status` TINYINT(2) UNSIGNED NOT NULL DEFAULT 0 AFTER `username`;
```
* The two classes `Auth` and `Base64` are now `final`, i.e. they can't be extended anymore, which has never been a good idea, anyway. If you still need to wrap your own methods around these classes, consider [object composition instead of class inheritance](https://en.wikipedia.org/wiki/Composition_over_inheritance).
## From `v3.x.x` to `v4.x.x`
* PHP 5.6.0 or higher is now required.
## From `v2.x.x` to `v3.x.x`
* The license has been changed from the [Apache License 2.0](http://www.apache.org/licenses/LICENSE-2.0) to the [MIT License](https://opensource.org/licenses/MIT).
## From `v1.x.x` to `v2.x.x`
* The MySQL schema has been changed from charset `utf8` to charset `utf8mb4` and from collation `utf8_general_ci` to collation `utf8mb4_unicode_ci`. Use the statements below to update the database schema:
```sql
ALTER TABLE `users` CHANGE `email` `email` VARCHAR(249) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL;
ALTER TABLE `users_confirmations` CHANGE `email` `email` VARCHAR(249) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL;
-- ALTER DATABASE `<DATABASE_NAME>` CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci;
ALTER TABLE `users` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
ALTER TABLE `users_confirmations` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
ALTER TABLE `users_remembered` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
ALTER TABLE `users_resets` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
ALTER TABLE `users_throttling` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
ALTER TABLE `users` CHANGE `email` `email` VARCHAR(249) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL;
ALTER TABLE `users` CHANGE `username` `username` VARCHAR(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL;
ALTER TABLE `users_confirmations` CHANGE `email` `email` VARCHAR(249) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL;
ALTER TABLE `users_throttling` CHANGE `action_type` `action_type` ENUM('login','register','confirm_email') CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL;
REPAIR TABLE users;
OPTIMIZE TABLE users;
REPAIR TABLE users_confirmations;
OPTIMIZE TABLE users_confirmations;
REPAIR TABLE users_remembered;
OPTIMIZE TABLE users_remembered;
REPAIR TABLE users_resets;
OPTIMIZE TABLE users_resets;
REPAIR TABLE users_throttling;
OPTIMIZE TABLE users_throttling;
```
+1337
View File
File diff suppressed because it is too large Load Diff
+20
View File
@@ -0,0 +1,20 @@
{
"name": "delight-im/auth",
"description": "Authentication for PHP. Simple, lightweight and secure.",
"require": {
"php": ">=5.6.0",
"ext-openssl": "*",
"delight-im/base64": "^1.0",
"delight-im/cookie": "^3.1",
"delight-im/db": "^1.3"
},
"type": "library",
"keywords": [ "auth", "authentication", "login", "security" ],
"homepage": "https://github.com/delight-im/PHP-Auth",
"license": "MIT",
"autoload": {
"psr-4": {
"Delight\\Auth\\": "src/"
}
}
}
Generated Vendored Executable
+180
View File
@@ -0,0 +1,180 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically"
],
"content-hash": "e4acd9e4ba13c4d0692f07a03a454859",
"packages": [
{
"name": "delight-im/base64",
"version": "v1.0.0",
"source": {
"type": "git",
"url": "https://github.com/delight-im/PHP-Base64.git",
"reference": "687b2a49f663e162030a8d27b32838bbe7f91c78"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/delight-im/PHP-Base64/zipball/687b2a49f663e162030a8d27b32838bbe7f91c78",
"reference": "687b2a49f663e162030a8d27b32838bbe7f91c78",
"shasum": ""
},
"require": {
"php": ">=5.3.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Delight\\Base64\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "Simple and convenient Base64 encoding and decoding for PHP",
"homepage": "https://github.com/delight-im/PHP-Base64",
"keywords": [
"URL-safe",
"base-64",
"base64",
"decode",
"decoding",
"encode",
"encoding",
"url"
],
"time": "2017-07-24T18:59:51+00:00"
},
{
"name": "delight-im/cookie",
"version": "v3.1.0",
"source": {
"type": "git",
"url": "https://github.com/delight-im/PHP-Cookie.git",
"reference": "76ef2a21817cf7a034f85fc3f4d4bfc60f873947"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/delight-im/PHP-Cookie/zipball/76ef2a21817cf7a034f85fc3f4d4bfc60f873947",
"reference": "76ef2a21817cf7a034f85fc3f4d4bfc60f873947",
"shasum": ""
},
"require": {
"delight-im/http": "^2.0",
"php": ">=5.4.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Delight\\Cookie\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "Modern cookie management for PHP",
"homepage": "https://github.com/delight-im/PHP-Cookie",
"keywords": [
"cookie",
"cookies",
"csrf",
"http",
"same-site",
"samesite",
"xss"
],
"time": "2017-10-18T19:48:59+00:00"
},
{
"name": "delight-im/db",
"version": "v1.3.0",
"source": {
"type": "git",
"url": "https://github.com/delight-im/PHP-DB.git",
"reference": "7a03da20b5592fa445c10cd6c7245d51037292c4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/delight-im/PHP-DB/zipball/7a03da20b5592fa445c10cd6c7245d51037292c4",
"reference": "7a03da20b5592fa445c10cd6c7245d51037292c4",
"shasum": ""
},
"require": {
"ext-pdo": "*",
"php": ">=5.6.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Delight\\Db\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "Safe and convenient SQL database access in a driver-agnostic way",
"homepage": "https://github.com/delight-im/PHP-DB",
"keywords": [
"database",
"mysql",
"pdo",
"pgsql",
"postgresql",
"sql",
"sqlite"
],
"time": "2018-08-28T18:23:01+00:00"
},
{
"name": "delight-im/http",
"version": "v2.0.0",
"source": {
"type": "git",
"url": "https://github.com/delight-im/PHP-HTTP.git",
"reference": "0a19a72a7eac8b1301aa972fb20cff494ac43e09"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/delight-im/PHP-HTTP/zipball/0a19a72a7eac8b1301aa972fb20cff494ac43e09",
"reference": "0a19a72a7eac8b1301aa972fb20cff494ac43e09",
"shasum": ""
},
"require": {
"php": ">=5.3.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Delight\\Http\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "Hypertext Transfer Protocol (HTTP) utilities for PHP",
"homepage": "https://github.com/delight-im/PHP-HTTP",
"keywords": [
"headers",
"http",
"https"
],
"time": "2016-07-21T15:05:01+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
"php": ">=5.6.0",
"ext-openssl": "*"
},
"platform-dev": []
}
+575
View File
@@ -0,0 +1,575 @@
<?php
/*
* PHP-Auth (https://github.com/delight-im/PHP-Auth)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Auth;
use Delight\Db\PdoDatabase;
use Delight\Db\PdoDsn;
use Delight\Db\Throwable\Error;
/** Component that can be used for administrative tasks by privileged and authorized users */
final class Administration extends UserManager {
/**
* @param PdoDatabase|PdoDsn|\PDO $databaseConnection the database connection to operate on
* @param string|null $dbTablePrefix (optional) the prefix for the names of all database tables used by this component
* @param string|null $dbSchema (optional) the schema name for all database tables used by this component
*/
public function __construct($databaseConnection, $dbTablePrefix = null, $dbSchema = null) {
parent::__construct($databaseConnection, $dbTablePrefix, $dbSchema);
}
/**
* Creates a new user
*
* @param string $email the email address to register
* @param string $password the password for the new account
* @param string|null $username (optional) the username that will be displayed
* @return int the ID of the user that has been created (if any)
* @throws InvalidEmailException if the email address was invalid
* @throws InvalidPasswordException if the password was invalid
* @throws UserAlreadyExistsException if a user with the specified email address already exists
* @throws AuthError if an internal problem occurred (do *not* catch)
*/
public function createUser($email, $password, $username = null) {
return $this->createUserInternal(false, $email, $password, $username, null);
}
/**
* Creates a new user while ensuring that the username is unique
*
* @param string $email the email address to register
* @param string $password the password for the new account
* @param string|null $username (optional) the username that will be displayed
* @return int the ID of the user that has been created (if any)
* @throws InvalidEmailException if the email address was invalid
* @throws InvalidPasswordException if the password was invalid
* @throws UserAlreadyExistsException if a user with the specified email address already exists
* @throws DuplicateUsernameException if the specified username wasn't unique
* @throws AuthError if an internal problem occurred (do *not* catch)
*/
public function createUserWithUniqueUsername($email, $password, $username = null) {
return $this->createUserInternal(true, $email, $password, $username, null);
}
/**
* Deletes the user with the specified ID
*
* This action cannot be undone
*
* @param int $id the ID of the user to delete
* @throws UnknownIdException if no user with the specified ID has been found
* @throws AuthError if an internal problem occurred (do *not* catch)
*/
public function deleteUserById($id) {
$numberOfDeletedUsers = $this->deleteUsersByColumnValue('id', (int) $id);
if ($numberOfDeletedUsers === 0) {
throw new UnknownIdException();
}
}
/**
* Deletes the user with the specified email address
*
* This action cannot be undone
*
* @param string $email the email address of the user to delete
* @throws InvalidEmailException if no user with the specified email address has been found
* @throws AuthError if an internal problem occurred (do *not* catch)
*/
public function deleteUserByEmail($email) {
$email = self::validateEmailAddress($email);
$numberOfDeletedUsers = $this->deleteUsersByColumnValue('email', $email);
if ($numberOfDeletedUsers === 0) {
throw new InvalidEmailException();
}
}
/**
* Deletes the user with the specified username
*
* This action cannot be undone
*
* @param string $username the username of the user to delete
* @throws UnknownUsernameException if no user with the specified username has been found
* @throws AmbiguousUsernameException if multiple users with the specified username have been found
* @throws AuthError if an internal problem occurred (do *not* catch)
*/
public function deleteUserByUsername($username) {
$userData = $this->getUserDataByUsername(
\trim($username),
[ 'id' ]
);
$this->deleteUsersByColumnValue('id', (int) $userData['id']);
}
/**
* Assigns the specified role to the user with the given ID
*
* A user may have any number of roles (i.e. no role at all, a single role, or any combination of roles)
*
* @param int $userId the ID of the user to assign the role to
* @param int $role the role as one of the constants from the {@see Role} class
* @throws UnknownIdException if no user with the specified ID has been found
*
* @see Role
*/
public function addRoleForUserById($userId, $role) {
$userFound = $this->addRoleForUserByColumnValue(
'id',
(int) $userId,
$role
);
if ($userFound === false) {
throw new UnknownIdException();
}
}
/**
* Assigns the specified role to the user with the given email address
*
* A user may have any number of roles (i.e. no role at all, a single role, or any combination of roles)
*
* @param string $userEmail the email address of the user to assign the role to
* @param int $role the role as one of the constants from the {@see Role} class
* @throws InvalidEmailException if no user with the specified email address has been found
*
* @see Role
*/
public function addRoleForUserByEmail($userEmail, $role) {
$userEmail = self::validateEmailAddress($userEmail);
$userFound = $this->addRoleForUserByColumnValue(
'email',
$userEmail,
$role
);
if ($userFound === false) {
throw new InvalidEmailException();
}
}
/**
* Assigns the specified role to the user with the given username
*
* A user may have any number of roles (i.e. no role at all, a single role, or any combination of roles)
*
* @param string $username the username of the user to assign the role to
* @param int $role the role as one of the constants from the {@see Role} class
* @throws UnknownUsernameException if no user with the specified username has been found
* @throws AmbiguousUsernameException if multiple users with the specified username have been found
*
* @see Role
*/
public function addRoleForUserByUsername($username, $role) {
$userData = $this->getUserDataByUsername(
\trim($username),
[ 'id' ]
);
$this->addRoleForUserByColumnValue(
'id',
(int) $userData['id'],
$role
);
}
/**
* Takes away the specified role from the user with the given ID
*
* A user may have any number of roles (i.e. no role at all, a single role, or any combination of roles)
*
* @param int $userId the ID of the user to take the role away from
* @param int $role the role as one of the constants from the {@see Role} class
* @throws UnknownIdException if no user with the specified ID has been found
*
* @see Role
*/
public function removeRoleForUserById($userId, $role) {
$userFound = $this->removeRoleForUserByColumnValue(
'id',
(int) $userId,
$role
);
if ($userFound === false) {
throw new UnknownIdException();
}
}
/**
* Takes away the specified role from the user with the given email address
*
* A user may have any number of roles (i.e. no role at all, a single role, or any combination of roles)
*
* @param string $userEmail the email address of the user to take the role away from
* @param int $role the role as one of the constants from the {@see Role} class
* @throws InvalidEmailException if no user with the specified email address has been found
*
* @see Role
*/
public function removeRoleForUserByEmail($userEmail, $role) {
$userEmail = self::validateEmailAddress($userEmail);
$userFound = $this->removeRoleForUserByColumnValue(
'email',
$userEmail,
$role
);
if ($userFound === false) {
throw new InvalidEmailException();
}
}
/**
* Takes away the specified role from the user with the given username
*
* A user may have any number of roles (i.e. no role at all, a single role, or any combination of roles)
*
* @param string $username the username of the user to take the role away from
* @param int $role the role as one of the constants from the {@see Role} class
* @throws UnknownUsernameException if no user with the specified username has been found
* @throws AmbiguousUsernameException if multiple users with the specified username have been found
*
* @see Role
*/
public function removeRoleForUserByUsername($username, $role) {
$userData = $this->getUserDataByUsername(
\trim($username),
[ 'id' ]
);
$this->removeRoleForUserByColumnValue(
'id',
(int) $userData['id'],
$role
);
}
/**
* Returns whether the user with the given ID has the specified role
*
* @param int $userId the ID of the user to check the roles for
* @param int $role the role as one of the constants from the {@see Role} class
* @return bool
* @throws UnknownIdException if no user with the specified ID has been found
*
* @see Role
*/
public function doesUserHaveRole($userId, $role) {
if (empty($role) || !\is_numeric($role)) {
return false;
}
$userId = (int) $userId;
$rolesBitmask = $this->db->selectValue(
'SELECT roles_mask FROM ' . $this->makeTableName('users') . ' WHERE id = ?',
[ $userId ]
);
if ($rolesBitmask === null) {
throw new UnknownIdException();
}
$role = (int) $role;
return ($rolesBitmask & $role) === $role;
}
/**
* Returns the roles of the user with the given ID, mapping the numerical values to their descriptive names
*
* @param int $userId the ID of the user to return the roles for
* @return array
* @throws UnknownIdException if no user with the specified ID has been found
*
* @see Role
*/
public function getRolesForUserById($userId) {
$userId = (int) $userId;
$rolesBitmask = $this->db->selectValue(
'SELECT roles_mask FROM ' . $this->makeTableName('users') . ' WHERE id = ?',
[ $userId ]
);
if ($rolesBitmask === null) {
throw new UnknownIdException();
}
return \array_filter(
Role::getMap(),
function ($each) use ($rolesBitmask) {
return ($rolesBitmask & $each) === $each;
},
\ARRAY_FILTER_USE_KEY
);
}
/**
* Signs in as the user with the specified ID
*
* @param int $id the ID of the user to sign in as
* @throws UnknownIdException if no user with the specified ID has been found
* @throws EmailNotVerifiedException if the user has not verified their email address via a confirmation method yet
* @throws AuthError if an internal problem occurred (do *not* catch)
*/
public function logInAsUserById($id) {
$numberOfMatchedUsers = $this->logInAsUserByColumnValue('id', (int) $id);
if ($numberOfMatchedUsers === 0) {
throw new UnknownIdException();
}
}
/**
* Signs in as the user with the specified email address
*
* @param string $email the email address of the user to sign in as
* @throws InvalidEmailException if no user with the specified email address has been found
* @throws EmailNotVerifiedException if the user has not verified their email address via a confirmation method yet
* @throws AuthError if an internal problem occurred (do *not* catch)
*/
public function logInAsUserByEmail($email) {
$email = self::validateEmailAddress($email);
$numberOfMatchedUsers = $this->logInAsUserByColumnValue('email', $email);
if ($numberOfMatchedUsers === 0) {
throw new InvalidEmailException();
}
}
/**
* Signs in as the user with the specified display name
*
* @param string $username the display name of the user to sign in as
* @throws UnknownUsernameException if no user with the specified username has been found
* @throws AmbiguousUsernameException if multiple users with the specified username have been found
* @throws EmailNotVerifiedException if the user has not verified their email address via a confirmation method yet
* @throws AuthError if an internal problem occurred (do *not* catch)
*/
public function logInAsUserByUsername($username) {
$numberOfMatchedUsers = $this->logInAsUserByColumnValue('username', \trim($username));
if ($numberOfMatchedUsers === 0) {
throw new UnknownUsernameException();
}
elseif ($numberOfMatchedUsers > 1) {
throw new AmbiguousUsernameException();
}
}
/**
* Changes the password for the user with the given ID
*
* @param int $userId the ID of the user whose password to change
* @param string $newPassword the new password to set
* @throws UnknownIdException if no user with the specified ID has been found
* @throws InvalidPasswordException if the desired new password has been invalid
* @throws AuthError if an internal problem occurred (do *not* catch)
*/
public function changePasswordForUserById($userId, $newPassword) {
$userId = (int) $userId;
$newPassword = self::validatePassword($newPassword);
$this->updatePasswordInternal(
$userId,
$newPassword
);
$this->forceLogoutForUserById($userId);
}
/**
* Changes the password for the user with the given username
*
* @param string $username the username of the user whose password to change
* @param string $newPassword the new password to set
* @throws UnknownUsernameException if no user with the specified username has been found
* @throws AmbiguousUsernameException if multiple users with the specified username have been found
* @throws InvalidPasswordException if the desired new password has been invalid
* @throws AuthError if an internal problem occurred (do *not* catch)
*/
public function changePasswordForUserByUsername($username, $newPassword) {
$userData = $this->getUserDataByUsername(
\trim($username),
[ 'id' ]
);
$this->changePasswordForUserById(
(int) $userData['id'],
$newPassword
);
}
/**
* Deletes all existing users where the column with the specified name has the given value
*
* You must never pass untrusted input to the parameter that takes the column name
*
* @param string $columnName the name of the column to filter by
* @param mixed $columnValue the value to look for in the selected column
* @return int the number of deleted users
* @throws AuthError if an internal problem occurred (do *not* catch)
*/
private function deleteUsersByColumnValue($columnName, $columnValue) {
try {
return $this->db->delete(
$this->makeTableNameComponents('users'),
[
$columnName => $columnValue
]
);
}
catch (Error $e) {
throw new DatabaseError($e->getMessage());
}
}
/**
* Modifies the roles for the user where the column with the specified name has the given value
*
* You must never pass untrusted input to the parameter that takes the column name
*
* @param string $columnName the name of the column to filter by
* @param mixed $columnValue the value to look for in the selected column
* @param callable $modification the modification to apply to the existing bitmask of roles
* @return bool whether any user with the given column constraints has been found
* @throws AuthError if an internal problem occurred (do *not* catch)
*
* @see Role
*/
private function modifyRolesForUserByColumnValue($columnName, $columnValue, callable $modification) {
try {
$userData = $this->db->selectRow(
'SELECT id, roles_mask FROM ' . $this->makeTableName('users') . ' WHERE ' . $columnName . ' = ?',
[ $columnValue ]
);
}
catch (Error $e) {
throw new DatabaseError($e->getMessage());
}
if ($userData === null) {
return false;
}
$newRolesBitmask = $modification($userData['roles_mask']);
try {
$this->db->exec(
'UPDATE ' . $this->makeTableName('users') . ' SET roles_mask = ? WHERE id = ?',
[
$newRolesBitmask,
(int) $userData['id']
]
);
return true;
}
catch (Error $e) {
throw new DatabaseError($e->getMessage());
}
}
/**
* Assigns the specified role to the user where the column with the specified name has the given value
*
* You must never pass untrusted input to the parameter that takes the column name
*
* @param string $columnName the name of the column to filter by
* @param mixed $columnValue the value to look for in the selected column
* @param int $role the role as one of the constants from the {@see Role} class
* @return bool whether any user with the given column constraints has been found
*
* @see Role
*/
private function addRoleForUserByColumnValue($columnName, $columnValue, $role) {
$role = (int) $role;
return $this->modifyRolesForUserByColumnValue(
$columnName,
$columnValue,
function ($oldRolesBitmask) use ($role) {
return $oldRolesBitmask | $role;
}
);
}
/**
* Takes away the specified role from the user where the column with the specified name has the given value
*
* You must never pass untrusted input to the parameter that takes the column name
*
* @param string $columnName the name of the column to filter by
* @param mixed $columnValue the value to look for in the selected column
* @param int $role the role as one of the constants from the {@see Role} class
* @return bool whether any user with the given column constraints has been found
*
* @see Role
*/
private function removeRoleForUserByColumnValue($columnName, $columnValue, $role) {
$role = (int) $role;
return $this->modifyRolesForUserByColumnValue(
$columnName,
$columnValue,
function ($oldRolesBitmask) use ($role) {
return $oldRolesBitmask & ~$role;
}
);
}
/**
* Signs in as the user for which the column with the specified name has the given value
*
* You must never pass untrusted input to the parameter that takes the column name
*
* @param string $columnName the name of the column to filter by
* @param mixed $columnValue the value to look for in the selected column
* @return int the number of matched users (where only a value of one means that the login may have been successful)
* @throws EmailNotVerifiedException if the user has not verified their email address via a confirmation method yet
* @throws AuthError if an internal problem occurred (do *not* catch)
*/
private function logInAsUserByColumnValue($columnName, $columnValue) {
try {
$users = $this->db->select(
'SELECT verified, id, email, username, status, roles_mask FROM ' . $this->makeTableName('users') . ' WHERE ' . $columnName . ' = ? LIMIT 2 OFFSET 0',
[ $columnValue ]
);
}
catch (Error $e) {
throw new DatabaseError($e->getMessage());
}
$numberOfMatchingUsers = ($users !== null) ? \count($users) : 0;
if ($numberOfMatchingUsers === 1) {
$user = $users[0];
if ((int) $user['verified'] === 1) {
$this->onLoginSuccessful($user['id'], $user['email'], $user['username'], $user['status'], $user['roles_mask'], \PHP_INT_MAX, false);
}
else {
throw new EmailNotVerifiedException();
}
}
return $numberOfMatchingUsers;
}
}
+11
View File
@@ -0,0 +1,11 @@
<?php
/*
* PHP-Auth (https://github.com/delight-im/PHP-Auth)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Auth;
class AmbiguousUsernameException extends AuthException {}
+11
View File
@@ -0,0 +1,11 @@
<?php
/*
* PHP-Auth (https://github.com/delight-im/PHP-Auth)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Auth;
class AttemptCancelledException extends AuthException {}
+1963
View File
File diff suppressed because it is too large Load Diff
+12
View File
@@ -0,0 +1,12 @@
<?php
/*
* PHP-Auth (https://github.com/delight-im/PHP-Auth)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Auth;
/** Base class for all (unchecked) errors */
class AuthError extends \Exception {}
+12
View File
@@ -0,0 +1,12 @@
<?php
/*
* PHP-Auth (https://github.com/delight-im/PHP-Auth)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Auth;
/** Base class for all (checked) exceptions */
class AuthException extends \Exception {}
+11
View File
@@ -0,0 +1,11 @@
<?php
/*
* PHP-Auth (https://github.com/delight-im/PHP-Auth)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Auth;
class ConfirmationRequestNotFound extends AuthException {}
+11
View File
@@ -0,0 +1,11 @@
<?php
/*
* PHP-Auth (https://github.com/delight-im/PHP-Auth)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Auth;
class DatabaseError extends AuthError {}
+11
View File
@@ -0,0 +1,11 @@
<?php
/*
* PHP-Auth (https://github.com/delight-im/PHP-Auth)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Auth;
class DuplicateUsernameException extends AuthException {}
+11
View File
@@ -0,0 +1,11 @@
<?php
/*
* PHP-Auth (https://github.com/delight-im/PHP-Auth)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Auth;
class EmailNotVerifiedException extends AuthException {}
+11
View File
@@ -0,0 +1,11 @@
<?php
/*
* PHP-Auth (https://github.com/delight-im/PHP-Auth)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Auth;
class EmailOrUsernameRequiredError extends AuthError {}
+11
View File
@@ -0,0 +1,11 @@
<?php
/*
* PHP-Auth (https://github.com/delight-im/PHP-Auth)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Auth;
class HeadersAlreadySentError extends AuthError {}
+11
View File
@@ -0,0 +1,11 @@
<?php
/*
* PHP-Auth (https://github.com/delight-im/PHP-Auth)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Auth;
class InvalidEmailException extends AuthException {}
+11
View File
@@ -0,0 +1,11 @@
<?php
/*
* PHP-Auth (https://github.com/delight-im/PHP-Auth)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Auth;
class InvalidPasswordException extends AuthException {}
+11
View File
@@ -0,0 +1,11 @@
<?php
/*
* PHP-Auth (https://github.com/delight-im/PHP-Auth)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Auth;
class InvalidSelectorTokenPairException extends AuthException {}
+11
View File
@@ -0,0 +1,11 @@
<?php
/*
* PHP-Auth (https://github.com/delight-im/PHP-Auth)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Auth;
class MissingCallbackError extends AuthError {}
+11
View File
@@ -0,0 +1,11 @@
<?php
/*
* PHP-Auth (https://github.com/delight-im/PHP-Auth)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Auth;
class NotLoggedInException extends AuthException {}
+11
View File
@@ -0,0 +1,11 @@
<?php
/*
* PHP-Auth (https://github.com/delight-im/PHP-Auth)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Auth;
class ResetDisabledException extends AuthException {}
+79
View File
@@ -0,0 +1,79 @@
<?php
/*
* PHP-Auth (https://github.com/delight-im/PHP-Auth)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Auth;
final class Role {
const ADMIN = 1;
const AUTHOR = 2;
const COLLABORATOR = 4;
const CONSULTANT = 8;
const CONSUMER = 16;
const CONTRIBUTOR = 32;
const COORDINATOR = 64;
const CREATOR = 128;
const DEVELOPER = 256;
const DIRECTOR = 512;
const EDITOR = 1024;
const EMPLOYEE = 2048;
const MAINTAINER = 4096;
const MANAGER = 8192;
const MODERATOR = 16384;
const PUBLISHER = 32768;
const REVIEWER = 65536;
const SUBSCRIBER = 131072;
const SUPER_ADMIN = 262144;
const SUPER_EDITOR = 524288;
const SUPER_MODERATOR = 1048576;
const TRANSLATOR = 2097152;
// const XYZ = 4194304;
// const XYZ = 8388608;
// const XYZ = 16777216;
// const XYZ = 33554432;
// const XYZ = 67108864;
// const XYZ = 134217728;
// const XYZ = 268435456;
// const XYZ = 536870912;
/**
* Returns an array mapping the numerical role values to their descriptive names
*
* @return array
*/
public static function getMap() {
$reflectionClass = new \ReflectionClass(static::class);
return \array_flip($reflectionClass->getConstants());
}
/**
* Returns the descriptive role names
*
* @return string[]
*/
public static function getNames() {
$reflectionClass = new \ReflectionClass(static::class);
return \array_keys($reflectionClass->getConstants());
}
/**
* Returns the numerical role values
*
* @return int[]
*/
public static function getValues() {
$reflectionClass = new \ReflectionClass(static::class);
return \array_values($reflectionClass->getConstants());
}
private function __construct() {}
}
+20
View File
@@ -0,0 +1,20 @@
<?php
/*
* PHP-Auth (https://github.com/delight-im/PHP-Auth)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Auth;
final class Status {
const NORMAL = 0;
const ARCHIVED = 1;
const BANNED = 2;
const LOCKED = 3;
const PENDING_REVIEW = 4;
const SUSPENDED = 5;
}
+11
View File
@@ -0,0 +1,11 @@
<?php
/*
* PHP-Auth (https://github.com/delight-im/PHP-Auth)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Auth;
class TokenExpiredException extends AuthException {}
+11
View File
@@ -0,0 +1,11 @@
<?php
/*
* PHP-Auth (https://github.com/delight-im/PHP-Auth)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Auth;
class TooManyRequestsException extends AuthException {}
+11
View File
@@ -0,0 +1,11 @@
<?php
/*
* PHP-Auth (https://github.com/delight-im/PHP-Auth)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Auth;
class UnknownIdException extends AuthException {}
+11
View File
@@ -0,0 +1,11 @@
<?php
/*
* PHP-Auth (https://github.com/delight-im/PHP-Auth)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Auth;
class UnknownUsernameException extends AuthException {}
+11
View File
@@ -0,0 +1,11 @@
<?php
/*
* PHP-Auth (https://github.com/delight-im/PHP-Auth)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Auth;
class UserAlreadyExistsException extends AuthException {}
+454
View File
@@ -0,0 +1,454 @@
<?php
/*
* PHP-Auth (https://github.com/delight-im/PHP-Auth)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Auth;
use Delight\Base64\Base64;
use Delight\Cookie\Session;
use Delight\Db\PdoDatabase;
use Delight\Db\PdoDsn;
use Delight\Db\Throwable\Error;
use Delight\Db\Throwable\IntegrityConstraintViolationException;
/**
* Abstract base class for components implementing user management
*
* @internal
*/
abstract class UserManager {
/** @var string session field for whether the client is currently signed in */
const SESSION_FIELD_LOGGED_IN = 'auth_logged_in';
/** @var string session field for the ID of the user who is currently signed in (if any) */
const SESSION_FIELD_USER_ID = 'auth_user_id';
/** @var string session field for the email address of the user who is currently signed in (if any) */
const SESSION_FIELD_EMAIL = 'auth_email';
/** @var string session field for the display name (if any) of the user who is currently signed in (if any) */
const SESSION_FIELD_USERNAME = 'auth_username';
/** @var string session field for the status of the user who is currently signed in (if any) as one of the constants from the {@see Status} class */
const SESSION_FIELD_STATUS = 'auth_status';
/** @var string session field for the roles of the user who is currently signed in (if any) as a bitmask using constants from the {@see Role} class */
const SESSION_FIELD_ROLES = 'auth_roles';
/** @var string session field for whether the user who is currently signed in (if any) has been remembered (instead of them having authenticated actively) */
const SESSION_FIELD_REMEMBERED = 'auth_remembered';
/** @var string session field for the UNIX timestamp in seconds of the session data's last resynchronization with its authoritative source in the database */
const SESSION_FIELD_LAST_RESYNC = 'auth_last_resync';
/** @var string session field for the counter that keeps track of forced logouts that need to be performed in the current session */
const SESSION_FIELD_FORCE_LOGOUT = 'auth_force_logout';
/** @var PdoDatabase the database connection to operate on */
protected $db;
/** @var string|null the schema name for all database tables used by this component */
protected $dbSchema;
/** @var string the prefix for the names of all database tables used by this component */
protected $dbTablePrefix;
/**
* Creates a random string with the given maximum length
*
* With the default parameter, the output should contain at least as much randomness as a UUID
*
* @param int $maxLength the maximum length of the output string (integer multiple of 4)
* @return string the new random string
*/
public static function createRandomString($maxLength = 24) {
// calculate how many bytes of randomness we need for the specified string length
$bytes = \floor((int) $maxLength / 4) * 3;
// get random data
$data = \openssl_random_pseudo_bytes($bytes);
// return the Base64-encoded result
return Base64::encodeUrlSafe($data);
}
/**
* @param PdoDatabase|PdoDsn|\PDO $databaseConnection the database connection to operate on
* @param string|null $dbTablePrefix (optional) the prefix for the names of all database tables used by this component
* @param string|null $dbSchema (optional) the schema name for all database tables used by this component
*/
protected function __construct($databaseConnection, $dbTablePrefix = null, $dbSchema = null) {
if ($databaseConnection instanceof PdoDatabase) {
$this->db = $databaseConnection;
}
elseif ($databaseConnection instanceof PdoDsn) {
$this->db = PdoDatabase::fromDsn($databaseConnection);
}
elseif ($databaseConnection instanceof \PDO) {
$this->db = PdoDatabase::fromPdo($databaseConnection, true);
}
else {
$this->db = null;
throw new \InvalidArgumentException('The database connection must be an instance of either `PdoDatabase`, `PdoDsn` or `PDO`');
}
$this->dbSchema = $dbSchema !== null ? (string) $dbSchema : null;
$this->dbTablePrefix = (string) $dbTablePrefix;
}
/**
* Creates a new user
*
* If you want the user's account to be activated by default, pass `null` as the callback
*
* If you want to make the user verify their email address first, pass an anonymous function as the callback
*
* The callback function must have the following signature:
*
* `function ($selector, $token)`
*
* Both pieces of information must be sent to the user, usually embedded in a link
*
* When the user wants to verify their email address as a next step, both pieces will be required again
*
* @param bool $requireUniqueUsername whether it must be ensured that the username is unique
* @param string $email the email address to register
* @param string $password the password for the new account
* @param string|null $username (optional) the username that will be displayed
* @param callable|null $callback (optional) the function that sends the confirmation email to the user
* @return int the ID of the user that has been created (if any)
* @throws InvalidEmailException if the email address has been invalid
* @throws InvalidPasswordException if the password has been invalid
* @throws UserAlreadyExistsException if a user with the specified email address already exists
* @throws DuplicateUsernameException if it was specified that the username must be unique while it was *not*
* @throws AuthError if an internal problem occurred (do *not* catch)
*
* @see confirmEmail
* @see confirmEmailAndSignIn
*/
protected function createUserInternal($requireUniqueUsername, $email, $password, $username = null, callable $callback = null) {
\ignore_user_abort(true);
$email = self::validateEmailAddress($email);
$password = self::validatePassword($password);
$username = isset($username) ? \trim($username) : null;
// if the supplied username is the empty string or has consisted of whitespace only
if ($username === '') {
// this actually means that there is no username
$username = null;
}
// if the uniqueness of the username is to be ensured
if ($requireUniqueUsername) {
// if a username has actually been provided
if ($username !== null) {
// count the number of users who do already have that specified username
$occurrencesOfUsername = $this->db->selectValue(
'SELECT COUNT(*) FROM ' . $this->makeTableName('users') . ' WHERE username = ?',
[ $username ]
);
// if any user with that username does already exist
if ($occurrencesOfUsername > 0) {
// cancel the operation and report the violation of this requirement
throw new DuplicateUsernameException();
}
}
}
$password = \password_hash($password, \PASSWORD_DEFAULT);
$verified = \is_callable($callback) ? 0 : 1;
try {
$this->db->insert(
$this->makeTableNameComponents('users'),
[
'email' => $email,
'password' => $password,
'username' => $username,
'verified' => $verified,
'registered' => \time()
]
);
}
// if we have a duplicate entry
catch (IntegrityConstraintViolationException $e) {
throw new UserAlreadyExistsException();
}
catch (Error $e) {
throw new DatabaseError($e->getMessage());
}
$newUserId = (int) $this->db->getLastInsertId();
if ($verified === 0) {
$this->createConfirmationRequest($newUserId, $email, $callback);
}
return $newUserId;
}
/**
* Updates the given user's password by setting it to the new specified password
*
* @param int $userId the ID of the user whose password should be updated
* @param string $newPassword the new password
* @throws UnknownIdException if no user with the specified ID has been found
* @throws AuthError if an internal problem occurred (do *not* catch)
*/
protected function updatePasswordInternal($userId, $newPassword) {
$newPassword = \password_hash($newPassword, \PASSWORD_DEFAULT);
try {
$affected = $this->db->update(
$this->makeTableNameComponents('users'),
[ 'password' => $newPassword ],
[ 'id' => $userId ]
);
if ($affected === 0) {
throw new UnknownIdException();
}
}
catch (Error $e) {
throw new DatabaseError($e->getMessage());
}
}
/**
* Called when a user has successfully logged in
*
* This may happen via the standard login, via the "remember me" feature, or due to impersonation by administrators
*
* @param int $userId the ID of the user
* @param string $email the email address of the user
* @param string $username the display name (if any) of the user
* @param int $status the status of the user as one of the constants from the {@see Status} class
* @param int $roles the roles of the user as a bitmask using constants from the {@see Role} class
* @param int $forceLogout the counter that keeps track of forced logouts that need to be performed in the current session
* @param bool $remembered whether the user has been remembered (instead of them having authenticated actively)
* @throws AuthError if an internal problem occurred (do *not* catch)
*/
protected function onLoginSuccessful($userId, $email, $username, $status, $roles, $forceLogout, $remembered) {
// re-generate the session ID to prevent session fixation attacks (requests a cookie to be written on the client)
Session::regenerate(true);
// save the user data in the session variables maintained by this library
$_SESSION[self::SESSION_FIELD_LOGGED_IN] = true;
$_SESSION[self::SESSION_FIELD_USER_ID] = (int) $userId;
$_SESSION[self::SESSION_FIELD_EMAIL] = $email;
$_SESSION[self::SESSION_FIELD_USERNAME] = $username;
$_SESSION[self::SESSION_FIELD_STATUS] = (int) $status;
$_SESSION[self::SESSION_FIELD_ROLES] = (int) $roles;
$_SESSION[self::SESSION_FIELD_FORCE_LOGOUT] = (int) $forceLogout;
$_SESSION[self::SESSION_FIELD_REMEMBERED] = $remembered;
$_SESSION[self::SESSION_FIELD_LAST_RESYNC] = \time();
}
/**
* Returns the requested user data for the account with the specified username (if any)
*
* You must never pass untrusted input to the parameter that takes the column list
*
* @param string $username the username to look for
* @param array $requestedColumns the columns to request from the user's record
* @return array the user data (if an account was found unambiguously)
* @throws UnknownUsernameException if no user with the specified username has been found
* @throws AmbiguousUsernameException if multiple users with the specified username have been found
* @throws AuthError if an internal problem occurred (do *not* catch)
*/
protected function getUserDataByUsername($username, array $requestedColumns) {
try {
$projection = \implode(', ', $requestedColumns);
$users = $this->db->select(
'SELECT ' . $projection . ' FROM ' . $this->makeTableName('users') . ' WHERE username = ? LIMIT 2 OFFSET 0',
[ $username ]
);
}
catch (Error $e) {
throw new DatabaseError($e->getMessage());
}
if (empty($users)) {
throw new UnknownUsernameException();
}
else {
if (\count($users) === 1) {
return $users[0];
}
else {
throw new AmbiguousUsernameException();
}
}
}
/**
* Validates an email address
*
* @param string $email the email address to validate
* @return string the sanitized email address
* @throws InvalidEmailException if the email address has been invalid
*/
protected static function validateEmailAddress($email) {
if (empty($email)) {
throw new InvalidEmailException();
}
$email = \trim($email);
if (!\filter_var($email, \FILTER_VALIDATE_EMAIL)) {
throw new InvalidEmailException();
}
return $email;
}
/**
* Validates a password
*
* @param string $password the password to validate
* @return string the sanitized password
* @throws InvalidPasswordException if the password has been invalid
*/
protected static function validatePassword($password) {
if (empty($password)) {
throw new InvalidPasswordException();
}
$password = \trim($password);
if (\strlen($password) < 1) {
throw new InvalidPasswordException();
}
return $password;
}
/**
* Creates a request for email confirmation
*
* The callback function must have the following signature:
*
* `function ($selector, $token)`
*
* Both pieces of information must be sent to the user, usually embedded in a link
*
* When the user wants to verify their email address as a next step, both pieces will be required again
*
* @param int $userId the user's ID
* @param string $email the email address to verify
* @param callable $callback the function that sends the confirmation email to the user
* @throws AuthError if an internal problem occurred (do *not* catch)
*/
protected function createConfirmationRequest($userId, $email, callable $callback) {
$selector = self::createRandomString(16);
$token = self::createRandomString(16);
$tokenHashed = \password_hash($token, \PASSWORD_DEFAULT);
$expires = \time() + 60 * 60 * 24;
try {
$this->db->insert(
$this->makeTableNameComponents('users_confirmations'),
[
'user_id' => (int) $userId,
'email' => $email,
'selector' => $selector,
'token' => $tokenHashed,
'expires' => $expires
]
);
}
catch (Error $e) {
throw new DatabaseError($e->getMessage());
}
if (\is_callable($callback)) {
$callback($selector, $token);
}
else {
throw new MissingCallbackError();
}
}
/**
* Clears an existing directive that keeps the user logged in ("remember me")
*
* @param int $userId the ID of the user who shouldn't be kept signed in anymore
* @param string $selector (optional) the selector which the deletion should be restricted to
* @throws AuthError if an internal problem occurred (do *not* catch)
*/
protected function deleteRememberDirectiveForUserById($userId, $selector = null) {
$whereMappings = [];
if (isset($selector)) {
$whereMappings['selector'] = (string) $selector;
}
$whereMappings['user'] = (int) $userId;
try {
$this->db->delete(
$this->makeTableNameComponents('users_remembered'),
$whereMappings
);
}
catch (Error $e) {
throw new DatabaseError($e->getMessage());
}
}
/**
* Triggers a forced logout in all sessions that belong to the specified user
*
* @param int $userId the ID of the user to sign out
* @throws AuthError if an internal problem occurred (do *not* catch)
*/
protected function forceLogoutForUserById($userId) {
$this->deleteRememberDirectiveForUserById($userId);
$this->db->exec(
'UPDATE ' . $this->makeTableName('users') . ' SET force_logout = force_logout + 1 WHERE id = ?',
[ $userId ]
);
}
/**
* Builds a (qualified) full table name from an optional qualifier, an optional prefix, and the table name itself
*
* The optional qualifier may be a database name or a schema name, for example
*
* @param string $name the name of the table
* @return string[] the components of the (qualified) full name of the table
*/
protected function makeTableNameComponents($name) {
$components = [];
if (!empty($this->dbSchema)) {
$components[] = $this->dbSchema;
}
if (!empty($name)) {
if (!empty($this->dbTablePrefix)) {
$components[] = $this->dbTablePrefix . $name;
}
else {
$components[] = $name;
}
}
return $components;
}
/**
* Builds a (qualified) full table name from an optional qualifier, an optional prefix, and the table name itself
*
* The optional qualifier may be a database name or a schema name, for example
*
* @param string $name the name of the table
* @return string the (qualified) full name of the table
*/
protected function makeTableName($name) {
$components = $this->makeTableNameComponents($name);
return \implode('.', $components);
}
}
+1086
View File
File diff suppressed because it is too large Load Diff
+13
View File
@@ -0,0 +1,13 @@
# editorconfig.org
root = true
[*]
charset = utf-8
indent_style = tab
trim_trailing_whitespace = true
end_of_line = lf
insert_final_newline = true
[*.md]
indent_style = space
indent_size = 4
+6
View File
@@ -0,0 +1,6 @@
# IntelliJ
.idea/
# Composer
vendor/
composer.phar
+21
View File
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) delight.im (https://www.delight.im/)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+87
View File
@@ -0,0 +1,87 @@
# PHP-Base64
Simple and convenient Base64 encoding and decoding for PHP
## Requirements
* PHP 5.3.0+
## Installation
1. Include the library via Composer [[?]](https://github.com/delight-im/Knowledge/blob/master/Composer%20(PHP).md):
```
$ composer require delight-im/base64
```
1. Include the Composer autoloader:
```php
require __DIR__ . '/vendor/autoload.php';
```
## Usage
### Standard
* Encoding data
```php
\Delight\Base64\Base64::encode('Gallia est omnis divisa in partes tres');
// string(52) "R2FsbGlhIGVzdCBvbW5pcyBkaXZpc2EgaW4gcGFydGVzIHRyZXM="
```
* Decoding data
```php
\Delight\Base64\Base64::decode('R2FsbGlhIGVzdCBvbW5pcyBkaXZpc2EgaW4gcGFydGVzIHRyZXM=');
// string(38) "Gallia est omnis divisa in partes tres"
```
### URL-safe
* Encoding data
```php
\Delight\Base64\Base64::encodeUrlSafe('πάντα χωρεῖ καὶ οὐδὲν μένει …');
// string(80) "z4DOrM69z4TOsSDPh8-Jz4HOteG_liDOus6x4b22IM6_4b2QzrThvbLOvSDOvM6tzr3Otc65IOKApg~~"
```
* Decoding data
```php
\Delight\Base64\Base64::decodeUrlSafe('z4DOrM69z4TOsSDPh8-Jz4HOteG_liDOus6x4b22IM6_4b2QzrThvbLOvSDOvM6tzr3Otc65IOKApg~~');
// string(58) "πάντα χωρεῖ καὶ οὐδὲν μένει …"
```
### URL-safe without padding
* Encoding data
```php
\Delight\Base64\Base64::encodeUrlSafeWithoutPadding('πάντα χωρεῖ καὶ οὐδὲν μένει …');
// string(78) "z4DOrM69z4TOsSDPh8-Jz4HOteG_liDOus6x4b22IM6_4b2QzrThvbLOvSDOvM6tzr3Otc65IOKApg"
```
* Decoding data
```php
\Delight\Base64\Base64::decodeUrlSafeWithoutPadding('z4DOrM69z4TOsSDPh8-Jz4HOteG_liDOus6x4b22IM6_4b2QzrThvbLOvSDOvM6tzr3Otc65IOKApg');
// string(58) "πάντα χωρεῖ καὶ οὐδὲν μένει …"
```
## Specifications
* [RFC 4648](https://tools.ietf.org/html/rfc4648)
* [RFC 3548](https://tools.ietf.org/html/rfc3548)
* [RFC 2045](https://tools.ietf.org/html/rfc2045)
* [RFC 1421](https://tools.ietf.org/html/rfc1421)
* [RFC 7515](https://tools.ietf.org/html/rfc7515)
## Contributing
All contributions are welcome! If you wish to contribute, please create an issue first so that your feature, problem or question can be discussed.
## License
This project is licensed under the terms of the [MIT License](https://opensource.org/licenses/MIT).
+16
View File
@@ -0,0 +1,16 @@
{
"name": "delight-im/base64",
"description": "Simple and convenient Base64 encoding and decoding for PHP",
"require": {
"php": ">=5.3.0"
},
"type": "library",
"keywords": [ "base64", "base-64", "encode", "decode", "encoding", "decoding", "url-safe", "url" ],
"homepage": "https://github.com/delight-im/PHP-Base64",
"license": "MIT",
"autoload": {
"psr-4": {
"Delight\\Base64\\": "src/"
}
}
}
Generated Vendored Executable
+19
View File
@@ -0,0 +1,19 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically"
],
"content-hash": "d69f90769e62bab4c7f053c4a3030138",
"packages": [],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
"php": ">=5.3.0"
},
"platform-dev": []
}
+132
View File
@@ -0,0 +1,132 @@
<?php
/*
* PHP-Base64 (https://github.com/delight-im/PHP-Base64)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Base64;
use Delight\Base64\Throwable\DecodingError;
use Delight\Base64\Throwable\EncodingError;
/** Utilities for encoding and decoding data using Base64 and variants thereof */
final class Base64 {
/**
* The last three characters from the alphabet of the standard implementation
*
* @var string
*/
const LAST_THREE_STANDARD = '+/=';
/**
* The last three characters from the alphabet of the URL-safe implementation
*
* @var string
*/
const LAST_THREE_URL_SAFE = '-_~';
/**
* Encodes the supplied data to Base64
*
* @param mixed $data
* @return string
* @throws EncodingError if the input has been invalid
*/
public static function encode($data) {
$encoded = \base64_encode($data);
if ($encoded === false) {
throw new EncodingError();
}
return $encoded;
}
/**
* Decodes the supplied data from Base64
*
* @param string $data
* @return mixed
* @throws DecodingError if the input has been invalid
*/
public static function decode($data) {
$decoded = \base64_decode($data, true);
if ($decoded === false) {
throw new DecodingError();
}
return $decoded;
}
/**
* Encodes the supplied data to a URL-safe variant of Base64
*
* @param mixed $data
* @return string
* @throws EncodingError if the input has been invalid
*/
public static function encodeUrlSafe($data) {
$encoded = self::encode($data);
return \strtr(
$encoded,
self::LAST_THREE_STANDARD,
self::LAST_THREE_URL_SAFE
);
}
/**
* Decodes the supplied data from a URL-safe variant of Base64
*
* @param string $data
* @return mixed
* @throws DecodingError if the input has been invalid
*/
public static function decodeUrlSafe($data) {
$data = \strtr(
$data,
self::LAST_THREE_URL_SAFE,
self::LAST_THREE_STANDARD
);
return self::decode($data);
}
/**
* Encodes the supplied data to a URL-safe variant of Base64 without padding
*
* @param mixed $data
* @return string
* @throws EncodingError if the input has been invalid
*/
public static function encodeUrlSafeWithoutPadding($data) {
$encoded = self::encode($data);
$encoded = \rtrim(
$encoded,
\substr(self::LAST_THREE_STANDARD, -1)
);
return \strtr(
$encoded,
\substr(self::LAST_THREE_STANDARD, 0, -1),
\substr(self::LAST_THREE_URL_SAFE, 0, -1)
);
}
/**
* Decodes the supplied data from a URL-safe variant of Base64 without padding
*
* @param string $data
* @return mixed
* @throws DecodingError if the input has been invalid
*/
public static function decodeUrlSafeWithoutPadding($data) {
return self::decodeUrlSafe($data);
}
}
+12
View File
@@ -0,0 +1,12 @@
<?php
/*
* PHP-Base64 (https://github.com/delight-im/PHP-Base64)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Base64\Throwable;
/** Error that is thrown when an attempt is being made to decode illegal input */
class DecodingError extends Error {}
+12
View File
@@ -0,0 +1,12 @@
<?php
/*
* PHP-Base64 (https://github.com/delight-im/PHP-Base64)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Base64\Throwable;
/** Error that is thrown when an attempt is being made to encode illegal input */
class EncodingError extends Error {}
+12
View File
@@ -0,0 +1,12 @@
<?php
/*
* PHP-Base64 (https://github.com/delight-im/PHP-Base64)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Base64\Throwable;
/** Base class for all (unchecked) errors */
abstract class Error extends \Exception {}
+12
View File
@@ -0,0 +1,12 @@
<?php
/*
* PHP-Base64 (https://github.com/delight-im/PHP-Base64)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Base64\Throwable;
/** Base class for all (checked) exceptions */
abstract class Exception extends \Exception {}
+34
View File
@@ -0,0 +1,34 @@
<?php
/*
* PHP-Base64 (https://github.com/delight-im/PHP-Base64)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
\error_reporting(E_ALL);
\ini_set('display_errors', 'stdout');
\header('Content-type: text/plain; charset=utf-8');
require __DIR__ . '/../vendor/autoload.php';
(\Delight\Base64\Base64::encode('Gallia est omnis divisa in partes tres') === 'R2FsbGlhIGVzdCBvbW5pcyBkaXZpc2EgaW4gcGFydGVzIHRyZXM=') or exit('Failed on line ' . __LINE__);
(\Delight\Base64\Base64::decode('R2FsbGlhIGVzdCBvbW5pcyBkaXZpc2EgaW4gcGFydGVzIHRyZXM=') === 'Gallia est omnis divisa in partes tres') or exit('Failed on line ' . __LINE__);
(\Delight\Base64\Base64::encodeUrlSafe('πάντα χωρεῖ καὶ οὐδὲν μένει …') === 'z4DOrM69z4TOsSDPh8-Jz4HOteG_liDOus6x4b22IM6_4b2QzrThvbLOvSDOvM6tzr3Otc65IOKApg~~') or exit('Failed on line ' . __LINE__);
(\Delight\Base64\Base64::decodeUrlSafe('z4DOrM69z4TOsSDPh8-Jz4HOteG_liDOus6x4b22IM6_4b2QzrThvbLOvSDOvM6tzr3Otc65IOKApg~~') === 'πάντα χωρεῖ καὶ οὐδὲν μένει …') or exit('Failed on line ' . __LINE__);
(\Delight\Base64\Base64::encodeUrlSafeWithoutPadding('πάντα χωρεῖ καὶ οὐδὲν μένει …') === 'z4DOrM69z4TOsSDPh8-Jz4HOteG_liDOus6x4b22IM6_4b2QzrThvbLOvSDOvM6tzr3Otc65IOKApg') or exit('Failed on line ' . __LINE__);
(\Delight\Base64\Base64::decodeUrlSafeWithoutPadding('z4DOrM69z4TOsSDPh8-Jz4HOteG_liDOus6x4b22IM6_4b2QzrThvbLOvSDOvM6tzr3Otc65IOKApg') === 'πάντα χωρεῖ καὶ οὐδὲν μένει …') or exit('Failed on line ' . __LINE__);
for ($i = 0; $i < 1000; $i++) {
$data = \openssl_random_pseudo_bytes(
\mt_rand(12, 24)
);
(\Delight\Base64\Base64::decode(\Delight\Base64\Base64::encode($data)) === $data) or exit('Failed on line ' . __LINE__);
(\Delight\Base64\Base64::decodeUrlSafe(\Delight\Base64\Base64::encodeUrlSafe($data)) === $data) or exit('Failed on line ' . __LINE__);
(\Delight\Base64\Base64::decodeUrlSafeWithoutPadding(\Delight\Base64\Base64::encodeUrlSafeWithoutPadding($data)) === $data) or exit('Failed on line ' . __LINE__);
}
echo 'ALL TESTS PASSED';
echo "\n";
+17
View File
@@ -0,0 +1,17 @@
# editorconfig.org
root = true
[*]
charset = utf-8
indent_style = tab
trim_trailing_whitespace = true
end_of_line = lf
insert_final_newline = true
[*.md]
indent_style = space
indent_size = 4
[*.yml]
indent_style = space
indent_size = 2
+6
View File
@@ -0,0 +1,6 @@
# IntelliJ
.idea/
# Composer
vendor/
composer.phar
+9
View File
@@ -0,0 +1,9 @@
<VirtualHost *:80>
DocumentRoot %TRAVIS_BUILD_DIR%
<IfModule mod_fastcgi.c>
AddHandler php5-fcgi .php
Action php5-fcgi /php5-fcgi
Alias /php5-fcgi /usr/lib/cgi-bin/php5-fcgi
FastCgiExternalServer /usr/lib/cgi-bin/php5-fcgi -host 127.0.0.1:9000 -pass-header Authorization
</IfModule>
</VirtualHost>
+29
View File
@@ -0,0 +1,29 @@
language: php
php:
- 5.4
- 5.5
- 5.6
- 7.0
- 7.1
cache:
directories:
- $HOME/.composer/cache
install:
- composer install
before_script:
- sudo apt-get update
- sudo apt-get install apache2 libapache2-mod-fastcgi
- sudo cp ~/.phpenv/versions/$(phpenv version-name)/etc/php-fpm.conf.default ~/.phpenv/versions/$(phpenv version-name)/etc/php-fpm.conf
- if [[ ${TRAVIS_PHP_VERSION:0:2} == "7." ]]; then sudo cp ~/.phpenv/versions/$(phpenv version-name)/etc/php-fpm.d/www.conf.default ~/.phpenv/versions/$(phpenv version-name)/etc/php-fpm.d/www.conf; fi
- sudo cp ~/.phpenv/versions/$(phpenv version-name)/etc/php-fpm.conf.default ~/.phpenv/versions/$(phpenv version-name)/etc/php-fpm.conf
- sudo a2enmod actions fastcgi alias
- ~/.phpenv/versions/$(phpenv version-name)/sbin/php-fpm
- sudo cp -f .travis-ci-apache /etc/apache2/sites-available/default
- sudo sed -e "s?%TRAVIS_BUILD_DIR%?$(pwd)?g" --in-place /etc/apache2/sites-available/default
- sudo service apache2 restart
script: test_result="$(curl -s http://localhost/tests/)" && echo "$test_result" && echo "$test_result" | grep -q 'ALL TESTS PASSED' || exit 1
+21
View File
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) delight.im (https://www.delight.im/)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+17
View File
@@ -0,0 +1,17 @@
# Migration
## General
Update your version of this library using Composer and its `composer update` or `composer require` commands [[?]](https://github.com/delight-im/Knowledge/blob/master/Composer%20(PHP).md#how-do-i-update-libraries-or-modules-within-my-application).
## From `v2.x.x` to `v3.x.x`
* The default domain scope for new `Cookie` instances is still the current host, but it does not include subdomains anymore, unless explicitly specified otherwise via the `Cookie#setDomain` method.
* For the domain scope, `www` subdomains are not automatically widened to the bare domain anymore. If you want to include the bare domain and all subdomains in addition to the `www` subdomain, you must now explicitly specifiy the bare domain instead of the `www` subdomain as the scope.
* When managing sessions via the methods `Session#start` or `Session#regenerate`, the session configuration is now correctly respected with regard to whether subdomains should be included in the domain scope or not. The scope is not automatically widened to include subdomains anymore.
* The second parameter of the `Cookie#setDomain` method, which was named `$keepWww`, has been removed.
* When creating `Cookie` instances from a string using the `Cookie#parse` method, the source’s decision of whether to include subdomains in the domain scope or not is now correctly respected. The scope is not automatically widened to include subdomains anymore.
## From `v1.x.x` to `v2.x.x`
* The license has been changed from the [Apache License 2.0](http://www.apache.org/licenses/LICENSE-2.0) to the [MIT License](https://opensource.org/licenses/MIT).
+203
View File
@@ -0,0 +1,203 @@
# PHP-Cookie
Modern cookie management for PHP
## Requirements
* PHP 5.4.0+
## Installation
1. Include the library via Composer [[?]](https://github.com/delight-im/Knowledge/blob/master/Composer%20(PHP).md):
```
$ composer require delight-im/cookie
```
1. Include the Composer autoloader:
```php
require __DIR__ . '/vendor/autoload.php';
```
## Upgrading
Migrating from an earlier version of this project? See our [upgrade guide](Migration.md) for help.
## Usage
### Static method
This library provides a static method that is compatible to PHP’s built-in `setcookie(...)` function but includes support for more recent features such as the `SameSite` attribute:
```php
\Delight\Cookie\Cookie::setcookie('SID', '31d4d96e407aad42');
// or
\Delight\Cookie\Cookie::setcookie('SID', '31d4d96e407aad42', time() + 3600, '/~rasmus/', 'example.com', true, true, 'Lax');
```
### Builder pattern
Instances of the `Cookie` class let you build a cookie conveniently by setting individual properties. This class uses reasonable defaults that may differ from defaults of the `setcookie` function.
```php
$cookie = new \Delight\Cookie\Cookie('SID');
$cookie->setValue('31d4d96e407aad42');
$cookie->setMaxAge(60 * 60 * 24);
// $cookie->setExpiryTime(time() + 60 * 60 * 24);
$cookie->setPath('/~rasmus/');
$cookie->setDomain('example.com');
$cookie->setHttpOnly(true);
$cookie->setSecureOnly(true);
$cookie->setSameSiteRestriction('Strict');
// echo $cookie;
// or
$cookie->save();
// or
// $cookie->saveAndSet();
```
The method calls can also be chained:
```php
(new \Delight\Cookie\Cookie('SID'))->setValue('31d4d96e407aad42')->setMaxAge(60 * 60 * 24)->setSameSiteRestriction('None')->save();
```
A cookie can later be deleted simply like this:
```php
$cookie->delete();
// or
$cookie->deleteAndUnset();
```
**Note:** For the deletion to work, the cookie must have the same settings as the cookie that was originally saved – except for its value, which doesn’t need to be set. So you should remember to pass appropriate values to `setPath(...)`, `setDomain(...)`, `setHttpOnly(...)` and `setSecureOnly(...)` again.
### Reading cookies
* Checking whether a cookie exists:
```php
\Delight\Cookie\Cookie::exists('first_visit');
```
* Reading a cookie’s value (with optional default value):
```php
\Delight\Cookie\Cookie::get('first_visit');
// or
\Delight\Cookie\Cookie::get('first_visit', \time());
```
### Managing sessions
Using the `Session` class, you can start and resume sessions in a way that is compatible to PHP’s built-in `session_start()` function, while having access to the improved cookie handling from this library as well:
```php
// start session and have session cookie with 'lax' same-site restriction
\Delight\Cookie\Session::start();
// or
\Delight\Cookie\Session::start('Lax');
// start session and have session cookie with 'strict' same-site restriction
\Delight\Cookie\Session::start('Strict');
// start session and have session cookie without any same-site restriction
\Delight\Cookie\Session::start(null);
// or
\Delight\Cookie\Session::start('None'); // Chrome 80+
```
All three calls respect the settings from PHP’s `session_set_cookie_params(...)` function and the configuration options `session.name`, `session.cookie_lifetime`, `session.cookie_path`, `session.cookie_domain`, `session.cookie_secure`, `session.cookie_httponly` and `session.use_cookies`.
Likewise, replacements for
```php
session_regenerate_id();
// and
session_regenerate_id(true);
```
are available via
```php
\Delight\Cookie\Session::regenerate();
// and
\Delight\Cookie\Session::regenerate(true);
```
if you want protection against session fixation attacks that comes with improved cookie handling.
Additionally, access to the current internal session ID is provided via
```php
\Delight\Cookie\Session::id();
```
as a replacement for
```php
session_id();
```
### Reading and writing session data
* Read a value from the session (with optional default value):
```php
$value = \Delight\Cookie\Session::get($key);
// or
$value = \Delight\Cookie\Session::get($key, $defaultValue);
```
* Write a value to the session:
```php
\Delight\Cookie\Session::set($key, $value);
```
* Check whether a value exists in the session:
```php
if (\Delight\Cookie\Session::has($key)) {
// ...
}
```
* Remove a value from the session:
```php
\Delight\Cookie\Session::delete($key);
```
* Read *and then* immediately remove a value from the session:
```php
$value = \Delight\Cookie\Session::take($key);
$value = \Delight\Cookie\Session::take($key, $defaultValue);
```
This is often useful for flash messages, e.g. in combination with the `has(...)` method.
### Parsing cookies
```php
$cookieHeader = 'Set-Cookie: test=php.net; expires=Thu, 09-Jun-2016 16:30:32 GMT; Max-Age=3600; path=/~rasmus/; secure';
$cookieInstance = \Delight\Cookie\Cookie::parse($cookieHeader);
```
## Specifications
* [RFC 2109](https://tools.ietf.org/html/rfc2109)
* [RFC 6265](https://tools.ietf.org/html/rfc6265)
* [Same-site Cookies](https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-04) (formerly [2016-06-20](https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00) and [2016-04-06](https://tools.ietf.org/html/draft-west-first-party-cookies-07))
* [Amendment](https://tools.ietf.org/html/draft-west-cookie-incrementalism-00): [Default to `Lax`](https://chromestatus.com/feature/5088147346030592) and [require `secure` attribute for `None`](https://chromestatus.com/feature/5633521622188032) (Note: There are [incompatible clients](https://www.chromium.org/updates/same-site/incompatible-clients))
## Contributing
All contributions are welcome! If you wish to contribute, please create an issue first so that your feature, problem or question can be discussed.
## License
This project is licensed under the terms of the [MIT License](https://opensource.org/licenses/MIT).
+17
View File
@@ -0,0 +1,17 @@
{
"name": "delight-im/cookie",
"description": "Modern cookie management for PHP",
"require": {
"php": ">=5.4.0",
"delight-im/http": "^2.0"
},
"type": "library",
"keywords": [ "cookie", "cookies", "http", "csrf", "xss", "samesite", "same-site" ],
"homepage": "https://github.com/delight-im/PHP-Cookie",
"license": "MIT",
"autoload": {
"psr-4": {
"Delight\\Cookie\\": "src/"
}
}
}
Generated Vendored Executable
+56
View File
@@ -0,0 +1,56 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically"
],
"content-hash": "660f7f831eec915a00485a0dd67853d0",
"packages": [
{
"name": "delight-im/http",
"version": "v2.0.0",
"source": {
"type": "git",
"url": "https://github.com/delight-im/PHP-HTTP.git",
"reference": "0a19a72a7eac8b1301aa972fb20cff494ac43e09"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/delight-im/PHP-HTTP/zipball/0a19a72a7eac8b1301aa972fb20cff494ac43e09",
"reference": "0a19a72a7eac8b1301aa972fb20cff494ac43e09",
"shasum": ""
},
"require": {
"php": ">=5.3.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Delight\\Http\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "Hypertext Transfer Protocol (HTTP) utilities for PHP",
"homepage": "https://github.com/delight-im/PHP-HTTP",
"keywords": [
"headers",
"http",
"https"
],
"time": "2016-07-21T15:05:01+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
"php": ">=5.4.0"
},
"platform-dev": []
}
+584
View File
@@ -0,0 +1,584 @@
<?php
/*
* PHP-Cookie (https://github.com/delight-im/PHP-Cookie)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Cookie;
/**
* Modern cookie management for PHP
*
* Cookies are a mechanism for storing data in the client's web browser and identifying returning clients on subsequent visits
*
* All cookies that have successfully been set will automatically be included in the global `$_COOKIE` array with future requests
*
* You can set a new cookie using the static method `Cookie::setcookie(...)` which is compatible to PHP's built-in `setcookie(...)` function
*
* Alternatively, you can construct an instance of this class, set properties individually, and finally call `save()`
*
* Note that cookies must always be set before the HTTP headers are sent to the client, i.e. before the actual output starts
*/
final class Cookie {
/** @var string name prefix indicating that the cookie must be from a secure origin (i.e. HTTPS) and the 'secure' attribute must be set */
const PREFIX_SECURE = '__Secure-';
/** @var string name prefix indicating that the 'domain' attribute must *not* be set, the 'path' attribute must be '/' and the effects of {@see PREFIX_SECURE} apply as well */
const PREFIX_HOST = '__Host-';
const HEADER_PREFIX = 'Set-Cookie: ';
const SAME_SITE_RESTRICTION_NONE = 'None';
const SAME_SITE_RESTRICTION_LAX = 'Lax';
const SAME_SITE_RESTRICTION_STRICT = 'Strict';
/** @var string the name of the cookie which is also the key for future accesses via `$_COOKIE[...]` */
private $name;
/** @var mixed|null the value of the cookie that will be stored on the client's machine */
private $value;
/** @var int the Unix timestamp indicating the time that the cookie will expire at, i.e. usually `time() + $seconds` */
private $expiryTime;
/** @var string the path on the server that the cookie will be valid for (including all sub-directories), e.g. an empty string for the current directory or `/` for the root directory */
private $path;
/** @var string|null the domain that the cookie will be valid for (including subdomains) or `null` for the current host (excluding subdomains) */
private $domain;
/** @var bool indicates that the cookie should be accessible through the HTTP protocol only and not through scripting languages */
private $httpOnly;
/** @var bool indicates that the cookie should be sent back by the client over secure HTTPS connections only */
private $secureOnly;
/** @var string|null indicates that the cookie should not be sent along with cross-site requests (either `null`, `None`, `Lax` or `Strict`) */
private $sameSiteRestriction;
/**
* Prepares a new cookie
*
* @param string $name the name of the cookie which is also the key for future accesses via `$_COOKIE[...]`
*/
public function __construct($name) {
$this->name = $name;
$this->value = null;
$this->expiryTime = 0;
$this->path = '/';
$this->domain = null;
$this->httpOnly = true;
$this->secureOnly = false;
$this->sameSiteRestriction = self::SAME_SITE_RESTRICTION_LAX;
}
/**
* Returns the name of the cookie
*
* @return string the name of the cookie which is also the key for future accesses via `$_COOKIE[...]`
*/
public function getName() {
return $this->name;
}
/**
* Returns the value of the cookie
*
* @return mixed|null the value of the cookie that will be stored on the client's machine
*/
public function getValue() {
return $this->value;
}
/**
* Sets the value for the cookie
*
* @param mixed|null $value the value of the cookie that will be stored on the client's machine
* @return static this instance for chaining
*/
public function setValue($value) {
$this->value = $value;
return $this;
}
/**
* Returns the expiry time of the cookie
*
* @return int the Unix timestamp indicating the time that the cookie will expire at, i.e. usually `time() + $seconds`
*/
public function getExpiryTime() {
return $this->expiryTime;
}
/**
* Sets the expiry time for the cookie
*
* @param int $expiryTime the Unix timestamp indicating the time that the cookie will expire at, i.e. usually `time() + $seconds`
* @return static this instance for chaining
*/
public function setExpiryTime($expiryTime) {
$this->expiryTime = $expiryTime;
return $this;
}
/**
* Returns the maximum age of the cookie (i.e. the remaining lifetime)
*
* @return int the maximum age of the cookie in seconds
*/
public function getMaxAge() {
return $this->expiryTime - \time();
}
/**
* Sets the expiry time for the cookie based on the specified maximum age (i.e. the remaining lifetime)
*
* @param int $maxAge the maximum age for the cookie in seconds
* @return static this instance for chaining
*/
public function setMaxAge($maxAge) {
$this->expiryTime = \time() + $maxAge;
return $this;
}
/**
* Returns the path of the cookie
*
* @return string the path on the server that the cookie will be valid for (including all sub-directories), e.g. an empty string for the current directory or `/` for the root directory
*/
public function getPath() {
return $this->path;
}
/**
* Sets the path for the cookie
*
* @param string $path the path on the server that the cookie will be valid for (including all sub-directories), e.g. an empty string for the current directory or `/` for the root directory
* @return static this instance for chaining
*/
public function setPath($path) {
$this->path = $path;
return $this;
}
/**
* Returns the domain of the cookie
*
* @return string|null the domain that the cookie will be valid for (including subdomains) or `null` for the current host (excluding subdomains)
*/
public function getDomain() {
return $this->domain;
}
/**
* Sets the domain for the cookie
*
* @param string|null $domain the domain that the cookie will be valid for (including subdomains) or `null` for the current host (excluding subdomains)
* @return static this instance for chaining
*/
public function setDomain($domain = null) {
$this->domain = self::normalizeDomain($domain);
return $this;
}
/**
* Returns whether the cookie should be accessible through HTTP only
*
* @return bool whether the cookie should be accessible through the HTTP protocol only and not through scripting languages
*/
public function isHttpOnly() {
return $this->httpOnly;
}
/**
* Sets whether the cookie should be accessible through HTTP only
*
* @param bool $httpOnly indicates that the cookie should be accessible through the HTTP protocol only and not through scripting languages
* @return static this instance for chaining
*/
public function setHttpOnly($httpOnly) {
$this->httpOnly = $httpOnly;
return $this;
}
/**
* Returns whether the cookie should be sent over HTTPS only
*
* @return bool whether the cookie should be sent back by the client over secure HTTPS connections only
*/
public function isSecureOnly() {
return $this->secureOnly;
}
/**
* Sets whether the cookie should be sent over HTTPS only
*
* @param bool $secureOnly indicates that the cookie should be sent back by the client over secure HTTPS connections only
* @return static this instance for chaining
*/
public function setSecureOnly($secureOnly) {
$this->secureOnly = $secureOnly;
return $this;
}
/**
* Returns the same-site restriction of the cookie
*
* @return string|null whether the cookie should not be sent along with cross-site requests (either `null`, `None`, `Lax` or `Strict`)
*/
public function getSameSiteRestriction() {
return $this->sameSiteRestriction;
}
/**
* Sets the same-site restriction for the cookie
*
* @param string|null $sameSiteRestriction indicates that the cookie should not be sent along with cross-site requests (either `null`, `None`, `Lax` or `Strict`)
* @return static this instance for chaining
*/
public function setSameSiteRestriction($sameSiteRestriction) {
$this->sameSiteRestriction = $sameSiteRestriction;
return $this;
}
/**
* Saves the cookie
*
* @return bool whether the cookie header has successfully been sent (and will *probably* cause the client to set the cookie)
*/
public function save() {
return self::addHttpHeader((string) $this);
}
/**
* Saves the cookie and immediately creates the corresponding variable in the superglobal `$_COOKIE` array
*
* The variable would otherwise only be available starting from the next HTTP request
*
* @return bool whether the cookie header has successfully been sent (and will *probably* cause the client to set the cookie)
*/
public function saveAndSet() {
$_COOKIE[$this->name] = $this->value;
return $this->save();
}
/**
* Deletes the cookie
*
* @return bool whether the cookie header has successfully been sent (and will *probably* cause the client to delete the cookie)
*/
public function delete() {
// create a temporary copy of this cookie so that it isn't corrupted
$copiedCookie = clone $this;
// set the copied cookie's value to an empty string which internally sets the required options for a deletion
$copiedCookie->setValue('');
// save the copied "deletion" cookie
return $copiedCookie->save();
}
/**
* Deletes the cookie and immediately removes the corresponding variable from the superglobal `$_COOKIE` array
*
* The variable would otherwise only be deleted at the start of the next HTTP request
*
* @return bool whether the cookie header has successfully been sent (and will *probably* cause the client to delete the cookie)
*/
public function deleteAndUnset() {
unset($_COOKIE[$this->name]);
return $this->delete();
}
public function __toString() {
return self::buildCookieHeader($this->name, $this->value, $this->expiryTime, $this->path, $this->domain, $this->secureOnly, $this->httpOnly, $this->sameSiteRestriction);
}
/**
* Sets a new cookie in a way compatible to PHP's `setcookie(...)` function
*
* @param string $name the name of the cookie which is also the key for future accesses via `$_COOKIE[...]`
* @param mixed|null $value the value of the cookie that will be stored on the client's machine
* @param int $expiryTime the Unix timestamp indicating the time that the cookie will expire at, i.e. usually `time() + $seconds`
* @param string|null $path the path on the server that the cookie will be valid for (including all sub-directories), e.g. an empty string for the current directory or `/` for the root directory
* @param string|null $domain the domain that the cookie will be valid for (including subdomains) or `null` for the current host (excluding subdomains)
* @param bool $secureOnly indicates that the cookie should be sent back by the client over secure HTTPS connections only
* @param bool $httpOnly indicates that the cookie should be accessible through the HTTP protocol only and not through scripting languages
* @param string|null $sameSiteRestriction indicates that the cookie should not be sent along with cross-site requests (either `null`, `None`, `Lax` or `Strict`)
* @return bool whether the cookie header has successfully been sent (and will *probably* cause the client to set the cookie)
*/
public static function setcookie($name, $value = null, $expiryTime = 0, $path = null, $domain = null, $secureOnly = false, $httpOnly = false, $sameSiteRestriction = null) {
return self::addHttpHeader(
self::buildCookieHeader($name, $value, $expiryTime, $path, $domain, $secureOnly, $httpOnly, $sameSiteRestriction)
);
}
/**
* Builds the HTTP header that can be used to set a cookie with the specified options
*
* @param string $name the name of the cookie which is also the key for future accesses via `$_COOKIE[...]`
* @param mixed|null $value the value of the cookie that will be stored on the client's machine
* @param int $expiryTime the Unix timestamp indicating the time that the cookie will expire at, i.e. usually `time() + $seconds`
* @param string|null $path the path on the server that the cookie will be valid for (including all sub-directories), e.g. an empty string for the current directory or `/` for the root directory
* @param string|null $domain the domain that the cookie will be valid for (including subdomains) or `null` for the current host (excluding subdomains)
* @param bool $secureOnly indicates that the cookie should be sent back by the client over secure HTTPS connections only
* @param bool $httpOnly indicates that the cookie should be accessible through the HTTP protocol only and not through scripting languages
* @param string|null $sameSiteRestriction indicates that the cookie should not be sent along with cross-site requests (either `null`, `None`, `Lax` or `Strict`)
* @return string the HTTP header
*/
public static function buildCookieHeader($name, $value = null, $expiryTime = 0, $path = null, $domain = null, $secureOnly = false, $httpOnly = false, $sameSiteRestriction = null) {
if (self::isNameValid($name)) {
$name = (string) $name;
}
else {
return null;
}
if (self::isExpiryTimeValid($expiryTime)) {
$expiryTime = (int) $expiryTime;
}
else {
return null;
}
$forceShowExpiry = false;
if (\is_null($value) || $value === false || $value === '') {
$value = 'deleted';
$expiryTime = 0;
$forceShowExpiry = true;
}
$maxAgeStr = self::formatMaxAge($expiryTime, $forceShowExpiry);
$expiryTimeStr = self::formatExpiryTime($expiryTime, $forceShowExpiry);
$headerStr = self::HEADER_PREFIX . $name . '=' . \urlencode($value);
if (!\is_null($expiryTimeStr)) {
$headerStr .= '; expires=' . $expiryTimeStr;
}
// The `Max-Age` property is supported on PHP 5.5+ only (https://bugs.php.net/bug.php?id=23955).
if (\PHP_VERSION_ID >= 50500) {
if (!\is_null($maxAgeStr)) {
$headerStr .= '; Max-Age=' . $maxAgeStr;
}
}
if (!empty($path) || $path === 0) {
$headerStr .= '; path=' . $path;
}
if (!empty($domain) || $domain === 0) {
$headerStr .= '; domain=' . $domain;
}
if ($secureOnly) {
$headerStr .= '; secure';
}
if ($httpOnly) {
$headerStr .= '; httponly';
}
if ($sameSiteRestriction === self::SAME_SITE_RESTRICTION_NONE) {
// if the 'secure' attribute is missing
if (!$secureOnly) {
\trigger_error('When the \'SameSite\' attribute is set to \'None\', the \'secure\' attribute should be set as well', \E_USER_WARNING);
}
$headerStr .= '; SameSite=None';
}
elseif ($sameSiteRestriction === self::SAME_SITE_RESTRICTION_LAX) {
$headerStr .= '; SameSite=Lax';
}
elseif ($sameSiteRestriction === self::SAME_SITE_RESTRICTION_STRICT) {
$headerStr .= '; SameSite=Strict';
}
return $headerStr;
}
/**
* Parses the given cookie header and returns an equivalent cookie instance
*
* @param string $cookieHeader the cookie header to parse
* @return \Delight\Cookie\Cookie|null the cookie instance or `null`
*/
public static function parse($cookieHeader) {
if (empty($cookieHeader)) {
return null;
}
if (\preg_match('/^' . self::HEADER_PREFIX . '(.*?)=(.*?)(?:; (.*?))?$/i', $cookieHeader, $matches)) {
$cookie = new self($matches[1]);
$cookie->setPath(null);
$cookie->setHttpOnly(false);
$cookie->setValue(
\urldecode($matches[2])
);
$cookie->setSameSiteRestriction(null);
if (\count($matches) >= 4) {
$attributes = \explode('; ', $matches[3]);
foreach ($attributes as $attribute) {
if (\strcasecmp($attribute, 'HttpOnly') === 0) {
$cookie->setHttpOnly(true);
}
elseif (\strcasecmp($attribute, 'Secure') === 0) {
$cookie->setSecureOnly(true);
}
elseif (\stripos($attribute, 'Expires=') === 0) {
$cookie->setExpiryTime((int) \strtotime(\substr($attribute, 8)));
}
elseif (\stripos($attribute, 'Domain=') === 0) {
$cookie->setDomain(\substr($attribute, 7));
}
elseif (\stripos($attribute, 'Path=') === 0) {
$cookie->setPath(\substr($attribute, 5));
}
elseif (\stripos($attribute, 'SameSite=') === 0) {
$cookie->setSameSiteRestriction(\substr($attribute, 9));
}
}
}
return $cookie;
}
else {
return null;
}
}
/**
* Checks whether a cookie with the specified name exists
*
* @param string $name the name of the cookie to check
* @return bool whether there is a cookie with the specified name
*/
public static function exists($name) {
return isset($_COOKIE[$name]);
}
/**
* Returns the value from the requested cookie or, if not found, the specified default value
*
* @param string $name the name of the cookie to retrieve the value from
* @param mixed $defaultValue the default value to return if the requested cookie cannot be found
* @return mixed the value from the requested cookie or the default value
*/
public static function get($name, $defaultValue = null) {
if (isset($_COOKIE[$name])) {
return $_COOKIE[$name];
}
else {
return $defaultValue;
}
}
private static function isNameValid($name) {
$name = (string) $name;
// The name of a cookie must not be empty on PHP 7+ (https://bugs.php.net/bug.php?id=69523).
if ($name !== '' || \PHP_VERSION_ID < 70000) {
if (!\preg_match('/[=,; \\t\\r\\n\\013\\014]/', $name)) {
return true;
}
}
return false;
}
private static function isExpiryTimeValid($expiryTime) {
return \is_numeric($expiryTime) || \is_null($expiryTime) || \is_bool($expiryTime);
}
private static function calculateMaxAge($expiryTime) {
if ($expiryTime === 0) {
return 0;
}
else {
$maxAge = $expiryTime - \time();
// The value of the `Max-Age` property must not be negative on PHP 7.0.19+ (< 7.1) and
// PHP 7.1.5+ (https://bugs.php.net/bug.php?id=72071).
if ((\PHP_VERSION_ID >= 70019 && \PHP_VERSION_ID < 70100) || \PHP_VERSION_ID >= 70105) {
if ($maxAge < 0) {
$maxAge = 0;
}
}
return $maxAge;
}
}
private static function formatExpiryTime($expiryTime, $forceShow = false) {
if ($expiryTime > 0 || $forceShow) {
if ($forceShow) {
$expiryTime = 1;
}
return \gmdate('D, d-M-Y H:i:s T', $expiryTime);
}
else {
return null;
}
}
private static function formatMaxAge($expiryTime, $forceShow = false) {
if ($expiryTime > 0 || $forceShow) {
return (string) self::calculateMaxAge($expiryTime);
}
else {
return null;
}
}
private static function normalizeDomain($domain = null) {
// make sure that the domain is a string
$domain = (string) $domain;
// if the cookie should be valid for the current host only
if ($domain === '') {
// no need for further normalization
return null;
}
// if the provided domain is actually an IP address
if (\filter_var($domain, \FILTER_VALIDATE_IP) !== false) {
// let the cookie be valid for the current host
return null;
}
// for local hostnames (which either have no dot at all or a leading dot only)
if (\strpos($domain, '.') === false || \strrpos($domain, '.') === 0) {
// let the cookie be valid for the current host while ensuring maximum compatibility
return null;
}
// unless the domain already starts with a dot
if ($domain[0] !== '.') {
// prepend a dot for maximum compatibility (e.g. with RFC 2109)
$domain = '.' . $domain;
}
// return the normalized domain
return $domain;
}
private static function addHttpHeader($header) {
if (!\headers_sent()) {
if (!empty($header)) {
\header($header, false);
return true;
}
}
return false;
}
}
+170
View File
@@ -0,0 +1,170 @@
<?php
/*
* PHP-Cookie (https://github.com/delight-im/PHP-Cookie)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Cookie;
use Delight\Http\ResponseHeader;
/**
* Session management with improved cookie handling
*
* You can start a session using the static method `Session::start(...)` which is compatible to PHP's built-in `session_start()` function
*
* Note that sessions must always be started before the HTTP headers are sent to the client, i.e. before the actual output starts
*/
final class Session {
private function __construct() { }
/**
* Starts or resumes a session in a way compatible to PHP's built-in `session_start()` function
*
* @param string|null $sameSiteRestriction indicates that the cookie should not be sent along with cross-site requests (either `null`, `None`, `Lax` or `Strict`)
*/
public static function start($sameSiteRestriction = Cookie::SAME_SITE_RESTRICTION_LAX) {
// run PHP's built-in equivalent
\session_start();
// intercept the cookie header (if any) and rewrite it
self::rewriteCookieHeader($sameSiteRestriction);
}
/**
* Returns or sets the ID of the current session
*
* In order to change the current session ID, pass the new ID as the only argument to this method
*
* Please note that there is rarely a need for the version of this method that *updates* the ID
*
* For most purposes, you may find the method `regenerate` from this same class more helpful
*
* @param string|null $newId (optional) a new session ID to replace the current session ID
* @return string the (old) session ID or an empty string
*/
public static function id($newId = null) {
if ($newId === null) {
return \session_id();
}
else {
return \session_id($newId);
}
}
/**
* Re-generates the session ID in a way compatible to PHP's built-in `session_regenerate_id()` function
*
* @param bool $deleteOldSession whether to delete the old session or not
* @param string|null $sameSiteRestriction indicates that the cookie should not be sent along with cross-site requests (either `null`, `None`, `Lax` or `Strict`)
*/
public static function regenerate($deleteOldSession = false, $sameSiteRestriction = Cookie::SAME_SITE_RESTRICTION_LAX) {
// run PHP's built-in equivalent
\session_regenerate_id($deleteOldSession);
// intercept the cookie header (if any) and rewrite it
self::rewriteCookieHeader($sameSiteRestriction);
}
/**
* Checks whether a value for the specified key exists in the session
*
* @param string $key the key to check
* @return bool whether there is a value for the specified key or not
*/
public static function has($key) {
return isset($_SESSION[$key]);
}
/**
* Returns the requested value from the session or, if not found, the specified default value
*
* @param string $key the key to retrieve the value for
* @param mixed $defaultValue the default value to return if the requested value cannot be found
* @return mixed the requested value or the default value
*/
public static function get($key, $defaultValue = null) {
if (isset($_SESSION[$key])) {
return $_SESSION[$key];
}
else {
return $defaultValue;
}
}
/**
* Returns the requested value and removes it from the session
*
* This is identical to calling `get` first and then `remove` for the same key
*
* @param string $key the key to retrieve and remove the value for
* @param mixed $defaultValue the default value to return if the requested value cannot be found
* @return mixed the requested value or the default value
*/
public static function take($key, $defaultValue = null) {
if (isset($_SESSION[$key])) {
$value = $_SESSION[$key];
unset($_SESSION[$key]);
return $value;
}
else {
return $defaultValue;
}
}
/**
* Sets the value for the specified key to the given value
*
* Any data that already exists for the specified key will be overwritten
*
* @param string $key the key to set the value for
* @param mixed $value the value to set
*/
public static function set($key, $value) {
$_SESSION[$key] = $value;
}
/**
* Removes the value for the specified key from the session
*
* @param string $key the key to remove the value for
*/
public static function delete($key) {
unset($_SESSION[$key]);
}
/**
* Intercepts and rewrites the session cookie header
*
* @param string|null $sameSiteRestriction indicates that the cookie should not be sent along with cross-site requests (either `null`, `None`, `Lax` or `Strict`)
*/
private static function rewriteCookieHeader($sameSiteRestriction = Cookie::SAME_SITE_RESTRICTION_LAX) {
// get and remove the original cookie header set by PHP
$originalCookieHeader = ResponseHeader::take('Set-Cookie', \session_name() . '=');
// if a cookie header has been found
if (isset($originalCookieHeader)) {
// parse it into a cookie instance
$parsedCookie = Cookie::parse($originalCookieHeader);
// if the cookie has successfully been parsed
if (isset($parsedCookie)) {
// apply the supplied same-site restriction
$parsedCookie->setSameSiteRestriction($sameSiteRestriction);
if ($parsedCookie->getSameSiteRestriction() === Cookie::SAME_SITE_RESTRICTION_NONE && !$parsedCookie->isSecureOnly()) {
\trigger_error('You may have to enable the \'session.cookie_secure\' directive in the configuration in \'php.ini\' or via the \'ini_set\' function', \E_USER_WARNING);
}
// save the cookie
$parsedCookie->save();
}
}
}
}
+320
View File
@@ -0,0 +1,320 @@
<?php
/*
* PHP-Cookie (https://github.com/delight-im/PHP-Cookie)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
// enable error reporting
\error_reporting(\E_ALL);
\ini_set('display_errors', 'stdout');
\header('Content-type: text/plain; charset=utf-8');
require __DIR__.'/../vendor/autoload.php';
/* BEGIN TEST COOKIES */
// start output buffering
\ob_start();
\testCookie(null);
\testCookie(false);
\testCookie('');
\testCookie(0);
\testCookie('hello');
\testCookie('hello', false);
\testCookie('hello', true);
\testCookie('hello', null);
\testCookie('hello', '');
\testCookie('hello', 0);
\testCookie('hello', 1);
\testCookie('hello', 'world');
\testCookie('hello', 123);
\testCookie(123, 'world');
\testCookie('greeting', '¡Buenos días!');
\testCookie('¡Buenos días!', 'greeting');
\testCookie('%a|b}c_$d!f"g-h(i)j$', 'value value value');
\testCookie('%a|b}c_$d!f"g-h(i)j$', '%a|b}c_$d!f"g-h(i)j$');
\testCookie('hello', 'world', '!');
\testCookie('hello', 'world', '');
\testCookie('hello', 'world', false);
\testCookie('hello', 'world', null);
\testCookie('hello', 'world', true);
\testCookie('hello', 'world', 0);
\testCookie('hello', 'world', '');
\testCookie('hello', 'world', -1);
\testCookie('hello', 'world', 234234);
\testCookie('hello', 'world', \time() + 60 * 60 * 24);
\testCookie('hello', 'world', \time() + 60 * 60 * 24 * 30);
\testCookie('hello', 'world', \time() + 86400, null);
\testCookie('hello', 'world', \time() + 86400, false);
\testCookie('hello', 'world', \time() + 86400, true);
\testCookie('hello', 'world', \time() + 86400, 0);
\testCookie('hello', 'world', \time() + 86400, '');
\testCookie('hello', 'world', \time() + 86400, '/');
\testCookie('hello', 'world', \time() + 86400, '/foo');
\testCookie('hello', 'world', \time() + 86400, '/foo/');
\testCookie('hello', 'world', \time() + 86400, '/buenos/días/');
\testCookie('hello', 'world', \time() + 86400, '/buenos días/');
\testCookie('hello', 'world', \time() + 86400, '/foo/', null);
\testCookie('hello', 'world', \time() + 86400, '/foo/', false);
\testCookie('hello', 'world', \time() + 86400, '/foo/', true);
\testCookie('hello', 'world', \time() + 86400, '/foo/', 0);
\testCookie('hello', 'world', \time() + 86400, '/foo/', '');
\testCookie('hello', 'world', \time() + 86400, '/foo/', 'example.com');
\testCookie('hello', 'world', \time() + 86400, '/foo/', '.example.com');
\testCookie('hello', 'world', \time() + 86400, '/foo/', 'www.example.com');
\testCookie('hello', 'world', \time() + 86400, '/foo/', 'días.example.com');
\testCookie('hello', 'world', \time() + 86400, '/foo/', 'localhost');
\testCookie('hello', 'world', \time() + 86400, '/foo/', '127.0.0.1');
\testCookie('hello', 'world', \time() + 86400, '/foo/', 'example.com', null);
\testCookie('hello', 'world', \time() + 86400, '/foo/', 'example.com', false);
\testCookie('hello', 'world', \time() + 86400, '/foo/', 'example.com', true);
\testCookie('hello', 'world', \time() + 86400, '/foo/', 'example.com', 0);
\testCookie('hello', 'world', \time() + 86400, '/foo/', 'example.com', '');
\testCookie('hello', 'world', \time() + 86400, '/foo/', 'example.com', 'hello');
\testCookie('hello', 'world', \time() + 86400, '/foo/', 'example.com', 7);
\testCookie('hello', 'world', \time() + 86400, '/foo/', 'example.com', -7);
\testCookie('hello', 'world', \time() + 86400, '/foo/', 'example.com', false, null);
\testCookie('hello', 'world', \time() + 86400, '/foo/', 'example.com', false, false);
\testCookie('hello', 'world', \time() + 86400, '/foo/', 'example.com', false, true);
\testCookie('hello', 'world', \time() + 86400, '/foo/', 'example.com', false, 0);
\testCookie('hello', 'world', \time() + 86400, '/foo/', 'example.com', false, '');
\testCookie('hello', 'world', \time() + 86400, '/foo/', 'example.com', false, 'hello');
\testCookie('hello', 'world', \time() + 86400, '/foo/', 'example.com', false, 5);
\testCookie('hello', 'world', \time() + 86400, '/foo/', 'example.com', false, -5);
\testCookie('hello', 'world', \time() + 86400, '/foo/', 'example.com', true, null);
\testCookie('hello', 'world', \time() + 86400, '/foo/', 'example.com', true, false);
\testCookie('hello', 'world', \time() + 86400, '/foo/', 'example.com', true, true);
\testCookie('hello', 'world', \time() + 86400, '/foo/', 'example.com', true, 0);
\testCookie('hello', 'world', \time() + 86400, '/foo/', 'example.com', true, '');
\testCookie('hello', 'world', \time() + 86400, '/foo/', 'example.com', true, 'hello');
\testCookie('hello', 'world', \time() + 86400, '/foo/', 'example.com', true, 5);
\testCookie('hello', 'world', \time() + 86400, '/foo/', 'example.com', true, -5);
\testCookie('TestCookie', 'php.net');
\testCookie('TestCookie', 'php.net', \time() + 3600);
\testCookie('TestCookie', 'php.net', \time() + 3600, '/~rasmus/', 'example.com', 1);
\testCookie('TestCookie', '', \time() - 3600);
\testCookie('TestCookie', '', \time() - 3600, '/~rasmus/', 'example.com', 1);
\testCookie('cookie[three]', 'cookiethree');
\testCookie('cookie[two]', 'cookietwo');
\testCookie('cookie[one]', 'cookieone');
\testEqual((new \Delight\Cookie\Cookie('SID'))->setValue('31d4d96e407aad42'), 'Set-Cookie: SID=31d4d96e407aad42; path=/; httponly; SameSite=Lax');
@\testEqual((new \Delight\Cookie\Cookie('SID'))->setValue('31d4d96e407aad42')->setSameSiteRestriction('None'), 'Set-Cookie: SID=31d4d96e407aad42; path=/; httponly; SameSite=None');
\testEqual((new \Delight\Cookie\Cookie('SID'))->setValue('31d4d96e407aad42')->setSameSiteRestriction('None')->setSecureOnly(true), 'Set-Cookie: SID=31d4d96e407aad42; path=/; secure; httponly; SameSite=None');
@\testEqual((new \Delight\Cookie\Cookie('SID'))->setValue('31d4d96e407aad42')->setDomain('localhost')->setSameSiteRestriction('None'), 'Set-Cookie: SID=31d4d96e407aad42; path=/; httponly; SameSite=None');
\testEqual((new \Delight\Cookie\Cookie('SID'))->setValue('31d4d96e407aad42')->setDomain('localhost')->setSameSiteRestriction('None')->setSecureOnly(true), 'Set-Cookie: SID=31d4d96e407aad42; path=/; secure; httponly; SameSite=None');
\testEqual((new \Delight\Cookie\Cookie('SID'))->setValue('31d4d96e407aad42')->setSameSiteRestriction('Strict'), 'Set-Cookie: SID=31d4d96e407aad42; path=/; httponly; SameSite=Strict');
\testEqual((new \Delight\Cookie\Cookie('SID'))->setValue('31d4d96e407aad42')->setDomain('localhost')->setSameSiteRestriction('Strict'), 'Set-Cookie: SID=31d4d96e407aad42; path=/; httponly; SameSite=Strict');
\testEqual((new \Delight\Cookie\Cookie('key'))->setValue('value')->setDomain('localhost'), 'Set-Cookie: key=value; path=/; httponly; SameSite=Lax');
\testEqual((new \Delight\Cookie\Cookie('key'))->setValue('value')->setDomain('.localhost'), 'Set-Cookie: key=value; path=/; httponly; SameSite=Lax');
\testEqual((new \Delight\Cookie\Cookie('key'))->setValue('value')->setDomain('127.0.0.1'), 'Set-Cookie: key=value; path=/; httponly; SameSite=Lax');
\testEqual((new \Delight\Cookie\Cookie('key'))->setValue('value')->setDomain('.local'), 'Set-Cookie: key=value; path=/; httponly; SameSite=Lax');
\testEqual((new \Delight\Cookie\Cookie('key'))->setValue('value')->setDomain('example.com'), 'Set-Cookie: key=value; path=/; domain=.example.com; httponly; SameSite=Lax');
\testEqual((new \Delight\Cookie\Cookie('key'))->setValue('value')->setDomain('.example.com'), 'Set-Cookie: key=value; path=/; domain=.example.com; httponly; SameSite=Lax');
\testEqual((new \Delight\Cookie\Cookie('key'))->setValue('value')->setDomain('www.example.com'), 'Set-Cookie: key=value; path=/; domain=.www.example.com; httponly; SameSite=Lax');
\testEqual((new \Delight\Cookie\Cookie('key'))->setValue('value')->setDomain('.www.example.com'), 'Set-Cookie: key=value; path=/; domain=.www.example.com; httponly; SameSite=Lax');
\testEqual((new \Delight\Cookie\Cookie('key'))->setValue('value')->setDomain('blog.example.com'), 'Set-Cookie: key=value; path=/; domain=.blog.example.com; httponly; SameSite=Lax');
\testEqual((new \Delight\Cookie\Cookie('key'))->setValue('value')->setDomain('.blog.example.com'), 'Set-Cookie: key=value; path=/; domain=.blog.example.com; httponly; SameSite=Lax');
\testEqual(\Delight\Cookie\Cookie::parse('Set-Cookie: SID'), '');
\testEqual(\Delight\Cookie\Cookie::parse('Set-Cookie: SID=31d4d96e407aad42'), 'Set-Cookie: SID=31d4d96e407aad42');
\testEqual(\Delight\Cookie\Cookie::parse('Set-Cookie: SID=31d4d96e407aad42; path=/; httponly'), 'Set-Cookie: SID=31d4d96e407aad42; path=/; httponly');
@\testEqual(\Delight\Cookie\Cookie::parse('Set-Cookie: SID=31d4d96e407aad42; path=/; httponly; SameSite=None'), 'Set-Cookie: SID=31d4d96e407aad42; path=/; httponly; SameSite=None');
\testEqual(\Delight\Cookie\Cookie::parse('Set-Cookie: SID=31d4d96e407aad42; path=/; secure; httponly; SameSite=None'), 'Set-Cookie: SID=31d4d96e407aad42; path=/; secure; httponly; SameSite=None');
\testEqual(\Delight\Cookie\Cookie::parse('Set-Cookie: SID=31d4d96e407aad42; path=/; httponly; SameSite=Strict'), 'Set-Cookie: SID=31d4d96e407aad42; path=/; httponly; SameSite=Strict');
\setcookie('hello', 'world', \time() + 86400, '/foo/', 'example.com', true, true);
$cookie = \Delight\Cookie\Cookie::parse(\Delight\Http\ResponseHeader::take('Set-Cookie'));
\testEqual($cookie, (new \Delight\Cookie\Cookie('hello'))->setValue('world')->setMaxAge(86400)->setPath('/foo/')->setDomain('example.com')->setHttpOnly(true)->setSecureOnly(true)->setSameSiteRestriction(null));
($cookie->getName() === 'hello') or \fail(__LINE__);
($cookie->getValue() === 'world') or \fail(__LINE__);
($cookie->getExpiryTime() === \time() + 86400) or \fail(__LINE__);
($cookie->getMaxAge() === 86400) or \fail(__LINE__);
($cookie->getPath() === '/foo/') or \fail(__LINE__);
($cookie->getDomain() === '.example.com') or \fail(__LINE__);
($cookie->isHttpOnly() === true) or \fail(__LINE__);
($cookie->isSecureOnly() === true) or \fail(__LINE__);
($cookie->getSameSiteRestriction() === null) or \fail(__LINE__);
\testEqual(\Delight\Cookie\Cookie::exists('SESSID'), isset($_COOKIE['SESSID']));
\testEqual(\Delight\Cookie\Cookie::exists('other'), isset($_COOKIE['other']));
\testEqual(\Delight\Cookie\Cookie::get('SESSID'), (isset($_COOKIE['SESSID']) ? $_COOKIE['SESSID'] : null));
\testEqual(\Delight\Cookie\Cookie::get('SESSID', 42), (isset($_COOKIE['SESSID']) ? $_COOKIE['SESSID'] : 42));
\testEqual(\Delight\Cookie\Cookie::get('other'), (isset($_COOKIE['other']) ? $_COOKIE['other'] : null));
\testEqual(\Delight\Cookie\Cookie::get('other', 42), (isset($_COOKIE['other']) ? $_COOKIE['other'] : 42));
!isset($_COOKIE['cefa389e458b']) or \fail(__LINE__);
(new \Delight\Cookie\Cookie('cefa389e458b'))->setValue('a858c4a03224')->save();
\testEqual(\Delight\Http\ResponseHeader::take('Set-Cookie'), 'Set-Cookie: cefa389e458b=a858c4a03224; path=/; httponly; SameSite=Lax');
!isset($_COOKIE['cefa389e458b']) or \fail(__LINE__);
!isset($_COOKIE['c0f3a2ea81aa']) or \fail(__LINE__);
(new \Delight\Cookie\Cookie('c0f3a2ea81aa'))->setValue('dabf2c1490a5')->setDomain('example.com')->save();
\testEqual(\Delight\Http\ResponseHeader::take('Set-Cookie'), 'Set-Cookie: c0f3a2ea81aa=dabf2c1490a5; path=/; domain=.example.com; httponly; SameSite=Lax');
!isset($_COOKIE['c0f3a2ea81aa']) or \fail(__LINE__);
!isset($_COOKIE['c19718a89afd']) or \fail(__LINE__);
(new \Delight\Cookie\Cookie('c19718a89afd'))->setValue('f638cfcf49a8')->saveAndSet();
\testEqual(\Delight\Http\ResponseHeader::take('Set-Cookie'), 'Set-Cookie: c19718a89afd=f638cfcf49a8; path=/; httponly; SameSite=Lax');
(isset($_COOKIE['c19718a89afd']) && $_COOKIE['c19718a89afd'] === 'f638cfcf49a8') or \fail(__LINE__);
!isset($_COOKIE['b377b18ca9e6']) or \fail(__LINE__);
(new \Delight\Cookie\Cookie('b377b18ca9e6'))->setValue('dbeaebb92e43')->setDomain('example.com')->saveAndSet();
\testEqual(\Delight\Http\ResponseHeader::take('Set-Cookie'), 'Set-Cookie: b377b18ca9e6=dbeaebb92e43; path=/; domain=.example.com; httponly; SameSite=Lax');
(isset($_COOKIE['b377b18ca9e6']) && $_COOKIE['b377b18ca9e6'] === 'dbeaebb92e43') or \fail(__LINE__);
$_COOKIE['a8a78153b857'] = 'ed2e8ff8629f';
(isset($_COOKIE['a8a78153b857']) && $_COOKIE['a8a78153b857'] === 'ed2e8ff8629f') or \fail(__LINE__);
(new \Delight\Cookie\Cookie('a8a78153b857'))->delete();
\testEqual(\Delight\Http\ResponseHeader::take('Set-Cookie'), 'Set-Cookie: a8a78153b857=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT; Max-Age=0; path=/; httponly; SameSite=Lax');
(isset($_COOKIE['a8a78153b857']) && $_COOKIE['a8a78153b857'] === 'ed2e8ff8629f') or \fail(__LINE__);
unset($_COOKIE['a8a78153b857']);
$_COOKIE['c5dd45883e4b'] = 'b42f7c9de3dd';
(isset($_COOKIE['c5dd45883e4b']) && $_COOKIE['c5dd45883e4b'] === 'b42f7c9de3dd') or \fail(__LINE__);
(new \Delight\Cookie\Cookie('c5dd45883e4b'))->setDomain('example.com')->delete();
\testEqual(\Delight\Http\ResponseHeader::take('Set-Cookie'), 'Set-Cookie: c5dd45883e4b=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT; Max-Age=0; path=/; domain=.example.com; httponly; SameSite=Lax');
(isset($_COOKIE['c5dd45883e4b']) && $_COOKIE['c5dd45883e4b'] === 'b42f7c9de3dd') or \fail(__LINE__);
unset($_COOKIE['c5dd45883e4b']);
$_COOKIE['ed050c7d7e62'] = 'a8b362c78a04';
(isset($_COOKIE['ed050c7d7e62']) && $_COOKIE['ed050c7d7e62'] === 'a8b362c78a04') or \fail(__LINE__);
(new \Delight\Cookie\Cookie('ed050c7d7e62'))->deleteAndUnset();
\testEqual(\Delight\Http\ResponseHeader::take('Set-Cookie'), 'Set-Cookie: ed050c7d7e62=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT; Max-Age=0; path=/; httponly; SameSite=Lax');
!isset($_COOKIE['ed050c7d7e62']) or \fail(__LINE__);
unset($_COOKIE['ed050c7d7e62']);
$_COOKIE['c8f13ff2a8ad'] = 'cd0a70b224e8';
(isset($_COOKIE['c8f13ff2a8ad']) && $_COOKIE['c8f13ff2a8ad'] === 'cd0a70b224e8') or \fail(__LINE__);
(new \Delight\Cookie\Cookie('c8f13ff2a8ad'))->setDomain('example.com')->deleteAndUnset();
\testEqual(\Delight\Http\ResponseHeader::take('Set-Cookie'), 'Set-Cookie: c8f13ff2a8ad=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT; Max-Age=0; path=/; domain=.example.com; httponly; SameSite=Lax');
!isset($_COOKIE['c8f13ff2a8ad']) or \fail(__LINE__);
unset($_COOKIE['c8f13ff2a8ad']);
/* END TEST COOKIES */
/* BEGIN TEST SESSION */
(isset($_SESSION) === false) or \fail(__LINE__);
(\Delight\Cookie\Session::id() === '') or \fail(__LINE__);
\Delight\Cookie\Session::start();
$sessionCookieReferenceHeader = \Delight\Http\ResponseHeader::take('Set-Cookie');
session_write_close();
\Delight\Cookie\Session::start(null);
\testEqual(\Delight\Http\ResponseHeader::take('Set-Cookie'), \str_replace('; SameSite=Lax', '', $sessionCookieReferenceHeader));
session_write_close();
@\Delight\Cookie\Session::start('None');
\testEqual(\Delight\Http\ResponseHeader::take('Set-Cookie'), \str_replace('; SameSite=Lax', '; SameSite=None', $sessionCookieReferenceHeader));
session_write_close();
\Delight\Cookie\Session::start('Lax');
\testEqual(\Delight\Http\ResponseHeader::take('Set-Cookie'), $sessionCookieReferenceHeader);
session_write_close();
\Delight\Cookie\Session::start('Strict');
\testEqual(\Delight\Http\ResponseHeader::take('Set-Cookie'), \str_replace('; SameSite=Lax', '; SameSite=Strict', $sessionCookieReferenceHeader));
session_write_close();
\Delight\Cookie\Session::start();
(isset($_SESSION) === true) or \fail(__LINE__);
(\Delight\Cookie\Session::id() !== '') or \fail(__LINE__);
$oldSessionId = \Delight\Cookie\Session::id();
\Delight\Cookie\Session::regenerate();
(\Delight\Cookie\Session::id() !== $oldSessionId) or \fail(__LINE__);
(\Delight\Cookie\Session::id() !== null) or \fail(__LINE__);
\session_unset();
(isset($_SESSION['key1']) === false) or \fail(__LINE__);
(\Delight\Cookie\Session::has('key1') === false) or \fail(__LINE__);
(\Delight\Cookie\Session::get('key1') === null) or \fail(__LINE__);
(\Delight\Cookie\Session::get('key1', 5) === 5) or \fail(__LINE__);
(\Delight\Cookie\Session::get('key1', 'monkey') === 'monkey') or \fail(__LINE__);
\Delight\Cookie\Session::set('key1', 'value1');
(isset($_SESSION['key1']) === true) or \fail(__LINE__);
(\Delight\Cookie\Session::has('key1') === true) or \fail(__LINE__);
(\Delight\Cookie\Session::get('key1') === 'value1') or \fail(__LINE__);
(\Delight\Cookie\Session::get('key1', 5) === 'value1') or \fail(__LINE__);
(\Delight\Cookie\Session::get('key1', 'monkey') === 'value1') or \fail(__LINE__);
(\Delight\Cookie\Session::take('key1') === 'value1') or \fail(__LINE__);
(\Delight\Cookie\Session::take('key1') === null) or \fail(__LINE__);
(\Delight\Cookie\Session::take('key1', 'value2') === 'value2') or \fail(__LINE__);
(isset($_SESSION['key1']) === false) or \fail(__LINE__);
(\Delight\Cookie\Session::has('key1') === false) or \fail(__LINE__);
\Delight\Cookie\Session::set('key2', 'value3');
(isset($_SESSION['key2']) === true) or \fail(__LINE__);
(\Delight\Cookie\Session::has('key2') === true) or \fail(__LINE__);
(\Delight\Cookie\Session::get('key2', 'value4') === 'value3') or \fail(__LINE__);
\Delight\Cookie\Session::delete('key2');
(\Delight\Cookie\Session::get('key2', 'value4') === 'value4') or \fail(__LINE__);
(\Delight\Cookie\Session::get('key2') === null) or \fail(__LINE__);
(\Delight\Cookie\Session::has('key2') === false) or \fail(__LINE__);
\session_destroy();
\Delight\Http\ResponseHeader::take('Set-Cookie');
/* END TEST SESSION */
echo 'ALL TESTS PASSED' . "\n";
function testCookie($name, $value = null, $expire = 0, $path = null, $domain = null, $secure = false, $httpOnly = false) {
$actualValue = \Delight\Cookie\Cookie::buildCookieHeader($name, $value, $expire, $path, $domain, $secure, $httpOnly);
if (\is_null($actualValue)) {
$expectedValue = @\simulateSetCookie($name, $value, $expire, $path, $domain, $secure, $httpOnly);
}
else {
$expectedValue = \simulateSetCookie($name, $value, $expire, $path, $domain, $secure, $httpOnly);
}
\testEqual($actualValue, $expectedValue);
}
function testEqual($actualValue, $expectedValue) {
$actualValue = (string) $actualValue;
$expectedValue = (string) $expectedValue;
echo '[';
echo $expectedValue;
echo ']';
echo "\n";
if (\strcasecmp($actualValue, $expectedValue) !== 0) {
echo 'FAILED: ';
echo '[';
echo $actualValue;
echo ']';
echo ' !== ';
echo '[';
echo $expectedValue;
echo ']';
echo "\n";
exit;
}
}
function simulateSetCookie($name, $value = null, $expire = 0, $path = null, $domain = null, $secure = false, $httpOnly = false) {
\setcookie($name, $value, $expire, $path, $domain, $secure, $httpOnly);
return \Delight\Http\ResponseHeader::take('Set-Cookie');
}
function fail($lineNumber) {
exit('Error in line ' . $lineNumber);
}
+13
View File
@@ -0,0 +1,13 @@
# editorconfig.org
root = true
[*]
charset = utf-8
indent_style = tab
trim_trailing_whitespace = true
end_of_line = lf
insert_final_newline = true
[*.md]
indent_style = space
indent_size = 4
+6
View File
@@ -0,0 +1,6 @@
# IntelliJ
.idea/
# Composer
vendor/
composer.phar
Vendored Executable
+21
View File
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) delight.im (https://www.delight.im/)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+316
View File
@@ -0,0 +1,316 @@
# PHP-DB
Safe and convenient SQL database access in a driver-agnostic way
## Features
* Convenient interface
* Write less code for the same tasks
* Safe against SQL injections through the use of prepared statements
* Data returned by the server has the correct native types (i.e. no stringified fields)
* Driver-agnostic support for multiple database systems
## Requirements
* PHP 5.6.0+
* PDO extension
## Installation
1. Include the library via Composer [[?]](https://github.com/delight-im/Knowledge/blob/master/Composer%20(PHP).md):
```
$ composer require delight-im/db
```
1. Include the Composer autoloader:
```php
require __DIR__ . '/vendor/autoload.php';
```
## Usage
### Connecting to a database
* From a dynamic database configuration:
```php
$dataSource = new \Delight\Db\PdoDataSource('mysql'); // see "Available drivers for database systems" below
$dataSource->setHostname('localhost');
$dataSource->setPort(3306);
$dataSource->setDatabaseName('my-database');
$dataSource->setCharset('utf8mb4');
$dataSource->setUsername('my-username');
$dataSource->setPassword('my-password');
$db = \Delight\Db\PdoDatabase::fromDataSource($dataSource);
```
This is the most convenient way to establish the database connection since the correct DSN will be created for you automatically. You can use the various setters with a fluent interface in order to set up your configuration.
You can leave out any options that you don't need and check the `PdoDataSource` class for other available setters.
Connections will be created lazily, i.e. they won't consume any resources before they're actually used. All that happens automatically.
* From a DSN (data source name):
```php
$db = \Delight\Db\PdoDatabase::fromDsn(
new \Delight\Db\PdoDsn(
'mysql:dbname=my-database;host=localhost',
'my-username',
'my-password'
)
);
```
The parameters of the `PdoDsn` constructor are exactly the same as the parameters to the `PDO` constructor. But if you don't need another `PDO` instance, it's more efficient to go without `PDO` and create this library's instance from a DSN directly.
Connections will be created lazily, i.e. they won't consume any resources before they're actually used.
* Using an existing `PDO` instance:
```php
// $pdo = new PDO('mysql:dbname=my-database;host=localhost;charset=utf8mb4', 'my-username', 'my-password');
$db = \Delight\Db\PdoDatabase::fromPdo($pdo);
```
This will not cause another database connection to be created but instead re-use the existing connection from the `PDO` instance that you provided.
Furthermore, you can benefit from the strong guarantees about this library's behavior and from its convenience while still being able to use your existing `PDO` instance without any side effects.
Just pass `true` as the second argument to the `fromPdo` method to completely preserve the original state of your `PDO` instance:
```php
$db = \Delight\Db\PdoDatabase::fromPdo($pdo, true);
```
#### Available drivers for database systems
```php
\Delight\Db\PdoDataSource::DRIVER_NAME_MYSQL;
\Delight\Db\PdoDataSource::DRIVER_NAME_POSTGRESQL;
\Delight\Db\PdoDataSource::DRIVER_NAME_SQLITE;
```
### Selecting data
When selecting data, you get the desired results with just one method call. And, of course, you can easily choose whether you want to get multiple rows, a single row, a single value only, or a single column from all rows.
```php
$rows = $db->select('SELECT id, name FROM books');
// or
$rows = $db->select(
'SELECT name, year FROM books WHERE author = ? ORDER BY copies DESC LIMIT 0, 10',
[ 'Charles Dickens' ]
);
// or
$row = $db->selectRow(
'SELECT author, year FROM books WHERE author <> ? ORDER BY year ASC LIMIT 0, 1',
[ 'Miguel de Cervantes' ]
);
// or
$value = $db->selectValue(
'SELECT year FROM books WHERE name <> ? AND name <> ? ORDER BY year DESC LIMIT 0, 1',
[
'Tale of Two Cities',
'Alice in Wonderland'
]
);
// or
$column = $db->selectColumn(
'SELECT author FROM books ORDER BY copies DESC LIMIT ?, ?',
[
0,
3
]
);
```
### Inserting data
For simple insertions, you can use a convenient shorthand:
```php
$db->insert(
'books',
[
// set
'name' => 'Don Quixote',
'author' => 'Miguel de Cervantes',
'year' => 1612
]
);
```
Does your table have automatically generated primary IDs? Access to these inserted IDs is available with another short method call. The sequence name is only required for *some* database drivers, e.g. PostgreSQL.
```php
$newId = $db->getLastInsertId();
// or
$newId = $db->getLastInsertId('my-sequence-name');
```
If you need to execute more advanced statements, please refer to section "Executing statements" below.
### Updating data
For simple updates, you can use a convenient shorthand as well:
```php
$db->update(
'books',
[
// set
'author' => 'J. K. Rowling',
'copies' => 2
],
[
// where
'name' => "Harry Potter and the Philosopher's Stone"
]
);
```
Do you want to know how many rows have been updated by this operation? Just grab the return value from the `update` method call.
If you need to execute more advanced statements, please refer to section "Executing statements" below.
### Deleting data
Again, for simple deletions, you can use a convenient shorthand:
```php
$db->delete(
'books',
[
// where
'author' => 'C. S. Lewis',
'year' => 1949
]
);
```
The return value from the `delete` method call will tell you how many rows have been deleted by this operation.
If you need to execute more advanced statements, please refer to section "Executing statements" below.
### Executing statements
You can execute any arbitrary SQL statements as shown in the following examples:
```php
$db->exec(
'INSERT INTO books (name, author, year) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE copies = copies + 1',
[
"Harry Potter and the Philosopher's Stone",
'J. K. Rowling',
1997
]
);
// or
$db->exec(
"UPDATE books SET name = CONCAT(LEFT(name, 5), ' in ', RIGHT(name, 10)) WHERE year >= ? AND year < ?",
[
1860,
1890
]
);
```
For every statement that you execute, the return value will be the number of rows affected.
If the statement that you're executing is an `INSERT`, you can get the inserted IDs via the `getLastInsertId` method, again.
### Transactions
Transaction controls are as easy as they should be:
```php
$db->beginTransaction();
// and later
$db->commit();
// or
$db->rollBack();
// and optionally
$active = $db->isTransactionActive();
```
### Error handling
The methods from this library do not return any error codes. You will be informed about any problem via an exception.
### Input escaping
There's no need for you to escape any input manually. Just use the methods as shown above by using placeholders and passing the data separately. All input will be escaped for you automatically.
### Performance profiling
In order to monitor query performance during development, you can enable performance profiling:
```php
$db->setProfiler(new \Delight\Db\SimpleProfiler());
```
Whenever you want to see the analyzed queries or store them in a log file, you can get all measurements recorded by the profiler as an array:
```php
$db->getProfiler()->getMeasurements();
```
Typically, you'll want to sort the measurements before retrieving them, so that the longest-running queries are listed first:
```php
$db->getProfiler()->sort();
```
### Server and client information
In order to retrieve some information about the database server that you're connected to or about the database client used by PHP, you can use one of the following methods:
```php
$db->getDriverName();
// e.g. 'MySQL'
$db->getServerInfo();
// e.g. 'Uptime: 82196 Threads: 1 Questions: 2840 Slow queries: 0 Opens: 23 Flush tables: 1 Open tables: 33 Queries per second avg: 0.736'
$db->getServerVersion();
// e.g. '5.5.5-10.1.13-MariaDB'
$db->getClientVersion();
// e.g. 'mysqlnd 5.0.1-dev'
```
### Listeners
#### Connection established
```php
$db->addOnConnectListener(function (\Delight\Db\PdoDatabase $db) {
// do something
});
```
## Contributing
All contributions are welcome! If you wish to contribute, please create an issue first so that your feature, problem or question can be discussed.
## License
This project is licensed under the terms of the [MIT License](https://opensource.org/licenses/MIT).
+17
View File
@@ -0,0 +1,17 @@
{
"name": "delight-im/db",
"description": "Safe and convenient SQL database access in a driver-agnostic way",
"require": {
"php": ">=5.6.0",
"ext-pdo": "*"
},
"type": "library",
"keywords": [ "database", "sql", "pdo", "mysql", "postgresql", "pgsql", "sqlite" ],
"homepage": "https://github.com/delight-im/PHP-DB",
"license": "MIT",
"autoload": {
"psr-4": {
"Delight\\Db\\": "src/"
}
}
}
Generated Vendored Executable
+21
View File
@@ -0,0 +1,21 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically"
],
"hash": "f40a675f76ed738a86242507aefec136",
"content-hash": "25f1b8ee1b78ffbaba4f8cef1a5f0dfa",
"packages": [],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
"php": ">=5.6.0",
"ext-pdo": "*"
},
"platform-dev": []
}
+21
View File
@@ -0,0 +1,21 @@
<?php
/*
* PHP-DB (https://github.com/delight-im/PHP-DB)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Db;
/** Description of a data source */
interface DataSource {
/**
* Converts this instance to a DSN
*
* @return Dsn
*/
public function toDsn();
}
+244
View File
@@ -0,0 +1,244 @@
<?php
/*
* PHP-DB (https://github.com/delight-im/PHP-DB)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Db;
use Delight\Db\Throwable\BeginTransactionFailureException;
use Delight\Db\Throwable\CommitTransactionFailureException;
use Delight\Db\Throwable\IntegrityConstraintViolationException;
use Delight\Db\Throwable\RollBackTransactionFailureException;
use Delight\Db\Throwable\TransactionFailureException;
/** Safe and convenient SQL database access in a driver-agnostic way */
interface Database {
/**
* Selects from the database using the specified query and returns all rows and columns
*
* You should not include any dynamic input in the query
*
* Instead, pass `?` characters (without any quotes) as placeholders and pass the actual values in the second argument
*
* @param string $query the SQL query to select with
* @param array|null $bindValues (optional) the values to bind as replacements for the `?` characters in the query
* @return array|null the rows and columns returned by the server or `null` if no results have been found
*/
public function select($query, array $bindValues = null);
/**
* Selects from the database using the specified query and returns the value of the first column in the first row
*
* You should not include any dynamic input in the query
*
* Instead, pass `?` characters (without any quotes) as placeholders and pass the actual values in the second argument
*
* @param string $query the SQL query to select with
* @param array|null $bindValues (optional) the values to bind as replacements for the `?` characters in the query
* @return mixed|null the value of the first column in the first row returned by the server or `null` if no results have been found
*/
public function selectValue($query, array $bindValues = null);
/**
* Selects from the database using the specified query and returns the first row
*
* You should not include any dynamic input in the query
*
* Instead, pass `?` characters (without any quotes) as placeholders and pass the actual values in the second argument
*
* @param string $query the SQL query to select with
* @param array|null $bindValues (optional) the values to bind as replacements for the `?` characters in the query
* @return array|null the first row returned by the server or `null` if no results have been found
*/
public function selectRow($query, array $bindValues = null);
/**
* Selects from the database using the specified query and returns the first column
*
* You should not include any dynamic input in the query
*
* Instead, pass `?` characters (without any quotes) as placeholders and pass the actual values in the second argument
*
* @param string $query the SQL query to select with
* @param array|null $bindValues (optional) the values to bind as replacements for the `?` characters in the query
* @return array|null the first column returned by the server or `null` if no results have been found
*/
public function selectColumn($query, array $bindValues = null);
/**
* Inserts the given mapping between columns and values into the specified table
*
* @param string|string[] $tableName the name of the table to insert into (or an array of components of the qualified name)
* @param array $insertMappings the mappings between columns and values to insert
* @return int the number of inserted rows
* @throws IntegrityConstraintViolationException
*/
public function insert($tableName, array $insertMappings);
/**
* Updates the specified table with the given mappings between columns and values
*
* @param string|string[] $tableName the name of the table to update (or an array of components of the qualified name)
* @param array $updateMappings the mappings between columns and values to update
* @param array $whereMappings the mappings between columns and values to filter by
* @return int the number of updated rows
* @throws IntegrityConstraintViolationException
*/
public function update($tableName, array $updateMappings, array $whereMappings);
/**
* Deletes from the specified table where the given mappings between columns and values are found
*
* @param string|string[] $tableName the name of the table to delete from (or an array of components of the qualified name)
* @param array $whereMappings the mappings between columns and values to filter by
* @return int the number of deleted rows
*/
public function delete($tableName, array $whereMappings);
/**
* Executes an arbitrary statement and returns the number of affected rows
*
* This is especially useful for custom `INSERT`, `UPDATE` or `DELETE` statements
*
* You should not include any dynamic input in the statement
*
* Instead, pass `?` characters (without any quotes) as placeholders and pass the actual values in the second argument
*
* @param string $statement the SQL statement to execute
* @param array|null $bindValues (optional) the values to bind as replacements for the `?` characters in the statement
* @return int the number of affected rows
* @throws IntegrityConstraintViolationException
* @throws TransactionFailureException
*/
public function exec($statement, array $bindValues = null);
/**
* Returns the ID of the last row that has been inserted or returns the last value from the specified sequence
*
* @param string|null $sequenceName (optional) the name of the sequence that the ID should be returned from
* @return string|int the ID or the number from the sequence
*/
public function getLastInsertId($sequenceName = null);
/**
* Starts a new transaction and turns off auto-commit mode
*
* Changes won't take effect until the transaction is either finished via `commit` or cancelled via `rollBack`
*
* @throws BeginTransactionFailureException
*/
public function beginTransaction();
/**
* Alias of `beginTransaction`
*
* @throws BeginTransactionFailureException
*/
public function startTransaction();
/**
* Returns whether a transaction is currently active
*
* @return bool
*/
public function isTransactionActive();
/**
* Finishes an existing transaction and turns on auto-commit mode again
*
* This makes all changes since the last commit or roll-back permanent
*
* @throws CommitTransactionFailureException
*/
public function commit();
/**
* Cancels an existing transaction and turns on auto-commit mode again
*
* This discards all changes since the last commit or roll-back
*
* @throws RollBackTransactionFailureException
*/
public function rollBack();
/**
* Returns the performance profiler currently used by this instance (if any)
*
* @return Profiler|null
*/
public function getProfiler();
/**
* Sets the performance profiler used by this instance
*
* This should only be used during development and not in production
*
* @param Profiler|null $profiler the profiler instance or `null` to disable profiling again
* @return static this instance for chaining
*/
public function setProfiler(Profiler $profiler = null);
/**
* Returns the name of the driver that is used for the current connection
*
* @return string
*/
public function getDriverName();
/**
* Quotes an identifier (e.g. a table name or column reference)
*
* This allows for special characters and reserved keywords to be used in identifiers
*
* There is usually no need to call this method
*
* Identifiers should not be set from untrusted user input and in most cases not even from dynamic expressions
*
* @param string $identifier the identifier to quote
* @return string the quoted identifier
*/
public function quoteIdentifier($identifier);
/**
* Quotes a table name
*
* This allows for special characters and reserved keywords to be used in table names
*
* There is usually no need to call this method
*
* Table names should not be set from untrusted user input and in most cases not even from dynamic expressions
*
* @param string|string[] $tableName the table name to quote (or an array of components of the qualified name)
* @return string the quoted table name
*/
public function quoteTableName($tableName);
/**
* Quotes a literal value (e.g. a string to insert or to use in a comparison) or an array thereof
*
* This allows for special characters to be used in literal values
*
* There is usually no need to call this method
*
* You should always use placeholders for literal values and pass the actual values to bind separately
*
* @param string $literal the literal value to quote
* @return string the quoted literal value
*/
public function quoteLiteral($literal);
/**
* Adds a listener that will execute as soon as the database connection has been established
*
* If the database connection has already been active before, the listener will execute immediately
*
* @param callable $onConnectListener the callback to execute
* @return static this instance for chaining
*/
public function addOnConnectListener(callable $onConnectListener);
}
+12
View File
@@ -0,0 +1,12 @@
<?php
/*
* PHP-DB (https://github.com/delight-im/PHP-DB)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Db;
/** Data source name or connection string describing how to connect to a data source */
interface Dsn {}
+92
View File
@@ -0,0 +1,92 @@
<?php
/*
* PHP-DB (https://github.com/delight-im/PHP-DB)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Db;
use PDOException;
use Delight\Db\Throwable\DatabaseNotFoundError;
use Delight\Db\Throwable\Error;
use Delight\Db\Throwable\Exception;
use Delight\Db\Throwable\IntegrityConstraintViolationException;
use Delight\Db\Throwable\NoDatabaseSelectedError;
use Delight\Db\Throwable\SyntaxError;
use Delight\Db\Throwable\TableNotFoundError;
use Delight\Db\Throwable\UnknownColumnError;
use Delight\Db\Throwable\WrongCredentialsError;
/**
* Handles, processes and re-throws exceptions and errors
*
* For more information about possible exceptions and errors, see:
*
* https://en.wikibooks.org/wiki/Structured_Query_Language/SQLSTATE
*
* https://dev.mysql.com/doc/refman/5.5/en/error-messages-server.html
*/
final class ErrorHandler {
/**
* Handles the specified exception thrown by PDO and tries to throw a more specific exception or error instead
*
* @param PDOException $e the exception thrown by PDO
* @throws Exception
* @throws Error
*/
public static function rethrow(PDOException $e) {
// the 2-character class of the error (if any) has the highest priority
$errorClass = null;
// the 3-character subclass of the error (if any) has a medium priority
$errorSubClass = null;
// the full error code itself has the lowest priority
$error = null;
// if an error code is available
if (!empty($e->getCode())) {
// remember the error code
$error = $e->getCode();
// if the error code is an "SQLSTATE" error
if (strlen($e->getCode()) === 5) {
// remember the class as well
$errorClass = substr($e->getCode(), 0, 2);
// and remember the subclass
$errorSubClass = substr($e->getCode(), 2);
}
}
if ($errorClass === '3D') {
throw new NoDatabaseSelectedError($e->getMessage());
}
elseif ($errorClass === '23') {
throw new IntegrityConstraintViolationException($e->getMessage());
}
elseif ($errorClass === '42') {
if ($errorSubClass === 'S02') {
throw new TableNotFoundError($e->getMessage());
}
elseif ($errorSubClass === 'S22') {
throw new UnknownColumnError($e->getMessage());
}
else {
throw new SyntaxError($e->getMessage());
}
}
else {
if ($error === 1044) {
throw new WrongCredentialsError($e->getMessage());
}
elseif ($error === 1049) {
throw new DatabaseNotFoundError($e->getMessage());
}
else {
throw new Error($e->getMessage());
}
}
}
}
+42
View File
@@ -0,0 +1,42 @@
<?php
/*
* PHP-DB (https://github.com/delight-im/PHP-DB)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Db;
/** Individual measurement of a profiler that monitors performance */
interface Measurement {
/**
* Returns the duration of the operation that was monitored
*
* @return float the duration in milliseconds
*/
public function getDuration();
/**
* Returns the (parameterized) SQL query or statement that was used during the operation
*
* @return string
*/
public function getSql();
/**
* Returns any values that might have been bound to the statement
*
* @return array|null
*/
public function getBoundValues();
/**
* Returns the trace that shows the path taken through the application until the operation was executed
*
* @return array
*/
public function getTrace();
}
+289
View File
@@ -0,0 +1,289 @@
<?php
/*
* PHP-DB (https://github.com/delight-im/PHP-DB)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Db;
/** Description of a data source for use with PHP's built-in PDO */
final class PdoDataSource implements DataSource {
/** The driver name for MySQL */
const DRIVER_NAME_MYSQL = 'mysql';
/** The driver name for PostgreSQL */
const DRIVER_NAME_POSTGRESQL = 'pgsql';
/** The driver name for SQLite */
const DRIVER_NAME_SQLITE = 'sqlite';
/** The driver name for Oracle */
const DRIVER_NAME_ORACLE = 'oci';
/** Hostname for the virtual loopback interface */
const HOST_LOOPBACK_NAME = 'localhost';
/** IPv4 address for the virtual loopback interface */
const HOST_LOOPBACK_IP = '127.0.0.1';
/** The default hostname */
const HOST_DEFAULT = self::HOST_LOOPBACK_NAME;
/** @var string the name of the driver, e.g. `mysql` or `pgsql` */
private $driverName;
/** @var string|null the hostname where the database can be accessed, e.g. `db.example.com` */
private $hostname;
/** @var int|null the port number to use at the given host, e.g. `3306` or `5432` */
private $port;
/** @var string|null the UNIX socket to use, e.g. `/tmp/db.sock` */
private $unixSocket;
/** @var bool|null whether to keep the database in memory only */
private $memory;
/** @var string|null the path to the file where the database can be accessed on disk, e.g. `/opt/databases/mydb.ext` */
private $filePath;
/** @var string|null the name of the database, e.g. `my_application` */
private $databaseName;
/** @var string|null the character encoding of the database, e.g. `utf8` */
private $charset;
/** @var string|null the name of a user that can access the database */
private $username;
/** @var string|null the password corresponding to the username */
private $password;
/**
* Constructor
*
* @param string $driverName the name of the driver, e.g. `mysql` or `pgsql`
*/
public function __construct($driverName) {
$this->driverName = (string) $driverName;
$this->hostname = self::HOST_DEFAULT;
$this->port = self::suggestPortFromDriverName($driverName);
$this->unixSocket = null;
$this->memory = null;
$this->filePath = null;
$this->databaseName = null;
$this->charset = self::suggestCharsetFromDriverName($driverName);
$this->username = null;
$this->password = null;
}
/**
* Sets the hostname
*
* @param string|null $hostname the hostname where the database can be accessed, e.g. `db.example.com`
* @return static this instance for chaining
*/
public function setHostname($hostname = null) {
$this->hostname = $hostname !== null ? (string) $hostname : null;
return $this;
}
/**
* Sets the port number
*
* @param int|null $port the port number to use at the given host, e.g. `3306` or `5432`
* @return static this instance for chaining
*/
public function setPort($port = null) {
$this->port = $port !== null ? (int) $port : null;
return $this;
}
/**
* Sets the unix socket
*
* @param string|null $unixSocket the UNIX socket to use, e.g. `/tmp/db.sock`
* @return static this instance for chaining
*/
public function setUnixSocket($unixSocket = null) {
$this->unixSocket = $unixSocket !== null ? (string) $unixSocket : null;
return $this;
}
/**
* Sets whether to keep the database in memory only
*
* @param bool|null $memory whether to keep the database in memory only
* @return static this instance for chaining
*/
public function setMemory($memory = null) {
$this->memory = $memory !== null ? (bool) $memory : null;
return $this;
}
/**
* Sets the file path
*
* @param string|null $filePath the path to the file where the database can be accessed on disk, e.g. `/opt/databases/mydb.ext`
* @return static this instance for chaining
*/
public function setFilePath($filePath = null) {
$this->filePath = $filePath !== null ? (string) $filePath : null;
return $this;
}
/**
* Sets the database name
*
* @param string|null $databaseName the name of the database, e.g. `my_application`
* @return static this instance for chaining
*/
public function setDatabaseName($databaseName = null) {
$this->databaseName = $databaseName !== null ? (string) $databaseName : null;
return $this;
}
/**
* Sets the charset
*
* @param string|null $charset the character encoding, e.g. `utf8`
* @return static this instance for chaining
*/
public function setCharset($charset = null) {
$this->charset = $charset !== null ? (string) $charset : null;
return $this;
}
/**
* Sets the username
*
* @param string|null $username the name of a user that can access the database
* @return static this instance for chaining
*/
public function setUsername($username = null) {
$this->username = $username;
return $this;
}
/**
* Sets the password
*
* @param string|null $password the password corresponding to the username
* @return static this instance for chaining
*/
public function setPassword($password = null) {
$this->password = $password;
return $this;
}
public function toDsn() {
$components = [];
if (isset($this->hostname)) {
if ($this->driverName !== self::DRIVER_NAME_ORACLE) {
$hostname = $this->hostname;
// if we're trying to connect to a local database
if ($this->hostname === self::HOST_LOOPBACK_NAME) {
// if we're using a non-standard port
if (isset($this->port) && $this->port !== self::suggestPortFromDriverName($this->driverName)) {
// force usage of TCP over UNIX sockets for the port change to take effect
$hostname = self::HOST_LOOPBACK_IP;
}
}
$components[] = 'host='.$hostname;
}
}
if (isset($this->port)) {
if ($this->driverName !== self::DRIVER_NAME_ORACLE) {
$components[] = 'port='.$this->port;
}
}
if (isset($this->unixSocket)) {
$components[] = 'unix_socket='.$this->unixSocket;
}
if (isset($this->memory)) {
if ($this->memory === true) {
$components[] = ':memory:';
}
}
if (isset($this->filePath)) {
$components[] = $this->filePath;
}
if (isset($this->databaseName)) {
if ($this->driverName === self::DRIVER_NAME_ORACLE) {
$oracleLocation = [];
if (isset($this->hostname)) {
$oracleLocation[] = $this->hostname;
}
if (isset($this->port)) {
$oracleLocation[] = $this->port;
}
if (count($oracleLocation) > 0) {
$components[] = 'dbname=//'.implode(':', $oracleLocation).'/'.$this->databaseName;
}
else {
$components[] = 'dbname='.$this->databaseName;
}
}
else {
$components[] = 'dbname='.$this->databaseName;
}
}
if (isset($this->charset)) {
if ($this->driverName === self::DRIVER_NAME_POSTGRESQL) {
$components[] = 'client_encoding='.$this->charset;
}
else {
$components[] = 'charset='.$this->charset;
}
}
if (isset($this->username)) {
$components[] = 'user='.$this->username;
}
if (isset($this->password)) {
$components[] = 'password='.$this->password;
}
$dsnStr = $this->driverName . ':' . implode(';', $components);
return new PdoDsn($dsnStr, $this->username, $this->password);
}
/**
* Suggests a default charset for a given driver
*
* @param string $driverName the name of the driver
* @return string|null the suggested charset
*/
private static function suggestCharsetFromDriverName($driverName) {
switch ($driverName) {
case self::DRIVER_NAME_MYSQL: return 'utf8mb4';
case self::DRIVER_NAME_POSTGRESQL: return 'UTF8';
default: return null;
}
}
/**
* Suggests a default port number for a given driver
*
* @param string $driverName the name of the driver
* @return int|null the suggested port number
*/
private static function suggestPortFromDriverName($driverName) {
switch ($driverName) {
case self::DRIVER_NAME_MYSQL: return 3306;
case self::DRIVER_NAME_POSTGRESQL: return 5432;
default: return null;
}
}
}
+615
View File
@@ -0,0 +1,615 @@
<?php
/*
* PHP-DB (https://github.com/delight-im/PHP-DB)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Db;
use PDO;
use PDOException;
use PDOStatement;
use Delight\Db\Throwable\BeginTransactionFailureException;
use Delight\Db\Throwable\CommitTransactionFailureException;
use Delight\Db\Throwable\EmptyValueListError;
use Delight\Db\Throwable\EmptyWhereClauseError;
use Delight\Db\Throwable\RollBackTransactionFailureException;
/** Database access using PHP's built-in PDO */
final class PdoDatabase implements Database {
/** @var array|null the old connection attributes to restore during denormalization */
private $previousAttributes;
/** @var array|null the new connection attributes to apply during normalization */
private $attributes;
/** @var PDO|null the connection that this class operates on (may be lazily loaded) */
private $pdo;
/** @var PdoDsn|null the PDO-specific DSN that may be used to establish the connection */
private $dsn;
/** @var string|null the name of the driver that is used for the current connection (may be lazily loaded) */
private $driverName;
/** @var Profiler|null the profiler that is used to analyze query performance during development */
private $profiler;
/** @var callable[] the list of pending callbacks to execute when the connection has been established */
private $onConnectListeners;
/**
* Constructor
*
* This is private to prevent direct usage
*
* Call one of the static factory methods instead
*
* @param PDO|null $pdoInstance (optional) the connection that this class operates on
* @param PdoDsn|null $pdoDsn (optional) the PDO-specific DSN that may be used to establish the connection
* @param bool|null (optional) $preserveOldState whether the old state of the connection should be preserved
*/
private function __construct(PDO $pdoInstance = null, PdoDsn $pdoDsn = null, $preserveOldState = null) {
// if the old state of the connection must be stored somewhere
if ($preserveOldState) {
// prepare an array for that task
$this->previousAttributes = [];
}
// if the old state of the connection doesn't need to be tracked
else {
$this->previousAttributes = null;
}
// track the new attributes that should be applied during normalization
$this->attributes = [
// set the error mode for this connection to throw exceptions
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
// set the default fetch mode for this connection to use associative arrays
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
// prefer native prepared statements over emulated ones
PDO::ATTR_EMULATE_PREPARES => false,
// use lowercase and uppercase as returned by the server
PDO::ATTR_CASE => PDO::CASE_NATURAL,
// don't convert numeric values to strings when fetching data
PDO::ATTR_STRINGIFY_FETCHES => false,
// keep `null` values and empty strings as returned by the server
PDO::ATTR_ORACLE_NULLS => PDO::NULL_NATURAL
];
$this->pdo = $pdoInstance;
$this->dsn = $pdoDsn;
$this->profiler = null;
$this->onConnectListeners = [];
}
/**
* Creates and returns a new instance from an existing PDO instance
*
* @param PDO $pdoInstance the PDO instance to use
* @param bool|null (optional) $preserveOldState whether the old state of the PDO instance should be preserved
* @return static the new instance
*/
public static function fromPdo(PDO $pdoInstance, $preserveOldState = null) {
return new static($pdoInstance, null, $preserveOldState);
}
/**
* Creates and returns a new instance from a PDO-specific DSN
*
* The connection will be lazily loaded, i.e. it won't be established before it's actually needed
*
* @param PdoDsn $pdoDsn the PDO-specific DSN to use
* @return static the new instance
*/
public static function fromDsn(PdoDsn $pdoDsn) {
return new static(null, $pdoDsn);
}
/**
* Creates and returns a new instance from a data source described for use with PDO
*
* The connection will be lazily loaded, i.e. it won't be established before it's actually needed
*
* @param PdoDataSource $pdoDataSource the data source to use
* @return static the new instance
*/
public static function fromDataSource(PdoDataSource $pdoDataSource) {
return new static(null, $pdoDataSource->toDsn());
}
public function select($query, array $bindValues = null) {
return $this->selectInternal(function ($stmt) {
/** @var PDOStatement $stmt */
return $stmt->fetchAll();
}, $query, $bindValues);
}
public function selectValue($query, array $bindValues = null) {
return $this->selectInternal(function ($stmt) {
/** @var PDOStatement $stmt */
return $stmt->fetchColumn(0);
}, $query, $bindValues);
}
public function selectRow($query, array $bindValues = null) {
return $this->selectInternal(function ($stmt) {
/** @var PDOStatement $stmt */
return $stmt->fetch();
}, $query, $bindValues);
}
public function selectColumn($query, array $bindValues = null) {
return $this->selectInternal(function ($stmt) {
/** @var PDOStatement $stmt */
return $stmt->fetchAll(PDO::FETCH_COLUMN, 0);
}, $query, $bindValues);
}
public function insert($tableName, array $insertMappings) {
// if no values have been provided that could be inserted
if (empty($insertMappings)) {
// we cannot perform an insert here
throw new EmptyValueListError();
}
// escape the table name
$tableName = $this->quoteTableName($tableName);
// get the column names
$columnNames = array_keys($insertMappings);
// escape the column names
$columnNames = array_map([ $this, 'quoteIdentifier' ], $columnNames);
// build the column list
$columnList = implode(', ', $columnNames);
// prepare the values (which are placeholders only)
$values = array_fill(0, count($insertMappings), '?');
// build the value list
$placeholderList = implode(', ', $values);
// and finally build the full statement (still using placeholders)
$statement = 'INSERT INTO '.$tableName.' ('.$columnList.') VALUES ('.$placeholderList.');';
// execute the (parameterized) statement and supply the values to be bound to it
return $this->exec($statement, array_values($insertMappings));
}
public function update($tableName, array $updateMappings, array $whereMappings) {
// if no values have been provided that we could update to
if (empty($updateMappings)) {
// we cannot perform an update here
throw new EmptyValueListError();
}
// if no values have been provided that we could filter by (which is possible but dangerous)
if (empty($whereMappings)) {
// we should not perform an update here
throw new EmptyWhereClauseError();
}
// escape the table name
$tableName = $this->quoteTableName($tableName);
// prepare a list for the values to be bound (both by the list of new values and by the conditions)
$bindValues = [];
// prepare a list for the individual directives of the `SET` clause
$setDirectives = [];
// for each mapping of a column name to its respective new value
foreach ($updateMappings as $updateColumn => $updateValue) {
// create an individual directive with the column name and a placeholder for the value
$setDirectives[] = $this->quoteIdentifier($updateColumn) . ' = ?';
// and remember which value to bind here
$bindValues[] = $updateValue;
}
// prepare a list for the individual predicates of the `WHERE` clause
$wherePredicates = [];
// for each mapping of a column name to its respective value to filter by
foreach ($whereMappings as $whereColumn => $whereValue) {
// create an individual predicate with the column name and a placeholder for the value
$wherePredicates[] = $this->quoteIdentifier($whereColumn) . ' = ?';
// and remember which value to bind here
$bindValues[] = $whereValue;
}
// build the full statement (still using placeholders)
$statement = 'UPDATE '.$tableName.' SET '.implode(', ', $setDirectives).' WHERE '.implode(' AND ', $wherePredicates).';';
// execute the (parameterized) statement and supply the values to be bound to it
return $this->exec($statement, $bindValues);
}
public function delete($tableName, array $whereMappings) {
// if no values have been provided that we could filter by (which is possible but dangerous)
if (empty($whereMappings)) {
// we should not perform a deletion here
throw new EmptyWhereClauseError();
}
// escape the table name
$tableName = $this->quoteTableName($tableName);
// prepare a list for the values to be bound by the conditions
$bindValues = [];
// prepare a list for the individual predicates of the `WHERE` clause
$wherePredicates = [];
// for each mapping of a column name to its respective value to filter by
foreach ($whereMappings as $whereColumn => $whereValue) {
// create an individual predicate with the column name and a placeholder for the value
$wherePredicates[] = $this->quoteIdentifier($whereColumn) . ' = ?';
// and remember which value to bind here
$bindValues[] = $whereValue;
}
// build the full statement (still using placeholders)
$statement = 'DELETE FROM '.$tableName.' WHERE '.implode(' AND ', $wherePredicates).';';
// execute the (parameterized) statement and supply the values to be bound to it
return $this->exec($statement, $bindValues);
}
public function exec($statement, array $bindValues = null) {
$this->normalizeConnection();
try {
// create a prepared statement from the supplied SQL string
$stmt = $this->pdo->prepare($statement);
}
catch (PDOException $e) {
ErrorHandler::rethrow($e);
}
// if a performance profiler has been defined
if (isset($this->profiler)) {
$this->profiler->beginMeasurement();
}
/** @var PDOStatement $stmt */
try {
// bind the supplied values to the statement and execute it
$stmt->execute($bindValues);
}
catch (PDOException $e) {
ErrorHandler::rethrow($e);
}
// if a performance profiler has been defined
if (isset($this->profiler)) {
$this->profiler->endMeasurement($statement, $bindValues);
}
// get the number of rows affected by this operation
$affectedRows = $stmt->rowCount();
$this->denormalizeConnection();
return $affectedRows;
}
public function getLastInsertId($sequenceName = null) {
$this->normalizeConnection();
$id = $this->pdo->lastInsertId($sequenceName);
$this->denormalizeConnection();
return $id;
}
public function beginTransaction() {
$this->normalizeConnection();
try {
$success = $this->pdo->beginTransaction();
}
catch (PDOException $e) {
$success = $e->getMessage();
}
$this->denormalizeConnection();
if ($success !== true) {
throw new BeginTransactionFailureException(is_string($success) ? $success : null);
}
}
public function startTransaction() {
$this->beginTransaction();
}
public function isTransactionActive() {
$this->normalizeConnection();
$state = $this->pdo->inTransaction();
$this->denormalizeConnection();
return $state;
}
public function commit() {
$this->normalizeConnection();
try {
$success = $this->pdo->commit();
}
catch (PDOException $e) {
$success = $e->getMessage();
}
$this->denormalizeConnection();
if ($success !== true) {
throw new CommitTransactionFailureException(is_string($success) ? $success : null);
}
}
public function rollBack() {
$this->normalizeConnection();
try {
$success = $this->pdo->rollBack();
}
catch (PDOException $e) {
$success = $e->getMessage();
}
$this->denormalizeConnection();
if ($success !== true) {
throw new RollBackTransactionFailureException(is_string($success) ? $success : null);
}
}
public function getProfiler() {
return $this->profiler;
}
public function setProfiler(Profiler $profiler = null) {
$this->profiler = $profiler;
return $this;
}
public function getDriverName() {
$this->ensureConnected();
switch ($this->driverName) {
case PdoDataSource::DRIVER_NAME_MYSQL: return 'MySQL';
case PdoDataSource::DRIVER_NAME_POSTGRESQL: return 'PostgreSQL';
case PdoDataSource::DRIVER_NAME_SQLITE: return 'SQLite';
case PdoDataSource::DRIVER_NAME_ORACLE: return 'Oracle';
default: return $this->driverName;
}
}
/**
* Returns some driver-specific information about the server that this instance is connected to
*
* @return string
*/
public function getServerInfo() {
$this->ensureConnected();
return $this->pdo->getAttribute(PDO::ATTR_SERVER_INFO);
}
/**
* Returns the version of the database software used on the server that this instance is connected to
*
* @return string
*/
public function getServerVersion() {
$this->ensureConnected();
return $this->pdo->getAttribute(PDO::ATTR_SERVER_VERSION);
}
/**
* Returns the version of the database client used by this instance
*
* @return string
*/
public function getClientVersion() {
$this->ensureConnected();
return $this->pdo->getAttribute(PDO::ATTR_CLIENT_VERSION);
}
public function quoteIdentifier($identifier) {
$this->ensureConnected();
if ($this->driverName === PdoDataSource::DRIVER_NAME_MYSQL) {
$char = '`';
}
else {
$char = '"';
}
return $char . str_replace($char, $char . $char, $identifier) . $char;
}
public function quoteTableName($tableName) {
if (\is_array($tableName)) {
$tableName = \array_map([ $this, 'quoteIdentifier' ], $tableName);
return \implode('.', $tableName);
}
else {
return $this->quoteIdentifier($tableName);
}
}
public function quoteLiteral($literal) {
$this->ensureConnected();
if (!empty($literal) && is_array($literal)) {
foreach ($literal as $key => $value) {
$literal[$key] = $this->pdo->quote($value);
}
return $literal;
}
else {
return $this->pdo->quote($literal);
}
}
public function addOnConnectListener(callable $onConnectListener) {
// if the database connection has not been established yet
if ($this->pdo === null) {
// schedule the callback for later execution
$this->onConnectListeners[] = $onConnectListener;
}
// if the database connection has already been established
else {
// execute the callback immediately
$onConnectListener($this);
}
return $this;
}
/** Makes sure that the connection is active and otherwise establishes it automatically */
private function ensureConnected() {
if ($this->pdo === null) {
try {
$this->pdo = new PDO($this->dsn->getDsn(), $this->dsn->getUsername(), $this->dsn->getPassword());
}
catch (PDOException $e) {
ErrorHandler::rethrow($e);
}
// iterate over all listeners waiting for the connection to be established
foreach ($this->onConnectListeners as $onConnectListener) {
// execute the callback
$onConnectListener($this);
}
// discard the listeners now that they have all been executed
$this->onConnectListeners = [];
$this->dsn = null;
}
if ($this->driverName === null) {
$this->driverName = $this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
}
}
/** Normalizes this connection by setting attributes that provide the strong guarantees about the connection's behavior that we need */
private function normalizeConnection() {
$this->ensureConnected();
$this->configureConnection($this->attributes, $this->previousAttributes);
}
/** Restores this connection's original behavior if desired */
private function denormalizeConnection() {
$this->configureConnection($this->previousAttributes, $this->attributes);
}
/**
* Configures this connection by setting appropriate attributes
*
* @param array|null $newAttributes the new attributes to set
* @param array|null $oldAttributes where old configurations may be saved to restore them later
*/
private function configureConnection(array &$newAttributes = null, array &$oldAttributes = null) {
// if a connection is available
if (isset($this->pdo)) {
// if there are attributes that need to be applied
if (isset($newAttributes)) {
// get the keys and values of the attributes to apply
foreach ($newAttributes as $key => $newValue) {
// if the old state of the connection must be preserved
if (isset($oldAttributes)) {
// retrieve the old value for this attribute
try {
$oldValue = @$this->pdo->getAttribute($key);
}
catch (PDOException $e) {
// the specified attribute is not supported by the driver
$oldValue = null;
}
// if an old value has been found
if (isset($oldValue)) {
// if the old value differs from the new value that we're going to set
if ($oldValue !== $newValue) {
// save the old value so that we're able to restore it later
$oldAttributes[$key] = $oldValue;
}
}
}
// and then set the desired new value
$this->pdo->setAttribute($key, $newValue);
}
// if the old state of the connection doesn't need to be preserved
if (!isset($oldAttributes)) {
// we're done updating attributes for this connection once and for all
$newAttributes = null;
}
}
}
}
/**
* Selects from the database using the specified query and returns what the supplied callback extracts from the result set
*
* You should not include any dynamic input in the query
*
* Instead, pass `?` characters (without any quotes) as placeholders and pass the actual values in the third argument
*
* @param callable $callback the callback that receives the executed statement and can then extract and return the desired results
* @param string $query the query to select with
* @param array|null $bindValues (optional) the values to bind as replacements for the `?` characters in the query
* @return mixed whatever the callback has extracted and returned from the result set
*/
private function selectInternal(callable $callback, $query, array $bindValues = null) {
$this->normalizeConnection();
try {
// create a prepared statement from the supplied SQL string
$stmt = $this->pdo->prepare($query);
}
catch (PDOException $e) {
ErrorHandler::rethrow($e);
}
// if a performance profiler has been defined
if (isset($this->profiler)) {
$this->profiler->beginMeasurement();
}
/** @var PDOStatement $stmt */
// bind the supplied values to the query and execute it
try {
$stmt->execute($bindValues);
}
catch (PDOException $e) {
ErrorHandler::rethrow($e);
}
// if a performance profiler has been defined
if (isset($this->profiler)) {
$this->profiler->endMeasurement($query, $bindValues, 1);
}
// fetch the desired results from the result set via the supplied callback
$results = $callback($stmt);
$this->denormalizeConnection();
// if the result is empty
if (empty($results) && $stmt->rowCount() === 0 && ($this->driverName !== PdoDataSource::DRIVER_NAME_SQLITE || \is_bool($results) || \is_array($results))) {
// consistently return `null`
return null;
}
// if some results have been found
else {
// return these as extracted by the callback
return $results;
}
}
}
+61
View File
@@ -0,0 +1,61 @@
<?php
/*
* PHP-DB (https://github.com/delight-im/PHP-DB)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Db;
/** DSN for use with PHP's built-in PDO */
final class PdoDsn implements Dsn {
/** @var string the DSN as a string */
private $dsn;
/** @var string|null the username that complements the DSN */
private $username;
/** @var string|null the password that complements the DSN */
private $password;
/**
* Constructor
*
* @param string $dsnStr the DSN as a string
* @param string $username (optional) the username that complements the DSN
* @param string $password (optional) the password that complements the DSN
*/
public function __construct($dsnStr, $username = null, $password = null) {
$this->dsn = (string) $dsnStr;
$this->username = (string) $username;
$this->password = (string) $password;
}
/**
* Returns the DSN as a string
*
* @return string
*/
public function getDsn() {
return $this->dsn;
}
/**
* Returns the username that complements the DSN
*
* @return string|null
*/
public function getUsername() {
return $this->username;
}
/**
* Returns the password that complements the DSN
*
* @return string|null
*/
public function getPassword() {
return $this->password;
}
}
+51
View File
@@ -0,0 +1,51 @@
<?php
/*
* PHP-DB (https://github.com/delight-im/PHP-DB)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Db;
/** Profiler that monitors performance of individual database queries and statements */
interface Profiler {
/** Starts a new measurement */
public function beginMeasurement();
/**
* Ends a previously started measurement
*
* @param string $sql the SQL query or statement that was monitored
* @param array|null $boundValues (optional) the values that have been bound to the query or statement
* @param int|null $discardMostRecentTraceEntries (optional) the number of trace entries that should be discarded (starting with the most recent ones)
*/
public function endMeasurement($sql, array $boundValues = null, $discardMostRecentTraceEntries = null);
/**
* Returns the number of measurements that this profiler has recorded
*
* @return int
*/
public function getCount();
/**
* Returns the measurement at the specified index
*
* @param int $index the index of the measurement to return
* @return Measurement
*/
public function getMeasurement($index);
/**
* Returns all measurements that this profiler has recorded
*
* @return Measurement[]
*/
public function getMeasurements();
/** Sorts the measurements that this profiler has recorded so that the longest-running operations are listed first */
public function sort();
}
+54
View File
@@ -0,0 +1,54 @@
<?php
/*
* PHP-DB (https://github.com/delight-im/PHP-DB)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Db;
/** Implementation of an individual measurement of a profiler that monitors performance */
final class SimpleMeasurement implements Measurement {
/** @var float the duration in milliseconds */
private $duration;
/** @var string the SQL query or statement */
private $sql;
/** @var array|null the values that have been bound to the query or statement */
private $boundValues;
/** @var array|null the trace that shows the path taken through the program until the operation was executed */
private $trace;
/**
* Constructor
*
* @param float $duration the duration in milliseconds
* @param string $sql the SQL query or statement
* @param array|null $boundValues (optional) the values that have been bound to the query or statement
* @param array|null $trace (optional) the trace that shows the path taken through the program until the operation was executed
*/
public function __construct($duration, $sql, array $boundValues = null, $trace = null) {
$this->duration = $duration;
$this->sql = $sql;
$this->boundValues = $boundValues;
$this->trace = $trace;
}
public function getDuration() {
return $this->duration;
}
public function getSql() {
return $this->sql;
}
public function getBoundValues() {
return $this->boundValues;
}
public function getTrace() {
return $this->trace;
}
}
+89
View File
@@ -0,0 +1,89 @@
<?php
/*
* PHP-DB (https://github.com/delight-im/PHP-DB)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Db;
/** Implementation of a profiler that monitors performance of individual database queries and statements */
final class SimpleProfiler implements Profiler {
/** The maximum number of entries in traces to use as the default */
const TRACE_MAX_LENGTH_DEFAULT = 10;
/** @var Measurement[] the measurements recorded by this instance */
private $measurements;
/** @var int the maximum number of entries in traces */
private $maxTraceLength;
/** @var float|null the start time of the current measurement in milliseconds */
private $currentMeasurementStartTime;
public function __construct($maxTraceLength = null) {
$this->measurements = [];
if ($maxTraceLength === null) {
$this->maxTraceLength = self::TRACE_MAX_LENGTH_DEFAULT;
}
else {
$this->maxTraceLength = (int) $maxTraceLength;
}
$this->currentMeasurementStartTime = null;
}
public function beginMeasurement() {
$this->currentMeasurementStartTime = microtime(true) * 1000;
}
public function endMeasurement($sql, array $boundValues = null, $discardMostRecentTraceEntries = null) {
if ($discardMostRecentTraceEntries === null) {
$discardMostRecentTraceEntries = 0;
}
else {
$discardMostRecentTraceEntries = (int) $discardMostRecentTraceEntries;
}
// get the trace at this point of the program execution
$trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, $this->maxTraceLength);
// discard as many of the most recent entries as desired (but always discard at least the current method)
for ($i = 0; $i < $discardMostRecentTraceEntries + 1; $i++) {
array_shift($trace);
}
// calculate the duration in milliseconds
$duration = (microtime(true) * 1000) - $this->currentMeasurementStartTime;
// and finally record the measurement
$this->measurements[] = new SimpleMeasurement(
$duration,
$sql,
$boundValues,
$trace
);
}
public function getCount() {
return count($this->measurements);
}
public function getMeasurement($index) {
return $this->measurements[$index];
}
public function getMeasurements() {
return $this->measurements;
}
public function sort() {
usort($this->measurements, function ($a, $b) {
/** @var Measurement $a */
/** @var Measurement $b */
return ($b->getDuration() - $a->getDuration());
});
}
}
+12
View File
@@ -0,0 +1,12 @@
<?php
/*
* PHP-DB (https://github.com/delight-im/PHP-DB)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Db\Throwable;
/** Exception that is thrown when a transaction cannot be started successfully for some reason */
class BeginTransactionFailureException extends TransactionFailureException {}
@@ -0,0 +1,12 @@
<?php
/*
* PHP-DB (https://github.com/delight-im/PHP-DB)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Db\Throwable;
/** Exception that is thrown when a transaction cannot be committed successfully for some reason */
class CommitTransactionFailureException extends TransactionFailureException {}
+12
View File
@@ -0,0 +1,12 @@
<?php
/*
* PHP-DB (https://github.com/delight-im/PHP-DB)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Db\Throwable;
/** Error that is thrown when the requested database cannot be found */
class DatabaseNotFoundError extends Error {}
+12
View File
@@ -0,0 +1,12 @@
<?php
/*
* PHP-DB (https://github.com/delight-im/PHP-DB)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Db\Throwable;
/** Error that is thrown when an empty list of values is provided where values are required */
class EmptyValueListError extends Error {}
+20
View File
@@ -0,0 +1,20 @@
<?php
/*
* PHP-DB (https://github.com/delight-im/PHP-DB)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Db\Throwable;
/**
* Error that is thrown when an empty `WHERE` clause is provided
*
* Although technically perfectly valid, an empty list of criteria is often provided by mistake
*
* This is why, for some operations, it is deemed too dangerous and thus disallowed
*
* Usually, one can simply execute a manual statement instead to get rid of this restriction
*/
class EmptyWhereClauseError extends Error {}
+12
View File
@@ -0,0 +1,12 @@
<?php
/*
* PHP-DB (https://github.com/delight-im/PHP-DB)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Db\Throwable;
/** Base class for all conditions that the application might not recover from and thus should not catch */
class Error extends \Exception {}
+12
View File
@@ -0,0 +1,12 @@
<?php
/*
* PHP-DB (https://github.com/delight-im/PHP-DB)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Db\Throwable;
/** Base class for all conditions that the application might recover from and thus should catch and handle */
class Exception extends \Exception {}
@@ -0,0 +1,18 @@
<?php
/*
* PHP-DB (https://github.com/delight-im/PHP-DB)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Db\Throwable;
/**
* Exception that is thrown when an integrity constraint is being violated
*
* Common constraints include 'UNIQUE', 'NOT NULL' and 'FOREIGN KEY'
*
* Ambiguous column references constitute violations as well
*/
class IntegrityConstraintViolationException extends Exception {}
+12
View File
@@ -0,0 +1,12 @@
<?php
/*
* PHP-DB (https://github.com/delight-im/PHP-DB)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Db\Throwable;
/** Error that is thrown when no database has been selected */
class NoDatabaseSelectedError extends Error {}
@@ -0,0 +1,12 @@
<?php
/*
* PHP-DB (https://github.com/delight-im/PHP-DB)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Db\Throwable;
/** Exception that is thrown when a transaction cannot be rolled back successfully for some reason */
class RollBackTransactionFailureException extends TransactionFailureException {}
+12
View File
@@ -0,0 +1,12 @@
<?php
/*
* PHP-DB (https://github.com/delight-im/PHP-DB)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Db\Throwable;
/** Error that is thrown when there is invalid syntax or when an access rule is being violated */
class SyntaxError extends Error {}
+12
View File
@@ -0,0 +1,12 @@
<?php
/*
* PHP-DB (https://github.com/delight-im/PHP-DB)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Db\Throwable;
/** Error that is thrown when a table cannot be found */
class TableNotFoundError extends Error {}
+12
View File
@@ -0,0 +1,12 @@
<?php
/*
* PHP-DB (https://github.com/delight-im/PHP-DB)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Db\Throwable;
/** Base class for all exceptions that occur with transaction controls */
abstract class TransactionFailureException extends Exception {}
+12
View File
@@ -0,0 +1,12 @@
<?php
/*
* PHP-DB (https://github.com/delight-im/PHP-DB)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Db\Throwable;
/** Error that is thrown when a column cannot be found */
class UnknownColumnError extends Error {}
+12
View File
@@ -0,0 +1,12 @@
<?php
/*
* PHP-DB (https://github.com/delight-im/PHP-DB)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
namespace Delight\Db\Throwable;
/** Error that is thrown when the supplied credentials cannot be used to access the database */
class WrongCredentialsError extends Error {}
+28
View File
@@ -0,0 +1,28 @@
<?php
/*
* PHP-DB (https://github.com/delight-im/PHP-DB)
* Copyright (c) delight.im (https://www.delight.im/)
* Licensed under the MIT License (https://opensource.org/licenses/MIT)
*/
// enable error reporting
\error_reporting(\E_ALL);
\ini_set('display_errors', 'stdout');
// enable assertions
\ini_set('assert.active', 1);
@\ini_set('zend.assertions', 1);
\ini_set('assert.exception', 1);
\header('Content-type: text/plain; charset=utf-8');
require __DIR__.'/../vendor/autoload.php';
$db = \Delight\Db\PdoDatabase::fromDsn(
new \Delight\Db\PdoDsn(
'mysql:dbname=my-database;host=localhost;charset=utf8mb4',
'my-username',
'my-password'
)
);
+13
View File
@@ -0,0 +1,13 @@
# editorconfig.org
root = true
[*]
charset = utf-8
indent_style = tab
trim_trailing_whitespace = true
end_of_line = lf
insert_final_newline = true
[*.md]
indent_style = space
indent_size = 4
+6
View File
@@ -0,0 +1,6 @@
# IntelliJ
.idea/
# Composer
vendor/
composer.phar
+21
View File
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) delight.im (https://www.delight.im/)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+9
View File
@@ -0,0 +1,9 @@
# Migration
## General
Update your version of this library using Composer and its `composer update` or `composer require` commands [[?]](https://github.com/delight-im/Knowledge/blob/master/Composer%20(PHP).md#how-do-i-update-libraries-or-modules-within-my-application).
## From `v1.x.x` to `v2.x.x`
* The license has been changed from the [Apache License 2.0](http://www.apache.org/licenses/LICENSE-2.0) to the [MIT License](https://opensource.org/licenses/MIT).
+69
View File
@@ -0,0 +1,69 @@
# PHP-HTTP
Hypertext Transfer Protocol (HTTP) utilities for PHP
## Requirements
* PHP 5.3.0+
## Installation
1. Include the library via Composer [[?]](https://github.com/delight-im/Knowledge/blob/master/Composer%20(PHP).md):
```
$ composer require delight-im/http
```
1. Include the Composer autoloader:
```php
require __DIR__ . '/vendor/autoload.php';
```
## Usage
### Response headers
* Retrieving a header (with optional value prefix)
```php
$header = \Delight\Http\ResponseHeader::get('Content-Type');
// or
$header = \Delight\Http\ResponseHeader::get('Content-Type', 'text/');
```
* Setting a header (overwriting other headers with the same name)
```php
\Delight\Http\ResponseHeader::set('X-Frame-Options', 'sameorigin');
```
* Adding a header (preserving other headers with the same name)
```php
\Delight\Http\ResponseHeader::add('Vary', 'User-Agent');
```
* Removing a header (with optional value prefix)
```php
$success = \Delight\Http\ResponseHeader::remove('X-Powered-By');
// or
$success = \Delight\Http\ResponseHeader::remove('X-Powered-By', 'PHP');
```
* Retrieving and removing a header at once (with optional value prefix)
```php
$header = \Delight\Http\ResponseHeader::take('Set-Cookie');
// or
$header = \Delight\Http\ResponseHeader::take('Set-Cookie', 'mysession=');
```
## Contributing
All contributions are welcome! If you wish to contribute, please create an issue first so that your feature, problem or question can be discussed.
## License
This project is licensed under the terms of the [MIT License](https://opensource.org/licenses/MIT).
+16
View File
@@ -0,0 +1,16 @@
{
"name": "delight-im/http",
"description": "Hypertext Transfer Protocol (HTTP) utilities for PHP",
"require": {
"php": ">=5.3.0"
},
"type": "library",
"keywords": [ "http", "https", "headers" ],
"homepage": "https://github.com/delight-im/PHP-HTTP",
"license": "MIT",
"autoload": {
"psr-4": {
"Delight\\Http\\": "src/"
}
}
}

Some files were not shown because too many files have changed in this diff Show More