diff --git a/.ddev/config.yaml b/.ddev/config.yaml
index 7d8495f6..4f8adfd8 100644
--- a/.ddev/config.yaml
+++ b/.ddev/config.yaml
@@ -9,6 +9,9 @@ additional_fqdns: []
database:
type: mariadb
version: "10.11"
+hooks:
+ post-start:
+ - exec-host: ddev mysql < .ddev/mysql/init_test_db.sql
use_dns_when_possible: true
composer_version: "2"
web_environment: []
diff --git a/.ddev/mysql/init_test_db.sql b/.ddev/mysql/init_test_db.sql
new file mode 100644
index 00000000..1f900b15
--- /dev/null
+++ b/.ddev/mysql/init_test_db.sql
@@ -0,0 +1,6 @@
+# This file gets executed every time the db container gets started via ddev.
+# See .ddev/config.yaml for more details.
+DROP DATABASE IF EXISTS db_test;
+CREATE DATABASE db_test;
+GRANT ALL ON db_test.* TO 'db'@'%';
+FLUSH PRIVILEGES;
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 10fcbdc7..62630db5 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -3,9 +3,7 @@ name: CI
on:
push:
branches:
- - '5.x'
- - '5.next'
- - '6.x'
+ - 'master'
pull_request:
branches:
- '*'
@@ -17,12 +15,23 @@ permissions:
jobs:
testsuite:
runs-on: ubuntu-24.04
+ services:
+ mysql:
+ image: mysql:9.6
+ env:
+ MYSQL_ROOT_PASSWORD: root
+ MYSQL_DATABASE: plugins_cakephp_test
+ ports:
+ - 3306:3306
+ options: >-
+ --health-cmd="mysqladmin ping -h 127.0.0.1 -proot"
+ --health-interval=10s
+ --health-timeout=5s
+ --health-retries=10
strategy:
fail-fast: false
matrix:
include:
- - php-version: '8.1'
- dependencies: 'lowest'
- php-version: '8.4'
dependencies: 'highest'
@@ -33,7 +42,7 @@ jobs:
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-version }}
- extensions: mbstring, intl, pdo_sqlite
+ extensions: mbstring, intl, pdo_mysql, pdo_sqlite
ini-values: zend.assertions=1
coverage: none
@@ -49,10 +58,12 @@ jobs:
- name: Run PHPUnit
run: vendor/bin/phpunit
env:
- DATABASE_TEST_URL: sqlite://./testdb.sqlite
+ DATABASE_TEST_URL: mysql://root:root@127.0.0.1:3306/plugins_cakephp_test?encoding=utf8mb4
+ AUTH_ID_GITHUB: someid
+ AUTH_SECRET_GITHUB: somesecret
coding-standard:
- name: Coding Standard & Static Analysis
+ name: Coding Standard
runs-on: ubuntu-24.04
steps:
@@ -61,19 +72,13 @@ jobs:
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
- php-version: '8.1'
+ php-version: '8.4'
extensions: mbstring, intl
coverage: none
- tools: cs2pr, phpstan:1.12
+ tools: cs2pr
- name: Composer install
uses: ramsey/composer-install@v3
- name: Run PHP CodeSniffer
run: vendor/bin/phpcs --report=checkstyle | cs2pr
-
- - name: Run phpstan
- if: always()
- run: phpstan
- env:
- SECURITY_SALT: f76f1c8475585c46c6acd3ddcb8f5e0f15de524637bb4080a08c4afe7cfc9144
diff --git a/README.md b/README.md
index 72fb306b..f5bb2f35 100644
--- a/README.md
+++ b/README.md
@@ -13,6 +13,7 @@ Next you need to apply migrations to create the database tables:
```bash
ddev exec bin/cake migrations migrate
ddev exec bin/cake migrations migrate -p Tags
+ddev exec bin/cake migrations migrate -p ADmad/SocialAuth
```
After that you can perform a fresh sync via
@@ -21,9 +22,27 @@ After that you can perform a fresh sync via
ddev exec bin/cake sync_packages
```
+While that is running (takes quite a while) you should install node modules and start the dev server via
+
+```bash
+ddev exec npm i
+ddev exec bin/cake devserver
+```
+
With that you should now see what is currently on the production site.
-## Cleaning up tables
+## Get social auth working
+
+If you want to test/develop social auth via Github you need to set the following 2 environment variables in your `config/.env` file:
+
+```
+export AUTH_ID_GITHUB="someid"
+export AUTH_SECRET_GITHUB="somesecret"
+```
+
+You can get these tokens via creating a Github OAuth App on https://github.com/settings/developers
+
+## Cleaning up packages
If you want to clean up package data, you can run the following command:
@@ -36,7 +55,7 @@ ddev exec bin/cake clean
If you want to start with a fresh DB, you can run the following command:
```bash
-ddev exec -s db mysql -uroot -proot -e "DROP DATABASE IF EXISTS db; CREATE DATABASE db;"
+ddev exec -s db mysql -uroot -proot -e "DROP DATABASE IF EXISTS db; CREATE DATABASE db; CREATE DATABASE testdb;"
```
> [!NOTE]
diff --git a/composer.json b/composer.json
index 1ee33754..8179f397 100644
--- a/composer.json
+++ b/composer.json
@@ -6,6 +6,8 @@
"homepage": "https://cakephp.org",
"require": {
"php": ">=8.1",
+ "admad/cakephp-social-auth": "^2.2",
+ "cakephp/authentication": "^4.0",
"cakephp/cakephp": "5.3.*",
"cakephp/migrations": "^5.0.0",
"cakephp/plugin-installer": "^2.0",
diff --git a/composer.lock b/composer.lock
index 50fde524..ea318829 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,8 +4,123 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "ba15e0ca32fa81323d335f4ea97318fc",
+ "content-hash": "e5f4a98db5aff3fb593ad53b2b2adb53",
"packages": [
+ {
+ "name": "admad/cakephp-social-auth",
+ "version": "2.2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/ADmad/cakephp-social-auth.git",
+ "reference": "9ee1f2c88705fed6868f81b970d754ea32878467"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/ADmad/cakephp-social-auth/zipball/9ee1f2c88705fed6868f81b970d754ea32878467",
+ "reference": "9ee1f2c88705fed6868f81b970d754ea32878467",
+ "shasum": ""
+ },
+ "require": {
+ "cakephp/cakephp": "^5.0",
+ "php": ">=8.1",
+ "socialconnect/auth": "^3.3"
+ },
+ "require-dev": {
+ "cakephp/cakephp-codesniffer": "^5.3",
+ "phpunit/phpunit": "^10.5.58 || ^11.5.3 || ^12.4"
+ },
+ "type": "cakephp-plugin",
+ "autoload": {
+ "psr-4": {
+ "ADmad\\SocialAuth\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "A CakePHP plugin which allows you to authenticate using social providers like Facebook/Google/Twitter etc.",
+ "support": {
+ "issues": "https://github.com/ADmad/cakephp-social-auth/issues",
+ "source": "https://github.com/ADmad/cakephp-social-auth/tree/2.2.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/ADmad",
+ "type": "github"
+ }
+ ],
+ "time": "2026-01-06T12:35:18+00:00"
+ },
+ {
+ "name": "cakephp/authentication",
+ "version": "4.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/cakephp/authentication.git",
+ "reference": "5c6afb1c6d858b25032d4d044bfecbb7ce4d6e57"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/cakephp/authentication/zipball/5c6afb1c6d858b25032d4d044bfecbb7ce4d6e57",
+ "reference": "5c6afb1c6d858b25032d4d044bfecbb7ce4d6e57",
+ "shasum": ""
+ },
+ "require": {
+ "cakephp/http": "^5.0",
+ "cakephp/utility": "^5.0",
+ "laminas/laminas-diactoros": "^3.0",
+ "php": ">=8.1",
+ "psr/http-client": "^1.0",
+ "psr/http-message": "^1.1 || ^2.0",
+ "psr/http-server-handler": "^1.0",
+ "psr/http-server-middleware": "^1.0"
+ },
+ "require-dev": {
+ "cakephp/cakephp": "^5.1.0",
+ "cakephp/cakephp-codesniffer": "^5.0",
+ "firebase/php-jwt": "^7.0",
+ "phpunit/phpunit": "^10.5.58 || ^11.5.3 || ^12.4"
+ },
+ "suggest": {
+ "cakephp/cakephp": "Install full core to use \"CookieAuthenticator\".",
+ "cakephp/orm": "To use \"OrmResolver\" (Not needed separately if using full CakePHP framework).",
+ "cakephp/utility": "Provides CakePHP security methods. Required for the JWT adapter and Legacy password hasher.",
+ "ext-ldap": "Make sure this php extension is installed and enabled on your system if you want to use the built-in LDAP adapter for \"LdapIdentifier\".",
+ "firebase/php-jwt": "If you want to use the JWT adapter add this dependency"
+ },
+ "type": "cakephp-plugin",
+ "autoload": {
+ "psr-4": {
+ "Authentication\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "CakePHP Community",
+ "homepage": "https://github.com/cakephp/authentication/graphs/contributors"
+ }
+ ],
+ "description": "Authentication plugin for CakePHP",
+ "homepage": "https://cakephp.org",
+ "keywords": [
+ "Authentication",
+ "auth",
+ "cakephp",
+ "middleware"
+ ],
+ "support": {
+ "docs": "https://book.cakephp.org/authentication/4/en/",
+ "forum": "https://discourse.cakephp.org/",
+ "issues": "https://github.com/cakephp/authentication/issues",
+ "source": "https://github.com/cakephp/authentication"
+ },
+ "time": "2026-02-27T02:57:39+00:00"
+ },
{
"name": "cakephp/cakephp",
"version": "5.3.3",
@@ -1993,6 +2108,134 @@
},
"time": "2019-03-08T08:55:37+00:00"
},
+ {
+ "name": "socialconnect/auth",
+ "version": "3.6.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/SocialConnect/auth.git",
+ "reference": "3a5ccbe7ca51e6bf0769904c3aeb8b6afc656d08"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/SocialConnect/auth/zipball/3a5ccbe7ca51e6bf0769904c3aeb8b6afc656d08",
+ "reference": "3a5ccbe7ca51e6bf0769904c3aeb8b6afc656d08",
+ "shasum": ""
+ },
+ "require": {
+ "ext-json": "*",
+ "php": "^7.4|^8.0",
+ "psr/http-client": "^1.0 || ^2.0",
+ "psr/http-factory": "^1.0 || ^2.0",
+ "psr/simple-cache": "^1.0 || ^2.0 || ^3.0",
+ "socialconnect/jwx": "^1.0"
+ },
+ "replace": {
+ "socialconnect/common": "self.version",
+ "socialconnect/oauth1": "self.version",
+ "socialconnect/oauth2": "self.version",
+ "socialconnect/openid": "self.version",
+ "socialconnect/openid-connect": "self.version",
+ "socialconnect/provider": "self.version"
+ },
+ "require-dev": {
+ "phpstan/phpstan": "^2.1",
+ "phpunit/phpunit": "^9.6",
+ "socialconnect/http-client": "^1.0",
+ "squizlabs/php_codesniffer": "^3.12"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "SocialConnect\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Dmitry Patsura",
+ "email": "talk@dmtry.me"
+ }
+ ],
+ "description": "Social Connect Auth Component",
+ "keywords": [
+ "amazon",
+ "auth",
+ "bitbucket",
+ "facebook",
+ "github",
+ "gitlab",
+ "google",
+ "login",
+ "oauth",
+ "slack",
+ "socialconnect",
+ "twitch",
+ "twitter",
+ "vk"
+ ],
+ "support": {
+ "issues": "https://github.com/SocialConnect/auth/issues",
+ "source": "https://github.com/SocialConnect/auth/tree/3.6.2"
+ },
+ "time": "2025-08-28T07:30:08+00:00"
+ },
+ {
+ "name": "socialconnect/jwx",
+ "version": "1.4.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/SocialConnect/jwx.git",
+ "reference": "973492237a3c06fe23199b420b48d43ea30e98f3"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/SocialConnect/jwx/zipball/973492237a3c06fe23199b420b48d43ea30e98f3",
+ "reference": "973492237a3c06fe23199b420b48d43ea30e98f3",
+ "shasum": ""
+ },
+ "require": {
+ "ext-json": "*",
+ "php": "^7.4|^8.0"
+ },
+ "require-dev": {
+ "phpstan/phpstan": "^2.1",
+ "phpunit/phpunit": "^9.6"
+ },
+ "suggest": {
+ "ext-openssl": "Please install openssl extension to use RS encryption"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "SocialConnect\\JWX\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Dmitry Patsura",
+ "email": "talk@dmtry.me"
+ }
+ ],
+ "description": "PHP library for JSON web tokens (JWT) and JWT",
+ "keywords": [
+ "JSON Web Token",
+ "JWE",
+ "jwt"
+ ],
+ "support": {
+ "issues": "https://github.com/SocialConnect/jwx/issues",
+ "source": "https://github.com/SocialConnect/jwx/tree/1.4.0"
+ },
+ "time": "2025-12-17T15:47:15+00:00"
+ },
{
"name": "symfony/deprecation-contracts",
"version": "v3.6.0",
diff --git a/config/Migrations/20260405153913_Users.php b/config/Migrations/20260405153913_Users.php
new file mode 100644
index 00000000..f70e2e1a
--- /dev/null
+++ b/config/Migrations/20260405153913_Users.php
@@ -0,0 +1,29 @@
+table('users')
+ ->addColumn('first_name', 'string', ['null' => true])
+ ->addColumn('last_name', 'string', ['null' => true])
+ ->addColumn('email', 'string')
+ ->addColumn('username', 'string')
+ ->addColumn('is_cakephp_dev', 'boolean', ['default' => false])
+ ->addTimestamps('created', 'modified')
+ ->addIndex('email', ['unique' => true])
+ ->addIndex('username', ['unique' => true])
+ ->create();
+ }
+}
diff --git a/config/Migrations/schema-dump-default.lock b/config/Migrations/schema-dump-default.lock
index 59339a4a..25fbd515 100644
Binary files a/config/Migrations/schema-dump-default.lock and b/config/Migrations/schema-dump-default.lock differ
diff --git a/config/app.php b/config/app.php
index 1568f7c2..f0926aef 100644
--- a/config/app.php
+++ b/config/app.php
@@ -92,6 +92,11 @@
// 'cacheTime' => '+1 year'
],
+ 'GitHub' => [
+ 'applicationId' => env('AUTH_ID_GITHUB'),
+ 'applicationSecret' => env('AUTH_SECRET_GITHUB'),
+ ],
+
'Packages' => [
'featured' => [
'markstory/asset_compress',
@@ -470,4 +475,8 @@
'errorLevel' => null,
'fixtureStrategy' => null,
],
+
+ 'Migrations' => [
+ 'add_timestamps_use_datetime' => true,
+ ],
];
diff --git a/config/bootstrap.php b/config/bootstrap.php
index 6eadafb0..103c9ec5 100644
--- a/config/bootstrap.php
+++ b/config/bootstrap.php
@@ -44,6 +44,8 @@
use Cake\Mailer\TransportFactory;
use Cake\Routing\Router;
use Cake\Utility\Security;
+use Detection\MobileDetect;
+use josegonzalez\Dotenv\Loader;
use function Cake\Core\env;
/*
@@ -66,7 +68,7 @@
* for more information for recommended practices.
*/
if (!env('APP_NAME') && file_exists(CONFIG . '.env')) {
- $dotenv = new \josegonzalez\Dotenv\Loader([CONFIG . '.env']);
+ $dotenv = new Loader([CONFIG . '.env']);
$dotenv->parse()
->putenv()
->toEnv()
@@ -83,7 +85,7 @@
try {
Configure::config('default', new PhpConfig());
Configure::load('app', 'default', false);
-} catch (\Exception $e) {
+} catch (Exception $e) {
exit($e->getMessage() . "\n");
}
@@ -193,12 +195,12 @@
* and the mobiledetect package from composer.json.
*/
ServerRequest::addDetector('mobile', function ($request) {
- $detector = new \Detection\MobileDetect();
+ $detector = new MobileDetect();
return $detector->isMobile();
});
ServerRequest::addDetector('tablet', function ($request) {
- $detector = new \Detection\MobileDetect();
+ $detector = new MobileDetect();
return $detector->isTablet();
});
diff --git a/config/plugins.php b/config/plugins.php
index 21b687a7..82f147f2 100644
--- a/config/plugins.php
+++ b/config/plugins.php
@@ -35,4 +35,6 @@
// Additional plugins here
'Search',
'Tags',
+ 'ADmad/SocialAuth',
+ 'Authentication',
];
diff --git a/package-lock.json b/package-lock.json
index 24d40a80..b3a1fea6 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,5 +1,5 @@
{
- "name": "plugins.cakephp.org",
+ "name": "html",
"lockfileVersion": 3,
"requires": true,
"packages": {
diff --git a/phpcs.xml b/phpcs.xml
index ea0eeeeb..42577f05 100644
--- a/phpcs.xml
+++ b/phpcs.xml
@@ -5,6 +5,13 @@
*/src/Controller/*
+
+
+
+
+
+
+
src/
tests/
diff --git a/src/Application.php b/src/Application.php
index 6675681b..1b87fd10 100644
--- a/src/Application.php
+++ b/src/Application.php
@@ -16,10 +16,17 @@
*/
namespace App;
+use ADmad\SocialAuth\Middleware\SocialAuthMiddleware;
+use App\Event\AfterGithubIdentify;
+use Authentication\AuthenticationService;
+use Authentication\AuthenticationServiceInterface;
+use Authentication\AuthenticationServiceProviderInterface;
+use Authentication\Middleware\AuthenticationMiddleware;
use Cake\Core\Configure;
use Cake\Core\ContainerInterface;
use Cake\Datasource\FactoryLocator;
use Cake\Error\Middleware\ErrorHandlerMiddleware;
+use Cake\Event\EventManager;
use Cake\Http\BaseApplication;
use Cake\Http\Middleware\BodyParserMiddleware;
use Cake\Http\Middleware\CsrfProtectionMiddleware;
@@ -28,6 +35,8 @@
use Cake\ORM\Locator\TableLocator;
use Cake\Routing\Middleware\AssetMiddleware;
use Cake\Routing\Middleware\RoutingMiddleware;
+use Psr\Http\Message\ServerRequestInterface;
+use SocialConnect\OAuth2\Provider\GitHub;
/**
* Application setup class.
@@ -37,7 +46,7 @@
*
* @extends \Cake\Http\BaseApplication<\App\Application>
*/
-class Application extends BaseApplication
+class Application extends BaseApplication implements AuthenticationServiceProviderInterface
{
/**
* Load all the application configuration and bootstrap logic.
@@ -55,6 +64,8 @@ public function bootstrap(): void
(new TableLocator())->allowFallbackClass(false),
);
}
+
+ EventManager::instance()->on(new AfterGithubIdentify());
}
/**
@@ -100,7 +111,31 @@ public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue
// Cross Site Request Forgery (CSRF) Protection Middleware
// https://book.cakephp.org/5/en/security/csrf.html#cross-site-request-forgery-csrf-middleware
- ->add($csrf);
+ ->add($csrf)
+
+ ->add(new SocialAuthMiddleware([
+ // SocialConnect Auth service's providers config. https://github.com/SocialConnect/auth/blob/master/README.md
+ 'serviceConfig' => [
+ 'provider' => [
+ GitHub::NAME => [
+ 'applicationId' => Configure::readOrFail('GitHub.applicationId'),
+ 'applicationSecret' => Configure::readOrFail('GitHub.applicationSecret'),
+ 'scope' => [
+ 'user:email',
+ 'read:org',
+ ],
+ 'options' => [
+ 'identity.fields' => [
+ 'email',
+ ],
+ 'fetch_emails' => true,
+ ],
+ ],
+ ],
+ ],
+ ]))
+
+ ->add(new AuthenticationMiddleware($this));
return $middlewareQueue;
}
@@ -115,4 +150,30 @@ public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue
public function services(ContainerInterface $container): void
{
}
+
+ /**
+ * @param \Psr\Http\Message\ServerRequestInterface $request
+ * @return \Authentication\AuthenticationServiceInterface
+ */
+ public function getAuthenticationService(ServerRequestInterface $request): AuthenticationServiceInterface
+ {
+ $service = new AuthenticationService();
+
+ // Define where users should be redirected to when they are not authenticated
+ $service->setConfig([
+ 'unauthenticatedRedirect' => [
+ 'prefix' => false,
+ 'plugin' => false,
+ 'controller' => 'Packages',
+ 'action' => 'index',
+ ],
+ 'queryParam' => 'redirect',
+ ]);
+
+ // Load the authenticators. Session should be first.
+ // Session just uses session data directly as identity, no identifier needed.
+ $service->loadAuthenticator('Authentication.Session');
+
+ return $service;
+ }
}
diff --git a/src/Controller/AppController.php b/src/Controller/AppController.php
index c1978c2a..2d90aa4a 100644
--- a/src/Controller/AppController.php
+++ b/src/Controller/AppController.php
@@ -43,6 +43,7 @@ public function initialize(): void
$this->loadComponent('Flash');
$this->loadComponent('Search.Search');
+ $this->loadComponent('Authentication.Authentication');
/*
* Enable the following component for recommended CakePHP form protection settings.
diff --git a/src/Controller/PackagesController.php b/src/Controller/PackagesController.php
index a375499f..612af8c2 100644
--- a/src/Controller/PackagesController.php
+++ b/src/Controller/PackagesController.php
@@ -13,6 +13,16 @@
*/
class PackagesController extends AppController
{
+ /**
+ * @return void
+ */
+ public function initialize(): void
+ {
+ parent::initialize();
+
+ $this->Authentication->allowUnauthenticated(['index']);
+ }
+
/**
* Index method
*
diff --git a/src/Controller/UsersController.php b/src/Controller/UsersController.php
new file mode 100644
index 00000000..dba10d63
--- /dev/null
+++ b/src/Controller/UsersController.php
@@ -0,0 +1,24 @@
+Authentication->logout();
+
+ return $this->redirect(['controller' => 'Packages', 'action' => 'index']);
+ }
+}
diff --git a/src/Event/AfterGithubIdentify.php b/src/Event/AfterGithubIdentify.php
new file mode 100644
index 00000000..46b485df
--- /dev/null
+++ b/src/Event/AfterGithubIdentify.php
@@ -0,0 +1,69 @@
+ 'afterIdentify',
+ ];
+ }
+
+ /**
+ * After identify callback.
+ *
+ * @param \Cake\Event\Event $event The event instance.
+ * @param \App\Model\Entity\User $user The user entity.
+ * @return void
+ */
+ public function afterIdentify(EventInterface $event, User $user): void
+ {
+ $token = $user->social_profile->access_token?->getToken();
+ if (!$token) {
+ return;
+ }
+
+ $http = new Client([
+ 'headers' => [
+ 'Authorization' => "Bearer {$token}",
+ 'Accept' => 'application/vnd.github+json',
+ 'User-Agent' => 'plugins.cakephp.org',
+ ],
+ ]);
+
+ $response = $http->get('https://api.github.com/user/orgs');
+ if (!$response->isOk()) {
+ return;
+ }
+
+ $data = $response->getJson();
+ $orgs = Hash::extract($data, '{n}.login');
+
+ $isCakePHPDev = false;
+ if (in_array('cakephp', $orgs, true)) {
+ $isCakePHPDev = true;
+ }
+
+ $usersTable = $this->getTableLocator()->get('Users');
+ $user = $usersTable->patchEntity($user, ['is_cakephp_dev' => $isCakePHPDev]);
+ $usersTable->save($user);
+ }
+}
diff --git a/src/Model/Behavior/.gitkeep b/src/Model/Behavior/.gitkeep
deleted file mode 100644
index e69de29b..00000000
diff --git a/src/Model/Entity/User.php b/src/Model/Entity/User.php
new file mode 100644
index 00000000..612f6e16
--- /dev/null
+++ b/src/Model/Entity/User.php
@@ -0,0 +1,36 @@
+
+ */
+ protected array $_accessible = [
+ '*' => true,
+ 'id' => false,
+ ];
+}
diff --git a/src/Model/Table/UsersTable.php b/src/Model/Table/UsersTable.php
new file mode 100644
index 00000000..dafd8cc1
--- /dev/null
+++ b/src/Model/Table/UsersTable.php
@@ -0,0 +1,135 @@
+ newEntities(array $data, array $options = [])
+ * @method \App\Model\Entity\User get(mixed $primaryKey, array|string $finder = 'all', \Psr\SimpleCache\CacheInterface|string|null $cache = null, \Closure|string|null $cacheKey = null, mixed ...$args)
+ * @method \App\Model\Entity\User findOrCreate($search, ?callable $callback = null, array $options = [])
+ * @method \App\Model\Entity\User patchEntity(\Cake\Datasource\EntityInterface $entity, array $data, array $options = [])
+ * @method array<\App\Model\Entity\User> patchEntities(iterable $entities, array $data, array $options = [])
+ * @method \App\Model\Entity\User|false save(\Cake\Datasource\EntityInterface $entity, array $options = [])
+ * @method \App\Model\Entity\User saveOrFail(\Cake\Datasource\EntityInterface $entity, array $options = [])
+ * @method iterable<\App\Model\Entity\User>|\Cake\Datasource\ResultSetInterface<\App\Model\Entity\User>|false saveMany(iterable $entities, array $options = [])
+ * @method iterable<\App\Model\Entity\User>|\Cake\Datasource\ResultSetInterface<\App\Model\Entity\User> saveManyOrFail(iterable $entities, array $options = [])
+ * @method iterable<\App\Model\Entity\User>|\Cake\Datasource\ResultSetInterface<\App\Model\Entity\User>|false deleteMany(iterable $entities, array $options = [])
+ * @method iterable<\App\Model\Entity\User>|\Cake\Datasource\ResultSetInterface<\App\Model\Entity\User> deleteManyOrFail(iterable $entities, array $options = [])
+ * @mixin \Cake\ORM\Behavior\TimestampBehavior
+ */
+class UsersTable extends Table
+{
+ /**
+ * Initialize method
+ *
+ * @param array $config The configuration for the Table.
+ * @return void
+ */
+ public function initialize(array $config): void
+ {
+ parent::initialize($config);
+
+ $this->setTable('users');
+ $this->setDisplayField('first_name');
+ $this->setPrimaryKey('id');
+
+ $this->addBehavior('Timestamp');
+ }
+
+ /**
+ * Default validation rules.
+ *
+ * @param \Cake\Validation\Validator $validator Validator instance.
+ * @return \Cake\Validation\Validator
+ */
+ public function validationDefault(Validator $validator): Validator
+ {
+ $validator
+ ->scalar('first_name')
+ ->maxLength('first_name', 255)
+ ->allowEmptyString('first_name');
+
+ $validator
+ ->scalar('last_name')
+ ->maxLength('last_name', 255)
+ ->allowEmptyString('last_name');
+
+ $validator
+ ->email('email')
+ ->requirePresence('email', 'create')
+ ->notEmptyString('email');
+
+ $validator
+ ->scalar('username')
+ ->maxLength('username', 255)
+ ->requirePresence('username', 'create')
+ ->notEmptyString('username');
+
+ return $validator;
+ }
+
+ /**
+ * Returns a rules checker object that will be used for validating
+ * application integrity.
+ *
+ * @param \Cake\ORM\RulesChecker $rules The rules object to be modified.
+ * @return \Cake\ORM\RulesChecker
+ */
+ public function buildRules(RulesChecker $rules): RulesChecker
+ {
+ $rules->add($rules->isUnique(['email']), ['errorField' => 'email']);
+ $rules->add($rules->isUnique(['username']), ['errorField' => 'username']);
+
+ return $rules;
+ }
+
+ /**
+ * @param \Cake\Datasource\EntityInterface $profile The data from github
+ * @param \Cake\Http\Session $session The current session
+ * @return \App\Model\Entity\User|\Cake\Datasource\EntityInterface|mixed
+ */
+ public function getUser(EntityInterface $profile, Session $session): mixed
+ {
+ // Make sure here that all the required fields are actually present
+ if (!$profile->email) {
+ throw new RuntimeException('Could not find email in social profile.');
+ }
+
+ // Check if user with same email exists. This avoids creating multiple
+ // user accounts for different social identities of same user. You should
+ // probably skip this check if your system doesn't enforce unique email
+ // per user.
+ $user = $this->find()
+ ->where(['email' => $profile->email])
+ ->first();
+
+ if (!$user) {
+ // Create new user account
+ $user = $this->newEntity([]);
+ }
+
+ $user = $this->patchEntity($user, [
+ 'first_name' => $profile->first_name,
+ 'last_name' => $profile->last_name,
+ 'email' => $profile->email,
+ 'username' => $profile->username,
+ ]);
+
+ if (!$this->save($user)) {
+ throw new RuntimeException('Unable to save new user');
+ }
+
+ return $user;
+ }
+}
diff --git a/src/View/AppView.php b/src/View/AppView.php
index 0857f8ee..796ad382 100644
--- a/src/View/AppView.php
+++ b/src/View/AppView.php
@@ -40,5 +40,6 @@ public function initialize(): void
$this->loadHelper('Paginator', ['templates' => 'paginator-templates']);
$this->loadHelper('Form', ['templates' => 'form-templates']);
$this->loadHelper('Html', ['templates' => 'html-templates']);
+ $this->loadHelper('Authentication.Identity');
}
}
diff --git a/templates/layout/default.php b/templates/layout/default.php
index 78e94f4e..749d88e7 100644
--- a/templates/layout/default.php
+++ b/templates/layout/default.php
@@ -88,21 +88,48 @@
= $this->Form->end() ?>
-
diff --git a/tests/Fixture/PackagesFixture.php b/tests/Fixture/PackagesFixture.php
index b571ec02..f355a0f0 100644
--- a/tests/Fixture/PackagesFixture.php
+++ b/tests/Fixture/PackagesFixture.php
@@ -20,14 +20,28 @@ public function init(): void
$this->records = [
[
'id' => 1,
- 'package' => 'Lorem ipsum dolor sit amet',
- 'description' => 'Lorem ipsum dolor sit amet, aliquet feugiat. Convallis morbi fringilla gravida, phasellus feugiat dapibus velit nunc, pulvinar eget sollicitudin venenatis cum nullam, vivamus ut a sed, mollitia lectus. Nulla vestibulum massa neque ut et, id hendrerit sit, feugiat in taciti enim proin nibh, tempor dignissim, rhoncus duis vestibulum nunc mattis convallis.',
- 'repo_url' => 'Lorem ipsum dolor sit amet',
- 'packagist_url' => 'Lorem ipsum dolor sit amet',
- 'downloads' => 1,
- 'stars' => 1,
+ 'package' => 'markstory/asset_compress',
+ 'description' => 'Featured package used to verify the homepage slider.',
+ 'repo_url' => 'https://github.com/markstory/asset_compress',
+ 'downloads' => 5000,
+ 'stars' => 450,
+ 'latest_stable_version' => '5.0.0',
],
];
+
+ for ($i = 2; $i <= 24; $i++) {
+ $packageNumber = str_pad((string)($i - 1), 2, '0', STR_PAD_LEFT);
+ $this->records[] = [
+ 'id' => $i,
+ 'package' => sprintf('vendor/package-%s', $packageNumber),
+ 'description' => sprintf('Test package %s for controller pagination and search coverage.', $packageNumber),
+ 'repo_url' => sprintf('https://github.com/vendor/package-%s', $packageNumber),
+ 'downloads' => 1000 - $i,
+ 'stars' => 100 - $i,
+ 'latest_stable_version' => sprintf('1.%d.0', $i - 2),
+ ];
+ }
+
parent::init();
}
}
diff --git a/tests/TestCase/Controller/PagesControllerTest.php b/tests/TestCase/Controller/PagesControllerTest.php
deleted file mode 100644
index f0f72c9d..00000000
--- a/tests/TestCase/Controller/PagesControllerTest.php
+++ /dev/null
@@ -1,113 +0,0 @@
-get('/pages/home');
- $this->assertResponseOk();
- $this->assertResponseContains('CakePHP');
- $this->assertResponseContains('');
- }
-
- /**
- * Test that missing template renders 404 page in production
- *
- * @return void
- */
- public function testMissingTemplate()
- {
- Configure::write('debug', false);
- $this->get('/pages/not_existing');
-
- $this->assertResponseError();
- $this->assertResponseContains('Error');
- }
-
- /**
- * Test that missing template in debug mode renders missing_template error page
- *
- * @return void
- */
- public function testMissingTemplateInDebug()
- {
- Configure::write('debug', true);
- $this->get('/pages/not_existing');
-
- $this->assertResponseFailure();
- $this->assertResponseContains('Missing Template');
- $this->assertResponseContains('stack-frames');
- $this->assertResponseContains('not_existing.php');
- }
-
- /**
- * Test directory traversal protection
- *
- * @return void
- */
- public function testDirectoryTraversalProtection()
- {
- $this->get('/pages/../Layout/ajax');
- $this->assertResponseCode(403);
- $this->assertResponseContains('Forbidden');
- }
-
- /**
- * Test that CSRF protection is applied to page rendering.
- *
- * @return void
- */
- public function testCsrfAppliedError()
- {
- $this->post('/pages/home', ['hello' => 'world']);
-
- $this->assertResponseCode(403);
- $this->assertResponseContains('CSRF');
- }
-
- /**
- * Test that CSRF protection is applied to page rendering.
- *
- * @return void
- */
- public function testCsrfAppliedOk()
- {
- $this->enableCsrfToken();
- $this->post('/pages/home', ['hello' => 'world']);
-
- $this->assertThat(403, $this->logicalNot(new StatusCode($this->_response)));
- $this->assertResponseNotContains('CSRF');
- }
-}
diff --git a/tests/bootstrap.php b/tests/bootstrap.php
index 2f38af99..3254d86c 100644
--- a/tests/bootstrap.php
+++ b/tests/bootstrap.php
@@ -60,6 +60,7 @@
// Connection aliasing needs to happen before migrations are run.
// Otherwise, table objects inside migrations would use the default datasource
ConnectionHelper::addTestAliases();
+ConnectionHelper::dropTables('test');
// Use migrations to build test database schema.
//
@@ -72,4 +73,8 @@
// use Cake\TestSuite\Fixture\SchemaLoader;
// (new SchemaLoader())->loadSqlFiles('./tests/schema.sql', 'test');
-(new Migrator())->run();
+(new Migrator())->runMany([
+ [],
+ ['plugin' => 'Tags'],
+ ['plugin' => 'ADmad/SocialAuth'],
+]);