├── app
├── Filters
│ └── .gitkeep
├── Helpers
│ ├── .gitkeep
│ └── auth_helper.php
├── Language
│ ├── .gitkeep
│ └── en
│ │ └── Validation.php
├── Libraries
│ ├── .gitkeep
│ └── AuthService.php
├── Models
│ ├── .gitkeep
│ └── User.php
├── ThirdParty
│ └── .gitkeep
├── Validation
│ ├── .gitkeep
│ └── UserRules.php
├── Database
│ ├── Seeds
│ │ ├── .gitkeep
│ │ └── DatabaseSeeder.php
│ └── Migrations
│ │ ├── .gitkeep
│ │ └── 20190911140357_create_user_table.php
├── Config
│ ├── ForeignCharacters.php
│ ├── Images.php
│ ├── Honeypot.php
│ ├── Boot
│ │ ├── production.php
│ │ ├── testing.php
│ │ └── development.php
│ ├── Validation.php
│ ├── Pager.php
│ ├── Filters.php
│ ├── Encryption.php
│ ├── View.php
│ ├── Services.php
│ ├── Exceptions.php
│ ├── Modules.php
│ ├── Migrations.php
│ ├── Kint.php
│ ├── Events.php
│ ├── ContentSecurityPolicy.php
│ ├── Routes.php
│ ├── Autoload.php
│ ├── DocTypes.php
│ ├── Toolbar.php
│ ├── Format.php
│ ├── Paths.php
│ ├── Database.php
│ ├── Email.php
│ ├── Constants.php
│ ├── Cache.php
│ ├── Logger.php
│ ├── UserAgents.php
│ └── App.php
├── .htaccess
├── Views
│ ├── errors
│ │ ├── cli
│ │ │ ├── error_404.php
│ │ │ ├── production.php
│ │ │ └── error_exception.php
│ │ └── html
│ │ │ ├── production.php
│ │ │ ├── error_404.php
│ │ │ ├── debug.css
│ │ │ └── debug.js
│ ├── mail
│ │ ├── contact.php
│ │ ├── resetPassword.php
│ │ ├── signup.php
│ │ └── emailVerification.php
│ ├── welcome_message.php
│ ├── user
│ │ ├── resetPassword.php
│ │ ├── requestPasswordReset.php
│ │ ├── resendVerificationEmail.php
│ │ ├── login.php
│ │ └── signup.php
│ ├── contact.php
│ ├── _further.php
│ ├── layout.php
│ └── _logo.php
├── index.html
├── Controllers
│ ├── Home.php
│ ├── BaseController.php
│ ├── Contact.php
│ └── User.php
├── Common.php
├── Forms
│ ├── LoginForm.php
│ ├── ResetPasswordForm.php
│ ├── ResendVerificationEmailForm.php
│ ├── ContactForm.php
│ ├── PasswordResetRequestForm.php
│ └── SignupForm.php
└── Entities
│ └── User.php
├── serve.bat
├── writable
├── debugbar
│ └── .gitkeep
├── .htaccess
├── cache
│ └── index.html
├── logs
│ └── index.html
├── session
│ └── index.html
└── uploads
│ └── index.html
├── public
├── robots.txt
├── favicon.ico
├── css
│ ├── custom.css
│ └── style.css
├── index.php
└── .htaccess
├── refresh.bat
├── _images
├── screen.png
├── screen_login.png
├── screen_contact.png
├── screen_signup.png
├── screen_welcome.png
├── screen_reset_password.png
└── screen_resend_verification.png
├── admin
├── Config
│ ├── Events.php
│ ├── Routes.php
│ └── Services.php
├── Controllers
│ ├── Home.php
│ ├── Logout.php
│ ├── BaseController.php
│ └── Login.php
├── Helpers
│ └── admin_auth_helper.php
├── Views
│ ├── welcome_message.php
│ ├── login.php
│ └── layout.php
└── Filters
│ └── Admin.php
├── tests
├── session
│ └── ExampleSessionTest.php
├── _support
│ ├── Models
│ │ └── ExampleModel.php
│ ├── SessionTestCase.php
│ ├── Database
│ │ ├── Seeds
│ │ │ └── ExampleSeeder.php
│ │ └── Migrations
│ │ │ └── 2020-02-22-222222_example_migration.php
│ ├── DatabaseTestCase.php
│ └── Libraries
│ │ └── ConfigReader.php
├── unit
│ └── HealthTest.php
├── database
│ └── ExampleDatabaseTest.php
└── README.md
├── LICENSE
├── license.txt
├── composer.json
├── .github
└── workflows
│ └── phpunit.yml
├── spark
├── phpunit.xml.dist
├── README.md
├── .gitignore
└── env
/app/Filters/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/Helpers/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/Language/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/Libraries/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/Models/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/ThirdParty/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/Validation/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/Database/Seeds/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/serve.bat:
--------------------------------------------------------------------------------
1 | php spark serve
--------------------------------------------------------------------------------
/writable/debugbar/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/Database/Migrations/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Disallow:
3 |
--------------------------------------------------------------------------------
/refresh.bat:
--------------------------------------------------------------------------------
1 | php spark migrate:refresh && php spark db:seed DatabaseSeeder
--------------------------------------------------------------------------------
/_images/screen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/denis303/codeigniter4-advanced-app/HEAD/_images/screen.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/denis303/codeigniter4-advanced-app/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/_images/screen_login.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/denis303/codeigniter4-advanced-app/HEAD/_images/screen_login.png
--------------------------------------------------------------------------------
/_images/screen_contact.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/denis303/codeigniter4-advanced-app/HEAD/_images/screen_contact.png
--------------------------------------------------------------------------------
/_images/screen_signup.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/denis303/codeigniter4-advanced-app/HEAD/_images/screen_signup.png
--------------------------------------------------------------------------------
/_images/screen_welcome.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/denis303/codeigniter4-advanced-app/HEAD/_images/screen_welcome.png
--------------------------------------------------------------------------------
/_images/screen_reset_password.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/denis303/codeigniter4-advanced-app/HEAD/_images/screen_reset_password.png
--------------------------------------------------------------------------------
/_images/screen_resend_verification.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/denis303/codeigniter4-advanced-app/HEAD/_images/screen_resend_verification.png
--------------------------------------------------------------------------------
/app/Config/ForeignCharacters.php:
--------------------------------------------------------------------------------
1 |
2 | Require all denied
3 |
4 |
5 | Deny from all
6 |
7 |
--------------------------------------------------------------------------------
/app/Language/en/Validation.php:
--------------------------------------------------------------------------------
1 |
2 | Require all denied
3 |
4 |
5 | Deny from all
6 |
7 |
--------------------------------------------------------------------------------
/app/Views/errors/cli/production.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 403 Forbidden
5 |
6 |
7 |
8 | Directory access is forbidden.
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/writable/cache/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 403 Forbidden
5 |
6 |
7 |
8 | Directory access is forbidden.
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/writable/logs/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 403 Forbidden
5 |
6 |
7 |
8 | Directory access is forbidden.
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/writable/session/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 403 Forbidden
5 |
6 |
7 |
8 | Directory access is forbidden.
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/writable/uploads/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 403 Forbidden
5 |
6 |
7 |
8 | Directory access is forbidden.
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/admin/Config/Routes.php:
--------------------------------------------------------------------------------
1 | add('admin', '\Admin\Controllers\Home::index');
4 | $routes->add('admin/login', '\Admin\Controllers\Login::index');
5 | $routes->add('admin/logout', '\Admin\Controllers\Logout::index');
--------------------------------------------------------------------------------
/app/Controllers/Home.php:
--------------------------------------------------------------------------------
1 | render('welcome_message');
11 | }
12 |
13 | }
--------------------------------------------------------------------------------
/admin/Controllers/Home.php:
--------------------------------------------------------------------------------
1 | render('welcome_message');
11 | }
12 |
13 | }
--------------------------------------------------------------------------------
/app/Database/Seeds/DatabaseSeeder.php:
--------------------------------------------------------------------------------
1 | call('Seeder');
11 | }
12 |
13 | }
--------------------------------------------------------------------------------
/admin/Controllers/Logout.php:
--------------------------------------------------------------------------------
1 | unsetId();
11 |
12 | return $this->goHome();
13 | }
14 |
15 | }
--------------------------------------------------------------------------------
/app/Views/mail/contact.php:
--------------------------------------------------------------------------------
1 | data['subject'] = 'Message from: ' . base_url();
4 |
5 | $this->data['mailType'] = 'html';
6 |
7 | ?>
8 | Created: = date('d.m.Y H:i');?>
9 |
10 | Subject: = esc($subject);?>
11 |
12 | Text: = esc($body);?>
--------------------------------------------------------------------------------
/admin/Helpers/admin_auth_helper.php:
--------------------------------------------------------------------------------
1 | getUserId();
16 | }
17 | }
--------------------------------------------------------------------------------
/app/Views/mail/resetPassword.php:
--------------------------------------------------------------------------------
1 | data['subject'] = 'Password reset for ' . base_url();
6 |
7 | $this->data['mailType'] = 'html';
8 |
9 | ?>
10 | Hello = esc($user->name);?>,
11 |
12 |
13 | Follow the link below to reset your password:
14 |
15 | = $resetLink;?>
--------------------------------------------------------------------------------
/app/Views/mail/signup.php:
--------------------------------------------------------------------------------
1 | data['subject'] = 'Account registration at ' . base_url();
6 |
7 | $this->data['mailType'] = 'html';
8 |
9 | ?>
10 | Hello = esc($user->name);?>,
11 |
12 |
13 | Follow the link below to verify your email:
14 |
15 | = $verifyLink;?>
--------------------------------------------------------------------------------
/app/Views/mail/emailVerification.php:
--------------------------------------------------------------------------------
1 | data['subject'] = 'Account verification at ' . base_url();
6 |
7 | $this->data['mailType'] = 'text';
8 |
9 | ?>
10 | Hello = esc($user->name);?>,
11 |
12 |
13 | Follow the link below to verify your email:
14 |
15 | = $verifyLink;?>
--------------------------------------------------------------------------------
/tests/session/ExampleSessionTest.php:
--------------------------------------------------------------------------------
1 | session->set('logged_in', 123);
13 |
14 | $value = $this->session->get('logged_in');
15 |
16 | $this->assertEquals(123, $value);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/app/Helpers/auth_helper.php:
--------------------------------------------------------------------------------
1 | getUserId();
21 | }
22 | }
--------------------------------------------------------------------------------
/admin/Config/Services.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Whoops!
8 |
9 |
12 |
13 |
14 |
15 |
16 |
17 |
Whoops!
18 |
19 |
We seem to have hit a snag. Please try again later...
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/tests/_support/Models/ExampleModel.php:
--------------------------------------------------------------------------------
1 | data['title'] = 'Welcome to CodeIgniter 4';
4 |
5 | $this->extend('Admin\layout');
6 |
7 | ?>
8 | section('content');?>
9 |
10 | About this page
11 |
12 | The page you are looking at is being generated dynamically by CodeIgniter.
13 |
14 | If you would like to edit this page you will find it located at:
15 |
16 | admin/Views/welcome_message.php
17 |
18 | The corresponding controller for this page can be found at:
19 |
20 | admin/Controllers/Home.php
21 |
22 | endSection();?>
--------------------------------------------------------------------------------
/app/Views/errors/cli/error_exception.php:
--------------------------------------------------------------------------------
1 | An uncaught Exception was encountered
2 |
3 | Type: = get_class($exception), "\n"; ?>
4 | Message: = $message, "\n"; ?>
5 | Filename: = $exception->getFile(), "\n"; ?>
6 | Line Number: = $exception->getLine(); ?>
7 |
8 |
9 |
10 | Backtrace:
11 | getTrace() as $error): ?>
12 |
13 | = trim('-' . $error['line'] . ' - ' . $error['file'] . '::' . $error['function']) . "\n" ?>
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/app/Common.php:
--------------------------------------------------------------------------------
1 | data['title'] = 'Welcome to CodeIgniter 4';
4 |
5 | $this->extend('layout');
6 |
7 | ?>
8 | section('content');?>
9 |
10 | About this page
11 |
12 | The page you are looking at is being generated dynamically by CodeIgniter.
13 |
14 | If you would like to edit this page you will find it located at:
15 |
16 | app/Views/welcome_message.php
17 |
18 | The corresponding controller for this page can be found at:
19 |
20 | app/Controllers/Home.php
21 |
22 | endSection();?>
23 |
24 | section('beforeFooter');?>
25 |
26 | = view('_further');?>
27 |
28 | endSection();?>
--------------------------------------------------------------------------------
/app/Forms/LoginForm.php:
--------------------------------------------------------------------------------
1 | [
17 | 'rules' => 'required|' . UserModel::EMAIL_RULES,
18 | 'label' => 'Email'
19 | ],
20 | 'password' => [
21 | 'rules' => 'required|' . UserModel::PASSWORD_RULES . '|verifiedUser[]',
22 | 'label' => 'Password'
23 | ],
24 | 'rememberMe' => [
25 | 'rules' => 'required|in_list[0,1]',
26 | 'label' => 'Remember Me'
27 | ]
28 | ];
29 |
30 | }
--------------------------------------------------------------------------------
/admin/Filters/Admin.php:
--------------------------------------------------------------------------------
1 | getId())
20 | {
21 | return redirect()->to('admin/login');
22 | }
23 | }
24 |
25 | public function after(RequestInterface $request, ResponseInterface $response, $arguments = null)
26 | {
27 | // Do something here
28 | }
29 |
30 | }
--------------------------------------------------------------------------------
/tests/unit/HealthTest.php:
--------------------------------------------------------------------------------
1 | assertTrue($test);
15 | }
16 |
17 | public function testBaseUrlHasBeenSet()
18 | {
19 | $env = $config = false;
20 |
21 | // First check in .env
22 | if (is_file(HOMEPATH . '.env'))
23 | {
24 | $env = (bool) preg_grep("/^app\.baseURL = './", file(HOMEPATH . '.env'));
25 | }
26 |
27 | // Then check the actual config file
28 | $reader = new \Tests\Support\Libraries\ConfigReader();
29 | $config = ! empty($reader->baseUrl);
30 |
31 | $this->assertTrue($env || $config);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/app/Config/Images.php:
--------------------------------------------------------------------------------
1 | \CodeIgniter\Images\Handlers\GDHandler::class,
29 | 'imagick' => \CodeIgniter\Images\Handlers\ImageMagickHandler::class,
30 | ];
31 | }
32 |
--------------------------------------------------------------------------------
/tests/_support/SessionTestCase.php:
--------------------------------------------------------------------------------
1 | mockSession();
19 | }
20 |
21 | /**
22 | * Pre-loads the mock session driver into $this->session.
23 | *
24 | * @var string
25 | */
26 | protected function mockSession()
27 | {
28 | $config = config('App');
29 | $this->session = new MockSession(new ArrayHandler($config, '0.0.0.0'), $config);
30 | \Config\Services::injectMock('session', $this->session);
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/app/Config/Honeypot.php:
--------------------------------------------------------------------------------
1 | {label} ';
35 |
36 | /**
37 | * Honeypot container
38 | *
39 | * @var string
40 | */
41 | public $container = '{template}
';
42 | }
43 |
--------------------------------------------------------------------------------
/app/Config/Boot/production.php:
--------------------------------------------------------------------------------
1 | [
17 | 'rules' => 'required|' . UserModel::PASSWORD_RULES,
18 | 'label' => 'Password'
19 | ]
20 | ];
21 |
22 | /**
23 | * Resets password.
24 | *
25 | * @return bool if password was reset.
26 | */
27 | public function resetPassword($user, $data, &$error)
28 | {
29 | $model = new UserModel;
30 |
31 | $user->setPassword($data['password']);
32 |
33 | $user->password_reset_token = null;
34 |
35 | $return = $model->save($user);
36 |
37 | if (!$return)
38 | {
39 | $errors = (array) $model->errors();
40 |
41 | $error = array_shift($errors);
42 | }
43 |
44 | return $return;
45 | }
46 |
47 | }
--------------------------------------------------------------------------------
/public/css/custom.css:
--------------------------------------------------------------------------------
1 | .alert
2 | {
3 | padding: 10px;
4 | margin-bottom: 10px;
5 | border-radius: 5px;
6 | }
7 |
8 | .alert-error
9 | {
10 | background-color: Red;
11 | color: white;
12 | }
13 |
14 | .alert-success
15 | {
16 | background-color: LimeGreen;
17 | color: white;
18 | }
19 |
20 | .alert-info
21 | {
22 | background-color: LightBlue;
23 | color: white;
24 | }
25 |
26 | .form-group
27 | {
28 | margin-bottom: 20px;
29 | }
30 |
31 | .form-group label
32 | {
33 | display: block;
34 | font-weight: bold;
35 | margin-bottom: 5px;
36 | }
37 |
38 | .form-group input, .form-group textarea
39 | {
40 | padding: 8px;
41 | display: block;
42 | width: 100%;
43 | box-sizing: border-box;
44 | margin-left: 0px;
45 | margin-right: 0px;
46 | }
47 |
48 | .form-group input[type=checkbox]
49 | {
50 | padding: 0px;
51 | margin: 0px;
52 | width: auto;
53 | margin-bottom: 10px;
54 | }
55 |
56 | .form-group input[type=submit]
57 | {
58 | width: auto;
59 | }
60 |
61 | .invalid-feedback
62 | {
63 | color: Red;
64 | }
65 |
--------------------------------------------------------------------------------
/app/Views/user/resetPassword.php:
--------------------------------------------------------------------------------
1 | data['title'] = 'Reset password';
7 |
8 | $this->data['breadcrumbs'][] = $this->data['title'];
9 |
10 | helper('form');
11 |
12 | $this->extend('layout');
13 |
14 | ?>
15 | section('content');?>
16 |
17 | Please choose your new password:
18 |
19 | = form_open('user/resetPassword/' . $id . '/' . $token, ['id' => 'reset-password-form']);?>
20 |
21 |
37 |
38 |
39 |
40 | = form_submit('send', 'Save', ['class' => 'btn btn-primary']);?>
41 |
42 |
43 |
44 | = form_close();?>
45 |
46 | endSection();?>
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 denis303
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/tests/_support/Database/Seeds/ExampleSeeder.php:
--------------------------------------------------------------------------------
1 | 'Test Factory',
12 | 'uid' => 'test001',
13 | 'class' => 'Factories\Tests\NewFactory',
14 | 'icon' => 'fas fa-puzzle-piece',
15 | 'summary' => 'Longer sample text for testing',
16 | ],
17 | [
18 | 'name' => 'Widget Factory',
19 | 'uid' => 'widget',
20 | 'class' => 'Factories\Tests\WidgetPlant',
21 | 'icon' => 'fas fa-puzzle-piece',
22 | 'summary' => 'Create widgets in your factory',
23 | ],
24 | [
25 | 'name' => 'Evil Factory',
26 | 'uid' => 'evil-maker',
27 | 'class' => 'Factories\Evil\MyFactory',
28 | 'icon' => 'fas fa-book-dead',
29 | 'summary' => 'Abandon all hope, ye who enter here',
30 | ],
31 | ];
32 |
33 | $builder = $this->db->table('factories');
34 |
35 | foreach ($factories as $factory)
36 | {
37 | $builder->insert($factory);
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/app/Config/Validation.php:
--------------------------------------------------------------------------------
1 | 'CodeIgniter\Validation\Views\list',
33 | 'single' => 'CodeIgniter\Validation\Views\single'
34 | ];
35 |
36 | //--------------------------------------------------------------------
37 | // Rules
38 | //--------------------------------------------------------------------
39 |
40 | }
--------------------------------------------------------------------------------
/app/Entities/User.php:
--------------------------------------------------------------------------------
1 | setTo($this->email, $this->name);
18 |
19 | return $email;
20 | }
21 |
22 | public function getResetPasswordUrl()
23 | {
24 | return site_url('user/resetPassword/' . $this->id . '/' . $this->password_reset_token);
25 | }
26 |
27 | public function getEmailVerificationUrl()
28 | {
29 | return site_url('user/verifyEmail/' . $this->id . '/'. $this->email_verification_token);
30 | }
31 |
32 | public function encodePassword(string $password) : string
33 | {
34 | return password_hash($password, PASSWORD_BCRYPT);
35 | }
36 |
37 | public function setPassword(string $password)
38 | {
39 | $this->password_hash = $this->encodePassword($password);
40 | }
41 |
42 | }
--------------------------------------------------------------------------------
/app/Config/Pager.php:
--------------------------------------------------------------------------------
1 | 'CodeIgniter\Pager\Views\default_full',
22 | 'default_simple' => 'CodeIgniter\Pager\Views\default_simple',
23 | 'default_head' => 'CodeIgniter\Pager\Views\default_head'
24 | ];
25 |
26 | /*
27 | |--------------------------------------------------------------------------
28 | | Items Per Page
29 | |--------------------------------------------------------------------------
30 | |
31 | | The default number of results shown in a single page.
32 | |
33 | */
34 | public $perPage = 20;
35 | }
36 |
--------------------------------------------------------------------------------
/tests/database/ExampleDatabaseTest.php:
--------------------------------------------------------------------------------
1 | findAll();
20 |
21 | // Make sure the count is as expected
22 | $this->assertCount(3, $objects);
23 | }
24 |
25 | public function testSoftDeleteLeavesRow()
26 | {
27 | $model = new ExampleModel();
28 | $this->setPrivateProperty($model, 'useSoftDeletes', true);
29 | $this->setPrivateProperty($model, 'tempUseSoftDeletes', true);
30 |
31 | $object = $model->first();
32 | $model->delete($object->id);
33 |
34 | // The model should no longer find it
35 | $this->assertNull($model->find($object->id));
36 |
37 | // ... but it should still be in the database
38 | $result = $model->builder()->where('id', $object->id)->get()->getResult();
39 |
40 | $this->assertCount(1, $result);
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/app/Libraries/AuthService.php:
--------------------------------------------------------------------------------
1 | getId();
15 |
16 | if (!$id)
17 | {
18 | return null;
19 | }
20 |
21 | return model('User')->find($id);
22 | }
23 |
24 | public function login(User $user, $rememberMe = false)
25 | {
26 | $model = model(UserModel::class);
27 |
28 | $id = $model->getIdValue($user);
29 |
30 | if (!$id)
31 | {
32 | return false;
33 | }
34 |
35 | $this->setId($id, $rememberMe);
36 |
37 | // Compability with https://codeigniter.com/user_guide/extending/authentication.html
38 | Events::trigger('login');
39 | }
40 |
41 | public function logout()
42 | {
43 | // Compability with https://codeigniter.com/user_guide/extending/authentication.html
44 | Events::trigger('logout');
45 |
46 | return $this->unsetId();
47 | }
48 |
49 | }
--------------------------------------------------------------------------------
/app/Config/Filters.php:
--------------------------------------------------------------------------------
1 | \CodeIgniter\Filters\CSRF::class,
13 | 'toolbar' => \CodeIgniter\Filters\DebugToolbar::class,
14 | 'honeypot' => \CodeIgniter\Filters\Honeypot::class,
15 | 'admin' => \Admin\Filters\Admin::class
16 | ];
17 |
18 | // Always applied before every request
19 | public $globals = [
20 | 'before' => [
21 | //'honeypot'
22 | // 'csrf',
23 | ],
24 | 'after' => [
25 | 'toolbar',
26 | //'honeypot'
27 | ]
28 | ];
29 |
30 | // Works on all of a particular HTTP method
31 | // (GET, POST, etc) as BEFORE filters only
32 | // like: 'post' => ['CSRF', 'throttle'],
33 | public $methods = [];
34 |
35 | // List filter aliases and any before/after uri patterns
36 | // that they should run on, like:
37 | // 'isLoggedIn' => ['before' => ['account/*', 'profiles/*']],
38 | public $filters = [
39 | 'admin' => [
40 | 'before' => [
41 | 'admin',
42 | 'admin/*'
43 | ]
44 | ]
45 | ];
46 |
47 | }
--------------------------------------------------------------------------------
/app/Config/Encryption.php:
--------------------------------------------------------------------------------
1 | session = Services::session();
24 |
25 | $this->user = auth()->getUser();
26 | }
27 |
28 | protected function render(string $view, array $params = [], array $options = [])
29 | {
30 | if (mb_strpos("\\", $view) === false)
31 | {
32 | if ($this->viewsNamespace)
33 | {
34 | $view = $this->viewsNamespace . "\\" . $view;
35 | }
36 | }
37 |
38 | if (array_key_exists('saveData', $options) == false)
39 | {
40 | $options['saveData'] = true;
41 | }
42 |
43 | return view($view, $params, $options);
44 | }
45 |
46 | public function goHome()
47 | {
48 | return $this->redirect(base_url());
49 | }
50 |
51 | public function redirect($url)
52 | {
53 | return redirect()->withCookies()->to($url);
54 | }
55 |
56 | }
--------------------------------------------------------------------------------
/tests/_support/Database/Migrations/2020-02-22-222222_example_migration.php:
--------------------------------------------------------------------------------
1 | [
13 | 'type' => 'varchar',
14 | 'constraint' => 31,
15 | ],
16 | 'uid' => [
17 | 'type' => 'varchar',
18 | 'constraint' => 31,
19 | ],
20 | 'class' => [
21 | 'type' => 'varchar',
22 | 'constraint' => 63,
23 | ],
24 | 'icon' => [
25 | 'type' => 'varchar',
26 | 'constraint' => 31,
27 | ],
28 | 'summary' => [
29 | 'type' => 'varchar',
30 | 'constraint' => 255,
31 | ],
32 | 'created_at' => [
33 | 'type' => 'datetime',
34 | 'null' => true,
35 | ],
36 | 'updated_at' => [
37 | 'type' => 'datetime',
38 | 'null' => true,
39 | ],
40 | 'deleted_at' => [
41 | 'type' => 'datetime',
42 | 'null' => true,
43 | ],
44 | ];
45 |
46 | $this->forge->addField('id');
47 | $this->forge->addField($fields);
48 |
49 | $this->forge->addKey('name');
50 | $this->forge->addKey('uid');
51 | $this->forge->addKey(['deleted_at', 'id']);
52 | $this->forge->addKey('created_at');
53 |
54 | $this->forge->createTable('factories');
55 | }
56 |
57 | public function down()
58 | {
59 | $this->forge->dropTable('factories');
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "denis303/codeigniter4-advanced-app",
3 | "type": "project",
4 | "description": "CodeIgniter4 advanced starter app",
5 | "homepage": "http://denis303.com",
6 | "license": "MIT",
7 | "minimum-stability": "dev",
8 | "prefer-stable": true,
9 | "require": {
10 | "php": ">=7.2",
11 | "codeigniter4/framework": "^4",
12 | "basic-app/email-helper": "*",
13 | "basic-app/auth": "*",
14 | "basic-app/superadmin": "*",
15 | "basic-app/config": "dev-main"
16 | },
17 | "require-dev": {
18 | "fzaninotto/faker": "^1.9@dev",
19 | "mikey179/vfsstream": "1.6.*",
20 | "phpunit/phpunit": "^8.5"
21 | },
22 | "autoload-dev": {
23 | "psr-4": {
24 | "Tests\\Support\\": "tests/_support"
25 | }
26 | },
27 | "scripts": {
28 | "post-update-cmd": [
29 | "@composer dump-autoload"
30 | ],
31 | "test": "phpunit",
32 | "post-create-project-cmd": [
33 | "php -r \"copy('env', '.env');\""
34 | ]
35 | },
36 | "support": {
37 | "forum": "http://forum.codeigniter.com/",
38 | "source": "https://github.com/codeigniter4/CodeIgniter4",
39 | "slack": "https://codeigniterchat.slack.com"
40 | },
41 | "config": {
42 | "preferred-install" : {
43 | "basic-app/*" : "source",
44 | "denis303/*" : "source",
45 | "*" : "dist"
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/app/Config/Modules.php:
--------------------------------------------------------------------------------
1 | data['title'] = 'Request password reset';
7 |
8 | $this->data['breadcrumbs'][] = $this->data['title'];
9 |
10 | helper(['form']);
11 |
12 | $this->extend('layout');
13 |
14 | ?>
15 | section('content');?>
16 |
17 | Please fill out your email. A link to reset password will be sent there.
18 |
19 |
20 |
21 | = $error;?>
22 |
23 |
24 |
25 | = form_open('user/requestPasswordReset', ['id' => 'request-password-reset-form']);?>
26 |
27 |
43 |
44 |
45 |
46 | = form_submit('submit', 'Send', ['class' => 'btn btn-primary']);?>
47 |
48 |
49 |
50 |
51 |
52 |
53 | If your server is not configured to send emails, you can create a link by manually constructing a URL with the following form:
54 | = site_url('user/resetPassword/:id/:token');?>
55 |
56 |
57 | endSection();?>
--------------------------------------------------------------------------------
/app/Views/user/resendVerificationEmail.php:
--------------------------------------------------------------------------------
1 | data['title'] = 'Resend verification email';
7 |
8 | $this->data['breadcrumbs'][] = $this->data['title'];
9 |
10 | helper('form');
11 |
12 | $this->extend('layout');
13 |
14 | ?>
15 |
16 | section('content');?>
17 |
18 | Please fill out your email. A verification email will be sent there.
19 |
20 |
21 |
22 | = $error;?>
23 |
24 |
25 |
26 | = form_open('user/resendVerificationEmail', ['id' => 'resend-verification-email-form']);?>
27 |
28 |
44 |
45 |
46 |
47 | = form_submit('send', 'Send', ['class' => 'btn btn-primary']);?>
48 |
49 |
50 |
51 | = form_close();?>
52 |
53 |
54 | If your server is not configured to send emails, you can create a link by manually constructing a URL with the following form:
55 | = site_url('user/verifyEmail/:id/:token');?>
56 |
57 |
58 | endSection();?>
--------------------------------------------------------------------------------
/public/index.php:
--------------------------------------------------------------------------------
1 | systemDirectory, '/ ') . '/bootstrap.php';
37 |
38 | /*
39 | *---------------------------------------------------------------
40 | * LAUNCH THE APPLICATION
41 | *---------------------------------------------------------------
42 | * Now that everything is setup, it's time to actually fire
43 | * up the engines and make this app do its thang.
44 | */
45 | $app->run();
46 |
--------------------------------------------------------------------------------
/app/Controllers/Contact.php:
--------------------------------------------------------------------------------
1 | request->getPost();
24 |
25 | $model = new ContactForm;
26 |
27 | if ($data)
28 | {
29 | $data = $model->load($data);
30 |
31 | if ($model->validate($data))
32 | {
33 | if ($model->sendEmail($data, $error))
34 | {
35 | $message = lang('Thank you for contacting us. We will respond to you as soon as possible.');
36 |
37 | $data = [];
38 | }
39 | else
40 | {
41 | if (!CI_DEBUG)
42 | {
43 | $error = lang('Sorry, we are unable to send a message.');
44 | }
45 |
46 | $customErrors[] = $error;
47 | }
48 | }
49 | else
50 | {
51 | $errors = (array) $model->errors();
52 | }
53 | }
54 |
55 | return $this->render('contact', [
56 | 'data' => $data,
57 | 'errors' => $errors,
58 | 'message' => $message,
59 | 'model' => $model,
60 | 'customErrors' => $customErrors
61 | ]);
62 | }
63 |
64 | }
--------------------------------------------------------------------------------
/app/Config/Migrations.php:
--------------------------------------------------------------------------------
1 | php spark migrate:create
43 | |
44 | | Typical formats:
45 | | YmdHis_
46 | | Y-m-d-His_
47 | | Y_m_d_His_
48 | |
49 | */
50 | public $timestampFormat = 'YmdHis_';
51 |
52 | }
53 |
--------------------------------------------------------------------------------
/app/Validation/UserRules.php:
--------------------------------------------------------------------------------
1 | findByEmail($data['email']);
16 |
17 | if ($user && $model->validatePassword($user, $data['password']))
18 | {
19 | return $user;
20 | }
21 |
22 | $error = lang('User not found or password incorrect.');
23 |
24 | return null;
25 | }
26 |
27 | protected function validateVerification(User $user, &$error = null)
28 | {
29 | if (!$user->email_verified_at)
30 | {
31 | $error = lang('Email is not verified.');
32 |
33 | return false;
34 | }
35 |
36 | return true;
37 | }
38 |
39 | public function verifiedUser($email, $reserved, array $data, &$error = null) : bool
40 | {
41 | if (empty($data['password']) || empty($data['email']))
42 | {
43 | return true;
44 | }
45 |
46 | if (!$user = $this->getUser($data, $error))
47 | {
48 | return false;
49 | }
50 |
51 | if (!$this->validateVerification($user, $error))
52 | {
53 | return false;
54 | }
55 |
56 | return true;
57 | }
58 |
59 | public function validUser($email, $reserved, array $data, &$error = null) : bool
60 | {
61 | if (empty($data['password']) || empty($data['email']))
62 | {
63 | return true;
64 | }
65 |
66 | return $this->getUser($data, $error) ? true : false;
67 | }
68 |
69 | }
--------------------------------------------------------------------------------
/admin/Controllers/BaseController.php:
--------------------------------------------------------------------------------
1 | session = Services::session();
23 |
24 | if (!$this->checkAuth())
25 | {
26 | throw SecurityException::forDisallowedAction();
27 | }
28 | }
29 |
30 | protected function checkAuth() : bool
31 | {
32 | return adminAuth()->getId() ? true : false;
33 | }
34 |
35 | protected function render(string $view, array $params = [], array $options = [])
36 | {
37 | if (mb_strpos("\\", $view) === false)
38 | {
39 | if ($this->viewsNamespace)
40 | {
41 | $view = $this->viewsNamespace . "\\" . $view;
42 | }
43 | }
44 |
45 | if (array_key_exists('saveData', $options) == false)
46 | {
47 | $options['saveData'] = true;
48 | }
49 |
50 | return view($view, $params, $options);
51 | }
52 |
53 | public function goHome()
54 | {
55 | return $this->redirect(site_url('admin'));
56 | }
57 |
58 | public function redirect($url)
59 | {
60 | return redirect()->withCookies()->to($url);
61 | }
62 |
63 | }
--------------------------------------------------------------------------------
/app/Config/Kint.php:
--------------------------------------------------------------------------------
1 | [
18 | 'label' => 'Email',
19 | 'rules' => 'required|' . UserModel::EMAIL_RULES . '|' . __CLASS__ . '::validateEmail|' . __CLASS__ . '::validateVerification'
20 | ]
21 | ];
22 |
23 | protected $validationMessages = [
24 | 'email' => [
25 | __CLASS__ . '::validateEmail' => 'There is no user with this email address.',
26 | __CLASS__ . '::validateVerification' => 'User already verified.'
27 | ]
28 | ];
29 |
30 | public static function validateEmail($email)
31 | {
32 | $model = new UserModel;
33 |
34 | static::$_user = $model->findByEmail($email);
35 |
36 | return static::$_user ? true : false;
37 | }
38 |
39 | public static function validateVerification($email)
40 | {
41 | if (static::$_user && static::$_user->email_verified_at)
42 | {
43 | return false;
44 | }
45 |
46 | return true;
47 | }
48 |
49 | public function getUser()
50 | {
51 | return static::$_user;
52 | }
53 |
54 | public function sendEmail(User $user, &$error)
55 | {
56 | return send_email(
57 | $user->composeEmail('mail/emailVerification', [
58 | 'verifyLink' => $user->getEmailVerificationUrl()
59 | ]),
60 | $error
61 | );
62 | }
63 |
64 | }
--------------------------------------------------------------------------------
/app/Config/Events.php:
--------------------------------------------------------------------------------
1 | 0)
36 | {
37 | ob_end_flush();
38 | }
39 |
40 | ob_start(function ($buffer) {
41 | return $buffer;
42 | });
43 | }
44 |
45 | /*
46 | * --------------------------------------------------------------------
47 | * Debug Toolbar Listeners.
48 | * --------------------------------------------------------------------
49 | * If you delete, they will no longer be collected.
50 | */
51 | if (!is_cli() && (ENVIRONMENT !== 'production'))
52 | {
53 | Events::on('DBQuery', 'CodeIgniter\Debug\Toolbar\Collectors\Database::collect');
54 |
55 | Services::toolbar()->respond();
56 | }
57 | });
--------------------------------------------------------------------------------
/.github/workflows/phpunit.yml:
--------------------------------------------------------------------------------
1 | name: PHPUnit
2 |
3 | on:
4 | pull_request:
5 | branches:
6 | - develop
7 |
8 | jobs:
9 | main:
10 | name: Build and test
11 |
12 | strategy:
13 | matrix:
14 | php-versions: ['7.2', '7.3', '7.4']
15 |
16 | runs-on: ubuntu-latest
17 |
18 | if: "!contains(github.event.head_commit.message, '[ci skip]')"
19 |
20 | steps:
21 | - name: Checkout
22 | uses: actions/checkout@v2
23 |
24 | - name: Setup PHP, with composer and extensions
25 | uses: shivammathur/setup-php@master
26 | with:
27 | php-version: ${{ matrix.php-versions }}
28 | tools: composer, pecl, phpunit
29 | extensions: intl, json, mbstring, mysqlnd, xdebug, xml, sqlite3
30 | coverage: xdebug
31 |
32 | - name: Get composer cache directory
33 | id: composer-cache
34 | run: echo "::set-output name=dir::$(composer config cache-files-dir)"
35 |
36 | - name: Cache composer dependencies
37 | uses: actions/cache@v1
38 | with:
39 | path: ${{ steps.composer-cache.outputs.dir }}
40 | key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}
41 | restore-keys: ${{ runner.os }}-composer-
42 |
43 | - name: Install dependencies
44 | run: composer install --no-progress --no-suggest --no-interaction --prefer-dist --optimize-autoloader
45 | # To prevent rate limiting you may need to supply an OAuth token in Settings > Secrets
46 | # env:
47 | # https://getcomposer.org/doc/articles/troubleshooting.md#api-rate-limit-and-oauth-tokens
48 | # COMPOSER_AUTH: ${{ secrets.COMPOSER_AUTH }}
49 |
50 | - name: Test with phpunit
51 | run: vendor/bin/phpunit --coverage-text
52 |
--------------------------------------------------------------------------------
/app/Config/ContentSecurityPolicy.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 404 Page Not Found
6 |
7 |
70 |
71 |
72 |
73 |
404 - File Not Found
74 |
75 |
76 |
77 | = esc($message) ?>
78 |
79 | Sorry! Cannot seem to find the page you were looking for.
80 |
81 |
82 |
83 |
84 |
85 |
--------------------------------------------------------------------------------
/admin/Views/login.php:
--------------------------------------------------------------------------------
1 | data['title'] = 'Login';
7 |
8 | $this->data['breadcrumbs'][] = $this->data['title'];
9 |
10 | helper(['form']);
11 |
12 | $this->extend('Admin\layout');
13 |
14 | ?>
15 |
16 | section('content');?>
17 |
18 | Please fill out the following fields to login:
19 |
20 | = form_open('admin/login', ['id' => 'login-form']);?>
21 |
22 |
38 |
39 |
50 |
51 |
64 |
65 |
66 |
67 | = form_submit('login-button', 'Login', ['class' => 'btn btn-primary']);?>
68 |
69 |
70 |
71 | = form_close();?>
72 |
73 | endSection();?>
--------------------------------------------------------------------------------
/public/.htaccess:
--------------------------------------------------------------------------------
1 | # Disable directory browsing
2 | Options All -Indexes
3 |
4 | # ----------------------------------------------------------------------
5 | # Rewrite engine
6 | # ----------------------------------------------------------------------
7 |
8 | # Turning on the rewrite engine is necessary for the following rules and features.
9 | # FollowSymLinks must be enabled for this to work.
10 |
11 | Options +FollowSymlinks
12 | RewriteEngine On
13 |
14 | # If you installed CodeIgniter in a subfolder, you will need to
15 | # change the following line to match the subfolder you need.
16 | # http://httpd.apache.org/docs/current/mod/mod_rewrite.html#rewritebase
17 | # RewriteBase /
18 |
19 | # Redirect Trailing Slashes...
20 | RewriteCond %{REQUEST_FILENAME} !-d
21 | RewriteRule ^(.*)/$ /$1 [L,R=301]
22 |
23 | # Rewrite "www.example.com -> example.com"
24 | RewriteCond %{HTTPS} !=on
25 | RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC]
26 | RewriteRule ^ http://%1%{REQUEST_URI} [R=301,L]
27 |
28 | # Checks to see if the user is attempting to access a valid file,
29 | # such as an image or css document, if this isn't true it sends the
30 | # request to the front controller, index.php
31 | RewriteCond %{REQUEST_FILENAME} !-f
32 | RewriteCond %{REQUEST_FILENAME} !-d
33 | RewriteRule ^([\s\S]*)$ index.php/$1 [L,NC,QSA]
34 |
35 | # Ensure Authorization header is passed along
36 | RewriteCond %{HTTP:Authorization} .
37 | RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
38 |
39 |
40 |
41 | # If we don't have mod_rewrite installed, all 404's
42 | # can be sent to index.php, and everything works as normal.
43 | ErrorDocument 404 index.php
44 |
45 |
46 | # Disable server signature start
47 | ServerSignature Off
48 | # Disable server signature end
49 |
--------------------------------------------------------------------------------
/app/Config/Routes.php:
--------------------------------------------------------------------------------
1 | setDefaultNamespace('App\Controllers');
19 | $routes->setDefaultController('Home');
20 | $routes->setDefaultMethod('index');
21 | $routes->setTranslateURIDashes(false);
22 | $routes->set404Override();
23 | $routes->setAutoRoute(true);
24 |
25 | /**
26 | * --------------------------------------------------------------------
27 | * Route Definitions
28 | * --------------------------------------------------------------------
29 | */
30 |
31 | // We get a performance increase by specifying the default
32 | // route since we don't have to scan directories.
33 | $routes->get('/', 'Home::index');
34 |
35 | /**
36 | * --------------------------------------------------------------------
37 | * Additional Routing
38 | * --------------------------------------------------------------------
39 | *
40 | * There will often be times that you need additional routing and you
41 | * need it to be able to override any defaults in this file. Environment
42 | * based routes is one such time. require() additional route files here
43 | * to make that happen.
44 | *
45 | * You will have access to the $routes object within that file without
46 | * needing to reload it.
47 | */
48 | if (file_exists(APPPATH . 'Config/' . ENVIRONMENT . '/Routes.php'))
49 | {
50 | require APPPATH . 'Config/' . ENVIRONMENT . '/Routes.php';
51 | }
52 |
--------------------------------------------------------------------------------
/app/Models/User.php:
--------------------------------------------------------------------------------
1 | = time();
54 | }
55 |
56 | public function findByEmail(string $email)
57 | {
58 | return $this->where(['email' => $email])->first();
59 | }
60 |
61 | public function validatePassword($user, string $password) : bool
62 | {
63 | return password_verify($password, $user->password_hash);
64 | }
65 |
66 | public function encodePassword($user, string $password) : string
67 | {
68 | return $user->encodePassword($password);
69 | }
70 |
71 | }
--------------------------------------------------------------------------------
/app/Forms/ContactForm.php:
--------------------------------------------------------------------------------
1 | [
23 | 'rules' => 'required|max_length[255]',
24 | 'label' => 'Name'
25 | ],
26 | 'email' => [
27 | 'rules' => 'required|valid_email|max_length[255]',
28 | 'label' => 'Email'
29 | ],
30 | 'subject' => [
31 | 'rules' => 'required|max_length[255]',
32 | 'label' => 'Subject'
33 | ],
34 | 'body' => [
35 | 'rules' => 'required|max_length[255]',
36 | 'label' => 'Body'
37 | ]
38 | ];
39 |
40 | public function load(array $data)
41 | {
42 | foreach($this->allowedFields as $field)
43 | {
44 | if (!array_key_exists($field, $data))
45 | {
46 | $data[$field] = '';
47 | }
48 | }
49 |
50 | return $data;
51 | }
52 |
53 | /**
54 | * Sends an email to the specified email address using the information collected by this model.
55 | *
56 | * @param string $email the target email address
57 | * @return bool whether the email was sent
58 | */
59 | public function sendEmail($data, &$error)
60 | {
61 | $email = compose_email('mail/contact', $data);
62 |
63 | $config = config(EmailConfig::class);
64 |
65 | $email->setTo($config->fromEmail, $config->fromName);
66 |
67 | $email->setReplyTo($data['email'], $data['name']);
68 |
69 | return send_email($email, $error);
70 | }
71 |
72 | }
--------------------------------------------------------------------------------
/app/Database/Migrations/20190911140357_create_user_table.php:
--------------------------------------------------------------------------------
1 | forge->addField([
11 | 'id' => [
12 | 'type' => 'INT',
13 | 'constraint' => 11,
14 | 'auto_increment' => true,
15 | 'unsigned' => true
16 | ],
17 | 'name' => [
18 | 'type' => 'VARCHAR',
19 | 'constraint' => '255',
20 | 'null' => true
21 | ],
22 | 'email' => [
23 | 'type' => 'VARCHAR',
24 | 'constraint' => '255',
25 | 'unique' => true,
26 | 'null' => true
27 | ],
28 | 'password_hash' => [
29 | 'type' => 'VARCHAR',
30 | 'constraint' => '60',
31 | 'null' => true
32 | ],
33 | 'created_at' => [
34 | 'type' => 'TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP'
35 | ],
36 | 'password_reset_token' => [
37 | 'type' => 'VARCHAR',
38 | 'constraint' => '255',
39 | 'unique' => true,
40 | 'null' => true
41 | ],
42 | 'email_verification_token' => [
43 | 'type' => 'VARCHAR',
44 | 'constraint' => '255',
45 | 'unique' => true,
46 | 'null' => true
47 | ],
48 | 'email_verified_at' => [
49 | 'type' => 'DATETIME',
50 | 'null' => true
51 | ]
52 | ]);
53 |
54 | $this->forge->addKey('id', true);
55 |
56 | $this->forge->createTable('users');
57 | }
58 |
59 | public function down()
60 | {
61 | $this->forge->dropTable('users');
62 | }
63 |
64 | }
--------------------------------------------------------------------------------
/app/Forms/PasswordResetRequestForm.php:
--------------------------------------------------------------------------------
1 | [
21 | 'rules' => 'required|' . UserModel::EMAIL_RULES . '|' . __CLASS__ . '::validateEmail|' . __CLASS__ .'::validateEmailVerification',
22 | 'label' => 'Email'
23 | ]
24 | ];
25 |
26 | protected $validationMessages = [
27 | 'email' => [
28 | __CLASS__ . '::validateEmail' => 'There is no user with this email address.',
29 | __CLASS__ . '::validateEmailVerification' => 'Unable to reset password for not verified email address.'
30 | ]
31 | ];
32 |
33 | public static function validateEmail($email)
34 | {
35 | $model = new UserModel;
36 |
37 | static::$_user = $model->findByEmail($email);
38 |
39 | return static::$_user ? true : false;
40 | }
41 |
42 | public static function validateEmailVerification($email)
43 | {
44 | if (static::$_user && !static::$_user->email_verified_at)
45 | {
46 | static::$_user = null;
47 |
48 | return false;
49 | }
50 |
51 | return true;
52 | }
53 |
54 | public function getUser()
55 | {
56 | return static::$_user;
57 | }
58 |
59 | /**
60 | * Sends an email with a link, for resetting the password.
61 | *
62 | * @return bool whether the email was send
63 | */
64 | public function sendEmail(User $user, &$error = null)
65 | {
66 | return send_email(
67 | $user->composeEmail('mail/resetPassword', [
68 | 'resetLink' => $user->getResetPasswordUrl()
69 | ]),
70 | $error
71 | );
72 | }
73 |
74 | }
--------------------------------------------------------------------------------
/admin/Controllers/Login.php:
--------------------------------------------------------------------------------
1 | getId())
18 | {
19 | return $this->goHome();
20 | }
21 |
22 | $validator = \Config\Services::validation();
23 |
24 | $data = $this->request->getPost();
25 |
26 | $errors = [];
27 |
28 | $validator->setRules([
29 | 'username' => [
30 | 'rules' => 'required',
31 | 'label' => lang('Login')
32 | ],
33 | 'password' => [
34 | 'rules' => 'required|validAdmin[]',
35 | 'label' => lang('Password')
36 | ],
37 | 'rememberMe' => [
38 | 'rules' => 'required|in_list[0,1]',
39 | 'label' => lang('Remember Me')
40 | ]
41 | ]);
42 |
43 | if ($data)
44 | {
45 | if ($validator->run($data))
46 | {
47 | $rememberMe = array_key_exists('rememberMe', $data) ? $data['rememberMe'] : false;
48 |
49 | adminAuth()->setId($data['username'], $rememberMe);
50 |
51 | return $this->goHome();
52 | }
53 | else
54 | {
55 | $errors = (array) $validator->getErrors();
56 | }
57 | }
58 | else
59 | {
60 | $data['rememberMe'] = 1;
61 | }
62 |
63 | return $this->render('login', [
64 | 'errors' => $errors,
65 | 'data' => $data,
66 | 'validation' => $validator
67 | ]);
68 | }
69 |
70 | /**
71 | * Logs out the current user.
72 | *
73 | * @return mixed
74 | */
75 | public function logout()
76 | {
77 | adminAuth()->unsetId();
78 |
79 | return $this->goHome();
80 | }
81 |
82 | }
--------------------------------------------------------------------------------
/spark:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env php
2 | systemDirectory, '/ ') . '/bootstrap.php';
45 |
46 | // Grab our Console
47 | $console = new \CodeIgniter\CLI\Console($app);
48 |
49 | // We want errors to be shown when using it from the CLI.
50 | error_reporting(-1);
51 | ini_set('display_errors', 1);
52 |
53 | // Show basic information before we do anything else.
54 | $console->showHeader();
55 |
56 | // fire off the command in the main framework.
57 | $response = $console->run();
58 | if ($response->getStatusCode() >= 300)
59 | {
60 | exit($response->getStatusCode());
61 | }
62 |
--------------------------------------------------------------------------------
/tests/_support/Libraries/ConfigReader.php:
--------------------------------------------------------------------------------
1 | data['title'] = 'Login';
7 |
8 | $this->data['breadcrumbs'][] = $this->data['title'];
9 |
10 | helper(['form']);
11 |
12 | $this->extend('layout');
13 |
14 | ?>
15 |
16 | section('content');?>
17 |
18 | Please fill out the following fields to login:
19 |
20 |
24 |
25 | = form_open('user/login', ['id' => 'login-form']);?>
26 |
27 |
43 |
44 |
59 |
60 |
78 |
79 |
80 |
81 | = form_submit('login-button', 'Login', ['class' => 'btn btn-primary']);?>
82 |
83 |
84 |
85 | = form_close();?>
86 |
87 | endSection();?>
--------------------------------------------------------------------------------
/app/Config/Autoload.php:
--------------------------------------------------------------------------------
1 | SYSTEMPATH,
37 | * 'App' => APPPATH
38 | * ];
39 | *
40 | * @var array
41 | */
42 | public $psr4 = [
43 | APP_NAMESPACE => APPPATH, // For custom app namespace
44 | 'Config' => APPPATH . 'Config',
45 | 'Admin' => APPPATH . '../Admin'
46 | ];
47 |
48 | /**
49 | * -------------------------------------------------------------------
50 | * Class Map
51 | * -------------------------------------------------------------------
52 | * The class map provides a map of class names and their exact
53 | * location on the drive. Classes loaded in this manner will have
54 | * slightly faster performance because they will not have to be
55 | * searched for within one or more directories as they would if they
56 | * were being autoloaded through a namespace.
57 | *
58 | * Prototype:
59 | *
60 | * $classmap = [
61 | * 'MyClass' => '/path/to/class/file.php'
62 | * ];
63 | *
64 | * @var array
65 | */
66 | public $classmap = [];
67 | }
68 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
14 | ./tests
15 |
16 |
17 |
18 |
19 |
20 | ./app
21 |
22 | ./app/Views
23 | ./app/Config/Routes.php
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # CodeIgniter 4 Advanced Project Template
2 |
3 | [Yii 2 Advanced Project Template](https://github.com/yiisoft/yii2-app-advanced) ported to CodeIgniter 4.
4 |
5 | |Screenshots|||
6 | | --- | --- | --- |
7 | |  |  |  |
8 | |  |  |  |
9 |
10 | ## Overview
11 |
12 | - Signup
13 | - Login
14 | - Logout
15 | - Email Confirmation
16 | - Password Reset
17 | - Contact Form
18 |
19 | Remember me feature not working correct in Chrome (and other browsers) when:
20 |
21 | 1. On Startup = "Continue where you left off"
22 | 2. Continue running background apps when Google Chrome is closed = "On"
23 |
24 | In this case browser not clean remember me cookie when remember me flag is not checked. This is not a bug in the code, but a feature of modern browsers.
25 |
26 | ## Installation
27 |
28 | `composer create-project denis303/codeigniter4-advanced-app --stability=dev`
29 |
30 | ## Setup
31 |
32 | 1. Copy `env` to `.env` and tailor for your app, specifically the baseURL
33 | and any database settings.
34 |
35 | 3. Run `php spark migrate -all`.
36 |
37 | ## Usage
38 |
39 | If you don't receive email, you can create links manually:
40 |
41 | - verification: /user/verifyEmail//
42 | - reset password: /user/resetPassword//
43 |
44 | ## Server Requirements
45 |
46 | PHP version 7.2 or higher is required, with the following extensions installed:
47 |
48 | - [intl](http://php.net/manual/en/intl.requirements.php)
49 | - [libcurl](http://php.net/manual/en/curl.requirements.php) if you plan to use the HTTP\CURLRequest library
50 |
51 | Additionally, make sure that the following extensions are enabled in your PHP:
52 |
53 | - json (enabled by default - don't turn it off)
54 | - [mbstring](http://php.net/manual/en/mbstring.installation.php)
55 | - [mysqlnd](http://php.net/manual/en/mysqlnd.install.php)
56 | - xml (enabled by default - don't turn it off)
57 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /public/libs
2 |
3 | #-------------------------
4 | # Operating Specific Junk Files
5 | #-------------------------
6 |
7 | # OS X
8 | .DS_Store
9 | .AppleDouble
10 | .LSOverride
11 |
12 | # OS X Thumbnails
13 | ._*
14 |
15 | # Windows image file caches
16 | Thumbs.db
17 | ehthumbs.db
18 | Desktop.ini
19 |
20 | # Recycle Bin used on file shares
21 | $RECYCLE.BIN/
22 |
23 | # Windows Installer files
24 | *.cab
25 | *.msi
26 | *.msm
27 | *.msp
28 |
29 | # Windows shortcuts
30 | *.lnk
31 |
32 | # Linux
33 | *~
34 |
35 | # KDE directory preferences
36 | .directory
37 |
38 | # Linux trash folder which might appear on any partition or disk
39 | .Trash-*
40 |
41 | #-------------------------
42 | # Environment Files
43 | #-------------------------
44 | # These should never be under version control,
45 | # as it poses a security risk.
46 | .env
47 | .vagrant
48 | Vagrantfile
49 |
50 | #-------------------------
51 | # Temporary Files
52 | #-------------------------
53 | writable/cache/*
54 | !writable/cache/index.html
55 |
56 | writable/logs/*
57 | !writable/logs/index.html
58 |
59 | writable/session/*
60 | !writable/session/index.html
61 |
62 | writable/uploads/*
63 | !writable/uploads/index.html
64 |
65 | writable/debugbar/*
66 |
67 | php_errors.log
68 |
69 | #-------------------------
70 | # User Guide Temp Files
71 | #-------------------------
72 | user_guide_src/build/*
73 | user_guide_src/cilexer/build/*
74 | user_guide_src/cilexer/dist/*
75 | user_guide_src/cilexer/pycilexer.egg-info/*
76 |
77 | #-------------------------
78 | # Test Files
79 | #-------------------------
80 | tests/coverage*
81 |
82 | # Don't save phpunit under version control.
83 | phpunit
84 |
85 | #-------------------------
86 | # Composer
87 | #-------------------------
88 | vendor/
89 |
90 | #-------------------------
91 | # IDE / Development Files
92 | #-------------------------
93 |
94 | # Modules Testing
95 | _modules/*
96 |
97 | # phpenv local config
98 | .php-version
99 |
100 | # Jetbrains editors (PHPStorm, etc)
101 | .idea/
102 | *.iml
103 |
104 | # Netbeans
105 | nbproject/
106 | build/
107 | nbbuild/
108 | dist/
109 | nbdist/
110 | nbactions.xml
111 | nb-configuration.xml
112 | .nb-gradle/
113 |
114 | # Sublime Text
115 | *.tmlanguage.cache
116 | *.tmPreferences.cache
117 | *.stTheme.cache
118 | *.sublime-workspace
119 | *.sublime-project
120 | .phpintel
121 | /api/
122 |
123 | # Visual Studio Code
124 | .vscode/
125 |
126 | /results/
127 | /phpunit*.xml
128 | /.phpunit.*.cache
129 |
130 |
--------------------------------------------------------------------------------
/app/Config/DocTypes.php:
--------------------------------------------------------------------------------
1 | '',
14 | 'xhtml1-strict' => '',
15 | 'xhtml1-trans' => '',
16 | 'xhtml1-frame' => '',
17 | 'xhtml-basic11' => '',
18 | 'html5' => '',
19 | 'html4-strict' => '',
20 | 'html4-trans' => '',
21 | 'html4-frame' => '',
22 | 'mathml1' => '',
23 | 'mathml2' => '',
24 | 'svg10' => '',
25 | 'svg11' => '',
26 | 'svg11-basic' => '',
27 | 'svg11-tiny' => '',
28 | 'xhtml-math-svg-xh' => '',
29 | 'xhtml-math-svg-sh' => '',
30 | 'xhtml-rdfa-1' => '',
31 | 'xhtml-rdfa-2' => '',
32 | ];
33 | }
34 |
--------------------------------------------------------------------------------
/app/Views/contact.php:
--------------------------------------------------------------------------------
1 | data['title'] = 'Contact';
7 |
8 | $this->data['breadcrumbs'][] = $this->data['title'];
9 |
10 | helper(['form']);
11 |
12 | $this->extend('layout');
13 |
14 | ?>
15 | section('content');?>
16 |
17 | Please fill out the following form to contact us. Thank you.
18 |
19 |
20 |
21 | = $message;?>
22 |
23 |
24 |
25 | = form_open('contact', ['id' => 'contact-form']);?>
26 |
27 |
28 |
29 | = $error;?>
30 |
31 |
32 |
33 |
49 |
50 |
65 |
66 |
81 |
82 |
98 |
99 |
100 |
101 | = form_submit('contact-button', 'Submit', ['class' => 'btn btn-primary']);?>
102 |
103 |
104 |
105 | = form_close();?>
106 |
107 | endSection();?>
--------------------------------------------------------------------------------
/app/Views/user/signup.php:
--------------------------------------------------------------------------------
1 | data['title'] = 'Signup';
7 |
8 | $this->data['breadcrumbs'][] = $this->data['title'];
9 |
10 | helper(['form']);
11 |
12 | $this->extend('layout');
13 |
14 | ?>
15 | section('content');?>
16 |
17 | Please fill out the following fields to signup:
18 |
19 |
23 |
24 | = form_open('user/signup', ['id' => 'form-signup']);?>
25 |
26 |
27 |
28 | = $error;?>
29 |
30 |
31 |
32 |
48 |
49 |
64 |
65 |
80 |
81 |
82 |
83 | = $error;?>
84 |
85 |
86 |
87 |
88 |
89 | = form_submit('signup-button', 'Signup', ['class' => 'btn btn-primary']);?>
90 |
91 |
92 |
93 | = form_close();?>
94 |
95 |
96 | If your server is not configured to send emails, you can create a link by manually constructing a URL with the following form:
97 | = site_url('user/verifyEmail/:id/:token');?>
98 |
99 |
100 | endSection();?>
--------------------------------------------------------------------------------
/app/Forms/SignupForm.php:
--------------------------------------------------------------------------------
1 | [
20 | 'rules' => 'required|max_length[255]|min_length[2]',
21 | 'label' => 'Name',
22 | ],
23 | 'email' => [
24 | 'rules' => 'required|' . UserModel::EMAIL_RULES . '|is_unique[users.email,id,{id}]',
25 | 'label' => 'Email',
26 | ],
27 | 'password' => [
28 | 'rules' => 'required|' . UserModel::PASSWORD_RULES,
29 | 'label' => 'Password'
30 | ]
31 | ];
32 |
33 | protected $validationMessages = [
34 | 'email' => [
35 | 'is_unique' => 'This email address has already been taken.'
36 | ]
37 | ];
38 |
39 | /**
40 | * Signs user up.
41 | *
42 | * @return bool whether the creating new account was successful and email was sent
43 | */
44 | public function signup(array $data, &$error = null)
45 | {
46 | $model = new UserModel;
47 |
48 | $user = new User([
49 | 'name' => $data['username'],
50 | 'email' => $data['email']
51 | ]);
52 |
53 | $user->setPassword($data['password']);
54 |
55 | $user->email_verification_token = $model->generateToken();
56 |
57 | $return = $model->save($user);
58 |
59 | if (!$return)
60 | {
61 | $errors = (array) $model->errors();
62 |
63 | $error = array_shift($errors);
64 |
65 | return false;
66 | }
67 |
68 | $id = $user->id;
69 |
70 | if (!$id)
71 | {
72 | $id = (int) $model->db->insertID();
73 | }
74 |
75 | if (!$id)
76 | {
77 | throw new Exception('User ID not defined.');
78 | }
79 |
80 | $user = $model->find($id);
81 |
82 | if (!$user)
83 | {
84 | throw new Exception('User not found.');
85 | }
86 |
87 | return $user;
88 | }
89 |
90 | /**
91 | * Sends confirmation email to user
92 | * @param User $user user data to with email should be send
93 | * @return bool whether the email was sent
94 | */
95 | public function sendEmail(User $user, &$error = null)
96 | {
97 | return send_email(
98 | $user->composeEmail('mail/signup', [
99 | 'verifyLink' => $user->getEmailVerificationUrl()
100 | ]),
101 | $error
102 | );
103 | }
104 |
105 | }
--------------------------------------------------------------------------------
/app/Config/Toolbar.php:
--------------------------------------------------------------------------------
1 | \CodeIgniter\Format\JSONFormatter::class,
39 | 'application/xml' => \CodeIgniter\Format\XMLFormatter::class,
40 | 'text/xml' => \CodeIgniter\Format\XMLFormatter::class,
41 | ];
42 |
43 | /*
44 | |--------------------------------------------------------------------------
45 | | Formatters Options
46 | |--------------------------------------------------------------------------
47 | |
48 | | Additional Options to adjust default formatters behaviour.
49 | | For each mime type, list the additional options that should be used.
50 | |
51 | */
52 | public $formatterOptions = [
53 | 'application/json' => JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES,
54 | 'application/xml' => 0,
55 | 'text/xml' => 0,
56 | ];
57 | //--------------------------------------------------------------------
58 |
59 | /**
60 | * A Factory method to return the appropriate formatter for the given mime type.
61 | *
62 | * @param string $mime
63 | *
64 | * @return \CodeIgniter\Format\FormatterInterface
65 | */
66 | public function getFormatter(string $mime)
67 | {
68 | if (! array_key_exists($mime, $this->formatters))
69 | {
70 | throw new \InvalidArgumentException('No Formatter defined for mime type: ' . $mime);
71 | }
72 |
73 | $class = $this->formatters[$mime];
74 |
75 | if (! class_exists($class))
76 | {
77 | throw new \BadMethodCallException($class . ' is not a valid Formatter.');
78 | }
79 |
80 | return new $class();
81 | }
82 |
83 | //--------------------------------------------------------------------
84 |
85 | }
86 |
--------------------------------------------------------------------------------
/app/Config/Paths.php:
--------------------------------------------------------------------------------
1 | '',
34 | 'hostname' => 'localhost',
35 | 'username' => '',
36 | 'password' => '',
37 | 'database' => '',
38 | 'DBDriver' => 'MySQLi',
39 | 'DBPrefix' => '',
40 | 'pConnect' => false,
41 | 'DBDebug' => (ENVIRONMENT !== 'production'),
42 | 'cacheOn' => false,
43 | 'cacheDir' => '',
44 | 'charset' => 'utf8',
45 | 'DBCollat' => 'utf8_general_ci',
46 | 'swapPre' => '',
47 | 'encrypt' => false,
48 | 'compress' => false,
49 | 'strictOn' => false,
50 | 'failover' => [],
51 | 'port' => 3306,
52 | ];
53 |
54 | /**
55 | * This database connection is used when
56 | * running PHPUnit database tests.
57 | *
58 | * @var array
59 | */
60 | public $tests = [
61 | 'DSN' => '',
62 | 'hostname' => '127.0.0.1',
63 | 'username' => '',
64 | 'password' => '',
65 | 'database' => ':memory:',
66 | 'DBDriver' => 'SQLite3',
67 | 'DBPrefix' => 'db_', // Needed to ensure we're working correctly with prefixes live. DO NOT REMOVE FOR CI DEVS
68 | 'pConnect' => false,
69 | 'DBDebug' => (ENVIRONMENT !== 'production'),
70 | 'cacheOn' => false,
71 | 'cacheDir' => '',
72 | 'charset' => 'utf8',
73 | 'DBCollat' => 'utf8_general_ci',
74 | 'swapPre' => '',
75 | 'encrypt' => false,
76 | 'compress' => false,
77 | 'strictOn' => false,
78 | 'failover' => [],
79 | 'port' => 3306,
80 | ];
81 |
82 | //--------------------------------------------------------------------
83 |
84 | public function __construct()
85 | {
86 | parent::__construct();
87 |
88 | // Ensure that we always set the database group to 'tests' if
89 | // we are currently running an automated test suite, so that
90 | // we don't overwrite live data on accident.
91 | if (ENVIRONMENT === 'testing')
92 | {
93 | $this->defaultGroup = 'tests';
94 |
95 | // Under Travis-CI, we can set an ENV var named 'DB_GROUP'
96 | // so that we can test against multiple databases.
97 | if ($group = getenv('DB'))
98 | {
99 | if (is_file(TESTPATH . 'travis/Database.php'))
100 | {
101 | require TESTPATH . 'travis/Database.php';
102 |
103 | if (! empty($dbconfig) && array_key_exists($group, $dbconfig))
104 | {
105 | $this->tests = $dbconfig[$group];
106 | }
107 | }
108 | }
109 | }
110 | }
111 |
112 | //--------------------------------------------------------------------
113 |
114 | }
115 |
--------------------------------------------------------------------------------
/app/Config/Email.php:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 | = esc($this->data['title'] ?? 'Welcome to CodeIgniter 4!');?>
11 |
12 |
13 |
14 |
15 | = link_tag('css/style.css');?>
16 | = link_tag('css/custom.css');?>
17 | = $this->renderSection('head');?>
18 |
19 |
20 | = $this->renderSection('beginBody');?>
21 |
22 |
23 |
24 |
47 |
48 |
49 |
50 | isLogged()):?>
51 |
52 |
Welcome to CodeIgniter, = adminAuth()->getId();?>!
53 |
54 |
55 |
56 | Welcome to CodeIgniter = CodeIgniter\CodeIgniter::CI_VERSION ?>
57 |
58 |
59 |
60 | The small framework with powerful features
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 | getFlashdata('success');?>
71 |
72 | = $success;?>
73 |
74 |
75 | getFlashdata('info');?>
76 |
77 | = $info;?>
78 |
79 |
80 | getFlashdata('error');?>
81 |
82 | = $error;?>
83 |
84 |
85 | = $this->renderSection('content');?>
86 |
87 |
88 |
89 | = $this->renderSection('beforeFooter');?>
90 |
91 |
92 |
93 |
110 |
111 |
112 |
113 |
122 |
123 |
124 | = $this->renderSection('endBody');?>
125 |
126 |
--------------------------------------------------------------------------------
/app/Views/errors/html/debug.css:
--------------------------------------------------------------------------------
1 | body {
2 | height: 100%;
3 | background: #fafafa;
4 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
5 | color: #777;
6 | font-weight: 300;
7 | margin: 0;
8 | padding: 0;
9 | }
10 | h1 {
11 | font-weight: lighter;
12 | letter-spacing: 0.8;
13 | font-size: 3rem;
14 | color: #222;
15 | margin: 0;
16 | }
17 | h1.headline {
18 | margin-top: 20%;
19 | font-size: 5rem;
20 | }
21 | .text-center {
22 | text-align: center;
23 | }
24 | p.lead {
25 | font-size: 1.6rem;
26 | }
27 | .container {
28 | max-width: 75rem;
29 | margin: 0 auto;
30 | padding: 1rem;
31 | }
32 | .header {
33 | background: #85271f;
34 | color: #fff;
35 | }
36 | .header h1 {
37 | color: #fff;
38 | }
39 | .header p {
40 | font-size: 1.2rem;
41 | margin: 0;
42 | line-height: 2.5;
43 | }
44 | .header a {
45 | color: rgba(255,255,255,0.5);
46 | margin-left: 2rem;
47 | display: none;
48 | text-decoration: none;
49 | }
50 | .header:hover a {
51 | display: inline;
52 | }
53 |
54 | .footer .container {
55 | border-top: 1px solid #e7e7e7;
56 | margin-top: 1rem;
57 | text-align: center;
58 | }
59 |
60 | .source {
61 | background: #333;
62 | color: #c7c7c7;
63 | padding: 0.5em 1em;
64 | border-radius: 5px;
65 | font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
66 | margin: 0;
67 | overflow-x: scroll;
68 | }
69 | .source span.line {
70 | line-height: 1.4;
71 | }
72 | .source span.line .number {
73 | color: #666;
74 | }
75 | .source .line .highlight {
76 | display: block;
77 | background: #555;
78 | color: #fff;
79 | }
80 | .source span.highlight .number {
81 | color: #fff;
82 | }
83 |
84 | .tabs {
85 | list-style: none;
86 | list-style-position: inside;
87 | margin: 0;
88 | padding: 0;
89 | margin-bottom: -1px;
90 | }
91 | .tabs li {
92 | display: inline;
93 | }
94 | .tabs a:link,
95 | .tabs a:visited {
96 | padding: 0rem 1rem;
97 | line-height: 2.7;
98 | text-decoration: none;
99 | color: #a7a7a7;
100 | background: #f1f1f1;
101 | border: 1px solid #e7e7e7;
102 | border-bottom: 0;
103 | border-top-left-radius: 5px;
104 | border-top-right-radius: 5px;
105 | display: inline-block;
106 | }
107 | .tabs a:hover {
108 | background: #e7e7e7;
109 | border-color: #e1e1e1;
110 | }
111 | .tabs a.active {
112 | background: #fff;
113 | }
114 | .tab-content {
115 | background: #fff;
116 | border: 1px solid #efefef;
117 | }
118 | .content {
119 | padding: 1rem;
120 | }
121 | .hide {
122 | display: none;
123 | }
124 |
125 | .alert {
126 | margin-top: 2rem;
127 | display: block;
128 | text-align: center;
129 | line-height: 3.0;
130 | background: #d9edf7;
131 | border: 1px solid #bcdff1;
132 | border-radius: 5px;
133 | color: #31708f;
134 | }
135 | ul, ol {
136 | line-height: 1.8;
137 | }
138 |
139 | table {
140 | width: 100%;
141 | overflow: hidden;
142 | }
143 | th {
144 | text-align: left;
145 | border-bottom: 1px solid #e7e7e7;
146 | padding-bottom: 0.5rem;
147 | }
148 | td {
149 | padding: 0.2rem 0.5rem 0.2rem 0;
150 | }
151 | tr:hover td {
152 | background: #f1f1f1;
153 | }
154 | td pre {
155 | white-space: pre-wrap;
156 | }
157 |
158 | .trace a {
159 | color: inherit;
160 | }
161 | .trace table {
162 | width: auto;
163 | }
164 | .trace tr td:first-child {
165 | min-width: 5em;
166 | font-weight: bold;
167 | }
168 | .trace td {
169 | background: #e7e7e7;
170 | padding: 0 1rem;
171 | }
172 | .trace td pre {
173 | margin: 0;
174 | }
175 | .args {
176 | display: none;
177 | }
--------------------------------------------------------------------------------
/app/Config/Constants.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Go further
6 |
7 |
8 |
9 | Learn
10 |
11 |
12 | The User Guide contains an introduction, tutorial, a number of "how to"
13 | guides, and then reference documentation for the components that make up
14 | the framework. Check the User Guide !
16 |
17 |
18 |
19 | Discuss
20 |
21 |
22 | CodeIgniter is a community-developed open source project, with several
23 | venues for the community members to gather and exchange ideas. View all
24 | the threads on CodeIgniter's forum , or chat on Slack !
27 |
28 |
29 |
30 | Contribute
31 |
32 |
33 | CodeIgniter is a community driven project and accepts contributions
34 | of code and documentation from the community. Why not
35 |
36 | join us ?
37 |
38 |
46 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/app/Views/layout.php:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 | = esc($this->data['title'] ?? 'Welcome to CodeIgniter 4!');?>
11 |
12 |
13 |
14 |
15 | = link_tag('css/style.css');?>
16 | = link_tag('css/custom.css');?>
17 | = $this->renderSection('head');?>
18 |
19 |
20 | = $this->renderSection('beginBody');?>
21 |
22 |
23 |
24 |
51 |
52 |
53 |
54 | isLogged()):?>
55 |
56 |
Welcome to CodeIgniter, = auth()->getUser()->name;?>!
57 |
58 |
59 |
60 | Welcome to CodeIgniter = CodeIgniter\CodeIgniter::CI_VERSION ?>
61 |
62 |
63 |
64 | The small framework with powerful features
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 | getFlashdata('success');?>
75 |
76 | = $success;?>
77 |
78 |
79 | getFlashdata('info');?>
80 |
81 | = $info;?>
82 |
83 |
84 | getFlashdata('error');?>
85 |
86 | = $error;?>
87 |
88 |
89 | = $this->renderSection('content');?>
90 |
91 |
92 |
93 | = $this->renderSection('beforeFooter');?>
94 |
95 |
96 |
97 |
114 |
115 |
116 |
117 |
126 |
127 |
128 | = $this->renderSection('endBody');?>
129 |
130 |
131 |
--------------------------------------------------------------------------------
/app/Config/Cache.php:
--------------------------------------------------------------------------------
1 | '127.0.0.1',
82 | 'port' => 11211,
83 | 'weight' => 1,
84 | 'raw' => false,
85 | ];
86 |
87 | /*
88 | | -------------------------------------------------------------------------
89 | | Redis settings
90 | | -------------------------------------------------------------------------
91 | | Your Redis server can be specified below, if you are using
92 | | the Redis or Predis drivers.
93 | |
94 | */
95 | public $redis = [
96 | 'host' => '127.0.0.1',
97 | 'password' => null,
98 | 'port' => 6379,
99 | 'timeout' => 0,
100 | 'database' => 0,
101 | ];
102 |
103 | /*
104 | |--------------------------------------------------------------------------
105 | | Available Cache Handlers
106 | |--------------------------------------------------------------------------
107 | |
108 | | This is an array of cache engine alias' and class names. Only engines
109 | | that are listed here are allowed to be used.
110 | |
111 | */
112 | public $validHandlers = [
113 | 'dummy' => \CodeIgniter\Cache\Handlers\DummyHandler::class,
114 | 'file' => \CodeIgniter\Cache\Handlers\FileHandler::class,
115 | 'memcached' => \CodeIgniter\Cache\Handlers\MemcachedHandler::class,
116 | 'predis' => \CodeIgniter\Cache\Handlers\PredisHandler::class,
117 | 'redis' => \CodeIgniter\Cache\Handlers\RedisHandler::class,
118 | 'wincache' => \CodeIgniter\Cache\Handlers\WincacheHandler::class,
119 | ];
120 | }
121 |
--------------------------------------------------------------------------------
/env:
--------------------------------------------------------------------------------
1 | #--------------------------------------------------------------------
2 | # Example Environment Configuration file
3 | #
4 | # This file can be used as a starting point for your own
5 | # custom .env files, and contains most of the possible settings
6 | # available in a default install.
7 | #
8 | # By default, all of the settings are commented out. If you want
9 | # to override the setting, you must un-comment it by removing the '#'
10 | # at the beginning of the line.
11 | #--------------------------------------------------------------------
12 |
13 | #--------------------------------------------------------------------
14 | # ADMIN
15 | #--------------------------------------------------------------------
16 |
17 | admin.username = admin
18 | admin.password = 12345
19 |
20 | #--------------------------------------------------------------------
21 | # ENVIRONMENT
22 | #--------------------------------------------------------------------
23 |
24 | # CI_ENVIRONMENT = production
25 |
26 | CI_ENVIRONMENT = development
27 |
28 | #--------------------------------------------------------------------
29 | # EMAIL
30 | #--------------------------------------------------------------------
31 |
32 | email.fromEmail = 'no-reply@example.com'
33 | email.fromName = 'No-reply';
34 |
35 | #--------------------------------------------------------------------
36 | # APP
37 | #--------------------------------------------------------------------
38 |
39 | app.baseURL = 'http://localhost:8080'
40 | app.indexPage = ''
41 | # app.forceGlobalSecureRequests = false
42 |
43 | # app.sessionDriver = 'CodeIgniter\Session\Handlers\FileHandler'
44 | # app.sessionCookieName = 'ci_session'
45 | # app.sessionSavePath = NULL
46 | # app.sessionMatchIP = false
47 | # app.sessionTimeToUpdate = 300
48 | # app.sessionRegenerateDestroy = false
49 |
50 | # app.cookiePrefix = ''
51 | # app.cookieDomain = ''
52 | # app.cookiePath = '/'
53 | # app.cookieSecure = false
54 | # app.cookieHTTPOnly = false
55 |
56 | # app.CSRFProtection = false
57 | # app.CSRFTokenName = 'csrf_test_name'
58 | # app.CSRFCookieName = 'csrf_cookie_name'
59 | # app.CSRFExpire = 7200
60 | # app.CSRFRegenerate = true
61 | # app.CSRFExcludeURIs = []
62 |
63 | # app.CSPEnabled = false
64 |
65 | #--------------------------------------------------------------------
66 | # DATABASE
67 | #--------------------------------------------------------------------
68 |
69 | database.default.hostname = localhost
70 | database.default.database = codeigniter4-advanced-app
71 | database.default.username = root
72 | database.default.password =
73 | database.default.DBDriver = MySQLi
74 | database.default.charset = utf8
75 | database.default.DBCollat = utf8_general_ci
76 |
77 | # database.tests.hostname = localhost
78 | # database.tests.database = ci4
79 | # database.tests.username = root
80 | # database.tests.password = root
81 | # database.tests.DBDriver = MySQLi
82 | # database.tests.charset = utf8
83 | # database.tests.DBCollat = utf8_general_ci
84 |
85 | #--------------------------------------------------------------------
86 | # CONTENT SECURITY POLICY
87 | #--------------------------------------------------------------------
88 |
89 | # contentsecuritypolicy.reportOnly = false
90 | # contentsecuritypolicy.defaultSrc = 'none'
91 | # contentsecuritypolicy.scriptSrc = 'self'
92 | # contentsecuritypolicy.styleSrc = 'self'
93 | # contentsecuritypolicy.imageSrc = 'self'
94 | # contentsecuritypolicy.base_uri = null
95 | # contentsecuritypolicy.childSrc = null
96 | # contentsecuritypolicy.connectSrc = 'self'
97 | # contentsecuritypolicy.fontSrc = null
98 | # contentsecuritypolicy.formAction = null
99 | # contentsecuritypolicy.frameAncestors = null
100 | # contentsecuritypolicy.mediaSrc = null
101 | # contentsecuritypolicy.objectSrc = null
102 | # contentsecuritypolicy.pluginTypes = null
103 | # contentsecuritypolicy.reportURI = null
104 | # contentsecuritypolicy.sandbox = false
105 | # contentsecuritypolicy.upgradeInsecureRequests = false
106 |
107 | #--------------------------------------------------------------------
108 | # ENCRYPTION
109 | #--------------------------------------------------------------------
110 |
111 | # encryption.key =
112 | # encryption.driver = OpenSSL
113 |
114 | #--------------------------------------------------------------------
115 | # HONEYPOT
116 | #--------------------------------------------------------------------
117 |
118 | # honeypot.hidden = 'true'
119 | # honeypot.label = 'Fill This Field'
120 | # honeypot.name = 'honeypot'
121 | # honeypot.template = '{label} '
122 | # honeypot.container = '{template}
'
123 |
--------------------------------------------------------------------------------
/public/css/style.css:
--------------------------------------------------------------------------------
1 | * {
2 | transition: background-color 300ms ease, color 300ms ease;
3 | }
4 | *:focus {
5 | background-color: rgba(221, 72, 20, .2);
6 | outline: none;
7 | }
8 | html, body {
9 | color: rgba(33, 37, 41, 1);
10 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
11 | font-size: 16px;
12 | margin: 0;
13 | padding: 0;
14 | -webkit-font-smoothing: antialiased;
15 | -moz-osx-font-smoothing: grayscale;
16 | text-rendering: optimizeLegibility;
17 | }
18 | header {
19 | background-color: rgba(247, 248, 249, 1);
20 | padding: .4rem 0 0;
21 | }
22 | .menu {
23 | padding: .4rem 2rem;
24 | }
25 | header ul {
26 | border-bottom: 1px solid rgba(242, 242, 242, 1);
27 | list-style-type: none;
28 | margin: 0;
29 | overflow: hidden;
30 | padding: 0;
31 | text-align: right;
32 | }
33 | header li {
34 | display: inline-block;
35 | }
36 | header li a {
37 | border-radius: 5px;
38 | color: rgba(0, 0, 0, .5);
39 | display: block;
40 | height: 44px;
41 | text-decoration: none;
42 | }
43 | header li.menu-item a {
44 | border-radius: 5px;
45 | margin: 5px 0;
46 | height: 38px;
47 | line-height: 36px;
48 | padding: .4rem .65rem;
49 | text-align: center;
50 | }
51 | header li.menu-item a:hover,
52 | header li.menu-item a:focus {
53 | background-color: rgba(221, 72, 20, .2);
54 | color: rgba(221, 72, 20, 1);
55 | }
56 | header .logo {
57 | float: left;
58 | height: 44px;
59 | padding: .4rem .5rem;
60 | }
61 | header .menu-toggle {
62 | display: none;
63 | float: right;
64 | font-size: 2rem;
65 | font-weight: bold;
66 | }
67 | header .menu-toggle button {
68 | background-color: rgba(221, 72, 20, .6);
69 | border: none;
70 | border-radius: 3px;
71 | color: rgba(255, 255, 255, 1);
72 | cursor: pointer;
73 | font: inherit;
74 | font-size: 1.3rem;
75 | height: 36px;
76 | padding: 0;
77 | margin: 11px 0;
78 | overflow: visible;
79 | width: 40px;
80 | }
81 | header .menu-toggle button:hover,
82 | header .menu-toggle button:focus {
83 | background-color: rgba(221, 72, 20, .8);
84 | color: rgba(255, 255, 255, .8);
85 | }
86 | header .heroe {
87 | margin: 0 auto;
88 | max-width: 1100px;
89 | padding: 1rem 1.75rem 1.75rem 1.75rem;
90 | }
91 | header .heroe h1 {
92 | font-size: 2.5rem;
93 | font-weight: 500;
94 | }
95 | header .heroe h2 {
96 | font-size: 1.5rem;
97 | font-weight: 300;
98 | }
99 | section {
100 | margin: 0 auto;
101 | max-width: 1100px;
102 | padding: 2.5rem 1.75rem 3.5rem 1.75rem;
103 | }
104 | section h1 {
105 | margin-bottom: 2.5rem;
106 | }
107 | section h2 {
108 | font-size: 120%;
109 | line-height: 2.5rem;
110 | padding-top: 1.5rem;
111 | }
112 | section pre {
113 | background-color: rgba(247, 248, 249, 1);
114 | border: 1px solid rgba(242, 242, 242, 1);
115 | display: block;
116 | font-size: .9rem;
117 | margin: 2rem 0;
118 | padding: 1rem 1.5rem;
119 | white-space: pre-wrap;
120 | word-break: break-all;
121 | }
122 | section code {
123 | display: block;
124 | }
125 | section a {
126 | color: rgba(221, 72, 20, 1);
127 | }
128 | section svg {
129 | margin-bottom: -5px;
130 | margin-right: 5px;
131 | width: 25px;
132 | }
133 | .further {
134 | background-color: rgba(247, 248, 249, 1);
135 | border-bottom: 1px solid rgba(242, 242, 242, 1);
136 | border-top: 1px solid rgba(242, 242, 242, 1);
137 | }
138 | .further h2:first-of-type {
139 | padding-top: 0;
140 | }
141 | footer {
142 | background-color: rgba(221, 72, 20, .8);
143 | text-align: center;
144 | }
145 | footer .environment {
146 | color: rgba(255, 255, 255, 1);
147 | padding: 2rem 1.75rem;
148 | }
149 | footer .copyrights {
150 | background-color: rgba(62, 62, 62, 1);
151 | color: rgba(200, 200, 200, 1);
152 | padding: .25rem 1.75rem;
153 | }
154 | @media (max-width: 559px) {
155 | header ul {
156 | padding: 0;
157 | }
158 | header .menu-toggle {
159 | padding: 0 1rem;
160 | }
161 | header .menu-item {
162 | background-color: rgba(244, 245, 246, 1);
163 | border-top: 1px solid rgba(242, 242, 242, 1);
164 | margin: 0 15px;
165 | width: calc(100% - 30px);
166 | }
167 | header .menu-toggle {
168 | display: block;
169 | }
170 | header .hidden {
171 | display: none;
172 | }
173 | header li.menu-item a {
174 | background-color: rgba(221, 72, 20, .1);
175 | }
176 | header li.menu-item a:hover,
177 | header li.menu-item a:focus {
178 | background-color: rgba(221, 72, 20, .7);
179 | color: rgba(255, 255, 255, .8);
180 | }
181 | }
182 |
--------------------------------------------------------------------------------
/app/Config/Logger.php:
--------------------------------------------------------------------------------
1 | [
79 |
80 | /*
81 | * The log levels that this handler will handle.
82 | */
83 | 'handles' => [
84 | 'critical',
85 | 'alert',
86 | 'emergency',
87 | 'debug',
88 | 'error',
89 | 'info',
90 | 'notice',
91 | 'warning',
92 | ],
93 |
94 | /*
95 | * The default filename extension for log files.
96 | * An extension of 'php' allows for protecting the log files via basic
97 | * scripting, when they are to be stored under a publicly accessible directory.
98 | *
99 | * Note: Leaving it blank will default to 'log'.
100 | */
101 | 'fileExtension' => '',
102 |
103 | /*
104 | * The file system permissions to be applied on newly created log files.
105 | *
106 | * IMPORTANT: This MUST be an integer (no quotes) and you MUST use octal
107 | * integer notation (i.e. 0700, 0644, etc.)
108 | */
109 | 'filePermissions' => 0644,
110 |
111 | /*
112 | * Logging Directory Path
113 | *
114 | * By default, logs are written to WRITEPATH . 'logs/'
115 | * Specify a different destination here, if desired.
116 | */
117 | 'path' => '',
118 | ],
119 |
120 | /**
121 | * The ChromeLoggerHandler requires the use of the Chrome web browser
122 | * and the ChromeLogger extension. Uncomment this block to use it.
123 | */
124 | // 'CodeIgniter\Log\Handlers\ChromeLoggerHandler' => [
125 | // /*
126 | // * The log levels that this handler will handle.
127 | // */
128 | // 'handles' => ['critical', 'alert', 'emergency', 'debug',
129 | // 'error', 'info', 'notice', 'warning'],
130 | // ]
131 | ];
132 | }
133 |
--------------------------------------------------------------------------------
/tests/README.md:
--------------------------------------------------------------------------------
1 | # Running Application Tests
2 |
3 | This is the quick-start to CodeIgniter testing. Its intent is to describe what
4 | it takes to set up your application and get it ready to run unit tests.
5 | It is not intended to be a full description of the test features that you can
6 | use to test your application. Those details can be found in the documentation.
7 |
8 | ## Resources
9 | * [CodeIgniter 4 User Guide on Testing](https://codeigniter4.github.io/userguide/testing/index.html)
10 | * [PHPUnit docs](https://phpunit.readthedocs.io/en/8.3/index.html)
11 |
12 | ## Requirements
13 |
14 | It is recommended to use the latest version of PHPUnit. At the time of this
15 | writing we are running version 8.5.2. Support for this has been built into the
16 | **composer.json** file that ships with CodeIgniter and can easily be installed
17 | via [Composer](https://getcomposer.org/) if you don't already have it installed globally.
18 |
19 | > composer install
20 |
21 | If running under OS X or Linux, you can create a symbolic link to make running tests a touch nicer.
22 |
23 | > ln -s ./vendor/bin/phpunit ./phpunit
24 |
25 | You also need to install [XDebug](https://xdebug.org/index.php) in order
26 | for code coverage to be calculated successfully.
27 |
28 | ## Setting Up
29 |
30 | A number of the tests use a running database.
31 | In order to set up the database edit the details for the `tests` group in
32 | **app/Config/Database.php** or **phpunit.xml**. Make sure that you provide a database engine
33 | that is currently running on your machine. More details on a test database setup are in the
34 | *Docs>>Testing>>Testing Your Database* section of the documentation.
35 |
36 | If you want to run the tests without using live database you can
37 | exclude @DatabaseLive group. Or make a copy of **phpunit.dist.xml** -
38 | call it **phpunit.xml** - and comment out the named "database". This will make
39 | the tests run quite a bit faster.
40 |
41 | ## Running the tests
42 |
43 | The entire test suite can be run by simply typing one command-line command from the main directory.
44 |
45 | > ./phpunit
46 |
47 | You can limit tests to those within a single test directory by specifying the
48 | directory name after phpunit.
49 |
50 | > ./phpunit app/Models
51 |
52 | ## Generating Code Coverage
53 |
54 | To generate coverage information, including HTML reports you can view in your browser,
55 | you can use the following command:
56 |
57 | > ./phpunit --colors --coverage-text=tests/coverage.txt --coverage-html=tests/coverage/ -d memory_limit=1024m
58 |
59 | This runs all of the tests again collecting information about how many lines,
60 | functions, and files are tested. It also reports the percentage of the code that is covered by tests.
61 | It is collected in two formats: a simple text file that provides an overview as well
62 | as a comprehensive collection of HTML files that show the status of every line of code in the project.
63 |
64 | The text file can be found at **tests/coverage.txt**.
65 | The HTML files can be viewed by opening **tests/coverage/index.html** in your favorite browser.
66 |
67 | ## PHPUnit XML Configuration
68 |
69 | The repository has a ``phpunit.xml.dist`` file in the project root that's used for
70 | PHPUnit configuration. This is used to provide a default configuration if you
71 | do not have your own configuration file in the project root.
72 |
73 | The normal practice would be to copy ``phpunit.xml.dist`` to ``phpunit.xml``
74 | (which is git ignored), and to tailor it as you see fit.
75 | For instance, you might wish to exclude database tests, or automatically generate
76 | HTML code coverage reports.
77 |
78 | ## Test Cases
79 |
80 | Every test needs a *test case*, or class that your tests extend. CodeIgniter 4
81 | provides a few that you may use directly:
82 | * `CodeIgniter\Test\CIUnitTestCase` - for basic tests with no other service needs
83 | * `CodeIgniter\Test\CIDatabaseTestCase` - for tests that need database access
84 |
85 | Most of the time you will want to write your own test cases to hold functions and services
86 | common to your test suites.
87 |
88 | ## Creating Tests
89 |
90 | All tests go in the **tests/** directory. Each test file is a class that extends a
91 | **Test Case** (see above) and contains methods for the individual tests. These method
92 | names must start with the word "test" and should have descriptive names for precisely what
93 | they are testing:
94 | `testUserCanModifyFile()` `testOutputColorMatchesInput()` `testIsLoggedInFailsWithInvalidUser()`
95 |
96 | Writing tests is an art, and there are many resources available to help learn how.
97 | Review the links above and always pay attention to your code coverage.
98 |
99 | ### Database Tests
100 |
101 | Tests can include migrating, seeding, and testing against a mock or live1 database.
102 | Be sure to modify the test case (or create your own) to point to your seed and migrations
103 | and include any additional steps to be run before tests in the `setUp()` method.
104 |
105 | 1 Note: If you are using database tests that require a live database connection
106 | you will need to rename **phpunit.xml.dist** to **phpunit.xml**, uncomment the database
107 | configuration lines and add your connection details. Prevent **phpunit.xml** from being
108 | tracked in your repo by adding it to **.gitignore**.
109 |
--------------------------------------------------------------------------------
/app/Views/_logo.php:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/Config/UserAgents.php:
--------------------------------------------------------------------------------
1 | 'Windows 10',
18 | 'windows nt 6.3' => 'Windows 8.1',
19 | 'windows nt 6.2' => 'Windows 8',
20 | 'windows nt 6.1' => 'Windows 7',
21 | 'windows nt 6.0' => 'Windows Vista',
22 | 'windows nt 5.2' => 'Windows 2003',
23 | 'windows nt 5.1' => 'Windows XP',
24 | 'windows nt 5.0' => 'Windows 2000',
25 | 'windows nt 4.0' => 'Windows NT 4.0',
26 | 'winnt4.0' => 'Windows NT 4.0',
27 | 'winnt 4.0' => 'Windows NT',
28 | 'winnt' => 'Windows NT',
29 | 'windows 98' => 'Windows 98',
30 | 'win98' => 'Windows 98',
31 | 'windows 95' => 'Windows 95',
32 | 'win95' => 'Windows 95',
33 | 'windows phone' => 'Windows Phone',
34 | 'windows' => 'Unknown Windows OS',
35 | 'android' => 'Android',
36 | 'blackberry' => 'BlackBerry',
37 | 'iphone' => 'iOS',
38 | 'ipad' => 'iOS',
39 | 'ipod' => 'iOS',
40 | 'os x' => 'Mac OS X',
41 | 'ppc mac' => 'Power PC Mac',
42 | 'freebsd' => 'FreeBSD',
43 | 'ppc' => 'Macintosh',
44 | 'linux' => 'Linux',
45 | 'debian' => 'Debian',
46 | 'sunos' => 'Sun Solaris',
47 | 'beos' => 'BeOS',
48 | 'apachebench' => 'ApacheBench',
49 | 'aix' => 'AIX',
50 | 'irix' => 'Irix',
51 | 'osf' => 'DEC OSF',
52 | 'hp-ux' => 'HP-UX',
53 | 'netbsd' => 'NetBSD',
54 | 'bsdi' => 'BSDi',
55 | 'openbsd' => 'OpenBSD',
56 | 'gnu' => 'GNU/Linux',
57 | 'unix' => 'Unknown Unix OS',
58 | 'symbian' => 'Symbian OS',
59 | ];
60 |
61 | // The order of this array should NOT be changed. Many browsers return
62 | // multiple browser types so we want to identify the sub-type first.
63 | public $browsers = [
64 | 'OPR' => 'Opera',
65 | 'Flock' => 'Flock',
66 | 'Edge' => 'Spartan',
67 | 'Chrome' => 'Chrome',
68 | // Opera 10+ always reports Opera/9.80 and appends Version/ to the user agent string
69 | 'Opera.*?Version' => 'Opera',
70 | 'Opera' => 'Opera',
71 | 'MSIE' => 'Internet Explorer',
72 | 'Internet Explorer' => 'Internet Explorer',
73 | 'Trident.* rv' => 'Internet Explorer',
74 | 'Shiira' => 'Shiira',
75 | 'Firefox' => 'Firefox',
76 | 'Chimera' => 'Chimera',
77 | 'Phoenix' => 'Phoenix',
78 | 'Firebird' => 'Firebird',
79 | 'Camino' => 'Camino',
80 | 'Netscape' => 'Netscape',
81 | 'OmniWeb' => 'OmniWeb',
82 | 'Safari' => 'Safari',
83 | 'Mozilla' => 'Mozilla',
84 | 'Konqueror' => 'Konqueror',
85 | 'icab' => 'iCab',
86 | 'Lynx' => 'Lynx',
87 | 'Links' => 'Links',
88 | 'hotjava' => 'HotJava',
89 | 'amaya' => 'Amaya',
90 | 'IBrowse' => 'IBrowse',
91 | 'Maxthon' => 'Maxthon',
92 | 'Ubuntu' => 'Ubuntu Web Browser',
93 | 'Vivaldi' => 'Vivaldi',
94 | ];
95 |
96 | public $mobiles = [
97 | // legacy array, old values commented out
98 | 'mobileexplorer' => 'Mobile Explorer',
99 | // 'openwave' => 'Open Wave',
100 | // 'opera mini' => 'Opera Mini',
101 | // 'operamini' => 'Opera Mini',
102 | // 'elaine' => 'Palm',
103 | 'palmsource' => 'Palm',
104 | // 'digital paths' => 'Palm',
105 | // 'avantgo' => 'Avantgo',
106 | // 'xiino' => 'Xiino',
107 | 'palmscape' => 'Palmscape',
108 | // 'nokia' => 'Nokia',
109 | // 'ericsson' => 'Ericsson',
110 | // 'blackberry' => 'BlackBerry',
111 | // 'motorola' => 'Motorola'
112 |
113 | // Phones and Manufacturers
114 | 'motorola' => 'Motorola',
115 | 'nokia' => 'Nokia',
116 | 'palm' => 'Palm',
117 | 'iphone' => 'Apple iPhone',
118 | 'ipad' => 'iPad',
119 | 'ipod' => 'Apple iPod Touch',
120 | 'sony' => 'Sony Ericsson',
121 | 'ericsson' => 'Sony Ericsson',
122 | 'blackberry' => 'BlackBerry',
123 | 'cocoon' => 'O2 Cocoon',
124 | 'blazer' => 'Treo',
125 | 'lg' => 'LG',
126 | 'amoi' => 'Amoi',
127 | 'xda' => 'XDA',
128 | 'mda' => 'MDA',
129 | 'vario' => 'Vario',
130 | 'htc' => 'HTC',
131 | 'samsung' => 'Samsung',
132 | 'sharp' => 'Sharp',
133 | 'sie-' => 'Siemens',
134 | 'alcatel' => 'Alcatel',
135 | 'benq' => 'BenQ',
136 | 'ipaq' => 'HP iPaq',
137 | 'mot-' => 'Motorola',
138 | 'playstation portable' => 'PlayStation Portable',
139 | 'playstation 3' => 'PlayStation 3',
140 | 'playstation vita' => 'PlayStation Vita',
141 | 'hiptop' => 'Danger Hiptop',
142 | 'nec-' => 'NEC',
143 | 'panasonic' => 'Panasonic',
144 | 'philips' => 'Philips',
145 | 'sagem' => 'Sagem',
146 | 'sanyo' => 'Sanyo',
147 | 'spv' => 'SPV',
148 | 'zte' => 'ZTE',
149 | 'sendo' => 'Sendo',
150 | 'nintendo dsi' => 'Nintendo DSi',
151 | 'nintendo ds' => 'Nintendo DS',
152 | 'nintendo 3ds' => 'Nintendo 3DS',
153 | 'wii' => 'Nintendo Wii',
154 | 'open web' => 'Open Web',
155 | 'openweb' => 'OpenWeb',
156 |
157 | // Operating Systems
158 | 'android' => 'Android',
159 | 'symbian' => 'Symbian',
160 | 'SymbianOS' => 'SymbianOS',
161 | 'elaine' => 'Palm',
162 | 'series60' => 'Symbian S60',
163 | 'windows ce' => 'Windows CE',
164 |
165 | // Browsers
166 | 'obigo' => 'Obigo',
167 | 'netfront' => 'Netfront Browser',
168 | 'openwave' => 'Openwave Browser',
169 | 'mobilexplorer' => 'Mobile Explorer',
170 | 'operamini' => 'Opera Mini',
171 | 'opera mini' => 'Opera Mini',
172 | 'opera mobi' => 'Opera Mobile',
173 | 'fennec' => 'Firefox Mobile',
174 |
175 | // Other
176 | 'digital paths' => 'Digital Paths',
177 | 'avantgo' => 'AvantGo',
178 | 'xiino' => 'Xiino',
179 | 'novarra' => 'Novarra Transcoder',
180 | 'vodafone' => 'Vodafone',
181 | 'docomo' => 'NTT DoCoMo',
182 | 'o2' => 'O2',
183 |
184 | // Fallback
185 | 'mobile' => 'Generic Mobile',
186 | 'wireless' => 'Generic Mobile',
187 | 'j2me' => 'Generic Mobile',
188 | 'midp' => 'Generic Mobile',
189 | 'cldc' => 'Generic Mobile',
190 | 'up.link' => 'Generic Mobile',
191 | 'up.browser' => 'Generic Mobile',
192 | 'smartphone' => 'Generic Mobile',
193 | 'cellphone' => 'Generic Mobile',
194 | ];
195 |
196 | // There are hundreds of bots but these are the most common.
197 | public $robots = [
198 | 'googlebot' => 'Googlebot',
199 | 'msnbot' => 'MSNBot',
200 | 'baiduspider' => 'Baiduspider',
201 | 'bingbot' => 'Bing',
202 | 'slurp' => 'Inktomi Slurp',
203 | 'yahoo' => 'Yahoo',
204 | 'ask jeeves' => 'Ask Jeeves',
205 | 'fastcrawler' => 'FastCrawler',
206 | 'infoseek' => 'InfoSeek Robot 1.0',
207 | 'lycos' => 'Lycos',
208 | 'yandex' => 'YandexBot',
209 | 'mediapartners-google' => 'MediaPartners Google',
210 | 'CRAZYWEBCRAWLER' => 'Crazy Webcrawler',
211 | 'adsbot-google' => 'AdsBot Google',
212 | 'feedfetcher-google' => 'Feedfetcher Google',
213 | 'curious george' => 'Curious George',
214 | 'ia_archiver' => 'Alexa Crawler',
215 | 'MJ12bot' => 'Majestic-12',
216 | 'Uptimebot' => 'Uptimebot',
217 | ];
218 | }
219 |
--------------------------------------------------------------------------------
/app/Config/App.php:
--------------------------------------------------------------------------------
1 | user)
26 | {
27 | return $this->goHome();
28 | }
29 |
30 | $model = new SignupForm;
31 |
32 | $data = $this->request->getPost();
33 |
34 | $errors = [];
35 |
36 | $customErrors = [];
37 |
38 | if ($data)
39 | {
40 | if ($model->validate($data))
41 | {
42 | $user = $model->signup($data, $error);
43 |
44 | if (!$user)
45 | {
46 | throw new Exception($error);
47 | }
48 |
49 | if ($model->sendEmail($user, $error))
50 | {
51 | $this->session->setFlashdata(
52 | 'success',
53 | lang('Thank you for registration. Please check your inbox for verification email.')
54 | );
55 |
56 | return $this->goHome();
57 | }
58 | else
59 | {
60 | if (!CI_DEBUG)
61 | {
62 | $error = lang('Sorry, we are unable to send a message to the provided email address.');
63 | }
64 |
65 | $customErrors[] = $error;
66 | }
67 | }
68 | else
69 | {
70 | $errors = (array) $model->errors();
71 | }
72 | }
73 |
74 | return $this->render('user/signup', [
75 | 'model' => $model,
76 | 'data' => $data,
77 | 'errors' => $errors,
78 | 'customErrors' => $customErrors
79 | ]);
80 | }
81 |
82 | /**
83 | * Logs in a user.
84 | *
85 | * @return mixed
86 | */
87 | public function login()
88 | {
89 | if ($this->user)
90 | {
91 | return $this->goHome();
92 | }
93 |
94 | $model = new LoginForm;
95 |
96 | $data = $this->request->getPost();
97 |
98 | $errors = [];
99 |
100 | if ($data)
101 | {
102 | if ($model->validate($data))
103 | {
104 | $user = model(UserModel::class)->findByEmail($data['email']);
105 |
106 | if (!$user)
107 | {
108 | throw new Exception(lang('User not found.'));
109 | }
110 |
111 | $rememberMe = array_key_exists('rememberMe', $data) ? $data['rememberMe'] : false;
112 |
113 | auth()->login($user, $rememberMe);
114 |
115 | return $this->goHome();
116 | }
117 | else
118 | {
119 | $errors = (array) $model->errors();
120 | }
121 | }
122 | else
123 | {
124 | $data['rememberMe'] = 1;
125 | }
126 |
127 | return $this->render('user/login', [
128 | 'model' => $model,
129 | 'errors' => $errors,
130 | 'data' => $data
131 | ]);
132 | }
133 |
134 | /**
135 | * Logs out the current user.
136 | *
137 | * @return mixed
138 | */
139 | public function logout()
140 | {
141 | auth()->logout();
142 |
143 | return $this->goHome();
144 | }
145 |
146 | /**
147 | * Verify email address
148 | *
149 | * @param string $token
150 | * @return mixed
151 | */
152 | public function verifyEmail($id, $token)
153 | {
154 | $model = model(UserModel::class);
155 |
156 | $user = $model->find($id);
157 |
158 | if (!$user)
159 | {
160 | throw new Exception(lang('User not found.'));
161 | }
162 |
163 | if ($user->email_verified_at)
164 | {
165 | $this->session->setFlashdata('info', lang('User already verified.'));
166 |
167 | return $this->redirect(site_url('user/login'));
168 | }
169 |
170 | if ($user->email_verification_token != $token)
171 | {
172 | $this->session->setFlashdata('error', lang('Unable to verify your account with provided token.'));
173 |
174 | return $this->redirect(site_url('user/resendVerificationEmail'));
175 | }
176 |
177 | $model->set('email_verified_at', 'NOW()', false);
178 |
179 | $model->set('email_verification_token', 'NULL', false);
180 |
181 | $model->protect(false);
182 |
183 | if (!$model->update($user->id))
184 | {
185 | $errors = $model->errors();
186 |
187 | $error = array_shift($errors);
188 |
189 | $model->protect(true);
190 |
191 | throw new Exception($error);
192 | }
193 |
194 | $model->protect(true);
195 |
196 | $this->session->setFlashdata('success', lang('Your email has been confirmed!'));
197 |
198 | return $this->redirect(site_url('user/login'));
199 | }
200 |
201 | /**
202 | * Resend verification email
203 | *
204 | * @return mixed
205 | */
206 | public function resendVerificationEmail()
207 | {
208 | $model = new ResendVerificationEmailForm;
209 |
210 | $errors = [];
211 |
212 | $customErrors = [];
213 |
214 | $data = $this->request->getPost();
215 |
216 | if ($data)
217 | {
218 | if ($model->validate($data))
219 | {
220 | $user = $model->getUser();
221 |
222 | $userModel = model(UserModel::class);
223 |
224 | if (!$userModel->isTokenValid($user->email_verification_token))
225 | {
226 | $user->email_verification_token = $userModel->generateToken();
227 |
228 | if (!$userModel->save($user))
229 | {
230 | $errors = $userModel->errors();
231 |
232 | $error = array_shift($error);
233 |
234 | throw new Exception($error);
235 | }
236 | }
237 |
238 | if ($model->sendEmail($user, $error))
239 | {
240 | $this->session->setFlashdata('success', lang('Check your email for further instructions.'));
241 |
242 | return $this->goHome();
243 | }
244 | else
245 | {
246 | if (!CI_DEBUG)
247 | {
248 | $error = lang('Sorry, we are unable to send a message to the provided email address.');
249 | }
250 |
251 | $customErrors[] = $error;
252 | }
253 | }
254 | else
255 | {
256 | $errors = (array) $model->errors();
257 | }
258 | }
259 |
260 | return $this->render('user/resendVerificationEmail', [
261 | 'model' => $model,
262 | 'data' => $data,
263 | 'errors' => $errors,
264 | 'customErrors' => $customErrors
265 | ]);
266 | }
267 |
268 | /**
269 | * Requests password reset.
270 | *
271 | * @return mixed
272 | */
273 | public function requestPasswordReset()
274 | {
275 | $model = new PasswordResetRequestForm;
276 |
277 | $data = $this->request->getPost();
278 |
279 | $errors = [];
280 |
281 | $customErrors = [];
282 |
283 | if ($data)
284 | {
285 | if ($model->validate($data))
286 | {
287 | $userModel = model(UserModel::class);
288 |
289 | $user = $model->getUser();
290 |
291 | if (!$user)
292 | {
293 | throw new Exception(lang('User not found.'));
294 | }
295 |
296 | if (!$user->password_reset_token || !$userModel->isTokenValid($user->password_reset_token))
297 | {
298 | $user->password_reset_token = $userModel->generateToken();
299 |
300 | if (!$userModel->save($user))
301 | {
302 | $errors = $userModel->errors();
303 |
304 | $error = array_shift($errors);
305 |
306 | throw new Exception($error);
307 | }
308 | }
309 |
310 | if ($model->sendEmail($user, $error))
311 | {
312 | $this->session->setFlashdata('success', lang('Check your email for further instructions.'));
313 |
314 | return $this->goHome();
315 | }
316 | else
317 | {
318 | if (!CI_DEBUG)
319 | {
320 | $error = lang('Sorry, we are unable to send a message to the provided email address.');
321 | }
322 |
323 | $customErrors[] = $error;
324 | }
325 | }
326 | else
327 | {
328 | $errors = (array) $model->errors();
329 | }
330 | }
331 |
332 | return $this->render('user/requestPasswordReset', [
333 | 'model' => $model,
334 | 'data' => $data,
335 | 'errors' => $errors,
336 | 'customErrors' => $customErrors
337 | ]);
338 | }
339 |
340 | /**
341 | * Resets password.
342 | *
343 | * @param string $token
344 | * @return mixed
345 | */
346 | public function resetPassword($id, $token)
347 | {
348 | $userModel = model(UserModel::class);
349 |
350 | $user = $userModel->find((int) $id);
351 |
352 | if (!$user)
353 | {
354 | throw new PageNotFoundException;
355 | }
356 |
357 | if ($token != $user->password_reset_token)
358 | {
359 | $this->session->setFlashdata('error', lang('Wrong password reset token.'));
360 |
361 | return $this->redirect(site_url('user/requestPasswordReset'));
362 | }
363 |
364 | $errors = [];
365 |
366 | $model = new ResetPasswordForm;
367 |
368 | $data = $this->request->getPost();
369 |
370 | if ($data)
371 | {
372 | if ($model->validate($data))
373 | {
374 | if ($model->resetPassword($user, $data, $error))
375 | {
376 | $this->session->setFlashdata('success', lang('New password saved.'));
377 |
378 | return $this->redirect(site_url('user/login'));
379 | }
380 | else
381 | {
382 | $errors[] = $error;
383 | }
384 | }
385 | else
386 | {
387 | $errors = (array) $model->errors();
388 | }
389 | }
390 |
391 | return $this->render('user/resetPassword', [
392 | 'model' => $model,
393 | 'data' => $data,
394 | 'errors' => $errors,
395 | 'id' => $id,
396 | 'token' => $token
397 | ]);
398 | }
399 |
400 | }
--------------------------------------------------------------------------------