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 @@ 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'], +]);