├── 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: 9 |
10 | Subject: 11 |
12 | Text: -------------------------------------------------------------------------------- /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 name);?>, 11 |
12 |
13 | Follow the link below to reset your password: 14 |
15 | -------------------------------------------------------------------------------- /app/Views/mail/signup.php: -------------------------------------------------------------------------------- 1 | data['subject'] = 'Account registration at ' . base_url(); 6 | 7 | $this->data['mailType'] = 'html'; 8 | 9 | ?> 10 | Hello name);?>, 11 |
12 |
13 | Follow the link below to verify your email: 14 |
15 | -------------------------------------------------------------------------------- /app/Views/mail/emailVerification.php: -------------------------------------------------------------------------------- 1 | data['subject'] = 'Account verification at ' . base_url(); 6 | 7 | $this->data['mailType'] = 'text'; 8 | 9 | ?> 10 | Hello name);?>, 11 |
12 |
13 | Follow the link below to verify your email: 14 |
15 | -------------------------------------------------------------------------------- /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: 4 | Message: 5 | Filename: getFile(), "\n"; ?> 6 | Line Number: getLine(); ?> 7 | 8 | 9 | 10 | Backtrace: 11 | getTrace() as $error): ?> 12 | 13 | 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 | 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 | 'reset-password-form']);?> 20 | 21 |
22 | 23 | 24 | 25 | 'form-control', 30 | 'autofocus' => true 31 | ] 32 | );?> 33 | 34 |
35 | 36 |
37 | 38 |
39 | 40 | 'btn btn-primary']);?> 41 | 42 |
43 | 44 | 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 |
22 | 23 | 24 | 25 | 'request-password-reset-form']);?> 26 | 27 |
28 | 29 | 30 | 31 | 'form-control', 36 | 'autofocus' => true 37 | ] 38 | );?> 39 | 40 |
41 | 42 |
43 | 44 |
45 | 46 | '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 |

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 |
23 | 24 | 25 | 26 | 'resend-verification-email-form']);?> 27 | 28 |
29 | 30 | 31 | 32 | 'form-control', 37 | 'autofocus' => true 38 | ] 39 | );?> 40 | 41 |
42 | 43 |
44 | 45 |
46 | 47 | 'btn btn-primary']);?> 48 | 49 |
50 | 51 | 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 |

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 | 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 | 'login-form']);?> 21 | 22 |
23 | 24 | 25 | 26 | true, 31 | 'class' => 'form-control' 32 | ] 33 | );?> 34 | 35 |
36 | 37 |
38 | 39 |
40 | 41 | 42 | 43 | 'form-control' 45 | ]);?> 46 | 47 |
48 | 49 |
50 | 51 |
52 | 53 | 54 | 55 | 56 | 57 | 'remember-me-checkbox' 59 | ]);?> 60 | 61 |
62 | 63 |
64 | 65 |
66 | 67 | 'btn btn-primary']);?> 68 | 69 |
70 | 71 | 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 | 'login-form']);?> 26 | 27 |
28 | 29 | 30 | 31 | true, 36 | 'class' => 'form-control' 37 | ] 38 | );?> 39 | 40 |
41 | 42 |
43 | 44 |
45 | 46 | 47 | 48 | 'form-control' 53 | ] 54 | );?> 55 | 56 |
57 | 58 |
59 | 60 |
61 | 62 | 63 | 64 | 65 | 66 | 'remember-me-checkbox' 72 | ] 73 | );?> 74 | 75 |
76 | 77 |
78 | 79 |
80 | 81 | 'btn btn-primary']);?> 82 | 83 |
84 | 85 | 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 | | ![Welcome Page](https://github.com/denis303/codeigniter4-advanced-app/raw/master/_images/screen_welcome.png) | ![Signup](https://github.com/denis303/codeigniter4-advanced-app/raw/master/_images/screen_signup.png) | ![Login](https://github.com/denis303/codeigniter4-advanced-app/raw/master/_images/screen_login.png) | 8 | | ![Reset Password](https://github.com/denis303/codeigniter4-advanced-app/raw/master/_images/screen_reset_password.png) | ![Resend Verification](https://github.com/denis303/codeigniter4-advanced-app/raw/master/_images/screen_resend_verification.png) | ![Contact Form](https://github.com/denis303/codeigniter4-advanced-app/raw/master/_images/screen_contact.png) | 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 |
22 | 23 | 24 | 25 | 'contact-form']);?> 26 | 27 | 28 | 29 |
30 | 31 | 32 | 33 |
34 | 35 | 36 | 37 | 'form-control', 42 | 'autofocus' => true 43 | ] 44 | );?> 45 | 46 |
47 | 48 |
49 | 50 |
51 | 52 | 53 | 54 | 'form-control' 59 | ] 60 | );?> 61 | 62 |
63 | 64 |
65 | 66 |
67 | 68 | 69 | 70 | 'form-control' 75 | ] 76 | );?> 77 | 78 |
79 | 80 |
81 | 82 |
83 | 84 | 85 | 86 | 'form-control', 91 | 'rows' => 6 92 | ] 93 | );?> 94 | 95 |
96 | 97 |
98 | 99 |
100 | 101 | 'btn btn-primary']);?> 102 | 103 |
104 | 105 | 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-signup']);?> 25 | 26 | 27 | 28 |
29 | 30 | 31 | 32 |
33 | 34 | 35 | 36 | true, 41 | 'class' => 'form-control' 42 | ] 43 | );?> 44 | 45 |
46 | 47 |
48 | 49 |
50 | 51 | 52 | 53 | 'form-control' 58 | ] 59 | );?> 60 | 61 |
62 | 63 |
64 | 65 |
66 | 67 | 68 | 69 | 'form-control' 74 | ] 75 | );?> 76 | 77 |
78 | 79 |
80 | 81 | 82 | 83 |
84 | 85 | 86 | 87 |
88 | 89 | 'btn btn-primary']);?> 90 | 91 |
92 | 93 | 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 |

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 | 16 | 17 | renderSection('head');?> 18 | 19 | 20 | renderSection('beginBody');?> 21 | 22 |
23 | 24 | 47 | 48 |
49 | 50 | isLogged()):?> 51 | 52 |

Welcome to CodeIgniter, getId();?>!

53 | 54 | 55 | 56 |

Welcome to CodeIgniter

57 | 58 | 59 | 60 |

The small framework with powerful features

61 | 62 |
63 | 64 |
65 | 66 | 67 | 68 |
69 | 70 | getFlashdata('success');?> 71 | 72 |
73 | 74 | 75 | getFlashdata('info');?> 76 | 77 |
78 | 79 | 80 | getFlashdata('error');?> 81 | 82 |
83 | 84 | 85 | renderSection('content');?> 86 | 87 |
88 | 89 | renderSection('beforeFooter');?> 90 | 91 | 92 | 93 |
94 |
95 | 96 |

Page rendered in {elapsed_time} seconds

97 | 98 |

Environment:

99 | 100 |
101 | 102 |
103 | 104 |

© CodeIgniter Foundation. CodeIgniter is open source project released under the MIT 105 | open source licence.

106 | 107 |
108 | 109 |
110 | 111 | 112 | 113 | 122 | 123 | 124 | 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 | 16 | 17 | renderSection('head');?> 18 | 19 | 20 | renderSection('beginBody');?> 21 | 22 |
23 | 24 | 51 | 52 |
53 | 54 | isLogged()):?> 55 | 56 |

Welcome to CodeIgniter, getUser()->name;?>!

57 | 58 | 59 | 60 |

Welcome to CodeIgniter

61 | 62 | 63 | 64 |

The small framework with powerful features

65 | 66 |
67 | 68 |
69 | 70 | 71 | 72 |
73 | 74 | getFlashdata('success');?> 75 | 76 |
77 | 78 | 79 | getFlashdata('info');?> 80 | 81 |
82 | 83 | 84 | getFlashdata('error');?> 85 | 86 |
87 | 88 | 89 | renderSection('content');?> 90 | 91 |
92 | 93 | renderSection('beforeFooter');?> 94 | 95 | 96 | 97 |
98 |
99 | 100 |

Page rendered in {elapsed_time} seconds

101 | 102 |

Environment:

103 | 104 |
105 | 106 |
107 | 108 |

© CodeIgniter Foundation. CodeIgniter is open source project released under the MIT 109 | open source licence.

110 | 111 |
112 | 113 |
114 | 115 | 116 | 117 | 126 | 127 | 128 | 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 = '' 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 | Visit CodeIgniter.com official website! -------------------------------------------------------------------------------- /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 | } --------------------------------------------------------------------------------