From 059e0d95d0c28bc5060e87e146eaf7411f51bf90 Mon Sep 17 00:00:00 2001 From: George Halkiadakis Date: Thu, 27 Apr 2023 03:47:30 +0300 Subject: skeleton commit; based on an anom project --- Dockerfile | 167 + README.md | 191 + composer.json | 12 + composer.lock | 149 + core/README.md | 266 + core/auth/.gitkeep | 0 core/auth/env.example | 36 + core/classes/Benchmark.php | 150 + core/classes/Cart.php | 71 + core/classes/Database.php | 333 + core/classes/Registry.php | 135 + core/classes/Render.php | 358 + core/classes/Repository.php | 43 + core/classes/Request.php | 161 + core/classes/Route.php | 97 + core/classes/Security.php | 923 + core/classes/authentication/Core/PasswordTrait.php | 38 + core/classes/authentication/Core/UserManager.php | 101 + .../authentication/Core/UserManagerInterface.php | 25 + core/classes/authentication/Token/UserToken.php | 44 + .../authentication/Token/UserTokenInterface.php | 18 + core/classes/authentication/User.php | 116 + core/classes/authentication/UserInterface.php | 19 + core/classes/authentication/info.md | 114 + core/classes/cache/Cache_interface.php | 34 + core/classes/cache/FileCache.php | 93 + core/classes/cache/MemcachedCache.php | 37 + core/classes/cache/RedisCache.php | 69 + core/classes/session/DatabaseSession.php | 138 + core/classes/session/DefaultSession.php | 42 + core/classes/session/FilesSession.php | 138 + core/classes/session/SessionHandlerInterface.php | 169 + core/cli/cli-cache.php | 65 + core/cli/curl-cache.php | 155 + core/cli/info.md | 23 + core/cli/micro/setup.php | 120 + core/cli/micro/term_utilities.php | 44 + core/config/anom_settings.php | 325 + core/config/constants.php | 28 + core/config/init.php | 64 + core/helpers/autoload.php | 43 + core/helpers/design_patterns.php | 104 + core/helpers/error_handling.php | 119 + data/.gitkeep | 0 docker-compose.redis.yml | 166 + docker-compose.yml | 53 + docker/bin/docker-entrypoint.sh | 27 + docker/bin/optimize.sh | 17 + docker/config/000-default.conf | 21 + docker/config/composer-naked.json | 12 + docker/config/composer-redis.json | 13 + docker/config/default-ssl.conf | 131 + docker/config/info.md | 69 + docker/config/msmtprc.kill | 12 + docker/config/opcache.ini | 9 + docker/config/php-ext-xdebug.ini | 24 + docker/config/php-overrides.ini | 3 + public/.htaccess | 99 + public/app/config/.gitkeep | 0 public/app/config/app_constants.php | 171 + public/app/config/init_routes.php | 12 + public/app/controllers/.gitkeep | 0 public/app/controllers/Admin.php | 58 + public/app/controllers/Auth.php | 382 + public/app/controllers/admin/.gitkeep | 0 public/app/controllers/admin/Course_admin.php | 676 + public/app/controllers/admin/Users_admin.php | 42 + public/app/controllers/api/.gitkeep | 0 public/app/controllers/api/Common_api.php | 281 + public/app/controllers/api/Doc_api.php | 34 + public/app/controllers/cli/CliContoller.php | 55 + public/app/controllers/cms/Course.php | 252 + public/app/extends/.gitkeep | 0 public/app/extends/Cache_service.php | 82 + public/app/extends/Classroom_manager.php | 106 + public/app/extends/Classroom_user.php | 121 + public/app/extends/Send_mail.php | 212 + public/app/models/.gitkeep | 0 public/app/models/Jorge.php | 172 + public/app/models/_info.md | 150 + public/app/models/admin/History_model.php | 34 + public/app/models/admin/Privilege_model.php | 42 + public/app/models/admin/User_model.php | 299 + public/app/models/cms/Course_model.php | 314 + public/app/models/cms/Media_model.php | 36 + public/app/models/cms/Page_model.php | 18 + public/app/models/market/Market_repository.php | 235 + public/app/models/market/Payment_template.txt | 98 + .../app/models/market/ProductCategories_model.php | 199 + public/app/models/market/Product_model.php | 49 + public/app/models/todo.md | 132 + public/app/routes/.gitkeep | 0 public/app/routes/api.php | 81 + public/app/routes/backend.php | 221 + public/app/routes/frontend.php | 169 + public/app/routes/user.php | 84 + public/app/views/.gitkeep | 0 public/app/views/admin/admin-menu.php | 47 + public/app/views/admin/categories.php | 91 + public/app/views/admin/edit_lesson.php | 159 + public/app/views/admin/edit_page.php | 139 + public/app/views/admin/files.php | 96 + public/app/views/admin/lessons.php | 126 + public/app/views/admin/pages.php | 109 + public/app/views/admin/panel.php | 4 + public/app/views/admin/privileges.php | 93 + public/app/views/admin/skeleton.php | 45 + public/app/views/components/breadcrumbs.php | 30 + public/app/views/components/courses_menu.php | 97 + public/app/views/components/footer.php | 240 + public/app/views/components/header_includes.php | 59 + public/app/views/components/lessons_list.php | 54 + public/app/views/components/subcategories.php | 19 + public/app/views/components/top_bar.php | 40 + public/app/views/components/user-management.php | 65 + public/app/views/error/404.php | 65 + public/app/views/error/general.php | 64 + public/app/views/group.php | 13 + public/app/views/item.php | 4 + public/app/views/simple/Model.php | 887 + public/app/views/simple/Render.php | 182 + public/app/views/simple/category.php | 156 + public/app/views/simple/index.php | 123 + public/app/views/simple/lorem-dates.php | 18 + public/app/views/simple/lorem-intros.php | 721 + public/app/views/simple/lorem-posts.php | 93 + public/app/views/simple/post.php | 236 + public/app/views/templates/course.php | 93 + public/app/views/templates/lesson.php | 82 + public/app/views/templates/page.php | 64 + public/app/views/user/login.php | 91 + public/app/views/user/new-password.php | 65 + public/app/views/user/registration.php | 120 + public/app/views/welcome.php | 71 + public/assets/css/admin.css | 150 + public/assets/css/class.css | 394 + public/assets/css/email.css | 478 + public/assets/css/main.css | 9 + public/assets/css/normalize.css | 349 + public/assets/css/overides.css | 148 + .../Buttons-2.3.6/css/buttons.bootstrap.css | 386 + .../Buttons-2.3.6/css/buttons.bootstrap.min.css | 1 + .../Buttons-2.3.6/css/buttons.bootstrap4.css | 430 + .../Buttons-2.3.6/css/buttons.bootstrap4.min.css | 1 + .../Buttons-2.3.6/css/buttons.bootstrap5.css | 432 + .../Buttons-2.3.6/css/buttons.bootstrap5.min.css | 1 + .../DataTables/Buttons-2.3.6/css/buttons.bulma.css | 429 + .../Buttons-2.3.6/css/buttons.bulma.min.css | 1 + .../Buttons-2.3.6/css/buttons.dataTables.css | 560 + .../Buttons-2.3.6/css/buttons.dataTables.min.css | 1 + .../Buttons-2.3.6/css/buttons.foundation.css | 371 + .../Buttons-2.3.6/css/buttons.foundation.min.css | 1 + .../Buttons-2.3.6/css/buttons.jqueryui.css | 387 + .../Buttons-2.3.6/css/buttons.jqueryui.min.css | 1 + .../Buttons-2.3.6/css/buttons.semanticui.css | 409 + .../Buttons-2.3.6/css/buttons.semanticui.min.css | 1 + .../js/DataTables/Buttons-2.3.6/css/common.scss | 106 + .../js/DataTables/Buttons-2.3.6/css/mixins.scss | 228 + .../Buttons-2.3.6/js/buttons.bootstrap.js | 106 + .../Buttons-2.3.6/js/buttons.bootstrap.min.js | 4 + .../Buttons-2.3.6/js/buttons.bootstrap4.js | 106 + .../Buttons-2.3.6/js/buttons.bootstrap4.min.js | 4 + .../Buttons-2.3.6/js/buttons.bootstrap5.js | 106 + .../Buttons-2.3.6/js/buttons.bootstrap5.min.js | 4 + .../DataTables/Buttons-2.3.6/js/buttons.bulma.js | 117 + .../Buttons-2.3.6/js/buttons.bulma.min.js | 4 + .../DataTables/Buttons-2.3.6/js/buttons.colVis.js | 250 + .../Buttons-2.3.6/js/buttons.colVis.min.js | 5 + .../Buttons-2.3.6/js/buttons.dataTables.js | 92 + .../Buttons-2.3.6/js/buttons.dataTables.min.js | 4 + .../Buttons-2.3.6/js/buttons.foundation.js | 134 + .../Buttons-2.3.6/js/buttons.foundation.min.js | 4 + .../DataTables/Buttons-2.3.6/js/buttons.html5.js | 1494 + .../Buttons-2.3.6/js/buttons.html5.min.js | 8 + .../Buttons-2.3.6/js/buttons.jqueryui.js | 92 + .../Buttons-2.3.6/js/buttons.jqueryui.min.js | 4 + .../DataTables/Buttons-2.3.6/js/buttons.print.js | 238 + .../Buttons-2.3.6/js/buttons.print.min.js | 5 + .../Buttons-2.3.6/js/buttons.semanticui.js | 104 + .../Buttons-2.3.6/js/buttons.semanticui.min.js | 4 + .../Buttons-2.3.6/js/dataTables.buttons.js | 2538 + .../Buttons-2.3.6/js/dataTables.buttons.min.js | 4 + .../ColReorder-1.6.2/css/colReorder.bootstrap.css | 11 + .../css/colReorder.bootstrap.min.css | 1 + .../ColReorder-1.6.2/css/colReorder.bootstrap4.css | 11 + .../css/colReorder.bootstrap4.min.css | 1 + .../ColReorder-1.6.2/css/colReorder.bootstrap5.css | 11 + .../css/colReorder.bootstrap5.min.css | 1 + .../ColReorder-1.6.2/css/colReorder.bulma.css | 11 + .../ColReorder-1.6.2/css/colReorder.bulma.min.css | 1 + .../ColReorder-1.6.2/css/colReorder.dataTables.css | 11 + .../css/colReorder.dataTables.min.css | 1 + .../ColReorder-1.6.2/css/colReorder.foundation.css | 11 + .../css/colReorder.foundation.min.css | 1 + .../ColReorder-1.6.2/css/colReorder.jqueryui.css | 11 + .../css/colReorder.jqueryui.min.css | 1 + .../ColReorder-1.6.2/css/colReorder.semanticui.css | 11 + .../css/colReorder.semanticui.min.css | 1 + .../ColReorder-1.6.2/js/colReorder.bootstrap.js | 58 + .../js/colReorder.bootstrap.min.js | 4 + .../ColReorder-1.6.2/js/colReorder.bootstrap4.js | 58 + .../js/colReorder.bootstrap4.min.js | 4 + .../ColReorder-1.6.2/js/colReorder.bootstrap5.js | 92 + .../js/colReorder.bootstrap5.min.js | 4 + .../ColReorder-1.6.2/js/colReorder.bulma.js | 58 + .../ColReorder-1.6.2/js/colReorder.bulma.min.js | 4 + .../ColReorder-1.6.2/js/colReorder.dataTables.js | 58 + .../js/colReorder.dataTables.min.js | 4 + .../ColReorder-1.6.2/js/colReorder.foundation.js | 58 + .../js/colReorder.foundation.min.js | 4 + .../ColReorder-1.6.2/js/colReorder.jqueryui.js | 58 + .../ColReorder-1.6.2/js/colReorder.jqueryui.min.js | 4 + .../ColReorder-1.6.2/js/colReorder.semanticui.js | 58 + .../js/colReorder.semanticui.min.js | 4 + .../ColReorder-1.6.2/js/dataTables.colReorder.js | 1518 + .../js/dataTables.colReorder.min.js | 4 + .../DataTables-1.13.4/css/dataTables.bootstrap.css | 416 + .../css/dataTables.bootstrap.min.css | 1 + .../css/dataTables.bootstrap4.css | 423 + .../css/dataTables.bootstrap4.min.css | 1 + .../css/dataTables.bootstrap5.css | 430 + .../css/dataTables.bootstrap5.min.css | 5 + .../DataTables-1.13.4/css/dataTables.bulma.css | 379 + .../DataTables-1.13.4/css/dataTables.bulma.min.css | 3 + .../css/dataTables.dataTables.css | 0 .../css/dataTables.dataTables.min.css | 0 .../css/dataTables.foundation.css | 357 + .../css/dataTables.foundation.min.css | 1 + .../DataTables-1.13.4/css/dataTables.jqueryui.css | 673 + .../css/dataTables.jqueryui.min.css | 1 + .../css/dataTables.semanticui.css | 348 + .../css/dataTables.semanticui.min.css | 1 + .../DataTables-1.13.4/css/jquery.dataTables.css | 584 + .../css/jquery.dataTables.min.css | 1 + .../DataTables-1.13.4/images/sort_asc.png | Bin 0 -> 160 bytes .../DataTables-1.13.4/images/sort_asc_disabled.png | Bin 0 -> 148 bytes .../DataTables-1.13.4/images/sort_both.png | Bin 0 -> 201 bytes .../DataTables-1.13.4/images/sort_desc.png | Bin 0 -> 158 bytes .../images/sort_desc_disabled.png | Bin 0 -> 146 bytes .../DataTables-1.13.4/js/dataTables.bootstrap.js | 200 + .../js/dataTables.bootstrap.min.js | 4 + .../DataTables-1.13.4/js/dataTables.bootstrap4.js | 202 + .../js/dataTables.bootstrap4.min.js | 4 + .../DataTables-1.13.4/js/dataTables.bootstrap5.js | 212 + .../js/dataTables.bootstrap5.min.js | 4 + .../DataTables-1.13.4/js/dataTables.bulma.js | 214 + .../DataTables-1.13.4/js/dataTables.bulma.min.js | 4 + .../DataTables-1.13.4/js/dataTables.dataTables.js | 55 + .../js/dataTables.dataTables.min.js | 4 + .../DataTables-1.13.4/js/dataTables.foundation.js | 200 + .../js/dataTables.foundation.min.js | 4 + .../DataTables-1.13.4/js/dataTables.jqueryui.js | 95 + .../js/dataTables.jqueryui.min.js | 4 + .../DataTables-1.13.4/js/dataTables.semanticui.js | 228 + .../js/dataTables.semanticui.min.js | 4 + .../DataTables-1.13.4/js/jquery.dataTables.js | 15686 +++ .../DataTables-1.13.4/js/jquery.dataTables.min.js | 4 + .../DateTime-1.4.0/css/dataTables.dateTime.css | 218 + .../DateTime-1.4.0/css/dataTables.dateTime.min.css | 1 + .../DateTime-1.4.0/js/dataTables.dateTime.js | 1708 + .../DateTime-1.4.0/js/dataTables.dateTime.min.js | 6 + .../css/fixedColumns.bootstrap.css | 42 + .../css/fixedColumns.bootstrap.min.css | 1 + .../css/fixedColumns.bootstrap4.css | 42 + .../css/fixedColumns.bootstrap4.min.css | 1 + .../css/fixedColumns.bootstrap5.css | 42 + .../css/fixedColumns.bootstrap5.min.css | 1 + .../FixedColumns-4.2.2/css/fixedColumns.bulma.css | 35 + .../css/fixedColumns.bulma.min.css | 1 + .../css/fixedColumns.dataTables.css | 19 + .../css/fixedColumns.dataTables.min.css | 1 + .../css/fixedColumns.foundation.css | 23 + .../css/fixedColumns.foundation.min.css | 1 + .../css/fixedColumns.jqueryui.css | 37 + .../css/fixedColumns.jqueryui.min.css | 1 + .../css/fixedColumns.semanticui.css | 46 + .../css/fixedColumns.semanticui.min.css | 1 + .../js/dataTables.fixedColumns.js | 613 + .../js/dataTables.fixedColumns.min.js | 4 + .../js/fixedColumns.bootstrap.js | 58 + .../js/fixedColumns.bootstrap.min.js | 4 + .../js/fixedColumns.bootstrap4.js | 58 + .../js/fixedColumns.bootstrap4.min.js | 4 + .../js/fixedColumns.bootstrap5.js | 58 + .../js/fixedColumns.bootstrap5.min.js | 4 + .../FixedColumns-4.2.2/js/fixedColumns.bulma.js | 58 + .../js/fixedColumns.bulma.min.js | 4 + .../js/fixedColumns.dataTables.js | 58 + .../js/fixedColumns.dataTables.min.js | 4 + .../js/fixedColumns.foundation.js | 58 + .../js/fixedColumns.foundation.min.js | 4 + .../FixedColumns-4.2.2/js/fixedColumns.jqueryui.js | 58 + .../js/fixedColumns.jqueryui.min.js | 4 + .../js/fixedColumns.semanticui.js | 58 + .../js/fixedColumns.semanticui.min.js | 4 + .../css/fixedHeader.bootstrap.css | 16 + .../css/fixedHeader.bootstrap.min.css | 1 + .../css/fixedHeader.bootstrap4.css | 16 + .../css/fixedHeader.bootstrap4.min.css | 1 + .../css/fixedHeader.bootstrap5.css | 16 + .../css/fixedHeader.bootstrap5.min.css | 1 + .../FixedHeader-3.3.2/css/fixedHeader.bulma.css | 16 + .../css/fixedHeader.bulma.min.css | 1 + .../css/fixedHeader.dataTables.css | 18 + .../css/fixedHeader.dataTables.min.css | 1 + .../css/fixedHeader.foundation.css | 16 + .../css/fixedHeader.foundation.min.css | 1 + .../FixedHeader-3.3.2/css/fixedHeader.jqueryui.css | 14 + .../css/fixedHeader.jqueryui.min.css | 1 + .../css/fixedHeader.semanticui.css | 13 + .../css/fixedHeader.semanticui.min.css | 1 + .../FixedHeader-3.3.2/js/dataTables.fixedHeader.js | 1103 + .../js/dataTables.fixedHeader.min.js | 4 + .../FixedHeader-3.3.2/js/fixedHeader.bootstrap.js | 58 + .../js/fixedHeader.bootstrap.min.js | 4 + .../FixedHeader-3.3.2/js/fixedHeader.bootstrap4.js | 58 + .../js/fixedHeader.bootstrap4.min.js | 4 + .../FixedHeader-3.3.2/js/fixedHeader.bootstrap5.js | 58 + .../js/fixedHeader.bootstrap5.min.js | 4 + .../FixedHeader-3.3.2/js/fixedHeader.bulma.js | 58 + .../FixedHeader-3.3.2/js/fixedHeader.bulma.min.js | 4 + .../FixedHeader-3.3.2/js/fixedHeader.dataTables.js | 58 + .../js/fixedHeader.dataTables.min.js | 4 + .../FixedHeader-3.3.2/js/fixedHeader.foundation.js | 58 + .../js/fixedHeader.foundation.min.js | 4 + .../FixedHeader-3.3.2/js/fixedHeader.jqueryui.js | 58 + .../js/fixedHeader.jqueryui.min.js | 4 + .../FixedHeader-3.3.2/js/fixedHeader.semanticui.js | 58 + .../js/fixedHeader.semanticui.min.js | 4 + public/assets/js/DataTables/JSZip-2.5.0/jszip.js | 9155 ++ .../assets/js/DataTables/JSZip-2.5.0/jszip.min.js | 14 + .../KeyTable-2.8.2/css/keyTable.bootstrap.css | 11 + .../KeyTable-2.8.2/css/keyTable.bootstrap.min.css | 1 + .../KeyTable-2.8.2/css/keyTable.bootstrap4.css | 11 + .../KeyTable-2.8.2/css/keyTable.bootstrap4.min.css | 1 + .../KeyTable-2.8.2/css/keyTable.bootstrap5.css | 11 + .../KeyTable-2.8.2/css/keyTable.bootstrap5.min.css | 1 + .../KeyTable-2.8.2/css/keyTable.bulma.css | 11 + .../KeyTable-2.8.2/css/keyTable.bulma.min.css | 1 + .../KeyTable-2.8.2/css/keyTable.dataTables.css | 11 + .../KeyTable-2.8.2/css/keyTable.dataTables.min.css | 1 + .../KeyTable-2.8.2/css/keyTable.foundation.css | 11 + .../KeyTable-2.8.2/css/keyTable.foundation.min.css | 1 + .../KeyTable-2.8.2/css/keyTable.jqueryui.css | 11 + .../KeyTable-2.8.2/css/keyTable.jqueryui.min.css | 1 + .../KeyTable-2.8.2/css/keyTable.semanticui.css | 11 + .../KeyTable-2.8.2/css/keyTable.semanticui.min.css | 1 + .../KeyTable-2.8.2/js/dataTables.keyTable.js | 1419 + .../KeyTable-2.8.2/js/dataTables.keyTable.min.js | 4 + .../KeyTable-2.8.2/js/keyTable.bootstrap.js | 58 + .../KeyTable-2.8.2/js/keyTable.bootstrap.min.js | 4 + .../KeyTable-2.8.2/js/keyTable.bootstrap4.js | 58 + .../KeyTable-2.8.2/js/keyTable.bootstrap4.min.js | 4 + .../KeyTable-2.8.2/js/keyTable.bootstrap5.js | 58 + .../KeyTable-2.8.2/js/keyTable.bootstrap5.min.js | 4 + .../DataTables/KeyTable-2.8.2/js/keyTable.bulma.js | 58 + .../KeyTable-2.8.2/js/keyTable.bulma.min.js | 4 + .../KeyTable-2.8.2/js/keyTable.dataTables.js | 58 + .../KeyTable-2.8.2/js/keyTable.dataTables.min.js | 4 + .../KeyTable-2.8.2/js/keyTable.foundation.js | 58 + .../KeyTable-2.8.2/js/keyTable.foundation.min.js | 4 + .../KeyTable-2.8.2/js/keyTable.jqueryui.js | 58 + .../KeyTable-2.8.2/js/keyTable.jqueryui.min.js | 4 + .../KeyTable-2.8.2/js/keyTable.semanticui.js | 58 + .../KeyTable-2.8.2/js/keyTable.semanticui.min.js | 4 + .../Responsive-2.4.1/css/responsive.bootstrap.css | 185 + .../css/responsive.bootstrap.min.css | 1 + .../Responsive-2.4.1/css/responsive.bootstrap4.css | 185 + .../css/responsive.bootstrap4.min.css | 1 + .../Responsive-2.4.1/css/responsive.bootstrap5.css | 190 + .../css/responsive.bootstrap5.min.css | 1 + .../Responsive-2.4.1/css/responsive.bulma.css | 191 + .../Responsive-2.4.1/css/responsive.bulma.min.css | 1 + .../Responsive-2.4.1/css/responsive.dataTables.css | 182 + .../css/responsive.dataTables.min.css | 1 + .../Responsive-2.4.1/css/responsive.foundation.css | 185 + .../css/responsive.foundation.min.css | 1 + .../Responsive-2.4.1/css/responsive.jqueryui.css | 182 + .../css/responsive.jqueryui.min.css | 1 + .../Responsive-2.4.1/css/responsive.semanticui.css | 185 + .../css/responsive.semanticui.min.css | 1 + .../Responsive-2.4.1/js/dataTables.responsive.js | 1533 + .../js/dataTables.responsive.min.js | 4 + .../Responsive-2.4.1/js/responsive.bootstrap.js | 102 + .../js/responsive.bootstrap.min.js | 4 + .../Responsive-2.4.1/js/responsive.bootstrap4.js | 102 + .../js/responsive.bootstrap4.min.js | 4 + .../Responsive-2.4.1/js/responsive.bootstrap5.js | 118 + .../js/responsive.bootstrap5.min.js | 4 + .../Responsive-2.4.1/js/responsive.bulma.js | 104 + .../Responsive-2.4.1/js/responsive.bulma.min.js | 4 + .../Responsive-2.4.1/js/responsive.dataTables.js | 58 + .../js/responsive.dataTables.min.js | 4 + .../Responsive-2.4.1/js/responsive.foundation.js | 88 + .../js/responsive.foundation.min.js | 4 + .../Responsive-2.4.1/js/responsive.jqueryui.js | 80 + .../Responsive-2.4.1/js/responsive.jqueryui.min.js | 4 + .../Responsive-2.4.1/js/responsive.semanticui.js | 97 + .../js/responsive.semanticui.min.js | 4 + .../RowReorder-1.3.3/css/rowReorder.bootstrap.css | 26 + .../css/rowReorder.bootstrap.min.css | 1 + .../RowReorder-1.3.3/css/rowReorder.bootstrap4.css | 26 + .../css/rowReorder.bootstrap4.min.css | 1 + .../RowReorder-1.3.3/css/rowReorder.bootstrap5.css | 26 + .../css/rowReorder.bootstrap5.min.css | 1 + .../RowReorder-1.3.3/css/rowReorder.bulma.css | 26 + .../RowReorder-1.3.3/css/rowReorder.bulma.min.css | 1 + .../RowReorder-1.3.3/css/rowReorder.dataTables.css | 26 + .../css/rowReorder.dataTables.min.css | 1 + .../RowReorder-1.3.3/css/rowReorder.foundation.css | 26 + .../css/rowReorder.foundation.min.css | 1 + .../RowReorder-1.3.3/css/rowReorder.jqueryui.css | 26 + .../css/rowReorder.jqueryui.min.css | 1 + .../RowReorder-1.3.3/css/rowReorder.semanticui.css | 26 + .../css/rowReorder.semanticui.min.css | 1 + .../RowReorder-1.3.3/js/dataTables.rowReorder.js | 850 + .../js/dataTables.rowReorder.min.js | 4 + .../RowReorder-1.3.3/js/rowReorder.bootstrap.js | 59 + .../js/rowReorder.bootstrap.min.js | 4 + .../RowReorder-1.3.3/js/rowReorder.bootstrap4.js | 58 + .../js/rowReorder.bootstrap4.min.js | 4 + .../RowReorder-1.3.3/js/rowReorder.bootstrap5.js | 58 + .../js/rowReorder.bootstrap5.min.js | 4 + .../RowReorder-1.3.3/js/rowReorder.bulma.js | 58 + .../RowReorder-1.3.3/js/rowReorder.bulma.min.js | 4 + .../RowReorder-1.3.3/js/rowReorder.dataTables.js | 58 + .../js/rowReorder.dataTables.min.js | 4 + .../RowReorder-1.3.3/js/rowReorder.foundation.js | 58 + .../js/rowReorder.foundation.min.js | 4 + .../RowReorder-1.3.3/js/rowReorder.jqueryui.js | 58 + .../RowReorder-1.3.3/js/rowReorder.jqueryui.min.js | 4 + .../RowReorder-1.3.3/js/rowReorder.semanticui.js | 58 + .../js/rowReorder.semanticui.min.js | 4 + .../Scroller-2.1.1/css/scroller.bootstrap.css | 37 + .../Scroller-2.1.1/css/scroller.bootstrap.min.css | 1 + .../Scroller-2.1.1/css/scroller.bootstrap4.css | 37 + .../Scroller-2.1.1/css/scroller.bootstrap4.min.css | 1 + .../Scroller-2.1.1/css/scroller.bootstrap5.css | 37 + .../Scroller-2.1.1/css/scroller.bootstrap5.min.css | 1 + .../Scroller-2.1.1/css/scroller.bulma.css | 33 + .../Scroller-2.1.1/css/scroller.bulma.min.css | 1 + .../Scroller-2.1.1/css/scroller.dataTables.css | 33 + .../Scroller-2.1.1/css/scroller.dataTables.min.css | 1 + .../Scroller-2.1.1/css/scroller.foundation.css | 17 + .../Scroller-2.1.1/css/scroller.foundation.min.css | 1 + .../Scroller-2.1.1/css/scroller.jqueryui.css | 33 + .../Scroller-2.1.1/css/scroller.jqueryui.min.css | 1 + .../Scroller-2.1.1/css/scroller.semanticui.css | 33 + .../Scroller-2.1.1/css/scroller.semanticui.min.css | 1 + .../Scroller-2.1.1/js/dataTables.scroller.js | 1353 + .../Scroller-2.1.1/js/dataTables.scroller.min.js | 4 + .../Scroller-2.1.1/js/scroller.bootstrap.js | 58 + .../Scroller-2.1.1/js/scroller.bootstrap.min.js | 4 + .../Scroller-2.1.1/js/scroller.bootstrap4.js | 58 + .../Scroller-2.1.1/js/scroller.bootstrap4.min.js | 4 + .../Scroller-2.1.1/js/scroller.bootstrap5.js | 58 + .../Scroller-2.1.1/js/scroller.bootstrap5.min.js | 4 + .../DataTables/Scroller-2.1.1/js/scroller.bulma.js | 58 + .../Scroller-2.1.1/js/scroller.bulma.min.js | 4 + .../Scroller-2.1.1/js/scroller.dataTables.js | 58 + .../Scroller-2.1.1/js/scroller.dataTables.min.js | 4 + .../Scroller-2.1.1/js/scroller.foundation.js | 58 + .../Scroller-2.1.1/js/scroller.foundation.min.js | 4 + .../Scroller-2.1.1/js/scroller.jqueryui.js | 58 + .../Scroller-2.1.1/js/scroller.jqueryui.min.js | 4 + .../Scroller-2.1.1/js/scroller.semanticui.js | 58 + .../Scroller-2.1.1/js/scroller.semanticui.min.js | 4 + .../Select-1.6.2/css/select.bootstrap.css | 61 + .../Select-1.6.2/css/select.bootstrap.min.css | 1 + .../Select-1.6.2/css/select.bootstrap4.css | 61 + .../Select-1.6.2/css/select.bootstrap4.min.css | 1 + .../Select-1.6.2/css/select.bootstrap5.css | 64 + .../Select-1.6.2/css/select.bootstrap5.min.css | 1 + .../DataTables/Select-1.6.2/css/select.bulma.css | 61 + .../Select-1.6.2/css/select.bulma.min.css | 1 + .../Select-1.6.2/css/select.dataTables.css | 61 + .../Select-1.6.2/css/select.dataTables.min.css | 1 + .../Select-1.6.2/css/select.foundation.css | 61 + .../Select-1.6.2/css/select.foundation.min.css | 1 + .../Select-1.6.2/css/select.jqueryui.css | 61 + .../Select-1.6.2/css/select.jqueryui.min.css | 1 + .../Select-1.6.2/css/select.semanticui.css | 61 + .../Select-1.6.2/css/select.semanticui.min.css | 1 + .../Select-1.6.2/js/dataTables.select.js | 1366 + .../Select-1.6.2/js/dataTables.select.min.js | 4 + .../DataTables/Select-1.6.2/js/select.bootstrap.js | 58 + .../Select-1.6.2/js/select.bootstrap.min.js | 4 + .../Select-1.6.2/js/select.bootstrap4.js | 58 + .../Select-1.6.2/js/select.bootstrap4.min.js | 4 + .../Select-1.6.2/js/select.bootstrap5.js | 58 + .../Select-1.6.2/js/select.bootstrap5.min.js | 4 + .../js/DataTables/Select-1.6.2/js/select.bulma.js | 58 + .../DataTables/Select-1.6.2/js/select.bulma.min.js | 4 + .../Select-1.6.2/js/select.dataTables.js | 58 + .../Select-1.6.2/js/select.dataTables.min.js | 4 + .../Select-1.6.2/js/select.foundation.js | 58 + .../Select-1.6.2/js/select.foundation.min.js | 4 + .../DataTables/Select-1.6.2/js/select.jqueryui.js | 58 + .../Select-1.6.2/js/select.jqueryui.min.js | 4 + .../Select-1.6.2/js/select.semanticui.js | 58 + .../Select-1.6.2/js/select.semanticui.min.js | 4 + .../css/stateRestore.bootstrap.css | 200 + .../css/stateRestore.bootstrap.min.css | 1 + .../css/stateRestore.bootstrap4.css | 231 + .../css/stateRestore.bootstrap4.min.css | 1 + .../css/stateRestore.bootstrap5.css | 231 + .../css/stateRestore.bootstrap5.min.css | 1 + .../StateRestore-1.2.2/css/stateRestore.bulma.css | 226 + .../css/stateRestore.bulma.min.css | 1 + .../css/stateRestore.dataTables.css | 174 + .../css/stateRestore.dataTables.min.css | 1 + .../css/stateRestore.foundation.css | 214 + .../css/stateRestore.foundation.min.css | 1 + .../css/stateRestore.jqueryui.css | 216 + .../css/stateRestore.jqueryui.min.css | 1 + .../css/stateRestore.semanticui.css | 264 + .../css/stateRestore.semanticui.min.css | 1 + .../js/dataTables.stateRestore.js | 2365 + .../js/dataTables.stateRestore.min.js | 4 + .../js/stateRestore.bootstrap.js | 71 + .../js/stateRestore.bootstrap.min.js | 4 + .../js/stateRestore.bootstrap4.js | 70 + .../js/stateRestore.bootstrap4.min.js | 4 + .../js/stateRestore.bootstrap5.js | 73 + .../js/stateRestore.bootstrap5.min.js | 4 + .../StateRestore-1.2.2/js/stateRestore.bulma.js | 71 + .../js/stateRestore.bulma.min.js | 4 + .../js/stateRestore.dataTables.js | 58 + .../js/stateRestore.dataTables.min.js | 4 + .../js/stateRestore.foundation.js | 73 + .../js/stateRestore.foundation.min.js | 4 + .../StateRestore-1.2.2/js/stateRestore.jqueryui.js | 74 + .../js/stateRestore.jqueryui.min.js | 4 + .../js/stateRestore.semanticui.js | 76 + .../js/stateRestore.semanticui.min.js | 4 + public/assets/js/DataTables/datatables.css | 1743 + public/assets/js/DataTables/datatables.js | 94225 +++++++++++++++++++ public/assets/js/DataTables/datatables.min.css | 52 + public/assets/js/DataTables/datatables.min.js | 257 + .../assets/js/DataTables/pdfmake-0.1.36/pdfmake.js | 50468 ++++++++++ .../js/DataTables/pdfmake-0.1.36/pdfmake.min.js | 3 + .../js/DataTables/pdfmake-0.1.36/vfs_fonts.js | 6 + public/assets/js/admin/categories.js | 288 + public/assets/js/admin/edit_lesson.js | 600 + public/assets/js/admin/edit_page.js | 382 + public/assets/js/admin/files.js | 302 + public/assets/js/admin/lessons.js | 364 + public/assets/js/admin/pages.js | 240 + public/assets/js/admin/panel.js | 1 + public/assets/js/admin/privileges.js | 291 + public/assets/js/chart.js/Plugin.Subtitle.js | 221 + public/assets/js/chart.js/chart.min.js | 10 + .../js/chart.js/chartjs-plugin-annotation.js | 928 + .../js/chart.js/chartjs-plugin-datalabels.min.js | 7 + public/assets/js/classroom.js | 27 + public/assets/js/cookie.js | 78 + public/assets/js/edit-post.js | 612 + public/assets/js/lesson-features.js | 218 + public/assets/js/manage-category.js | 287 + public/assets/js/manage-posts.js | 177 + public/assets/js/manage-tags.js | 163 + public/assets/js/marked/marked.cjs | 2723 + public/assets/js/marked/marked.esm.js | 2824 + public/assets/js/marked/marked.min.js | 6 + public/assets/js/marked/marked.umd.js | 2729 + public/assets/js/pdfmake/fonts/Roboto.js | 27 + .../js/pdfmake/fonts/Roboto/Roboto-Italic.ttf | Bin 0 -> 152384 bytes .../js/pdfmake/fonts/Roboto/Roboto-Medium.ttf | Bin 0 -> 146884 bytes .../pdfmake/fonts/Roboto/Roboto-MediumItalic.ttf | Bin 0 -> 152572 bytes .../js/pdfmake/fonts/Roboto/Roboto-Regular.ttf | Bin 0 -> 146768 bytes public/assets/js/pdfmake/pdfmake.js | 74980 +++++++++++++++ public/assets/js/pdfmake/pdfmake.js.map | 1 + public/assets/js/pdfmake/pdfmake.min.js | 3 + public/assets/js/pdfmake/pdfmake.min.js.map | 1 + public/assets/js/pdfmake/standard-fonts/Courier.js | 27 + .../assets/js/pdfmake/standard-fonts/Helvetica.js | 27 + public/assets/js/pdfmake/standard-fonts/Symbol.js | 21 + public/assets/js/pdfmake/standard-fonts/Times.js | 27 + .../js/pdfmake/standard-fonts/ZapfDingbats.js | 21 + public/assets/js/pdfmake/vfs_fonts.js | 7 + public/assets/js/select2/css/select2_4.0.6.min.css | 1 + public/assets/js/select2/i18n/el.js | 3 + public/assets/js/select2/js/select2_4.0.6.min.js | 1 + public/favicon.ico | Bin 0 -> 15086 bytes public/files/.gitkeep | 0 public/index.php | 63 + sceleton.txt | 79 + tests/abstract.php | 46 + tests/algo-hashes.php | 52 + tests/auth.php | 58 + tests/code/abstract.php | 46 + tests/code/bits.php | 51 + tests/code/echo.php | 43 + tests/code/exceptions.php | 73 + tests/code/interface.php | 31 + tests/code/login.php | 25 + tests/code/menu.php | 42 + tests/code/proxy.php | 38 + tests/code/session.php | 50 + tests/code/str-replace.php | 5 + tests/compare/compress.php | 80 + tests/compare/hashcost.php | 54 + tests/compare/md5-v-sha1.php | 18 + tests/compare/memcached-v-filecache.php | 35 + tests/compare/redis-v-filecache.php | 35 + tests/compare/serialize-v-jsonencode.php | 35 + tests/compare/sha1-v-sha256.php | 22 + tests/dotnet-pass.php | 503 + tests/mail.php | 2 + tests/php.php | 2 + tests/repository.php | 65 + tests/service/entropy.php | 61 + tests/service/lorem-md.php | 71 + tests/service/mailjet.php | 56 + tests/service/memcached.php | 12 + tests/service/redis.php | 27 + tests/userman.php | 27 + 617 files changed, 317157 insertions(+) create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 composer.json create mode 100644 composer.lock create mode 100644 core/README.md create mode 100644 core/auth/.gitkeep create mode 100644 core/auth/env.example create mode 100644 core/classes/Benchmark.php create mode 100644 core/classes/Cart.php create mode 100644 core/classes/Database.php create mode 100644 core/classes/Registry.php create mode 100644 core/classes/Render.php create mode 100644 core/classes/Repository.php create mode 100644 core/classes/Request.php create mode 100644 core/classes/Route.php create mode 100644 core/classes/Security.php create mode 100644 core/classes/authentication/Core/PasswordTrait.php create mode 100644 core/classes/authentication/Core/UserManager.php create mode 100644 core/classes/authentication/Core/UserManagerInterface.php create mode 100644 core/classes/authentication/Token/UserToken.php create mode 100644 core/classes/authentication/Token/UserTokenInterface.php create mode 100644 core/classes/authentication/User.php create mode 100644 core/classes/authentication/UserInterface.php create mode 100644 core/classes/authentication/info.md create mode 100644 core/classes/cache/Cache_interface.php create mode 100644 core/classes/cache/FileCache.php create mode 100644 core/classes/cache/MemcachedCache.php create mode 100644 core/classes/cache/RedisCache.php create mode 100644 core/classes/session/DatabaseSession.php create mode 100644 core/classes/session/DefaultSession.php create mode 100644 core/classes/session/FilesSession.php create mode 100644 core/classes/session/SessionHandlerInterface.php create mode 100644 core/cli/cli-cache.php create mode 100755 core/cli/curl-cache.php create mode 100644 core/cli/info.md create mode 100644 core/cli/micro/setup.php create mode 100644 core/cli/micro/term_utilities.php create mode 100644 core/config/anom_settings.php create mode 100644 core/config/constants.php create mode 100644 core/config/init.php create mode 100644 core/helpers/autoload.php create mode 100644 core/helpers/design_patterns.php create mode 100644 core/helpers/error_handling.php create mode 100644 data/.gitkeep create mode 100644 docker-compose.redis.yml create mode 100644 docker-compose.yml create mode 100644 docker/bin/docker-entrypoint.sh create mode 100644 docker/bin/optimize.sh create mode 100644 docker/config/000-default.conf create mode 100644 docker/config/composer-naked.json create mode 100644 docker/config/composer-redis.json create mode 100644 docker/config/default-ssl.conf create mode 100644 docker/config/info.md create mode 100644 docker/config/msmtprc.kill create mode 100644 docker/config/opcache.ini create mode 100644 docker/config/php-ext-xdebug.ini create mode 100644 docker/config/php-overrides.ini create mode 100644 public/.htaccess create mode 100644 public/app/config/.gitkeep create mode 100644 public/app/config/app_constants.php create mode 100644 public/app/config/init_routes.php create mode 100644 public/app/controllers/.gitkeep create mode 100644 public/app/controllers/Admin.php create mode 100644 public/app/controllers/Auth.php create mode 100644 public/app/controllers/admin/.gitkeep create mode 100644 public/app/controllers/admin/Course_admin.php create mode 100644 public/app/controllers/admin/Users_admin.php create mode 100644 public/app/controllers/api/.gitkeep create mode 100644 public/app/controllers/api/Common_api.php create mode 100644 public/app/controllers/api/Doc_api.php create mode 100644 public/app/controllers/cli/CliContoller.php create mode 100644 public/app/controllers/cms/Course.php create mode 100644 public/app/extends/.gitkeep create mode 100644 public/app/extends/Cache_service.php create mode 100644 public/app/extends/Classroom_manager.php create mode 100644 public/app/extends/Classroom_user.php create mode 100644 public/app/extends/Send_mail.php create mode 100644 public/app/models/.gitkeep create mode 100644 public/app/models/Jorge.php create mode 100644 public/app/models/_info.md create mode 100644 public/app/models/admin/History_model.php create mode 100644 public/app/models/admin/Privilege_model.php create mode 100644 public/app/models/admin/User_model.php create mode 100644 public/app/models/cms/Course_model.php create mode 100644 public/app/models/cms/Media_model.php create mode 100644 public/app/models/cms/Page_model.php create mode 100644 public/app/models/market/Market_repository.php create mode 100644 public/app/models/market/Payment_template.txt create mode 100644 public/app/models/market/ProductCategories_model.php create mode 100644 public/app/models/market/Product_model.php create mode 100644 public/app/models/todo.md create mode 100644 public/app/routes/.gitkeep create mode 100644 public/app/routes/api.php create mode 100644 public/app/routes/backend.php create mode 100644 public/app/routes/frontend.php create mode 100644 public/app/routes/user.php create mode 100644 public/app/views/.gitkeep create mode 100644 public/app/views/admin/admin-menu.php create mode 100644 public/app/views/admin/categories.php create mode 100644 public/app/views/admin/edit_lesson.php create mode 100644 public/app/views/admin/edit_page.php create mode 100644 public/app/views/admin/files.php create mode 100644 public/app/views/admin/lessons.php create mode 100644 public/app/views/admin/pages.php create mode 100644 public/app/views/admin/panel.php create mode 100644 public/app/views/admin/privileges.php create mode 100644 public/app/views/admin/skeleton.php create mode 100644 public/app/views/components/breadcrumbs.php create mode 100644 public/app/views/components/courses_menu.php create mode 100644 public/app/views/components/footer.php create mode 100644 public/app/views/components/header_includes.php create mode 100644 public/app/views/components/lessons_list.php create mode 100644 public/app/views/components/subcategories.php create mode 100644 public/app/views/components/top_bar.php create mode 100644 public/app/views/components/user-management.php create mode 100644 public/app/views/error/404.php create mode 100644 public/app/views/error/general.php create mode 100644 public/app/views/group.php create mode 100644 public/app/views/item.php create mode 100644 public/app/views/simple/Model.php create mode 100644 public/app/views/simple/Render.php create mode 100644 public/app/views/simple/category.php create mode 100644 public/app/views/simple/index.php create mode 100644 public/app/views/simple/lorem-dates.php create mode 100644 public/app/views/simple/lorem-intros.php create mode 100644 public/app/views/simple/lorem-posts.php create mode 100644 public/app/views/simple/post.php create mode 100644 public/app/views/templates/course.php create mode 100644 public/app/views/templates/lesson.php create mode 100644 public/app/views/templates/page.php create mode 100644 public/app/views/user/login.php create mode 100644 public/app/views/user/new-password.php create mode 100644 public/app/views/user/registration.php create mode 100644 public/app/views/welcome.php create mode 100644 public/assets/css/admin.css create mode 100644 public/assets/css/class.css create mode 100644 public/assets/css/email.css create mode 100644 public/assets/css/main.css create mode 100644 public/assets/css/normalize.css create mode 100644 public/assets/css/overides.css create mode 100644 public/assets/js/DataTables/Buttons-2.3.6/css/buttons.bootstrap.css create mode 100644 public/assets/js/DataTables/Buttons-2.3.6/css/buttons.bootstrap.min.css create mode 100644 public/assets/js/DataTables/Buttons-2.3.6/css/buttons.bootstrap4.css create mode 100644 public/assets/js/DataTables/Buttons-2.3.6/css/buttons.bootstrap4.min.css create mode 100644 public/assets/js/DataTables/Buttons-2.3.6/css/buttons.bootstrap5.css create mode 100644 public/assets/js/DataTables/Buttons-2.3.6/css/buttons.bootstrap5.min.css create mode 100644 public/assets/js/DataTables/Buttons-2.3.6/css/buttons.bulma.css create mode 100644 public/assets/js/DataTables/Buttons-2.3.6/css/buttons.bulma.min.css create mode 100644 public/assets/js/DataTables/Buttons-2.3.6/css/buttons.dataTables.css create mode 100644 public/assets/js/DataTables/Buttons-2.3.6/css/buttons.dataTables.min.css create mode 100644 public/assets/js/DataTables/Buttons-2.3.6/css/buttons.foundation.css create mode 100644 public/assets/js/DataTables/Buttons-2.3.6/css/buttons.foundation.min.css create mode 100644 public/assets/js/DataTables/Buttons-2.3.6/css/buttons.jqueryui.css create mode 100644 public/assets/js/DataTables/Buttons-2.3.6/css/buttons.jqueryui.min.css create mode 100644 public/assets/js/DataTables/Buttons-2.3.6/css/buttons.semanticui.css create mode 100644 public/assets/js/DataTables/Buttons-2.3.6/css/buttons.semanticui.min.css create mode 100644 public/assets/js/DataTables/Buttons-2.3.6/css/common.scss create mode 100644 public/assets/js/DataTables/Buttons-2.3.6/css/mixins.scss create mode 100644 public/assets/js/DataTables/Buttons-2.3.6/js/buttons.bootstrap.js create mode 100644 public/assets/js/DataTables/Buttons-2.3.6/js/buttons.bootstrap.min.js create mode 100644 public/assets/js/DataTables/Buttons-2.3.6/js/buttons.bootstrap4.js create mode 100644 public/assets/js/DataTables/Buttons-2.3.6/js/buttons.bootstrap4.min.js create mode 100644 public/assets/js/DataTables/Buttons-2.3.6/js/buttons.bootstrap5.js create mode 100644 public/assets/js/DataTables/Buttons-2.3.6/js/buttons.bootstrap5.min.js create mode 100644 public/assets/js/DataTables/Buttons-2.3.6/js/buttons.bulma.js create mode 100644 public/assets/js/DataTables/Buttons-2.3.6/js/buttons.bulma.min.js create mode 100644 public/assets/js/DataTables/Buttons-2.3.6/js/buttons.colVis.js create mode 100644 public/assets/js/DataTables/Buttons-2.3.6/js/buttons.colVis.min.js create mode 100644 public/assets/js/DataTables/Buttons-2.3.6/js/buttons.dataTables.js create mode 100644 public/assets/js/DataTables/Buttons-2.3.6/js/buttons.dataTables.min.js create mode 100644 public/assets/js/DataTables/Buttons-2.3.6/js/buttons.foundation.js create mode 100644 public/assets/js/DataTables/Buttons-2.3.6/js/buttons.foundation.min.js create mode 100644 public/assets/js/DataTables/Buttons-2.3.6/js/buttons.html5.js create mode 100644 public/assets/js/DataTables/Buttons-2.3.6/js/buttons.html5.min.js create mode 100644 public/assets/js/DataTables/Buttons-2.3.6/js/buttons.jqueryui.js create mode 100644 public/assets/js/DataTables/Buttons-2.3.6/js/buttons.jqueryui.min.js create mode 100644 public/assets/js/DataTables/Buttons-2.3.6/js/buttons.print.js create mode 100644 public/assets/js/DataTables/Buttons-2.3.6/js/buttons.print.min.js create mode 100644 public/assets/js/DataTables/Buttons-2.3.6/js/buttons.semanticui.js create mode 100644 public/assets/js/DataTables/Buttons-2.3.6/js/buttons.semanticui.min.js create mode 100644 public/assets/js/DataTables/Buttons-2.3.6/js/dataTables.buttons.js create mode 100644 public/assets/js/DataTables/Buttons-2.3.6/js/dataTables.buttons.min.js create mode 100644 public/assets/js/DataTables/ColReorder-1.6.2/css/colReorder.bootstrap.css create mode 100644 public/assets/js/DataTables/ColReorder-1.6.2/css/colReorder.bootstrap.min.css create mode 100644 public/assets/js/DataTables/ColReorder-1.6.2/css/colReorder.bootstrap4.css create mode 100644 public/assets/js/DataTables/ColReorder-1.6.2/css/colReorder.bootstrap4.min.css create mode 100644 public/assets/js/DataTables/ColReorder-1.6.2/css/colReorder.bootstrap5.css create mode 100644 public/assets/js/DataTables/ColReorder-1.6.2/css/colReorder.bootstrap5.min.css create mode 100644 public/assets/js/DataTables/ColReorder-1.6.2/css/colReorder.bulma.css create mode 100644 public/assets/js/DataTables/ColReorder-1.6.2/css/colReorder.bulma.min.css create mode 100644 public/assets/js/DataTables/ColReorder-1.6.2/css/colReorder.dataTables.css create mode 100644 public/assets/js/DataTables/ColReorder-1.6.2/css/colReorder.dataTables.min.css create mode 100644 public/assets/js/DataTables/ColReorder-1.6.2/css/colReorder.foundation.css create mode 100644 public/assets/js/DataTables/ColReorder-1.6.2/css/colReorder.foundation.min.css create mode 100644 public/assets/js/DataTables/ColReorder-1.6.2/css/colReorder.jqueryui.css create mode 100644 public/assets/js/DataTables/ColReorder-1.6.2/css/colReorder.jqueryui.min.css create mode 100644 public/assets/js/DataTables/ColReorder-1.6.2/css/colReorder.semanticui.css create mode 100644 public/assets/js/DataTables/ColReorder-1.6.2/css/colReorder.semanticui.min.css create mode 100644 public/assets/js/DataTables/ColReorder-1.6.2/js/colReorder.bootstrap.js create mode 100644 public/assets/js/DataTables/ColReorder-1.6.2/js/colReorder.bootstrap.min.js create mode 100644 public/assets/js/DataTables/ColReorder-1.6.2/js/colReorder.bootstrap4.js create mode 100644 public/assets/js/DataTables/ColReorder-1.6.2/js/colReorder.bootstrap4.min.js create mode 100644 public/assets/js/DataTables/ColReorder-1.6.2/js/colReorder.bootstrap5.js create mode 100644 public/assets/js/DataTables/ColReorder-1.6.2/js/colReorder.bootstrap5.min.js create mode 100644 public/assets/js/DataTables/ColReorder-1.6.2/js/colReorder.bulma.js create mode 100644 public/assets/js/DataTables/ColReorder-1.6.2/js/colReorder.bulma.min.js create mode 100644 public/assets/js/DataTables/ColReorder-1.6.2/js/colReorder.dataTables.js create mode 100644 public/assets/js/DataTables/ColReorder-1.6.2/js/colReorder.dataTables.min.js create mode 100644 public/assets/js/DataTables/ColReorder-1.6.2/js/colReorder.foundation.js create mode 100644 public/assets/js/DataTables/ColReorder-1.6.2/js/colReorder.foundation.min.js create mode 100644 public/assets/js/DataTables/ColReorder-1.6.2/js/colReorder.jqueryui.js create mode 100644 public/assets/js/DataTables/ColReorder-1.6.2/js/colReorder.jqueryui.min.js create mode 100644 public/assets/js/DataTables/ColReorder-1.6.2/js/colReorder.semanticui.js create mode 100644 public/assets/js/DataTables/ColReorder-1.6.2/js/colReorder.semanticui.min.js create mode 100644 public/assets/js/DataTables/ColReorder-1.6.2/js/dataTables.colReorder.js create mode 100644 public/assets/js/DataTables/ColReorder-1.6.2/js/dataTables.colReorder.min.js create mode 100644 public/assets/js/DataTables/DataTables-1.13.4/css/dataTables.bootstrap.css create mode 100644 public/assets/js/DataTables/DataTables-1.13.4/css/dataTables.bootstrap.min.css create mode 100644 public/assets/js/DataTables/DataTables-1.13.4/css/dataTables.bootstrap4.css create mode 100644 public/assets/js/DataTables/DataTables-1.13.4/css/dataTables.bootstrap4.min.css create mode 100644 public/assets/js/DataTables/DataTables-1.13.4/css/dataTables.bootstrap5.css create mode 100644 public/assets/js/DataTables/DataTables-1.13.4/css/dataTables.bootstrap5.min.css create mode 100644 public/assets/js/DataTables/DataTables-1.13.4/css/dataTables.bulma.css create mode 100644 public/assets/js/DataTables/DataTables-1.13.4/css/dataTables.bulma.min.css create mode 100644 public/assets/js/DataTables/DataTables-1.13.4/css/dataTables.dataTables.css create mode 100644 public/assets/js/DataTables/DataTables-1.13.4/css/dataTables.dataTables.min.css create mode 100644 public/assets/js/DataTables/DataTables-1.13.4/css/dataTables.foundation.css create mode 100644 public/assets/js/DataTables/DataTables-1.13.4/css/dataTables.foundation.min.css create mode 100644 public/assets/js/DataTables/DataTables-1.13.4/css/dataTables.jqueryui.css create mode 100644 public/assets/js/DataTables/DataTables-1.13.4/css/dataTables.jqueryui.min.css create mode 100644 public/assets/js/DataTables/DataTables-1.13.4/css/dataTables.semanticui.css create mode 100644 public/assets/js/DataTables/DataTables-1.13.4/css/dataTables.semanticui.min.css create mode 100644 public/assets/js/DataTables/DataTables-1.13.4/css/jquery.dataTables.css create mode 100644 public/assets/js/DataTables/DataTables-1.13.4/css/jquery.dataTables.min.css create mode 100644 public/assets/js/DataTables/DataTables-1.13.4/images/sort_asc.png create mode 100644 public/assets/js/DataTables/DataTables-1.13.4/images/sort_asc_disabled.png create mode 100644 public/assets/js/DataTables/DataTables-1.13.4/images/sort_both.png create mode 100644 public/assets/js/DataTables/DataTables-1.13.4/images/sort_desc.png create mode 100644 public/assets/js/DataTables/DataTables-1.13.4/images/sort_desc_disabled.png create mode 100644 public/assets/js/DataTables/DataTables-1.13.4/js/dataTables.bootstrap.js create mode 100644 public/assets/js/DataTables/DataTables-1.13.4/js/dataTables.bootstrap.min.js create mode 100644 public/assets/js/DataTables/DataTables-1.13.4/js/dataTables.bootstrap4.js create mode 100644 public/assets/js/DataTables/DataTables-1.13.4/js/dataTables.bootstrap4.min.js create mode 100644 public/assets/js/DataTables/DataTables-1.13.4/js/dataTables.bootstrap5.js create mode 100644 public/assets/js/DataTables/DataTables-1.13.4/js/dataTables.bootstrap5.min.js create mode 100644 public/assets/js/DataTables/DataTables-1.13.4/js/dataTables.bulma.js create mode 100644 public/assets/js/DataTables/DataTables-1.13.4/js/dataTables.bulma.min.js create mode 100644 public/assets/js/DataTables/DataTables-1.13.4/js/dataTables.dataTables.js create mode 100644 public/assets/js/DataTables/DataTables-1.13.4/js/dataTables.dataTables.min.js create mode 100644 public/assets/js/DataTables/DataTables-1.13.4/js/dataTables.foundation.js create mode 100644 public/assets/js/DataTables/DataTables-1.13.4/js/dataTables.foundation.min.js create mode 100644 public/assets/js/DataTables/DataTables-1.13.4/js/dataTables.jqueryui.js create mode 100644 public/assets/js/DataTables/DataTables-1.13.4/js/dataTables.jqueryui.min.js create mode 100644 public/assets/js/DataTables/DataTables-1.13.4/js/dataTables.semanticui.js create mode 100644 public/assets/js/DataTables/DataTables-1.13.4/js/dataTables.semanticui.min.js create mode 100644 public/assets/js/DataTables/DataTables-1.13.4/js/jquery.dataTables.js create mode 100644 public/assets/js/DataTables/DataTables-1.13.4/js/jquery.dataTables.min.js create mode 100644 public/assets/js/DataTables/DateTime-1.4.0/css/dataTables.dateTime.css create mode 100644 public/assets/js/DataTables/DateTime-1.4.0/css/dataTables.dateTime.min.css create mode 100644 public/assets/js/DataTables/DateTime-1.4.0/js/dataTables.dateTime.js create mode 100644 public/assets/js/DataTables/DateTime-1.4.0/js/dataTables.dateTime.min.js create mode 100644 public/assets/js/DataTables/FixedColumns-4.2.2/css/fixedColumns.bootstrap.css create mode 100644 public/assets/js/DataTables/FixedColumns-4.2.2/css/fixedColumns.bootstrap.min.css create mode 100644 public/assets/js/DataTables/FixedColumns-4.2.2/css/fixedColumns.bootstrap4.css create mode 100644 public/assets/js/DataTables/FixedColumns-4.2.2/css/fixedColumns.bootstrap4.min.css create mode 100644 public/assets/js/DataTables/FixedColumns-4.2.2/css/fixedColumns.bootstrap5.css create mode 100644 public/assets/js/DataTables/FixedColumns-4.2.2/css/fixedColumns.bootstrap5.min.css create mode 100644 public/assets/js/DataTables/FixedColumns-4.2.2/css/fixedColumns.bulma.css create mode 100644 public/assets/js/DataTables/FixedColumns-4.2.2/css/fixedColumns.bulma.min.css create mode 100644 public/assets/js/DataTables/FixedColumns-4.2.2/css/fixedColumns.dataTables.css create mode 100644 public/assets/js/DataTables/FixedColumns-4.2.2/css/fixedColumns.dataTables.min.css create mode 100644 public/assets/js/DataTables/FixedColumns-4.2.2/css/fixedColumns.foundation.css create mode 100644 public/assets/js/DataTables/FixedColumns-4.2.2/css/fixedColumns.foundation.min.css create mode 100644 public/assets/js/DataTables/FixedColumns-4.2.2/css/fixedColumns.jqueryui.css create mode 100644 public/assets/js/DataTables/FixedColumns-4.2.2/css/fixedColumns.jqueryui.min.css create mode 100644 public/assets/js/DataTables/FixedColumns-4.2.2/css/fixedColumns.semanticui.css create mode 100644 public/assets/js/DataTables/FixedColumns-4.2.2/css/fixedColumns.semanticui.min.css create mode 100644 public/assets/js/DataTables/FixedColumns-4.2.2/js/dataTables.fixedColumns.js create mode 100644 public/assets/js/DataTables/FixedColumns-4.2.2/js/dataTables.fixedColumns.min.js create mode 100644 public/assets/js/DataTables/FixedColumns-4.2.2/js/fixedColumns.bootstrap.js create mode 100644 public/assets/js/DataTables/FixedColumns-4.2.2/js/fixedColumns.bootstrap.min.js create mode 100644 public/assets/js/DataTables/FixedColumns-4.2.2/js/fixedColumns.bootstrap4.js create mode 100644 public/assets/js/DataTables/FixedColumns-4.2.2/js/fixedColumns.bootstrap4.min.js create mode 100644 public/assets/js/DataTables/FixedColumns-4.2.2/js/fixedColumns.bootstrap5.js create mode 100644 public/assets/js/DataTables/FixedColumns-4.2.2/js/fixedColumns.bootstrap5.min.js create mode 100644 public/assets/js/DataTables/FixedColumns-4.2.2/js/fixedColumns.bulma.js create mode 100644 public/assets/js/DataTables/FixedColumns-4.2.2/js/fixedColumns.bulma.min.js create mode 100644 public/assets/js/DataTables/FixedColumns-4.2.2/js/fixedColumns.dataTables.js create mode 100644 public/assets/js/DataTables/FixedColumns-4.2.2/js/fixedColumns.dataTables.min.js create mode 100644 public/assets/js/DataTables/FixedColumns-4.2.2/js/fixedColumns.foundation.js create mode 100644 public/assets/js/DataTables/FixedColumns-4.2.2/js/fixedColumns.foundation.min.js create mode 100644 public/assets/js/DataTables/FixedColumns-4.2.2/js/fixedColumns.jqueryui.js create mode 100644 public/assets/js/DataTables/FixedColumns-4.2.2/js/fixedColumns.jqueryui.min.js create mode 100644 public/assets/js/DataTables/FixedColumns-4.2.2/js/fixedColumns.semanticui.js create mode 100644 public/assets/js/DataTables/FixedColumns-4.2.2/js/fixedColumns.semanticui.min.js create mode 100644 public/assets/js/DataTables/FixedHeader-3.3.2/css/fixedHeader.bootstrap.css create mode 100644 public/assets/js/DataTables/FixedHeader-3.3.2/css/fixedHeader.bootstrap.min.css create mode 100644 public/assets/js/DataTables/FixedHeader-3.3.2/css/fixedHeader.bootstrap4.css create mode 100644 public/assets/js/DataTables/FixedHeader-3.3.2/css/fixedHeader.bootstrap4.min.css create mode 100644 public/assets/js/DataTables/FixedHeader-3.3.2/css/fixedHeader.bootstrap5.css create mode 100644 public/assets/js/DataTables/FixedHeader-3.3.2/css/fixedHeader.bootstrap5.min.css create mode 100644 public/assets/js/DataTables/FixedHeader-3.3.2/css/fixedHeader.bulma.css create mode 100644 public/assets/js/DataTables/FixedHeader-3.3.2/css/fixedHeader.bulma.min.css create mode 100644 public/assets/js/DataTables/FixedHeader-3.3.2/css/fixedHeader.dataTables.css create mode 100644 public/assets/js/DataTables/FixedHeader-3.3.2/css/fixedHeader.dataTables.min.css create mode 100644 public/assets/js/DataTables/FixedHeader-3.3.2/css/fixedHeader.foundation.css create mode 100644 public/assets/js/DataTables/FixedHeader-3.3.2/css/fixedHeader.foundation.min.css create mode 100644 public/assets/js/DataTables/FixedHeader-3.3.2/css/fixedHeader.jqueryui.css create mode 100644 public/assets/js/DataTables/FixedHeader-3.3.2/css/fixedHeader.jqueryui.min.css create mode 100644 public/assets/js/DataTables/FixedHeader-3.3.2/css/fixedHeader.semanticui.css create mode 100644 public/assets/js/DataTables/FixedHeader-3.3.2/css/fixedHeader.semanticui.min.css create mode 100644 public/assets/js/DataTables/FixedHeader-3.3.2/js/dataTables.fixedHeader.js create mode 100644 public/assets/js/DataTables/FixedHeader-3.3.2/js/dataTables.fixedHeader.min.js create mode 100644 public/assets/js/DataTables/FixedHeader-3.3.2/js/fixedHeader.bootstrap.js create mode 100644 public/assets/js/DataTables/FixedHeader-3.3.2/js/fixedHeader.bootstrap.min.js create mode 100644 public/assets/js/DataTables/FixedHeader-3.3.2/js/fixedHeader.bootstrap4.js create mode 100644 public/assets/js/DataTables/FixedHeader-3.3.2/js/fixedHeader.bootstrap4.min.js create mode 100644 public/assets/js/DataTables/FixedHeader-3.3.2/js/fixedHeader.bootstrap5.js create mode 100644 public/assets/js/DataTables/FixedHeader-3.3.2/js/fixedHeader.bootstrap5.min.js create mode 100644 public/assets/js/DataTables/FixedHeader-3.3.2/js/fixedHeader.bulma.js create mode 100644 public/assets/js/DataTables/FixedHeader-3.3.2/js/fixedHeader.bulma.min.js create mode 100644 public/assets/js/DataTables/FixedHeader-3.3.2/js/fixedHeader.dataTables.js create mode 100644 public/assets/js/DataTables/FixedHeader-3.3.2/js/fixedHeader.dataTables.min.js create mode 100644 public/assets/js/DataTables/FixedHeader-3.3.2/js/fixedHeader.foundation.js create mode 100644 public/assets/js/DataTables/FixedHeader-3.3.2/js/fixedHeader.foundation.min.js create mode 100644 public/assets/js/DataTables/FixedHeader-3.3.2/js/fixedHeader.jqueryui.js create mode 100644 public/assets/js/DataTables/FixedHeader-3.3.2/js/fixedHeader.jqueryui.min.js create mode 100644 public/assets/js/DataTables/FixedHeader-3.3.2/js/fixedHeader.semanticui.js create mode 100644 public/assets/js/DataTables/FixedHeader-3.3.2/js/fixedHeader.semanticui.min.js create mode 100644 public/assets/js/DataTables/JSZip-2.5.0/jszip.js create mode 100644 public/assets/js/DataTables/JSZip-2.5.0/jszip.min.js create mode 100644 public/assets/js/DataTables/KeyTable-2.8.2/css/keyTable.bootstrap.css create mode 100644 public/assets/js/DataTables/KeyTable-2.8.2/css/keyTable.bootstrap.min.css create mode 100644 public/assets/js/DataTables/KeyTable-2.8.2/css/keyTable.bootstrap4.css create mode 100644 public/assets/js/DataTables/KeyTable-2.8.2/css/keyTable.bootstrap4.min.css create mode 100644 public/assets/js/DataTables/KeyTable-2.8.2/css/keyTable.bootstrap5.css create mode 100644 public/assets/js/DataTables/KeyTable-2.8.2/css/keyTable.bootstrap5.min.css create mode 100644 public/assets/js/DataTables/KeyTable-2.8.2/css/keyTable.bulma.css create mode 100644 public/assets/js/DataTables/KeyTable-2.8.2/css/keyTable.bulma.min.css create mode 100644 public/assets/js/DataTables/KeyTable-2.8.2/css/keyTable.dataTables.css create mode 100644 public/assets/js/DataTables/KeyTable-2.8.2/css/keyTable.dataTables.min.css create mode 100644 public/assets/js/DataTables/KeyTable-2.8.2/css/keyTable.foundation.css create mode 100644 public/assets/js/DataTables/KeyTable-2.8.2/css/keyTable.foundation.min.css create mode 100644 public/assets/js/DataTables/KeyTable-2.8.2/css/keyTable.jqueryui.css create mode 100644 public/assets/js/DataTables/KeyTable-2.8.2/css/keyTable.jqueryui.min.css create mode 100644 public/assets/js/DataTables/KeyTable-2.8.2/css/keyTable.semanticui.css create mode 100644 public/assets/js/DataTables/KeyTable-2.8.2/css/keyTable.semanticui.min.css create mode 100644 public/assets/js/DataTables/KeyTable-2.8.2/js/dataTables.keyTable.js create mode 100644 public/assets/js/DataTables/KeyTable-2.8.2/js/dataTables.keyTable.min.js create mode 100644 public/assets/js/DataTables/KeyTable-2.8.2/js/keyTable.bootstrap.js create mode 100644 public/assets/js/DataTables/KeyTable-2.8.2/js/keyTable.bootstrap.min.js create mode 100644 public/assets/js/DataTables/KeyTable-2.8.2/js/keyTable.bootstrap4.js create mode 100644 public/assets/js/DataTables/KeyTable-2.8.2/js/keyTable.bootstrap4.min.js create mode 100644 public/assets/js/DataTables/KeyTable-2.8.2/js/keyTable.bootstrap5.js create mode 100644 public/assets/js/DataTables/KeyTable-2.8.2/js/keyTable.bootstrap5.min.js create mode 100644 public/assets/js/DataTables/KeyTable-2.8.2/js/keyTable.bulma.js create mode 100644 public/assets/js/DataTables/KeyTable-2.8.2/js/keyTable.bulma.min.js create mode 100644 public/assets/js/DataTables/KeyTable-2.8.2/js/keyTable.dataTables.js create mode 100644 public/assets/js/DataTables/KeyTable-2.8.2/js/keyTable.dataTables.min.js create mode 100644 public/assets/js/DataTables/KeyTable-2.8.2/js/keyTable.foundation.js create mode 100644 public/assets/js/DataTables/KeyTable-2.8.2/js/keyTable.foundation.min.js create mode 100644 public/assets/js/DataTables/KeyTable-2.8.2/js/keyTable.jqueryui.js create mode 100644 public/assets/js/DataTables/KeyTable-2.8.2/js/keyTable.jqueryui.min.js create mode 100644 public/assets/js/DataTables/KeyTable-2.8.2/js/keyTable.semanticui.js create mode 100644 public/assets/js/DataTables/KeyTable-2.8.2/js/keyTable.semanticui.min.js create mode 100644 public/assets/js/DataTables/Responsive-2.4.1/css/responsive.bootstrap.css create mode 100644 public/assets/js/DataTables/Responsive-2.4.1/css/responsive.bootstrap.min.css create mode 100644 public/assets/js/DataTables/Responsive-2.4.1/css/responsive.bootstrap4.css create mode 100644 public/assets/js/DataTables/Responsive-2.4.1/css/responsive.bootstrap4.min.css create mode 100644 public/assets/js/DataTables/Responsive-2.4.1/css/responsive.bootstrap5.css create mode 100644 public/assets/js/DataTables/Responsive-2.4.1/css/responsive.bootstrap5.min.css create mode 100644 public/assets/js/DataTables/Responsive-2.4.1/css/responsive.bulma.css create mode 100644 public/assets/js/DataTables/Responsive-2.4.1/css/responsive.bulma.min.css create mode 100644 public/assets/js/DataTables/Responsive-2.4.1/css/responsive.dataTables.css create mode 100644 public/assets/js/DataTables/Responsive-2.4.1/css/responsive.dataTables.min.css create mode 100644 public/assets/js/DataTables/Responsive-2.4.1/css/responsive.foundation.css create mode 100644 public/assets/js/DataTables/Responsive-2.4.1/css/responsive.foundation.min.css create mode 100644 public/assets/js/DataTables/Responsive-2.4.1/css/responsive.jqueryui.css create mode 100644 public/assets/js/DataTables/Responsive-2.4.1/css/responsive.jqueryui.min.css create mode 100644 public/assets/js/DataTables/Responsive-2.4.1/css/responsive.semanticui.css create mode 100644 public/assets/js/DataTables/Responsive-2.4.1/css/responsive.semanticui.min.css create mode 100644 public/assets/js/DataTables/Responsive-2.4.1/js/dataTables.responsive.js create mode 100644 public/assets/js/DataTables/Responsive-2.4.1/js/dataTables.responsive.min.js create mode 100644 public/assets/js/DataTables/Responsive-2.4.1/js/responsive.bootstrap.js create mode 100644 public/assets/js/DataTables/Responsive-2.4.1/js/responsive.bootstrap.min.js create mode 100644 public/assets/js/DataTables/Responsive-2.4.1/js/responsive.bootstrap4.js create mode 100644 public/assets/js/DataTables/Responsive-2.4.1/js/responsive.bootstrap4.min.js create mode 100644 public/assets/js/DataTables/Responsive-2.4.1/js/responsive.bootstrap5.js create mode 100644 public/assets/js/DataTables/Responsive-2.4.1/js/responsive.bootstrap5.min.js create mode 100644 public/assets/js/DataTables/Responsive-2.4.1/js/responsive.bulma.js create mode 100644 public/assets/js/DataTables/Responsive-2.4.1/js/responsive.bulma.min.js create mode 100644 public/assets/js/DataTables/Responsive-2.4.1/js/responsive.dataTables.js create mode 100644 public/assets/js/DataTables/Responsive-2.4.1/js/responsive.dataTables.min.js create mode 100644 public/assets/js/DataTables/Responsive-2.4.1/js/responsive.foundation.js create mode 100644 public/assets/js/DataTables/Responsive-2.4.1/js/responsive.foundation.min.js create mode 100644 public/assets/js/DataTables/Responsive-2.4.1/js/responsive.jqueryui.js create mode 100644 public/assets/js/DataTables/Responsive-2.4.1/js/responsive.jqueryui.min.js create mode 100644 public/assets/js/DataTables/Responsive-2.4.1/js/responsive.semanticui.js create mode 100644 public/assets/js/DataTables/Responsive-2.4.1/js/responsive.semanticui.min.js create mode 100644 public/assets/js/DataTables/RowReorder-1.3.3/css/rowReorder.bootstrap.css create mode 100644 public/assets/js/DataTables/RowReorder-1.3.3/css/rowReorder.bootstrap.min.css create mode 100644 public/assets/js/DataTables/RowReorder-1.3.3/css/rowReorder.bootstrap4.css create mode 100644 public/assets/js/DataTables/RowReorder-1.3.3/css/rowReorder.bootstrap4.min.css create mode 100644 public/assets/js/DataTables/RowReorder-1.3.3/css/rowReorder.bootstrap5.css create mode 100644 public/assets/js/DataTables/RowReorder-1.3.3/css/rowReorder.bootstrap5.min.css create mode 100644 public/assets/js/DataTables/RowReorder-1.3.3/css/rowReorder.bulma.css create mode 100644 public/assets/js/DataTables/RowReorder-1.3.3/css/rowReorder.bulma.min.css create mode 100644 public/assets/js/DataTables/RowReorder-1.3.3/css/rowReorder.dataTables.css create mode 100644 public/assets/js/DataTables/RowReorder-1.3.3/css/rowReorder.dataTables.min.css create mode 100644 public/assets/js/DataTables/RowReorder-1.3.3/css/rowReorder.foundation.css create mode 100644 public/assets/js/DataTables/RowReorder-1.3.3/css/rowReorder.foundation.min.css create mode 100644 public/assets/js/DataTables/RowReorder-1.3.3/css/rowReorder.jqueryui.css create mode 100644 public/assets/js/DataTables/RowReorder-1.3.3/css/rowReorder.jqueryui.min.css create mode 100644 public/assets/js/DataTables/RowReorder-1.3.3/css/rowReorder.semanticui.css create mode 100644 public/assets/js/DataTables/RowReorder-1.3.3/css/rowReorder.semanticui.min.css create mode 100644 public/assets/js/DataTables/RowReorder-1.3.3/js/dataTables.rowReorder.js create mode 100644 public/assets/js/DataTables/RowReorder-1.3.3/js/dataTables.rowReorder.min.js create mode 100644 public/assets/js/DataTables/RowReorder-1.3.3/js/rowReorder.bootstrap.js create mode 100644 public/assets/js/DataTables/RowReorder-1.3.3/js/rowReorder.bootstrap.min.js create mode 100644 public/assets/js/DataTables/RowReorder-1.3.3/js/rowReorder.bootstrap4.js create mode 100644 public/assets/js/DataTables/RowReorder-1.3.3/js/rowReorder.bootstrap4.min.js create mode 100644 public/assets/js/DataTables/RowReorder-1.3.3/js/rowReorder.bootstrap5.js create mode 100644 public/assets/js/DataTables/RowReorder-1.3.3/js/rowReorder.bootstrap5.min.js create mode 100644 public/assets/js/DataTables/RowReorder-1.3.3/js/rowReorder.bulma.js create mode 100644 public/assets/js/DataTables/RowReorder-1.3.3/js/rowReorder.bulma.min.js create mode 100644 public/assets/js/DataTables/RowReorder-1.3.3/js/rowReorder.dataTables.js create mode 100644 public/assets/js/DataTables/RowReorder-1.3.3/js/rowReorder.dataTables.min.js create mode 100644 public/assets/js/DataTables/RowReorder-1.3.3/js/rowReorder.foundation.js create mode 100644 public/assets/js/DataTables/RowReorder-1.3.3/js/rowReorder.foundation.min.js create mode 100644 public/assets/js/DataTables/RowReorder-1.3.3/js/rowReorder.jqueryui.js create mode 100644 public/assets/js/DataTables/RowReorder-1.3.3/js/rowReorder.jqueryui.min.js create mode 100644 public/assets/js/DataTables/RowReorder-1.3.3/js/rowReorder.semanticui.js create mode 100644 public/assets/js/DataTables/RowReorder-1.3.3/js/rowReorder.semanticui.min.js create mode 100644 public/assets/js/DataTables/Scroller-2.1.1/css/scroller.bootstrap.css create mode 100644 public/assets/js/DataTables/Scroller-2.1.1/css/scroller.bootstrap.min.css create mode 100644 public/assets/js/DataTables/Scroller-2.1.1/css/scroller.bootstrap4.css create mode 100644 public/assets/js/DataTables/Scroller-2.1.1/css/scroller.bootstrap4.min.css create mode 100644 public/assets/js/DataTables/Scroller-2.1.1/css/scroller.bootstrap5.css create mode 100644 public/assets/js/DataTables/Scroller-2.1.1/css/scroller.bootstrap5.min.css create mode 100644 public/assets/js/DataTables/Scroller-2.1.1/css/scroller.bulma.css create mode 100644 public/assets/js/DataTables/Scroller-2.1.1/css/scroller.bulma.min.css create mode 100644 public/assets/js/DataTables/Scroller-2.1.1/css/scroller.dataTables.css create mode 100644 public/assets/js/DataTables/Scroller-2.1.1/css/scroller.dataTables.min.css create mode 100644 public/assets/js/DataTables/Scroller-2.1.1/css/scroller.foundation.css create mode 100644 public/assets/js/DataTables/Scroller-2.1.1/css/scroller.foundation.min.css create mode 100644 public/assets/js/DataTables/Scroller-2.1.1/css/scroller.jqueryui.css create mode 100644 public/assets/js/DataTables/Scroller-2.1.1/css/scroller.jqueryui.min.css create mode 100644 public/assets/js/DataTables/Scroller-2.1.1/css/scroller.semanticui.css create mode 100644 public/assets/js/DataTables/Scroller-2.1.1/css/scroller.semanticui.min.css create mode 100644 public/assets/js/DataTables/Scroller-2.1.1/js/dataTables.scroller.js create mode 100644 public/assets/js/DataTables/Scroller-2.1.1/js/dataTables.scroller.min.js create mode 100644 public/assets/js/DataTables/Scroller-2.1.1/js/scroller.bootstrap.js create mode 100644 public/assets/js/DataTables/Scroller-2.1.1/js/scroller.bootstrap.min.js create mode 100644 public/assets/js/DataTables/Scroller-2.1.1/js/scroller.bootstrap4.js create mode 100644 public/assets/js/DataTables/Scroller-2.1.1/js/scroller.bootstrap4.min.js create mode 100644 public/assets/js/DataTables/Scroller-2.1.1/js/scroller.bootstrap5.js create mode 100644 public/assets/js/DataTables/Scroller-2.1.1/js/scroller.bootstrap5.min.js create mode 100644 public/assets/js/DataTables/Scroller-2.1.1/js/scroller.bulma.js create mode 100644 public/assets/js/DataTables/Scroller-2.1.1/js/scroller.bulma.min.js create mode 100644 public/assets/js/DataTables/Scroller-2.1.1/js/scroller.dataTables.js create mode 100644 public/assets/js/DataTables/Scroller-2.1.1/js/scroller.dataTables.min.js create mode 100644 public/assets/js/DataTables/Scroller-2.1.1/js/scroller.foundation.js create mode 100644 public/assets/js/DataTables/Scroller-2.1.1/js/scroller.foundation.min.js create mode 100644 public/assets/js/DataTables/Scroller-2.1.1/js/scroller.jqueryui.js create mode 100644 public/assets/js/DataTables/Scroller-2.1.1/js/scroller.jqueryui.min.js create mode 100644 public/assets/js/DataTables/Scroller-2.1.1/js/scroller.semanticui.js create mode 100644 public/assets/js/DataTables/Scroller-2.1.1/js/scroller.semanticui.min.js create mode 100644 public/assets/js/DataTables/Select-1.6.2/css/select.bootstrap.css create mode 100644 public/assets/js/DataTables/Select-1.6.2/css/select.bootstrap.min.css create mode 100644 public/assets/js/DataTables/Select-1.6.2/css/select.bootstrap4.css create mode 100644 public/assets/js/DataTables/Select-1.6.2/css/select.bootstrap4.min.css create mode 100644 public/assets/js/DataTables/Select-1.6.2/css/select.bootstrap5.css create mode 100644 public/assets/js/DataTables/Select-1.6.2/css/select.bootstrap5.min.css create mode 100644 public/assets/js/DataTables/Select-1.6.2/css/select.bulma.css create mode 100644 public/assets/js/DataTables/Select-1.6.2/css/select.bulma.min.css create mode 100644 public/assets/js/DataTables/Select-1.6.2/css/select.dataTables.css create mode 100644 public/assets/js/DataTables/Select-1.6.2/css/select.dataTables.min.css create mode 100644 public/assets/js/DataTables/Select-1.6.2/css/select.foundation.css create mode 100644 public/assets/js/DataTables/Select-1.6.2/css/select.foundation.min.css create mode 100644 public/assets/js/DataTables/Select-1.6.2/css/select.jqueryui.css create mode 100644 public/assets/js/DataTables/Select-1.6.2/css/select.jqueryui.min.css create mode 100644 public/assets/js/DataTables/Select-1.6.2/css/select.semanticui.css create mode 100644 public/assets/js/DataTables/Select-1.6.2/css/select.semanticui.min.css create mode 100644 public/assets/js/DataTables/Select-1.6.2/js/dataTables.select.js create mode 100644 public/assets/js/DataTables/Select-1.6.2/js/dataTables.select.min.js create mode 100644 public/assets/js/DataTables/Select-1.6.2/js/select.bootstrap.js create mode 100644 public/assets/js/DataTables/Select-1.6.2/js/select.bootstrap.min.js create mode 100644 public/assets/js/DataTables/Select-1.6.2/js/select.bootstrap4.js create mode 100644 public/assets/js/DataTables/Select-1.6.2/js/select.bootstrap4.min.js create mode 100644 public/assets/js/DataTables/Select-1.6.2/js/select.bootstrap5.js create mode 100644 public/assets/js/DataTables/Select-1.6.2/js/select.bootstrap5.min.js create mode 100644 public/assets/js/DataTables/Select-1.6.2/js/select.bulma.js create mode 100644 public/assets/js/DataTables/Select-1.6.2/js/select.bulma.min.js create mode 100644 public/assets/js/DataTables/Select-1.6.2/js/select.dataTables.js create mode 100644 public/assets/js/DataTables/Select-1.6.2/js/select.dataTables.min.js create mode 100644 public/assets/js/DataTables/Select-1.6.2/js/select.foundation.js create mode 100644 public/assets/js/DataTables/Select-1.6.2/js/select.foundation.min.js create mode 100644 public/assets/js/DataTables/Select-1.6.2/js/select.jqueryui.js create mode 100644 public/assets/js/DataTables/Select-1.6.2/js/select.jqueryui.min.js create mode 100644 public/assets/js/DataTables/Select-1.6.2/js/select.semanticui.js create mode 100644 public/assets/js/DataTables/Select-1.6.2/js/select.semanticui.min.js create mode 100644 public/assets/js/DataTables/StateRestore-1.2.2/css/stateRestore.bootstrap.css create mode 100644 public/assets/js/DataTables/StateRestore-1.2.2/css/stateRestore.bootstrap.min.css create mode 100644 public/assets/js/DataTables/StateRestore-1.2.2/css/stateRestore.bootstrap4.css create mode 100644 public/assets/js/DataTables/StateRestore-1.2.2/css/stateRestore.bootstrap4.min.css create mode 100644 public/assets/js/DataTables/StateRestore-1.2.2/css/stateRestore.bootstrap5.css create mode 100644 public/assets/js/DataTables/StateRestore-1.2.2/css/stateRestore.bootstrap5.min.css create mode 100644 public/assets/js/DataTables/StateRestore-1.2.2/css/stateRestore.bulma.css create mode 100644 public/assets/js/DataTables/StateRestore-1.2.2/css/stateRestore.bulma.min.css create mode 100644 public/assets/js/DataTables/StateRestore-1.2.2/css/stateRestore.dataTables.css create mode 100644 public/assets/js/DataTables/StateRestore-1.2.2/css/stateRestore.dataTables.min.css create mode 100644 public/assets/js/DataTables/StateRestore-1.2.2/css/stateRestore.foundation.css create mode 100644 public/assets/js/DataTables/StateRestore-1.2.2/css/stateRestore.foundation.min.css create mode 100644 public/assets/js/DataTables/StateRestore-1.2.2/css/stateRestore.jqueryui.css create mode 100644 public/assets/js/DataTables/StateRestore-1.2.2/css/stateRestore.jqueryui.min.css create mode 100644 public/assets/js/DataTables/StateRestore-1.2.2/css/stateRestore.semanticui.css create mode 100644 public/assets/js/DataTables/StateRestore-1.2.2/css/stateRestore.semanticui.min.css create mode 100644 public/assets/js/DataTables/StateRestore-1.2.2/js/dataTables.stateRestore.js create mode 100644 public/assets/js/DataTables/StateRestore-1.2.2/js/dataTables.stateRestore.min.js create mode 100644 public/assets/js/DataTables/StateRestore-1.2.2/js/stateRestore.bootstrap.js create mode 100644 public/assets/js/DataTables/StateRestore-1.2.2/js/stateRestore.bootstrap.min.js create mode 100644 public/assets/js/DataTables/StateRestore-1.2.2/js/stateRestore.bootstrap4.js create mode 100644 public/assets/js/DataTables/StateRestore-1.2.2/js/stateRestore.bootstrap4.min.js create mode 100644 public/assets/js/DataTables/StateRestore-1.2.2/js/stateRestore.bootstrap5.js create mode 100644 public/assets/js/DataTables/StateRestore-1.2.2/js/stateRestore.bootstrap5.min.js create mode 100644 public/assets/js/DataTables/StateRestore-1.2.2/js/stateRestore.bulma.js create mode 100644 public/assets/js/DataTables/StateRestore-1.2.2/js/stateRestore.bulma.min.js create mode 100644 public/assets/js/DataTables/StateRestore-1.2.2/js/stateRestore.dataTables.js create mode 100644 public/assets/js/DataTables/StateRestore-1.2.2/js/stateRestore.dataTables.min.js create mode 100644 public/assets/js/DataTables/StateRestore-1.2.2/js/stateRestore.foundation.js create mode 100644 public/assets/js/DataTables/StateRestore-1.2.2/js/stateRestore.foundation.min.js create mode 100644 public/assets/js/DataTables/StateRestore-1.2.2/js/stateRestore.jqueryui.js create mode 100644 public/assets/js/DataTables/StateRestore-1.2.2/js/stateRestore.jqueryui.min.js create mode 100644 public/assets/js/DataTables/StateRestore-1.2.2/js/stateRestore.semanticui.js create mode 100644 public/assets/js/DataTables/StateRestore-1.2.2/js/stateRestore.semanticui.min.js create mode 100644 public/assets/js/DataTables/datatables.css create mode 100644 public/assets/js/DataTables/datatables.js create mode 100644 public/assets/js/DataTables/datatables.min.css create mode 100644 public/assets/js/DataTables/datatables.min.js create mode 100644 public/assets/js/DataTables/pdfmake-0.1.36/pdfmake.js create mode 100644 public/assets/js/DataTables/pdfmake-0.1.36/pdfmake.min.js create mode 100644 public/assets/js/DataTables/pdfmake-0.1.36/vfs_fonts.js create mode 100644 public/assets/js/admin/categories.js create mode 100644 public/assets/js/admin/edit_lesson.js create mode 100644 public/assets/js/admin/edit_page.js create mode 100644 public/assets/js/admin/files.js create mode 100644 public/assets/js/admin/lessons.js create mode 100644 public/assets/js/admin/pages.js create mode 100644 public/assets/js/admin/panel.js create mode 100644 public/assets/js/admin/privileges.js create mode 100644 public/assets/js/chart.js/Plugin.Subtitle.js create mode 100644 public/assets/js/chart.js/chart.min.js create mode 100644 public/assets/js/chart.js/chartjs-plugin-annotation.js create mode 100644 public/assets/js/chart.js/chartjs-plugin-datalabels.min.js create mode 100644 public/assets/js/classroom.js create mode 100644 public/assets/js/cookie.js create mode 100644 public/assets/js/edit-post.js create mode 100644 public/assets/js/lesson-features.js create mode 100644 public/assets/js/manage-category.js create mode 100644 public/assets/js/manage-posts.js create mode 100644 public/assets/js/manage-tags.js create mode 100644 public/assets/js/marked/marked.cjs create mode 100644 public/assets/js/marked/marked.esm.js create mode 100644 public/assets/js/marked/marked.min.js create mode 100644 public/assets/js/marked/marked.umd.js create mode 100644 public/assets/js/pdfmake/fonts/Roboto.js create mode 100644 public/assets/js/pdfmake/fonts/Roboto/Roboto-Italic.ttf create mode 100644 public/assets/js/pdfmake/fonts/Roboto/Roboto-Medium.ttf create mode 100644 public/assets/js/pdfmake/fonts/Roboto/Roboto-MediumItalic.ttf create mode 100644 public/assets/js/pdfmake/fonts/Roboto/Roboto-Regular.ttf create mode 100644 public/assets/js/pdfmake/pdfmake.js create mode 100644 public/assets/js/pdfmake/pdfmake.js.map create mode 100644 public/assets/js/pdfmake/pdfmake.min.js create mode 100644 public/assets/js/pdfmake/pdfmake.min.js.map create mode 100644 public/assets/js/pdfmake/standard-fonts/Courier.js create mode 100644 public/assets/js/pdfmake/standard-fonts/Helvetica.js create mode 100644 public/assets/js/pdfmake/standard-fonts/Symbol.js create mode 100644 public/assets/js/pdfmake/standard-fonts/Times.js create mode 100644 public/assets/js/pdfmake/standard-fonts/ZapfDingbats.js create mode 100644 public/assets/js/pdfmake/vfs_fonts.js create mode 100644 public/assets/js/select2/css/select2_4.0.6.min.css create mode 100644 public/assets/js/select2/i18n/el.js create mode 100644 public/assets/js/select2/js/select2_4.0.6.min.js create mode 100644 public/favicon.ico create mode 100644 public/files/.gitkeep create mode 100644 public/index.php create mode 100644 sceleton.txt create mode 100755 tests/abstract.php create mode 100755 tests/algo-hashes.php create mode 100755 tests/auth.php create mode 100755 tests/code/abstract.php create mode 100755 tests/code/bits.php create mode 100755 tests/code/echo.php create mode 100755 tests/code/exceptions.php create mode 100755 tests/code/interface.php create mode 100755 tests/code/login.php create mode 100644 tests/code/menu.php create mode 100755 tests/code/proxy.php create mode 100755 tests/code/session.php create mode 100755 tests/code/str-replace.php create mode 100755 tests/compare/compress.php create mode 100755 tests/compare/hashcost.php create mode 100755 tests/compare/md5-v-sha1.php create mode 100755 tests/compare/memcached-v-filecache.php create mode 100755 tests/compare/redis-v-filecache.php create mode 100755 tests/compare/serialize-v-jsonencode.php create mode 100755 tests/compare/sha1-v-sha256.php create mode 100755 tests/dotnet-pass.php create mode 100755 tests/mail.php create mode 100755 tests/php.php create mode 100755 tests/repository.php create mode 100755 tests/service/entropy.php create mode 100755 tests/service/lorem-md.php create mode 100644 tests/service/mailjet.php create mode 100755 tests/service/memcached.php create mode 100755 tests/service/redis.php create mode 100755 tests/userman.php diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..adb4c79 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,167 @@ +FROM php:8.1-apache as builder + +# ARG MODE="PRODUCTION" +ARG MODE="debug" + + +# mailhog (fake mail) configuration +# ------------------------------------------------------------------------------ +# RUN apt-get update &&\ +# apt-get install --no-install-recommends --assume-yes --quiet ca-certificates curl git &&\ +# rm -rf /var/lib/apt/lists/* +# RUN curl -Lsf 'https://storage.googleapis.com/golang/go1.8.3.linux-amd64.tar.gz' | tar -C '/usr/local' -xvzf - +# ENV PATH /usr/local/go/bin:$PATH +# RUN go get github.com/mailhog/mhsendmail +# RUN cp /root/go/bin/mhsendmail /usr/bin/mhsendmail +# RUN echo 'sendmail_path = /usr/bin/mhsendmail --smtp-addr mailhog:1025' > /usr/local/etc/php/php.ini + + +# msmtp (real mail) configuration +# ------------------------------------------------------------------------------ +# RUN apt-get update && apt-get install -y msmtp && apt-get clean && rm -r /var/lib/apt/lists/* +# configure sendmail +# RUN { \ +# echo "php_admin_value[sendmail_path] = $(which msmtp) -ti "; \ +# } >> /usr/local/etc/php-fpm.d/www.conf + + + + + +# setup apache +# ------------------------------------------------------------------------------ + +# enable apache mod_rewrite and headers +RUN a2enmod rewrite +RUN a2enmod headers + +# install pdo and mysql for php +RUN docker-php-ext-install pdo pdo_mysql mysqli + +# copy application code +# ------------------------------------------------------------------------------ + +# Copy local code to the container image. +COPY public /var/www/public + +# Copy core files behind public +COPY core /var/www/core + +# Copy storage folder structure behind public +COPY storage /var/www/storage + +# Copy configuration files for the container (application based) +COPY docker /var/www/docker +COPY tests /var/www/tests +COPY composer.json /var/www/ + + +# Install Composer +# ------------------------------------------------------------------------------ + +# Install Composer with required libraries +RUN apt-get update --fix-missing +RUN DEBIAN_FRONTEND=noninteractive apt-get install git unzip wget -y +RUN curl -sS https://getcomposer.org/installer -o composer-setup.php +RUN php composer-setup.php --install-dir=/usr/local/bin --filename=composer +RUN rm composer-setup.php + + +# Add Greek locales to server +# ------------------------------------------------------------------------------ +WORKDIR / +RUN apt-get install -y locales +RUN sed -i 's/^# *\(el_GR\)/\1/' /etc/locale.gen +RUN locale-gen + + +# Manage permitions in specific (writable) directories +# ------------------------------------------------------------------------------ +# (make local storage directories writable) +RUN chown -R www-data:www-data /var/www +RUN chmod -R 757 /var/www/storage +RUN chown -R www-data:www-data /var/www/public/files +RUN chmod -R 757 /var/www/public/files + +# change apache's default-root to /var/www/public +RUN sed -ri -e 's!/var/www/html!/var/www/public!g' /etc/apache2/sites-available/*.conf + + +# Use the PORT environment variable in Apache configuration files. +RUN sed -i 's/80/${PORT}/g' /etc/apache2/sites-available/000-default.conf /etc/apache2/ports.conf + +# Override with custom settings +# TODO: COPY config/php-ini-overrides.ini $PHP_INI_DIR/conf.d/ + + +# Instal Memcached +# ------------------------------------------------------------------------------ +# RUN apt-get update && apt-get install -y libmemcached-dev zlib1g-dev && pecl install memcached-3.2.0 && docker-php-ext-enable memcached +# CMD ["/usr/sbin/apache2ctl", "-D", "FOREGROUND"] + + +# Install and setup cron +# ------------------------------------------------------------------------------ +# RUN apt-get update && apt-get install cron -y +# RUN crontab -l | { cat; echo "10 * * * * /usr/local/bin/php /var/www/core/cli/cache-categories.php"; } | crontab - +# or? RUN crontab -l | { cat; echo "10 * * * * php /var/www/core/cli/cache-categories.php"; } | crontab - +# ... or copy cronjobs file into/as /etc/cron.d/cron ; then RUN crontab /etc/cron.d/cron +# ... do that while on entrypoint +# (+) run once after ex. 4 minutes + + + +# Install XDebug (only for develop/debud modes) +# ------------------------------------------------------------------------------ +RUN apt-get install -y apt-utils \ + && pecl install xdebug \ + && docker-php-ext-enable xdebug + +COPY docker/config/php-ext-xdebug.ini /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini +RUN touch /usr/local/etc/php/xdebug.log +RUN chown www-data:www-data /usr/local/etc/php/xdebug.log +RUN chmod 664 /usr/local/etc/php/xdebug.log + + + +# Prepare the entrypoint as in the original image +# ------------------------------------------------------------------------------ +RUN cp /var/www/docker/bin/docker-entrypoint.sh /usr/local/bin +RUN chmod a+x /usr/local/bin/docker-entrypoint.sh + +ENTRYPOINT [] + + +# TODO: +## Enable OPcache and JIT on production +# ------------------------------------------------------------------------------ +## set environemt variable + +# RUN load OPcache and JIT from script +### if [ "${MODE}" = "PRODUCTION" ]; then +### docker-php-ext-install opcache +### docker-php-ext-install opcache +### mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini" + +### chk: https://stackoverflow.com/questions/68308209/docker-compose-failed-to-solve-rpc-error-code-unknown-desc-failed-to-comp + +# TODO: +# Enable JIT + +### fi + + + +# TODO: +## Enable OPcache and JIT on production +# ------------------------------------------------------------------------------ +### optimize.sh +### #!/bin/bash -x +### +### if test ["$1" == "PRODUCTION"] ; then +### docker-php-ext-install opcache +### docker-php-ext-install opcache +### install and enable JIT +### else +### maybe install some dev-tool +### fi diff --git a/README.md b/README.md new file mode 100644 index 0000000..8e7a43c --- /dev/null +++ b/README.md @@ -0,0 +1,191 @@ +# x-anom +(another MVC framework) + + + +## pre-Check + +Update ```~./container/config/composer-copy.json``` according to +```~./composer.json``` or vice-versa. You actualy need just one of them; +if pulling the project into a server (bare-metar/vps/shared etc) you only +need the first; if pulling into container you need the second/ + + +On a Linux system you can also create a link from the one file to the other +(then you need to use ralative paths on autoload.classmap section). + +When deploying on PRODUCTION make sure that all development routes are deleted +and debuging messages are disabled. + + + + +## Build + + docker build -t x-anom . + +Force build without using cache: + + docker build --no-cache -t x-anom . + + + + +## Run + + docker-compose up + +Force rebuild; then run: + + docker-compose up --build + + + + +## Remove builded containers + + docker-compose down + + + + +## While developing... + +By default autoloading is handled by Composer (this is highly recommended). +While developing your application (or the framework itself) you will need to +update the autoloading of your new classes. To do so, attach a shell, change +to ```/var/www``` directory and run ```composer dump-autoload``` + + cd /var/www + + composer dump-autoload + +If Composer is not an option and you use the framework's autoload implementation +(enabling ```define('AUTOLOADER' , '../core/helpers/autoload.php');```), +do not forger to update the (new) paths to your new classes in the ```CLASSPATHS``` +array . Both ```AUTOLOADER``` and ```CLASSPATHS``` definitions can be found +into ```core/confing/anom_settings.php``` file. + + + + +## XDebug + +anom comes with most configuration of XDebug ready; all you need is + +1. on Doockerfile: uncoomment the commands under XDebug section + +2. on docker-compose.yml: uncomment the services.web.extra_hosts section + +3. you may need configure your ide to listen onto XDebuger host port + +example: +for vscode you need to add a ~./.vscode/launch.json file like this: + + { + "version": "0.2.0", + "configurations": [ + { + "name": "Listen for Xdebug", + "type": "php", + "request": "launch", + "port": 9003, + "pathMappings": { + "/var/www/public": "${workspaceRoot}/public" + } + } + ] + } + + + + +## Customization + +The project includes several almost-ready-to-use technologies; +You can enable / customize / disable / configure all these with a few edits. + +Each time you change the configuration of your environment, do not forget to set the +related parameters on the main configuration file (```/core/config/anom_settings.php)``` +and/or define environmental variables or credentials to the ```/core/auth/.env``` file. + +NOTE: #1 +We assume that you use composer; if not, then whatever composer does you have to +make it manually (copy dependency files and setup the autoloader's paths). + +NOTE: #2 +There is no need to use both Redis and Memcached, thus I have not test if these +two caching technologies will work together using the proposed comfiguration. + + + +#### Redis + +To enable Redis... + +Onto docker-compose.yml: + +1. Uncomment the lines of 'redis' section; Your Redis-server host name will be the one that is specified in the container_name field (default: redis) + +2. Uncomment the networks subsections; web-server and Redis-server will communicate through a bridge network + +3. With composer: + +Include predis/redis into your project's requirements using ```/docker/config/composer-redis.json``` +as your composer.json file -or- install it via: + + composer require predis/predis + + + +#### Memcached + +To host the Memcached service: + +1. Onto docker-compose.yml: uncomment the lines of 'memcached' section; Memcached-server host name will be the one that is specified in the container_name field (default: anomemcached) + +2. Onto Dockerfile: uncomment the lines under subsection 'Install Memcached' + + + +* TODO: Maria database + + +* TODO: Adminer +(https://stackoverflow.com/questions/33631085/how-to-use-adminer-with-composer-autoload-php) + + +* TODO: phpMyAdmin + + +* Apache + + +* Opcache and JIT + + +TODO: + +## Staging + +#### Build + +#### Deploy + + +## Production + +### Build + +### Deploy + +! Run the above commands ONLY when you need to manually deploy the app to Cloud Run, otherwise everything is automated using Cloud Builds + + + + +## Notes + +* '~./' refers to the root of the project + +* Furhter documentation about the framework can be found in ~./core/README.md diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..22b1d93 --- /dev/null +++ b/composer.json @@ -0,0 +1,12 @@ +{ + "require": { + "phpmailer/phpmailer": "^6.8", + "erusev/parsedown": "^1.7" + }, + "autoload": { + "classmap": [ + "public/app", + "core/classes" + ] + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..a48cbdf --- /dev/null +++ b/composer.lock @@ -0,0 +1,149 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "da78e21983526d7429f27126efb61469", + "packages": [ + { + "name": "erusev/parsedown", + "version": "1.7.4", + "source": { + "type": "git", + "url": "https://github.com/erusev/parsedown.git", + "reference": "cb17b6477dfff935958ba01325f2e8a2bfa6dab3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/erusev/parsedown/zipball/cb17b6477dfff935958ba01325f2e8a2bfa6dab3", + "reference": "cb17b6477dfff935958ba01325f2e8a2bfa6dab3", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35" + }, + "type": "library", + "autoload": { + "psr-0": { + "Parsedown": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Emanuil Rusev", + "email": "hello@erusev.com", + "homepage": "http://erusev.com" + } + ], + "description": "Parser for Markdown.", + "homepage": "http://parsedown.org", + "keywords": [ + "markdown", + "parser" + ], + "support": { + "issues": "https://github.com/erusev/parsedown/issues", + "source": "https://github.com/erusev/parsedown/tree/1.7.x" + }, + "time": "2019-12-30T22:54:17+00:00" + }, + { + "name": "phpmailer/phpmailer", + "version": "v6.8.0", + "source": { + "type": "git", + "url": "https://github.com/PHPMailer/PHPMailer.git", + "reference": "df16b615e371d81fb79e506277faea67a1be18f1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/df16b615e371d81fb79e506277faea67a1be18f1", + "reference": "df16b615e371d81fb79e506277faea67a1be18f1", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-filter": "*", + "ext-hash": "*", + "php": ">=5.5.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.7.2", + "doctrine/annotations": "^1.2.6 || ^1.13.3", + "php-parallel-lint/php-console-highlighter": "^1.0.0", + "php-parallel-lint/php-parallel-lint": "^1.3.2", + "phpcompatibility/php-compatibility": "^9.3.5", + "roave/security-advisories": "dev-latest", + "squizlabs/php_codesniffer": "^3.7.1", + "yoast/phpunit-polyfills": "^1.0.4" + }, + "suggest": { + "ext-mbstring": "Needed to send email in multibyte encoding charset or decode encoded addresses", + "ext-openssl": "Needed for secure SMTP sending and DKIM signing", + "greew/oauth2-azure-provider": "Needed for Microsoft Azure XOAUTH2 authentication", + "hayageek/oauth2-yahoo": "Needed for Yahoo XOAUTH2 authentication", + "league/oauth2-google": "Needed for Google XOAUTH2 authentication", + "psr/log": "For optional PSR-3 debug logging", + "symfony/polyfill-mbstring": "To support UTF-8 if the Mbstring PHP extension is not enabled (^1.2)", + "thenetworg/oauth2-azure": "Needed for Microsoft XOAUTH2 authentication" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPMailer\\PHPMailer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1-only" + ], + "authors": [ + { + "name": "Marcus Bointon", + "email": "phpmailer@synchromedia.co.uk" + }, + { + "name": "Jim Jagielski", + "email": "jimjag@gmail.com" + }, + { + "name": "Andy Prevost", + "email": "codeworxtech@users.sourceforge.net" + }, + { + "name": "Brent R. Matzelle" + } + ], + "description": "PHPMailer is a full-featured email creation and transfer class for PHP", + "support": { + "issues": "https://github.com/PHPMailer/PHPMailer/issues", + "source": "https://github.com/PHPMailer/PHPMailer/tree/v6.8.0" + }, + "funding": [ + { + "url": "https://github.com/Synchro", + "type": "github" + } + ], + "time": "2023-03-06T14:43:22+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [], + "plugin-api-version": "2.3.0" +} diff --git a/core/README.md b/core/README.md new file mode 100644 index 0000000..8dee940 --- /dev/null +++ b/core/README.md @@ -0,0 +1,266 @@ +## x-anom +### X is ANOther Mvc + +x-anom is an Object-Oriented MVC php-framework. + +Main advantages of the framework: + +* It is super-light and fast; +* Core has almost zero dependences and contains almost anything you need to start and optimize a server-based web-application. +* It is extensible especialy if you use Composer (which is recommended, though not required) +* Handles security; and as long as you write code using secure practices, it will be secure +* It's easy to configure, easy to code, easy to use + + + +### Requirements + +* PHP 8+ +* Some webserver (Apache/Nginx) +* Some SQL server (MySQL/MariaDB/Postgress) +* Basic knowledge of php and SQL + + + + +### What is included + +Core components: + + +#### Router + +Routes request to the appropriate Controller +- matches static paths +- matches dynamic paths through regex expressions +- takes care of request-method + + +#### Controller and Model + +(just write your Controller and Model classes) + + +#### View (rendering engine) + +Usually you just need to call the render_view(template, data) function. +For templating we use the php short-tag syntax. + +main functions: + +* load_template( template, data) + +* render_view( view_file , data ) + +* load_asset( type, assets_array ) + +* set_headers( content_type, ttl, more_array ) + +* render_text( data, content_type, ttl ) + +* reply_json( data, ttl ) + + +#### Class autoloader + +Composer is recomende; +but if is not available this one will do the job + + +#### Caching + +* File Caching mechanism + +* Redis Cache + +* Memcached Caching + + +#### Session Management + +* DefaultSession: psevdo-handler with passthrough methods + +* FileSession: custom file-based session handler + +* DatabaseSession: database session handler + + + +#### More + +* Proxy design pattern + +* Error handling + +* Repository + +* Cli interface (*summarizes anom*) + + + + + +### Other features + +* Docker ready + +You can run the framework as is in a Docker environment; +Also. you can edit just a bit the configuration files to customize the project +to support various technologies; Check the README file in the project's root +folder to find out more. + + + +### What is not included + +* It lacks a query builder (SQL is easy and very powerful); do not forget to use prepared statements for all your SQL queries. + +* TODO: User-Role based administration class. + +* TODO: Shoping-Cart class. + + + +## Life (and death) of a Client_Request–Server_reply session + +1. THE REQUEST: client makes a request -> request arives to the server -> .htaccess sends the request to the index.php + +2. index loads Configuration (constants.php + config.php) and AUTOLOADER in order load classes easily + +3. index loads init.php -> after initialization the APP is READY -> index loads the routes; Route::run() -> the APP is RUNNING + +4. Router resolves the request pattern and calls a Controller + +5. (if needed) Controller asks data from the Model; then responds a reply to the client using **render** functions -> APP dies + + + +## Direcrtory structure + +NOTE: directory structure needs updata, but the main structure ramains untouched. + + . + |-- container * for docker/container configuration + | |-- bin * scripts + | `-- config * settings + | + |-- core * core code + | |-- auth * authentication and credentials + | | + | |-- config * application parametres + | | |-- config.php + | | |-- constants.php + | | |-- credentials.php + | | `-- init.php + | | + | |-- classes * core classes + | | |-- cacher + | | |-- Benchmark.php + | | |-- Database.php + | | |-- Route.php + | | |-- Security.php + | | `-- Session.php + | | + | `-- helpers * core helpers + | |-- autoload.php + | |-- error-handling.php + | `-- render.php + | + |-- data * folder for batch data-imports to database + | + |-- public ** PUBLIC directory + | |-- app * APP + | | |-- Controllers * Controllers + | | | |-- Art.php + | | | `-- ... + | | | + | | |-- Models * Models + | | | |-- Art_model.php + | | | `-- ... + | | | + | | |-- Views * Views + | | | |-- group.php + | | | `-- item.php + | | | + | | `-- routes.php * application routes + | | + | `-- cache * Caching folder + | + `-- vendor * Vendor classes and autoloader + |-- composer + `-- ... + + + + +## Naming Conventions and good practices + +1. Keep Controllers, Models, Views in their folders + +2. Organize View elements in subfolders + +3. Controller and Model names shall be camelcased; + - fist letter should be Uppercase; + - classes shall be named exactly as their filenames + +4. Model names shall be suffixed with _model + +5. Comment every-single Controller/Model/class and comment every method + +6. On Views (php templates) use php short-tags when possible + - Views are about rendering data; avoid complex-logic + - [if/else], [foreach] and some flag/temp [variables] sould be fair enough + +7. All of the above rules are strongly recommended (although not obligatory); + + + +## Notes and brainstorming + +### for template system check + https://css-tricks.com/php-is-a-ok-for-templating/ + +### for rendering system you may check volt too + https://docs.phalcon.io/4.0/en/volt + + + +## Brainstorming + +* take care of various hacks + chk: https://stackoverflow.com/questions/1996122/how-to-prevent-xss-with-html-php + +* HTML to MarkDown! + chk: https://github.com/thephpleague/html-to-markdown + + + + + +VIEW data + +-> ceo -> title + -> description + -> keywords + -> ... + +-> content -> view : view_filename + -> key : some_variable_name + -> data : the_data + + + + +## Knowledge Requirements + +## Tools of work + +None of the following tools is necessary, but they will help very very-much. + +- Composer + +- Docker + +- Code editor + +- Coffee + Nicotine diff --git a/core/auth/.gitkeep b/core/auth/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/core/auth/env.example b/core/auth/env.example new file mode 100644 index 0000000..fd47dd8 --- /dev/null +++ b/core/auth/env.example @@ -0,0 +1,36 @@ +ENVIRONMENT='development' + +# MySQL connection +# --- -- -- - - - +DB_NAME= +DB_USER= +DB_PASS= +PDO_HOST= + +# mailhog +# == testting/fake email service +# --- -- -- - - - +# MAIL_HOST=mailhog_server +# MAIL_PORT=1025 +# MAIL_USERNAME= +# MAIL_PASSWORD= +# MAIL_ENCRYPTION= + +# SMTP setup +# == read mail server +# --- -- -- - - - +MAIL_MAILER=smtp +MAIL_HOST= +MAIL_PORT=465 +MAIL_USERNAME= +MAIL_PASSWORD= +MAIL_ENCRYPTION=ssl +NO_REPLY_EMAIL=noreply@... +REPLY_TO_EMAIL=webmaster@r... +MAIL_FROM_NAME=Classroom + +# redis cache server +# --- -- -- - - - +# REDIS_HOST=redis +# REDIS_PASSWORD= +# REDIS_PORT=6379 \ No newline at end of file diff --git a/core/classes/Benchmark.php b/core/classes/Benchmark.php new file mode 100644 index 0000000..a7e93ea --- /dev/null +++ b/core/classes/Benchmark.php @@ -0,0 +1,150 @@ +'; + $close = '

'; + $divider = ":"; + $prefix = '
'; + $suffix = '
'; + break; + case 'code': + $open = ''; + $close = ''; + $divider = ": "; + $prefix = '
';
+                    $suffix = '
'; + break; + case 'comment': + default: + $open = ''; + $divider = ":"; + $prefix = ''; + $suffix = ''; + break; + } + + echo $prefix; + + // TIME REPORT + // --------------------------------------------------------------------- + echo "\n\n{$open} Timings {$close}"; + + // valid from php 7.3 + // $start = self::$timeSpots[ array_key_first(self::$timespots) ]; + // $end = self::$timeSpots[ array_key_last(self::$timespots) ]; + $start = self::$timeSpots['start']; + $end = self::$timeSpots['end']; + $all_dt = number_format($end - $start , 4)*1000 ." ms"; + + + + // if more than 2 timespots + // echo dt between each spot + if (count(self::$timeSpots) > 2) { + + // calculate dt between spots + $time_results = array(); + $prev_key = ''; + $prev_time = 0; + foreach(self::$timeSpots as $key => $t) { + if (!$prev_time) { + $prev_time = $t; + $prev_key = $key; + } + else { + $dt = number_format($t - $prev_time , 4)*1000 ." ms"; + $time_results[] = array( + 'part' => "{$prev_key}[..{$key}]", ///$key, + 'dt' => $dt + ); + $prev_key = $key; //// "{$prev_key}[..{$key}]"; + $prev_time = $t; + } + } + + // echo timings + foreach ($time_results as $key => $val) { + echo "\n\t{$open} {$val['part']} {$divider} {$val['dt']} {$close}"; + } + } + + // echo total time + echo "\n\t{$open} total {$divider} {$all_dt} {$close}"; + + + // MEMORY REPORT + // --------------------------------------------------------------------- + echo "\n\n{$open} Memory Usage {$close}"; + + if (count(self::$memoryUse) > 2) { + + foreach (self::$memoryUse as $key => $val) { + echo "\n\t{$open} {$key} {$divider} {$val}MB {$close}"; + } + + } else { + echo "\n\t{$open} memory usage {$divider} ". round(memory_get_usage()/(1024*1024),2) ."MB {$close}"; + } + + echo $suffix; + + } + + } +} diff --git a/core/classes/Cart.php b/core/classes/Cart.php new file mode 100644 index 0000000..4e6f585 --- /dev/null +++ b/core/classes/Cart.php @@ -0,0 +1,71 @@ +uid], 10) + // + // oranize in results = [ + // { + // oid, + // prods: [ + // { + // pid, + // prodlabel, + // count + // }, + // ... + // ] + // }, + // ..., + // ] + // + // return $results + + } +} + + + +/* --- +orders: + : id + : uid (user id) + +order_products + : id + : oid (order id) + : pid (product id) + -- */ \ No newline at end of file diff --git a/core/classes/Database.php b/core/classes/Database.php new file mode 100644 index 0000000..8cac479 --- /dev/null +++ b/core/classes/Database.php @@ -0,0 +1,333 @@ +throw_errors = PRODUCTION ? false : true; + + if (null == $this->connection) { + try { + $this->connection = new PDO( + "mysql:" . PDO_HOST . ";" . "dbname=" . DB_NAME, + DB_USER, + DB_PASS, + array( + PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES 'utf8'", + PDO::MYSQL_ATTR_LOCAL_INFILE => true + ) + ); + + // handle error reporting + if ($this->throw_errors) { + $this->connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + } + $this->setTimezone(); + + } catch (PDOException $exc) { handle_exception($exc); } + } + # return $this->connection; + } + + + /** disconnect + * CHECK: not sure if needed + */ + public function disconnect() + { + $this->connection = null; + } + + + /** set Timezone + * (Self-explanatory) + */ + public function setTimezone($timezone = DB_TIMEZONE) { + // $this->connection->prepare($timezone)->execute(); + } + + + /** lastInsertID + * returns the ID of last inserted record + */ + public function lastInsertID() + { + return $this->connection->lastInsertId(); + } + + + /** query + * --- + * set and execute a query safely; + * save results as associative array; DO NOT RETURN RESULTS + * + * @param $sql (string): SQL query + * @param $args (array): array of values to bind into SQL + * @param $pypass (boolean): flag to bypass security check + * + * @return $this (database handler) + */ + public function query($sql, $args=[]) + { + try { + + $stmt = $this->connection->prepare($sql); + + if ($args == []) { + $result = $stmt->execute(); + + } else { + $result = $stmt->execute($args); + + } + + $result = $stmt->fetchAll(PDO::FETCH_ASSOC); + + $this->result = $result; + + return $this; + + } catch (PDOException $exc) { handle_exception($exc); } + + } + + + /** getAll + * --- + * return all resulted records + * use it after db->query(); + */ + public function getAll() + { + return ($this->result === null) ? false : $this->result; + } + + + /** getFirst + * --- + * get first row of the resulted query; + * used when one row is expected + * ex. $db->query('SELECT * FROM users WHERE id = :id',['id'=>1])->getFirst(); + */ + public function getFirst() + { + if (($this->result === null) || ($this->result == [])) { + return false; + + } else { return $this->result[0]; } + } + + + /** getOnly + * --- + * return the first column value of the first row + * used when only one value is needed + * ex. $db->query('SELECT Count(id) FROM table',[])->getOnly(); + */ + public function getOnly() + { + if (($this->result === null) || ($this->result == [])) { + return false; + + } else { return array_values($this->result[0])[0]; } + } + + + /** runQuery + * --- (shortcut method) + * execute a query safely; + * return results as associative array + * @param $sql (string): SQL query + * @param $args (array): array of values to bind into SQL + * @param $pypass (boolean): flag to bypass security check + */ + public function runQuery($sql, $args=[]) + { + return $this->query($sql, $args)->getAll(); + } + + + /** runLimitQuery( sql, args, limit=100, offset = null ) + * set LIMIT / OFFSET clauses in a secure way + * @param $sql (string): SQL query + * @param $args (array): array of values to bind into SQL + * @param $limit (int): LIMIT number + * @param $offset (int): OFFSET number + */ + public function runLimitQuery($sql, $args, $limit = 100, $offset = null) + { + $limitStr = $offsetStr = ""; + + // construct LIMIT clause + if (is_int($limit)) { + $limitStr = " LIMIT {$limit}"; + + // construct OFFSET clause (when a LIMIT pre-exists) + if (is_int($offset)) { + $offsetStr =" OFFSET {$offset}"; + } + } + + $sql = $sql . $limitStr . $offsetStr; + + return $this->runQuery($sql, $args); + } + + + /** insert + * @param $table (string): name of table + * @param $values: an associative of (fieldName => value) pairs + * + * example call: + * --- + * $db->insert('products', + * [ + * 'title' => 'My Dark Chocolate 200g', + * 'text' => 'Lorem ipsum dolor sit amet, consectetur adipiscing elit ...', + * 'isFood' => 1, + * 'isToxic' => 0 + * ] + * ); + * + * ...which prepares the SQL query: + * INSERT INTO products (title, text, isFood, isToxic) + * VALUES (:title, :text, :isFood, :isToxic) + * + * ...and injects the values: [ :title => 'My Dark Chocolate 200g' , ... ] + */ + public function insert($table, array $values) + { + $fieldSets = []; + $valueSets = []; + $bindSets = []; + + foreach($values as $key => $val) { + $fieldSets[] = $key; + $valueSets[] =':'. $key; + $bindSets[':'. $key] = $val; + } + + $sql = "INSERT INTO {$table} (". implode(', ', $fieldSets) .") + VALUES (". implode(', ', $valueSets) .")"; + + return $this->runQuery($sql, $bindSets); + } + + + /** update + * @param $table (string): name of table + * @param $values: an associative of (fieldName => value) pairs + * @param $id: an associative of (fieldName => value) index fields + * + * example call: + * --- + * $db->update('products', + * [ 'title' => 'My Chocolate','isFood' => 1 ], + * [ 'id' => 123 ] + * ); + * + * ...which prepares the SQL query: + * UPDATE products SET `title` = :title, `isFood` = :isFood WHERE id = :id + * + * ...and injects: [':title'=> 'My Chocolate' , ':isFood'=> 1 , ':id'=> 123] + */ + public function update( $table, array $values, array $identity) + { + $fieldSets = []; // array of field names + $idSets = []; // array of data-holders + $bindSets = []; // array of data-bindings + + foreach($values as $key => $val) { + $fieldSets[] = "{$key} = :{$key}"; + $bindSets[':'. $key] = $val; + } + + foreach($identity as $key => $val) { + $idSets = "{$key} = :{$key}"; + $bindSets[':'. $key] = $val; + } + + $sql = "UPDATE {$table} SET ". implode(', ', $fieldSets) + ." WHERE ". implode(" AND ", $idSets); + + return $this->runQuery($sql, $bindsArray); + } + + + /** multiInsert( table, fields , values ) + * Construct a multiple-insert clause + * + * @param $table (string): name of table + * @param $fields (array): array with field-names + * @param $values (array): array of value-arrays + * + * example call: + * --- + * $db->multiInsert('order_products', + * [ 'orderID', 'productID', 'unitPrice', 'quantity', 'note' ], + * [ + * [ 124, 102030, 1.25, 5, '' ], + * [ 124, 102040, 10.50, 2, 'some note about product #102040' ], + * [ 124, 102050, 7.20, 3, '' ], + * [ 124, 102060, 3.25, 1, 'some other note' ] + * ] + * ); + */ + public function multiInsert($table, array $fieldsArray, array $valuesArray) + { + if (count($fieldsArray) != count($valuesArray[0])) { + throw new Exception('Fields and value arrays don\'t match.'); + } + + // setup fieldsSet + // ex. "(Title, Price, Status)" + $fieldsSet = ' (`'. implode( + '`, `', // make sure fieldnames are not SQL-bound terms + str_replace('`', '', $fieldsArray) // clean fieldnames + ) .'`) '; + + // setup holders array and bind-values array + // ex. "(:Title1, :Price1, :Status1), (:Title2, :Price2, :Status2), ...", + $holdersArray = []; + $bindsArray = []; + $counter = 1; + foreach($valuesArray as $key => $rowArray) { + $rowHolders = []; + + foreach($itemArray as $key => $val) { + $rowHolders = ':'. $fieldsArray[$key] . $counter; + $bindsArray[ ':'. $fieldsArray[$key] . $counter ] = $val; + } + $holdersArray[] = '('. implode(', ', $rowHolders ) .')'; + $counter++; + } + + $sql = "INSERT INTO {$table}" . $fieldsSet + . ' VALUES '. impload(', ', $holdersArray); + + return $this->runQuery($sql, $bindsArray); + } + +} diff --git a/core/classes/Registry.php b/core/classes/Registry.php new file mode 100644 index 0000000..047fd54 --- /dev/null +++ b/core/classes/Registry.php @@ -0,0 +1,135 @@ +"; + } + + } + + /** parse_sections + * --- + * parse a group of views (sections) + * @param $sections: an array of sections + * + * each section group is an array with view, key and data properties + * + view: defines the view template/file + * + key: the variable name that view uses to parse data OR empty-string* + * * if key is an empty then $data should be an array (which + * includes all [variable-name:data] pairs utilized by the view) + * + data: holds the actual data + */ + + public static function sections($sections) { + + foreach($sections as $sect) { + + if ($sect['key'] == '') { + self::view( $sect['view'], $sect['data'] ); + + } else { + self::view( $sect['view'], [ $sect['key'] => $sect['data']] ); + } + } + } + + + /** render function + * --- + * uses php's short-tag syntax for templating system + * extract data into template + * @param $view: view-template filename + * @param $data: data to embed into view-template + * @param $sanitize: of true then sanitize data. + * important NOTE: data is an array [key => value] + */ + public static function view($view, $data=[], $sanitize = false) { + + $file = VIEWS_DIRECTORY . $view . '.php'; + + if (file_exists($file)) { + if (!defined('OUTPUT_STARTED')) define('OUTPUT_STARTED', 1); + + extract( $sanitize ? self::sanitize_output($data) : $data ); + require( $file ); + + } else if (!PRODUCTION) { + + echo ""; + } + + } + + + /** render asap + * --- + * render_view then output code + * so that client will get html to render + * while server calculates next html + */ + public static function asap($view, $data, $sanitize = false) { + self::view($view, $data, $sanitize); + ob_flush(); + } + + + /** render text + * --- + * This function simply echoes text + * with Content-Type and Cache Headers + * @param $data : the text to echo + * @param $cType : Content-Type header + * @param $ttl : cache-contol headers; if false response is not-cached; else (int) chache for $ttl seconds + */ + public static function text( $data, $contentType = 'text/html; charset=UTF-8', $ttl = false ) { + header('Content-Type: '. $contentType); + if ($ttl) { + $ts = gmdate("D, d M Y H:i:s", time() + $ttl) . " GMT"; + header("Expires: {$ts}"); + header("Pragma: cache"); + header("Cache-Control: max-age={$ttl}"); + + } else { + $ts = gmdate("D, d M Y H:i:s") . " GMT"; + header("Expires: {$ts}"); + header("Last-Modified: {$ts}"); + header("Pragma: no-cache"); + header("Cache-Control: no-cache, must-revalidate"); + } + echo htmlspecialchars($data); + } + + + /** reply_json + * --- + * transform a php-array to json and echo to client + * Can be used for API calls + * + * @param $data : the php array) + * @param $ttl : cache-contol headers; if false response is not-cached; else chache for $ttl seconds + */ + public static function json( $data, $ttl = false ) { + header('Content-Type: application/json'); + if ($ttl) { + $ts = gmdate("D, d M Y H:i:s", time() + $ttl) . " GMT"; + header("Expires: {$ts}"); + header("Pragma: cache"); + header("Cache-Control: max-age={$ttl}"); + + } else { + $ts = gmdate("D, d M Y H:i:s") . " GMT"; + header("Expires: {$ts}"); + header("Last-Modified: {$ts}"); + header("Pragma: no-cache"); + header("Cache-Control: no-cache, must-revalidate"); + } + echo json_encode($data, JSON_UNESCAPED_UNICODE); + + // NOTE: + // after Rendering a JSON ... + // you probably do not need to export anything else; + die(); + } + + + + + // HELPER FUNCTIONS + // ----------------------------------------------------------------------------- + + + /** link asset + * --- + * the function defines the assets (css or js) + * to be loaded on the client (creates the HTML) + * @param $type : type of asset [css|js] + * @param $assetIDs : an array of ('assetID' => 'paramaters') + * + * example calls: + * load_asset('css', ['main' => 'media="all"', 'filters' => 'media="all"']); + * load_asset( 'js', ['jquery' => '', 'lazyloader' => 'async']); + */ + public static function asset( $type, $assetIDs, $paramatres = '' ) { + + $code = ""; // code to return + + // make sure $assetIDs is array (for coding simplicity) + if (!is_array($assetIDs)) { + $assetIDs = [ $assetIDs ]; + } + + // check asset type and construct all asset inserts + switch ($type) { + + case 'font': + foreach($assetIDs as $asset) { + $code .= "\n\t"; + } + break; + + case 'js': + foreach($assetIDs as $asset) { + $code .= "\n\t"; + } + break; + + case 'css': + default: + foreach($assetIDs as $asset) { + $code .= "\n\t"; + } + } + + echo $code; + } + + + /** sanitize_output (recursive) + * --- + * Sanitizes data that are about to rendered + * (usually when render_view() is called). + * + * NOTE: + * mitigates XSS attachs + * + * @param $data: (array) + */ + public static function sanitize_output($data) { + //// check https://stackoverflow.com/questions/2002710/php-how-to-perform-htmlspecialchar-on-an-array-of-arrays + + //// $output = array_map("myFunc", $data); + + global $secure; + + $output = array(); + foreach($data as $key => $val) { + + if (is_string($val)) { + $output[$key] = htmlspecialchars(self::remove_invisible_characters($val)); + + } else if (is_array($val)) { + $output[$key] = self::sanitize_output($val); + + } else { + $output[$key] = $val; + } + } + return $output; + } + + + /** remove_invisible_characters() + * --- + * @used by sanitize_output() + */ + public static function remove_invisible_characters($str, $url_encoded = TRUE) + { + $non_displayables = array(); + + // every control character except newline (dec 10), + // carriage return (dec 13) and horizontal tab (dec 09) + if ($url_encoded) { + $non_displayables[] = '/%0[0-8bcef]/i'; // url encoded 00-08, 11, 12, 14, 15 + $non_displayables[] = '/%1[0-9a-f]/i'; // url encoded 16-31 + $non_displayables[] = '/%7f/i'; // url encoded 127 + } + + $non_displayables[] = '/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]+/S'; // 00-08, 11, 12, 14-31, 127 + + do { + $str = preg_replace($non_displayables, '', $str, -1, $count); + } while ($count); + + return $str; + } + + + /** html + * --- + * outputs code as html + * + * @param $str (string) + * @return html5 (string) + */ + public static function html($str) { + if (!isset($str) || $str== null) return; + if (($str == '') || is_numeric($str)) return $str; + return htmlspecialchars_decode($str, ENT_QUOTES|ENT_HTML5); + } + + + /** set_headers + * --- + * set custom response-Headers + * + * @param $contentType + * @param $ttl: int or false) + * @param $more: array of (content-type => content-value) pairs + */ + public static function set_headers($contentType = 'text/html; charset=UTF-8', $ttl = false, $more = [] ) { + + // handle common content-type shorcuts + switch ($contentType) { + case 'text': + case 'html': + $contentType = 'text/html; charset=UTF-8'; + break; + case 'json': + $contentType = 'application/json; charset=utf-8'; + break; + case 'js': + $contentType = 'application/javascript; charset=utf-8'; + break; + case 'css': + $contentType = 'text/css'; + break; + default: + // $contentType stays as-is + break; + } + + // send headers + header('Content-Type: '. $contentType); + if ($ttl) { + $ts = gmdate("D, d M Y H:i:s", time() + $ttl) . " GMT"; + header("Expires: {$ts}"); + header("Pragma: cache"); + header("Cache-Control: max-age={$ttl}"); + + } else { + $ts = gmdate("D, d M Y H:i:s") . " GMT"; + header("Expires: {$ts}"); + header("Last-Modified: {$ts}"); + header("Pragma: no-cache"); + header("Cache-Control: no-cache, must-revalidate"); + } + + // send more headers + if ($more != []) { + foreach($more as $header => $value) { + header($header .': '. $value); + } + } + + } + + + /** render file + * + */ + public static function file($path, $madia_type) + { + $content = file_get_contents($path); + header("Content-Type: {$media_type}"); + echo $content; + } + + +} \ No newline at end of file diff --git a/core/classes/Repository.php b/core/classes/Repository.php new file mode 100644 index 0000000..c01d0c1 --- /dev/null +++ b/core/classes/Repository.php @@ -0,0 +1,43 @@ +URL = $_SERVER['REQUEST_URI']; + $this->PATH = (!empty($parsed['path'])) ? urldecode($parsed['path']) : ''; + $this->QUERY = (!empty($parsed['query'])) ? urldecode($parsed['query']) : false; + $this->HOST = $_SERVER['HTTP_HOST']; + $this->PORT = $_SERVER['SERVER_PORT']; + $this->TIME = $_SERVER['REQUEST_TIME']; + $this->IP = $_SERVER['REMOTE_ADDR']; + + $this->METHOD = strtolower($_SERVER['REQUEST_METHOD']); + + $this->GET = $_GET; // $_GET should only used + // to request data or specify options (never to perform + // system-changes) thus should not need any validation; + // * If (for any reason) you requide $_GET sanitization + // enable it later on the method's code + + $this->FILES = $_FILES; // TODO: sanitize the files array + + // $this->INTERFACE = php_sapi_name(); + + $this->AGENT = $_SERVER['HTTP_USER_AGENT'] ?? 'unknown'; + $this->SIGNATURE = sha1( + $_SERVER['HTTP_USER_AGENT'] ?? 'unknown' + . $_SERVER['HTTP_ACCEPT'] ?? '' + . $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? '' + . $_SERVER['HTTP_ACCEPT_ENCODING'] ?? '' + ); + + // sanitize user input + // if (isset($_GET)) { $this->GET = $this->sanitize($_GET); } + $this->GET = $this->sanitize($_GET); + if (isset($_POST)) { $this->POST = $this->sanitize($_POST); } + if (isset($_COOKIE)) { $this->COOKIE = $this->sanitize($_COOKIE); } + + // check anti-CSRF token if needed + // (again, GET requests should not need CSRF cheking) + if (in_array($this->METHOD, ['post', 'put', 'patch', 'delete'])) { + // TODO: only if CSRF protection enabled... + $this->checkCsrfToken(); + } + + } + + + /** + * Check: + * + * 1. + * https://dev.to/anastasionico/good-practices-how-to-sanitize-validate-and-escape-in-php-3-methods-139b + * + * 2. + * https://benhoyt.com/writings/dont-sanitize-do-escape/ + * + */ + private function sanitize($array) + { + // TODO: + // ... + return $array; + } + + + public function checkCsrfToken() + { + // TODO: + // ... + // if SCRF-token is not valideted, serve 403 + return true; + } + + + + + public function isAjax(): bool + { + // check headers 'XMLHttpRequest' == $this->headers->get('X-Requested-With'); + } + + + public function isSecure(): bool + { + // chech if HTTPS + } + + + public function hasSession(): bool + { + // chech if Session exist + } + + + /** + * Get the user making the request. + * + * @param string|null $guard + * @return mixed + */ + public function user($guard = null) + { + // return call_user_func($this->getUserResolver(), $guard); + } + + public function getUserResolver() + { + // return $this->userResolver ?: function () { + // + // }; + } + + /** + * Set the user resolver callback. + * + * @param \Closure $callback + * @return $this + */ + public function setUserResolver(Closure $callback) + { + // $this->userResolver = $callback; + // return $this; + } + + + +} + diff --git a/core/classes/Route.php b/core/classes/Route.php new file mode 100644 index 0000000..072723c --- /dev/null +++ b/core/classes/Route.php @@ -0,0 +1,97 @@ + $expression, + 'function' => $function, + 'method' => strtolower($method) + )); + } + + + /** notFound($function) + * --- + * @param $function : call back function to be executed + */ + public static function notFound($function) + { + self::$notFound = $function; + } + + + /** run() + * --- + * Parse request ; Find mathing route ; + * then call route's function + * usualy a Controller::method([poarametres]) + */ + public static function run(Request $request) + { + // $request = Registry::get('REQUEST'); + $path = $request->PATH; // request path + $method = $request->METHOD; // request method + + $path_match_found = false; + $route_match_found = false; + + foreach(self::$routes as $route) { + + // If method matched check the path + if ($route['method'] == $method || $method == 'any') { + + // Add 'find string start' automatically + $route['expression'] = '^'.$route['expression']; + + // Add 'find string end' automatically + $route['expression'] = $route['expression'].'$'; + + // Check path match + if (preg_match('#'. $route['expression'] .'#', $path, $matches)) { + + $route_match_found = true; + + array_shift($matches); // Always remove first element. This contains the whole string + + call_user_func_array($route['function'], $matches); + break; // Do not check other routes + + } + } + } + + // No matching route was found + if (!$route_match_found) { + header("HTTP/1.0 404 Not Found"); + if (self::$notFound) { + call_user_func_array(self::$notFound, []); + } + } + + } + +} diff --git a/core/classes/Security.php b/core/classes/Security.php new file mode 100644 index 0000000..61979db --- /dev/null +++ b/core/classes/Security.php @@ -0,0 +1,923 @@ +', '<', '>', + "'", '"', '&', '$', '#', + '{', '}', '[', ']', '=', + ';', '?', '%20', '%22', + '%3c', // < + '%253c', // < + '%3e', // > + '%0e', // > + '%28', // ( + '%29', // ) + '%2528', // ( + '%26', // & + '%24', // $ + '%3f', // ? + '%3b', // ; + '%3d' // = + ); + + public $charset = 'UTF-8'; // Character set @var (string) Will be overridden by the constructor. + + protected $_xss_hash; // XSS Hash: @var (string) Random Hash for protecting URLs. + + protected $_csrf_hash; // CSRF Hash: @var (string) Random hash for Cross Site Request Forgery protection cookie + + // CSRF Expire time @var (int) + // Expiration time for Cross Site Request Forgery protection cookie. + protected $_csrf_expire = 7200; // defaults to 2hours (=7200 seconds) + + // CSRF Token name: @var (string) Token name for Cross Site Request Forgery protection cookie. + protected $_csrf_token_name = 'pi_csrf_token'; + + // CSRF Cookie name: @var (string) Cookie name for Cross Site Request Forgery protection cookie. + protected $_csrf_cookie_name = 'pi_csrf_token'; + + // List of never allowed strings @var (array) + protected $_never_allowed_str = array( + 'document.cookie' => '[removed]', + '(document).cookie' => '[removed]', + 'document.write' => '[removed]', + '(document).write' => '[removed]', + '.parentNode' => '[removed]', + '.innerHTML' => '[removed]', + '-moz-binding' => '[removed]', + '' => '-->', + ' '<![CDATA[', + '' => '<comment>', + '<%' => '<%' + ); + + // List of never allowed regex replacements @var (array) + protected $_never_allowed_regex = array( + 'javascript\s*:', + '(\(?document\)?|\(?window\)?(\.document)?)\.(location|on\w*)', + 'expression\s*(\(|&\#40;)', // CSS and IE + 'vbscript\s*:', // IE, surprise! + 'wscript\s*:', // IE + 'jscript\s*:', // IE + 'vbs\s*:', // IE + 'Redirect\s+30\d', + "([\"'])?data\s*:[^\\1]*?base64[^\\1]*?,[^\\1]*?\\1?" + ); + + + /** Class constructor + * --- + * @return void + */ + public function __construct($charset = 'UTF-8') + { + $this->charset = $charset; + + /* --- + // Is CSRF protection enabled? + if (CSRF_PROTCTION') && ! is_cli()) + { --*/ + /************** + // CSRF config + foreach (array('csrf_expire', 'csrf_token_name', 'csrf_cookie_name') as $key) + { + //// if (NULL !== ($val = config_item($key))) + //// { + $this->{'_'.$key} = $val; + //// } + } + + // Append application specific cookie prefix + //// if ($cookie_prefix = config_item('cookie_prefix')) + //// { + $this->_csrf_cookie_name = $cookie_prefix . $this->_csrf_cookie_name; + //// } + + // Set the CSRF hash + $this->_csrf_set_hash(); + $this->csrf_verify(); + ***************/ + /* --- + } --*/ + + //// log_message('info', 'Security Class Initialized'); + } + + + /** CSRF Verify + * --- + * @return Security + */ + public function csrf_verify() + { + // If it's not a POST request we will set the CSRF cookie + if (strtoupper($_SERVER['REQUEST_METHOD']) !== 'POST') + { + return $this->csrf_set_cookie(); + } + + /// // Check if URI has been whitelisted from CSRF checks + /// if ($exclude_uris = config_item('csrf_exclude_uris')) + /// { + /// $uri = load_class('URI', 'core'); + /// foreach ($exclude_uris as $excluded) + /// { + /// if (preg_match('#^'.$excluded.'$#i'.(UTF8_ENABLED ? 'u' : ''), $uri->uri_string())) + /// { + /// return $this; + /// } + /// } + /// } + + // Check CSRF token validity, but don't error on mismatch just yet - we'll want to regenerate + $valid = isset($_POST[$this->_csrf_token_name], $_COOKIE[$this->_csrf_cookie_name]) + && is_string($_POST[$this->_csrf_token_name]) && is_string($_COOKIE[$this->_csrf_cookie_name]) + && hash_equals($_POST[$this->_csrf_token_name], $_COOKIE[$this->_csrf_cookie_name]); + + // We kill this since we're done and we don't want to pollute the _POST array + unset($_POST[$this->_csrf_token_name]); + + // Regenerate on every submission? + if (config_item('csrf_regenerate')) + { + // Nothing should last forever + unset($_COOKIE[$this->_csrf_cookie_name]); + $this->_csrf_hash = NULL; + } + + $this->_csrf_set_hash(); + $this->csrf_set_cookie(); + + if ($valid !== TRUE) + { + $this->csrf_show_error(); + } + + log_message('info', 'CSRF token verified'); + return $this; + } + + + /** CSRF Set Cookie + * --- + * @codeCoverageIgnore + * @return Security + */ + public function csrf_set_cookie() + { + $expire = time() + $this->_csrf_expire; + $secure_cookie = (bool) config_item('cookie_secure'); + + if ($secure_cookie && ! is_https()) + { + return FALSE; + } + + // php7.3+ + setcookie( + $this->_csrf_cookie_name, + $this->_csrf_hash, + array( + 'expires' => $expire, + 'path' => config_item('cookie_path'), + 'domain' => config_item('cookie_domain'), + 'secure' => $secure_cookie, + 'httponly' => config_item('cookie_httponly'), + 'samesite' => 'Strict' + ) + ); + + log_message('info', 'CSRF cookie sent'); + + return $this; + } + + + /** Show CSRF Error + * -- + * @return void + */ + public function csrf_show_error() + { + show_error('The action you have requested is not allowed.', 403); + } + + + /** Get CSRF Hash + * --- + * @see CI_Security::$_csrf_hash + * @return string CSRF hash + */ + public function get_csrf_hash() + { + return $this->_csrf_hash; + } + + + /** Get CSRF Token Name + * --- + * @see CI_Security::$_csrf_token_name + * @return string CSRF token name + */ + public function get_csrf_token_name() + { + return $this->_csrf_token_name; + } + + + /** XSS Clean + * --- + * Sanitizes data so that Cross Site Scripting Hacks can be + * prevented. This method does a fair amount of work but + * it is extremely thorough, designed to prevent even the + * most obscure XSS attempts. Nothing is ever 100% foolproof, + * of course, but I haven't been able to get anything passed + * the filter. + * + * Note: Should only be used to deal with data upon submission. + * It's not something that should be used for general + * runtime processing. + * + * @link http://channel.bitflux.ch/wiki/XSS_Prevention + * Based in part on some code and ideas from Bitflux. + * + * @link http://ha.ckers.org/xss.html + * To help develop this script I used this great list of + * vulnerabilities along with a few other hacks I've + * harvested from examining vulnerabilities in other programs. + * + * @param string|string[] $str Input data + * @param bool $is_image Whether the input is an image + * @return string + */ + public function xss_clean($str, $is_image = FALSE) + { + // Is the string an array? + if (is_array($str)) + { + foreach ($str as $key => &$value) + { + $str[$key] = $this->xss_clean($value); + } + + return $str; + } + + // Remove Invisible Characters + $str = remove_invisible_characters($str); + + // URL Decode + // Just in case stuff like this is submitted: + // Google + // NOTE: Use rawurldecode() so it does not remove plus signs + if (stripos($str, '%') !== false) + { + do + { + $oldstr = $str; + $str = rawurldecode($str); + $str = preg_replace_callback('#%(?:\s*[0-9a-f]){2,}#i', array($this, '_urldecodespaces'), $str); + } + while ($oldstr !== $str); + unset($oldstr); + } + + // Convert character entities to ASCII + // --- + // This permits our tests below to work reliably. + // We only convert entities that are within tags since + // these are the ones that will pose security problems. + $str = preg_replace_callback("/[^a-z0-9>]+[a-z0-9]+=([\'\"]).*?\\1/si", array($this, '_convert_attribute'), $str); + $str = preg_replace_callback('/<\w+.*/si', array($this, '_decode_entity'), $str); + + // Remove Invisible Characters Again! + $str = remove_invisible_characters($str); + + + // Convert all tabs to spaces + // --- + // This prevents strings like this: ja vascript + // NOTE: we deal with spaces between characters later. + // NOTE: preg_replace was found to be amazingly slow here on + // large blocks of data, so we use str_replace. + $str = str_replace("\t", ' ', $str); + + // Capture converted string for later comparison + $converted_string = $str; + + // Remove Strings that are never allowed + $str = $this->_do_never_allowed($str); + + + // Makes PHP tags safe + // NOTE: XML tags are inadvertently replaced too: + // '), array('<?', '?>'), $str); + } + + // Compact any exploded words + // --- + // This corrects words like: j a v a s c r i p t + // These words are compacted back to their correct state. + $words = array( + 'javascript', 'expression', 'vbscript', 'jscript', 'wscript', + 'vbs', 'script', 'base64', 'applet', 'alert', 'document', + 'write', 'cookie', 'window', 'confirm', 'prompt', 'eval' + ); + + foreach ($words as $word) + { + $word = implode('\s*', str_split($word)).'\s*'; + + // We only want to do this when it is followed by a non-word character + // That way valid stuff like "dealer to" does not become "dealerto" + $str = preg_replace_callback('#('.substr($word, 0, -3).')(\W)#is', array($this, '_compact_exploded_words'), $str); + } + + + // Remove disallowed Javascript in links or img tags + // We used to do some version comparisons and use of stripos(), + // but it is dog slow compared to these simplified non-capturing + // preg_match(), especially if the pattern exists in the string + // + // Note: It was reported that not only space characters, but all in + // the following pattern can be parsed as separators between a tag name + // and its attributes: [\d\s"\'`;,\/\=\(\x00\x0B\x09\x0C] + // ... however, remove_invisible_characters() above already strips the + // hex-encoded ones, so we'll skip them below. + do + { + $original = $str; + + if (preg_match('/]+([^>]*?)(?:>|$)#si', array($this, '_js_link_removal'), $str); + } + + if (preg_match('/]*?)(?:\s?/?>|$)#si', array($this, '_js_img_removal'), $str); + } + + if (preg_match('/script|xss/i', $str)) + { + $str = preg_replace('##si', '[removed]', $str); + } + } + while ($original !== $str); + unset($original); + + + // Sanitize naughty HTML elements + // --- + // If a tag containing any of the words in the list + // below is found, the tag gets converted to entities. + // So this: + // Becomes: <blink> + $pattern = '#' + .'<((?/*\s*)((?[a-z0-9]+)(?=[^a-z0-9]|$)|.+)' // tag start and name, followed by a non-tag character + .'[^\s\042\047a-z0-9>/=]*' // a valid attribute character immediately after the tag would count as a separator + // optional attributes + .'(?(?:[\s\042\047/=]*' // non-attribute characters, excluding > (tag close) for obvious reasons + .'[^\s\042\047>/=]+' // attribute characters + // optional attribute-value + .'(?:\s*=' // attribute-value separator + .'(?:[^\s\042\047=><`]+|\s*\042[^\042]*\042|\s*\047[^\047]*\047|\s*(?U:[^\s\042\047=><`]*))' // single, double or non-quoted value + .')?' // end optional attribute-value group + .')*)' // end optional attributes group + .'[^>]*)(?\>)?#isS'; + + // Note: It would be nice to optimize this for speed, BUT + // only matching the naughty elements here results in + // false positives and in turn - vulnerabilities! + do + { + $old_str = $str; + $str = preg_replace_callback($pattern, array($this, '_sanitize_naughty_html'), $str); + } + while ($old_str !== $str); + unset($old_str); + + // Sanitize naughty scripting elements + // --- + // Similar to above, only instead of looking for + // tags it looks for PHP and JavaScript commands + // that are disallowed. Rather than removing the + // code, it simply converts the parenthesis to entities + // rendering the code un-executable. + // * For example: eval('some code') + // ... Becomes: eval('some code') + $str = preg_replace( + '#(alert|prompt|confirm|cmd|passthru|eval|exec|expression|system|fopen|fsockopen|file|file_get_contents|readfile|unlink)(\s*)\((.*?)\)#si', + '\\1\\2(\\3)', + $str + ); + + // Same thing, but for "tag functions" (e.g. eval`some code`) + // See https://github.com/bcit-ci/CodeIgniter/issues/5420 + $str = preg_replace( + '#(alert|prompt|confirm|cmd|passthru|eval|exec|expression|system|fopen|fsockopen|file|file_get_contents|readfile|unlink)(\s*)`(.*?)`#si', + '\\1\\2`\\3`', + $str + ); + + // Final clean up + // This adds a bit of extra precaution in case + // something got through the above filters + $str = $this->_do_never_allowed($str); + + // Images are Handled in a Special Way + // - Essentially, we want to know that after all of the character + // conversion is done whether any unwanted, likely XSS, code was found. + // If not, we return TRUE, as the image is clean. + // However, if the string post-conversion does not matched the + // string post-removal of XSS, then it fails, as there was unwanted XSS + // code found and removed/changed during processing. + if ($is_image === TRUE) + { + return ($str === $converted_string); + } + + return $str; + } + + + /** XSS Hash + * --- + * Generates the XSS hash if needed and returns it. + * @see CI_Security::$_xss_hash + * @return string XSS hash + */ + public function xss_hash() + { + if ($this->_xss_hash === NULL) + { + $rand = $this->get_random_bytes(16); + $this->_xss_hash = ($rand === FALSE) + ? md5(uniqid(mt_rand(), TRUE)) + : bin2hex($rand); + } + + return $this->_xss_hash; + } + + + /** Get random bytes + * --- + * @param int $length Output length + * @return string + */ + public function get_random_bytes($length) + { + if (empty($length) OR ! ctype_digit((string) $length)) + { + return FALSE; + } + + if (function_exists('random_bytes')) + { + try + { + // The cast is required to avoid TypeError + return random_bytes((int) $length); + } + catch (Exception $e) + { + // If random_bytes() can't do the job, we can't either ... + // There's no point in using fallbacks. + log_message('error', $e->getMessage()); + return FALSE; + } + } + + // Unfortunately, none of the following PRNGs is guaranteed to exist ... + if (defined('MCRYPT_DEV_URANDOM') && ($output = mcrypt_create_iv($length, MCRYPT_DEV_URANDOM)) !== FALSE) + { + return $output; + } + + if (is_readable('/dev/urandom') && ($fp = fopen('/dev/urandom', 'rb')) !== FALSE) + { + // Try not to waste entropy ... + stream_set_chunk_size($fp, $length); + $output = fread($fp, $length); + fclose($fp); + if ($output !== FALSE) + { + return $output; + } + } + + if (function_exists('openssl_random_pseudo_bytes')) + { + return openssl_random_pseudo_bytes($length); + } + + return FALSE; + } + + + /** HTML Entities Decode + * --- + * A replacement for html_entity_decode() + * + * The reason we are not using html_entity_decode() by itself is because + * while it is not technically correct to leave out the semicolon + * at the end of an entity most browsers will still interpret the entity + * correctly. html_entity_decode() does not convert entities without + * semicolons, so we are left with our own little solution here. Bummer. + * + * @link https://secure.php.net/html-entity-decode + * + * @param string $str Input + * @param string $charset Character set + * @return string + */ + public function entity_decode($str, $charset = NULL) + { + if (strpos($str, '&') === FALSE) + { + return $str; + } + + static $_entities; + + isset($charset) OR $charset = $this->charset; + isset($_entities) OR $_entities = array_map('strtolower', get_html_translation_table(HTML_ENTITIES, ENT_COMPAT | ENT_HTML5, $charset)); + + do + { + $str_compare = $str; + + // Decode standard entities, avoiding false positives + if (preg_match_all('/&[a-z]{2,}(?![a-z;])/i', $str, $matches)) + { + $replace = array(); + $matches = array_unique(array_map('strtolower', $matches[0])); + foreach ($matches as &$match) + { + if (($char = array_search($match.';', $_entities, TRUE)) !== FALSE) + { + $replace[$match] = $char; + } + } + + $str = str_replace(array_keys($replace), array_values($replace), $str); + } + + // Decode numeric & UTF16 two byte entities + $str = html_entity_decode( + preg_replace('/(&#(?:x0*[0-9a-f]{2,5}(?![0-9a-f;])|(?:0*\d{2,4}(?![0-9;]))))/iS', '$1;', $str), + ENT_COMPAT | ENT_HTML5, + $charset + ); + } + while ($str_compare !== $str); + return $str; + } + + + /** Sanitize Filename + * --- + * @param string $str Input file name + * @param bool $relative_path Whether to preserve paths + * @return string + */ + public function sanitize_filename($str, $relative_path = FALSE) + { + $bad = $this->filename_bad_chars; + + if ( ! $relative_path) + { + $bad[] = './'; + $bad[] = '/'; + } + + $str = remove_invisible_characters($str, FALSE); + + do + { + $old = $str; + $str = str_replace($bad, '', $str); + } + while ($old !== $str); + + return stripslashes($str); + } + + + /** Strip Image Tags + * --- + * @param string $str + * @return string + */ + public function strip_image_tags($str) + { + return preg_replace( + array( + '##i', + '#`]+)).*?\>#i' + ), + '\\2', + $str + ); + } + + + /** URL-decode taking spaces into account + * --- + * @see https://github.com/bcit-ci/CodeIgniter/issues/4877 + * @param array $matches + * @return string + */ + protected function _urldecodespaces($matches) + { + $input = $matches[0]; + $nospaces = preg_replace('#\s+#', '', $input); + return ($nospaces === $input) + ? $input + : rawurldecode($nospaces); + } + + + /** Compact Exploded Words + * --- + * Callback method for xss_clean() to remove whitespace from + * things like 'j a v a s c r i p t'. + * + * @used-by Security::xss_clean() + * @param array $matches + * @return string + */ + protected function _compact_exploded_words($matches) + { + return preg_replace('/\s+/s', '', $matches[1]).$matches[2]; + } + + + /** Sanitize Naughty HTML + * --- + * Callback method for xss_clean() to remove naughty HTML elements. + * @used-by Security::xss_clean() + * @param array $matches + * @return string + */ + protected function _sanitize_naughty_html($matches) + { + static $naughty_tags = array( + 'alert', 'area', 'prompt', 'confirm', 'applet', 'audio', 'basefont', 'base', 'behavior', 'bgsound', + 'blink', 'body', 'embed', 'expression', 'form', 'frameset', 'frame', 'head', 'html', 'ilayer', + 'iframe', 'input', 'button', 'select', 'isindex', 'layer', 'link', 'meta', 'keygen', 'object', + 'plaintext', 'style', 'script', 'textarea', 'title', 'math', 'video', 'svg', 'xml', 'xss' + ); + + static $evil_attributes = array( + 'on\w+', 'style', 'xmlns', 'formaction', 'form', 'xlink:href', 'FSCommand', 'seekSegmentTime' + ); + + // First, escape unclosed tags + if (empty($matches['closeTag'])) + { + return '<'.$matches[1]; + } + // Is the element that we caught naughty? If so, escape it + elseif (in_array(strtolower($matches['tagName']), $naughty_tags, TRUE)) + { + return '<'.$matches[1].'>'; + } + // For other tags, see if their attributes are "evil" and strip those + elseif (isset($matches['attributes'])) + { + // We'll store the already filtered attributes here + $attributes = array(); + + // Attribute-catching pattern + $attributes_pattern = '#' + .'(?[^\s\042\047>/=]+)' // attribute characters + // optional attribute-value + .'(?:\s*=(?[^\s\042\047=><`]+|\s*\042[^\042]*\042|\s*\047[^\047]*\047|\s*(?U:[^\s\042\047=><`]*)))' // attribute-value separator + .'#i'; + + // Blacklist pattern for evil attribute names + $is_evil_pattern = '#^('.implode('|', $evil_attributes).')$#i'; + + // Each iteration filters a single attribute + do + { + // Strip any non-alpha characters that may precede an attribute. + // Browsers often parse these incorrectly and that has been a + // of numerous XSS issues we've had. + $matches['attributes'] = preg_replace('#^[^a-z]+#i', '', $matches['attributes']); + + if ( ! preg_match($attributes_pattern, $matches['attributes'], $attribute, PREG_OFFSET_CAPTURE)) + { + // No (valid) attribute found? Discard everything else inside the tag + break; + } + + if ( + // Is it indeed an "evil" attribute? + preg_match($is_evil_pattern, $attribute['name'][0]) + // Or does it have an equals sign, but no value and not quoted? Strip that too! + OR (trim($attribute['value'][0]) === '') + ) + { + $attributes[] = 'xss=removed'; + } + else + { + $attributes[] = $attribute[0][0]; + } + + $matches['attributes'] = substr($matches['attributes'], $attribute[0][1] + strlen($attribute[0][0])); + } + while ($matches['attributes'] !== ''); + + $attributes = empty($attributes) + ? '' + : ' '.implode(' ', $attributes); + return '<'.$matches['slash'].$matches['tagName'].$attributes.'>'; + } + + return $matches[0]; + } + + + /** JS Link Removal + * --- + * Callback method for xss_clean() to sanitize links. + * + * This limits the PCRE backtracks, making it more performance friendly + * and prevents PREG_BACKTRACK_LIMIT_ERROR from being triggered in + * PHP 5.2+ on link-heavy strings. + * + * @used-by CI_Security::xss_clean() + * @param array $match + * @return string + */ + protected function _js_link_removal($match) + { + return str_replace( + $match[1], + preg_replace( + '#href=.*?(?:(?:alert|prompt|confirm)(?:\(|&\#40;|`|&\#96;)|javascript:|livescript:|mocha:|charset=|window\.|\(?document\)?\.|\.cookie|_filter_attributes($match[1]) + ), + $match[0] + ); + } + + + /** JS Image Removal + * --- + * Callback method for xss_clean() to sanitize image tags. + * + * This limits the PCRE backtracks, making it more performance friendly + * and prevents PREG_BACKTRACK_LIMIT_ERROR from being triggered in + * PHP 5.2+ on image tag heavy strings. + * + * @used-by Security::xss_clean() + * @param array $match + * @return string + */ + protected function _js_img_removal($match) + { + return str_replace( + $match[1], + preg_replace( + '#src=.*?(?:(?:alert|prompt|confirm|eval)(?:\(|&\#40;|`|&\#96;)|javascript:|livescript:|mocha:|charset=|window\.|\(?document\)?\.|\.cookie|_filter_attributes($match[1]) + ), + $match[0] + ); + } + + + /** Attribute Conversion + * --- + * @used-by CI_Security::xss_clean() + * @param array $match + * @return string + */ + protected function _convert_attribute($match) + { + return str_replace(array('>', '<', '\\'), array('>', '<', '\\\\'), $match[0]); + } + + + /** Filter Attributes + * --- + * Filters tag attributes for consistency and safety. + * @used-by CI_Security::_js_img_removal() + * @used-by CI_Security::_js_link_removal() + * @param string $str + * @return string + */ + protected function _filter_attributes($str) + { + $out = ''; + if (preg_match_all('#\s*[a-z\-]+\s*=\s*(\042|\047)([^\\1]*?)\\1#is', $str, $matches)) + { + foreach ($matches[0] as $match) + { + $out .= preg_replace('#/\*.*?\*/#s', '', $match); + } + } + + return $out; + } + + + /** HTML Entity Decode Callback + * --- + * @used-by CI_Security::xss_clean() + * @param array $match + * @return string + */ + protected function _decode_entity($match) + { + // Protect GET variables in URLs + // 901119URL5918AMP18930PROTECT8198 + $match = preg_replace('|\&([a-z\_0-9\-]+)\=([a-z\_0-9\-/]+)|i', $this->xss_hash().'\\1=\\2', $match[0]); + + // Decode, then un-protect URL GET vars + return str_replace( + $this->xss_hash(), + '&', + $this->entity_decode($match, $this->charset) + ); + } + + + /** Do Never Allowed + * --- + * @used-by CI_Security::xss_clean() + * @param string + * @return string + */ + protected function _do_never_allowed($str) + { + $str = str_replace(array_keys($this->_never_allowed_str), $this->_never_allowed_str, $str); + + foreach ($this->_never_allowed_regex as $regex) + { + $str = preg_replace('#'.$regex.'#is', '[removed]', $str); + } + + return $str; + } + + + /** Set CSRF Hash and Cookie + * --- + * @return string + */ + protected function _csrf_set_hash() + { + if ($this->_csrf_hash === NULL) + { + // If the cookie exists we will use its value. + // We don't necessarily want to regenerate it with + // each page load since a page could contain embedded + // sub-pages causing this feature to fail + if (isset($_COOKIE[$this->_csrf_cookie_name]) && is_string($_COOKIE[$this->_csrf_cookie_name]) + && preg_match('#^[0-9a-f]{32}$#iS', $_COOKIE[$this->_csrf_cookie_name]) === 1) + { + return $this->_csrf_hash = $_COOKIE[$this->_csrf_cookie_name]; + } + + $rand = $this->get_random_bytes(16); + $this->_csrf_hash = ($rand === FALSE) + ? md5(uniqid(mt_rand(), TRUE)) + : bin2hex($rand); + } + + return $this->_csrf_hash; + } +} diff --git a/core/classes/authentication/Core/PasswordTrait.php b/core/classes/authentication/Core/PasswordTrait.php new file mode 100644 index 0000000..40a7381 --- /dev/null +++ b/core/classes/authentication/Core/PasswordTrait.php @@ -0,0 +1,38 @@ + $this->cost]); + } + + + public function isPasswordValid(UserInterface $user, string $plainPassword): bool + { + return password_verify($plainPassword, $user->getPassword()); + } + + + public function setCost(int $cost): void + { + if ($cost < 4 || $cost > 31) { + throw new \InvalidArgumentException('Cost must be in the range of 4-31.'); + } + $this->cost = $cost; + } +} diff --git a/core/classes/authentication/Core/UserManager.php b/core/classes/authentication/Core/UserManager.php new file mode 100644 index 0000000..54cd2cb --- /dev/null +++ b/core/classes/authentication/Core/UserManager.php @@ -0,0 +1,101 @@ +hasUserToken()) { + $userToken = unserialize($_SESSION[UserTokenInterface::DEFAULT_PREFIX_KEY]); + } + + return $userToken; + } + + + /** hasUserToken() + * == is user loged in + * + */ + public function hasUserToken(): bool + { + $key = UserTokenInterface::DEFAULT_PREFIX_KEY; + return (array_key_exists($key, $_SESSION) && unserialize($_SESSION[$key]) !== false); + } + + + /** isGranted + * + * checks if user is granded some role(s) + * from the array of roles that are passed + * + * ex. $token->isGranted(['editor', 'designer']) ... + * returns true if the user is editor or designer (or both) + * + */ + public function isGranted(array $roles): bool + { + // if (!is_null($userToken = $this->getUserToken())) { + if (is_null($userToken = $this->getUserToken())) { + return false; + } + + if ($userToken->getUser() instanceof UserInterface) { + return (!empty(array_intersect($roles, $userToken->getUser()->getRoles()))); + } + + return false; + } + + + /** createUserToken() + * == serializes user and stores it into session + * so $_SESSION[DEFAULT_PREFIX_KEY] has the serialized representaion of user + */ + public function createUserToken(UserInterface $user): UserTokenInterface + { + $userToken = new UserToken($user); + $_SESSION[UserTokenInterface::DEFAULT_PREFIX_KEY] = $userToken->serialize(); + + return $userToken; + } + + + /** logout + * == clear session + * + */ + public function logout(): void + { + if ($this->hasUserToken()) { + unset($_SESSION[UserTokenInterface::DEFAULT_PREFIX_KEY]); + } + } + +} diff --git a/core/classes/authentication/Core/UserManagerInterface.php b/core/classes/authentication/Core/UserManagerInterface.php new file mode 100644 index 0000000..05110fb --- /dev/null +++ b/core/classes/authentication/Core/UserManagerInterface.php @@ -0,0 +1,25 @@ +user = $user; + } + + + /** getUser + * + */ + public function getUser(): UserInterface + { + return $this->user; + } + + + /** serialize() + * + * serializes the user structure (with user's property values) + * ... then it will be saved into session[DEFAULT_PREFIX_KEY] + * + */ + public function serialize(): string + { + return serialize($this); + } +} diff --git a/core/classes/authentication/Token/UserTokenInterface.php b/core/classes/authentication/Token/UserTokenInterface.php new file mode 100644 index 0000000..8e8b642 --- /dev/null +++ b/core/classes/authentication/Token/UserTokenInterface.php @@ -0,0 +1,18 @@ +userName; + } + + /** getPassword + * @return null|string + */ + public function getPassword(): ?string + { + return $this->password; + } + + /** getRoles + * @return array + */ + public function getRoles(): array + { + return $this->roles; + } + + /** isEnabled + * @return bool + */ + public function isEnabled(): bool + { + return $this->enabled; + } + + + /** SETs + * ------------------------------------------------------------------------- + */ + + /** setUserName() + * + * @param string $userName + * @return User + */ + public function setUserName(string $userName): self + { + $this->userName = $userName; + return $this; + } + + /** setPassword() + * + * @param string $password + * @return User + */ + public function setPassword(string $password): self + { + $this->password = $password; + return $this; + } + + /** setRoles() + * + * @param array $roles + * @return User + */ + public function setRoles(array $roles): self + { + $this->roles = $roles; + return $this; + } + + + /** setEnabled + * + * @param bool $enabled + * @return User + */ + public function setEnabled(bool $enabled): self + { + $this->enabled = $enabled; + return $this; + } +} diff --git a/core/classes/authentication/UserInterface.php b/core/classes/authentication/UserInterface.php new file mode 100644 index 0000000..b035215 --- /dev/null +++ b/core/classes/authentication/UserInterface.php @@ -0,0 +1,19 @@ +cryptPassword($req->POST['password']); + + $user = (new User()) + ->setUserName($req->POST['username']) + ->setPassword($password) + ->setRoles(['ROLE_USER']); + + $userManager->createUserToken($user); + + // check Token in Session + var_dump($userManager->getUserToken()); + // object(DevCoder\Authentication\Token\UserToken)[4] + // private 'user' => + // object(DevCoder\Authentication\User)[5] + // private 'userName' => string 'username' (length=8) + // private 'password' => string '$2y$10$iWdcmebmikUFlgKMqW7/rOmUp1DjFAuWKqdUHBhL08FZ7LL6bwRey' (length=60) + // private 'roles' => + // array (size=1) + // 0 => string 'ROLE_USER' (length=9) + // private 'enabled' => boolean true + + +## Connected or not + + hasUserToken()) { + // connected + + $token = $userManager->getUserToken(); + $user = $token->getUser(); + var_dump($user); + // object(DevCoder\Authentication\User)[5] + // private 'userName' => string 'username' (length=8) + // private 'password' => string '$2y$10$OBobeLhdvdiftuedlv1a6e4.qF6sCG/usq5WEV4E3uB.UiS1egv/m' (length=60) + // private 'roles' => + // array (size=1) + // 0 => string 'ROLE_USER' (length=9) + // private 'enabled' => boolean true + + } else { + // not connected + } + + +## Access management + + $userManager = new UserManager(); + if ($userManager->isGranted(['ROLE_ADMIN'])) { + //is admin + // return Response 200 + }else { + //is not admin + // return Response 403 + } + + Logout + + $userManager = new UserManager(); + $userManager->logout(); + + +## Login + + prepare("SELECT * FROM users WHERE username=?"); + $stmt->execute([$_POST['username']]); + $userFromDataBase = $stmt->fetch(); + /** + * Hydration + */ + $user = (new \Test\DevCoder\Authentication\User()) + ->setUserName($userFromDataBase['username']) + ->setPassword($userFromDataBase['password']) + ->setRoles(json_decode($userFromDataBase['roles'])) + ->setEnabled($userFromDataBase['active']); + + $userManager = new UserManager(); + if ($userManager->isPasswordValid($user, $_POST['password'])) { + + // login OK, set Token in session + $userManager->createUserToken($user); + + } else { + // login failed , return error + } diff --git a/core/classes/cache/Cache_interface.php b/core/classes/cache/Cache_interface.php new file mode 100644 index 0000000..7c49789 --- /dev/null +++ b/core/classes/cache/Cache_interface.php @@ -0,0 +1,34 @@ + $data[0]) { + + // Unlinking + unlink($filename); + return false; + + } + + return $data[1]; + } + + + public function flush(int $olderThan = 0) + { + return false; // reply false until implementation + } + +} diff --git a/core/classes/cache/MemcachedCache.php b/core/classes/cache/MemcachedCache.php new file mode 100644 index 0000000..c89d86a --- /dev/null +++ b/core/classes/cache/MemcachedCache.php @@ -0,0 +1,37 @@ +addServer(\MEMCACHE_HOST, 11211); + + $mc->set($hashkey, $data, $ttl); + } + + + public function get($key) + { + $hashkey = md5($key); + + $mc = new Memcached(); + $mc->addServer(\MEMCACHE_HOST, 11211); + + return $mc->get($hashkey); + + } + + public function flush($olderThan = 0) + { + return false; + } + +} \ No newline at end of file diff --git a/core/classes/cache/RedisCache.php b/core/classes/cache/RedisCache.php new file mode 100644 index 0000000..ec50f08 --- /dev/null +++ b/core/classes/cache/RedisCache.php @@ -0,0 +1,69 @@ + \REDIS_HOST + ]); + return $redis->set($key, $serialized, 'EX', $ttl); + + } catch (Exception $e) { + // ... + } + } + + + /** get + * fetch previously stored $data under label $key + * return $data (or false) + */ + public function get($key) + { + + try { + + $redis = new \Predis\Client([ + 'host' => \REDIS_HOST + ]); + + if ($redis->exists($key)) { + return unserialize($redis->get($key)); + + } else { + return false; + } + + } catch (Exception $e) { + // ... + } + + } + + + /** flush + * --- not umplemented yet; may not needed + */ + public function flush($olderThan = 0) + { + return false; // until implementation + } + +} \ No newline at end of file diff --git a/core/classes/session/DatabaseSession.php b/core/classes/session/DatabaseSession.php new file mode 100644 index 0000000..aa31b62 --- /dev/null +++ b/core/classes/session/DatabaseSession.php @@ -0,0 +1,138 @@ +db = Registry::use('database'); + + // Set handler to overide SESSION + session_set_save_handler( + array($this, "open"), + array($this, "close"), + array($this, "read"), + array($this, "write"), + array($this, "destroy"), + array($this, "gc") + ); + + // Start the session + session_name(SESSION_NAME); + session_start(); + } + + // public function open(string $path, string $name) : bool + // NOTE: $path and $name are unsued + public function open() : bool + { + if ($this->db) { + return true; + } + else { + // database connection may be closed from another class + // (ex. from some user class); in this case... + // create a new database connection, then recheck. + $conn = new Database(); + // $this->db = $conn->connect(); + + if ($conn) { + $this->db = $conn; + return true; + } + } + return false; + } + + + public function close() : bool + { + // depricated: the database object is shared (don;t clode it!) + // // Close the database connection + // if ($this->db = null) { return true; } + // else return false; + + return true; + } + + + public function read(string $id) : string|false + { + $exist = $this->db->runQuery( + "SELECT data FROM sessions WHERE id = :id", + [':id', $id] + ); + + if (($exist === false) || (count($exist) == 0)) { + return false; + + } else { + $data = $exist[0]; + } + + if (is_null($row['data'])) { + return ''; + } + return $row['data']; + } + + + public function write(string $id, string $data) : bool + { + // Create timestamp + $access = time(); + $check = $this->db->runQuery( + "REPLACE INTO sessions VALUES (:id, :access, :data)", + [':id' => $id, ':access' => $access, ':data' => $data ] + ); + return ($check == false) ? false : true; + } + + + public function destroy(string $sassionID) : bool + { + $check = $this->db->runQuery( + 'DELETE FROM sessions WHERE id = :id', + [':id' => $sassionID] + ); + return ($check == false) ? false : true; + } + + + public function gc(int $max) + { + // Calculate what is to be deemed old + $old = time() - $max; + $check = $this->db->runQuery( + 'DELETE FROM sessions WHERE access < :old', + [':old' => $old] + ); + return ($check == false) ? false : 1; + // check garbage-collector probability to run + // echo "probability: ". ini_get("session.gc_probability") ." / ". ini_get("session.gc_divisor") . ", ttl: ". ini_get("session.gc_maxlifetime"); die(); + + } + + # callable $create_sid = ?, + # callable $validate_sid = ?, + # callable $update_timestamp = ? +} diff --git a/core/classes/session/DefaultSession.php b/core/classes/session/DefaultSession.php new file mode 100644 index 0000000..079ab9d --- /dev/null +++ b/core/classes/session/DefaultSession.php @@ -0,0 +1,42 @@ +sessionName = SESSION_NAME; + $this->sess_filename = FILESESSION_PATH; + $this->minutes = $minutes; + + // Set handler to overide SESSION + session_set_save_handler( + array($this, "open"), + array($this, "close"), + array($this, "read"), + array($this, "write"), + array($this, "destroy"), + array($this, "gc") + ); + + // Start the session + session_name($this->sessionName); + session_start(); + } + + /** open + * set filename; + * no need to touch the filesystem yet + * return true (always) + */ + public function open($savePath, $sessionName): bool + { + $this->sess_filename = $this->$path .'/'. $sessionName; + return true; + } + + /** close + * nothing needs to be closed; + * return true (always) + */ + public function close(): bool + { + return true; + } + + /** read + * chech if file exists; if not return false + * read data + */ + public function read($sessionId): string|false + { + if (!file_exists($filename) || !is_readable($filename)) return false; + $data = file_get_contents($filename); + + return @unserialize($data); + } + + /** write + * data serielized already by php's internal session engine + */ + public function write($sessionId, $data): bool + { + $h = fopen($filename, 'w'); + if (fwrite($h, serialize($data)) === false) { + throw new Exception('Could not write session data'); + return false; + } + fclose($h); + + return true; + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function destroy($sassionID): bool + { + unlink($this->sassionID); + + return true; + } + + /** + * {@inheritdoc} + * + * @return int + */ + public function gc($lifetime) : int + { + $files = Finder::create() + ->in($this->path) + ->files() + ->ignoreDotFiles(true) + ->date('<= now - '. $lifetime .' seconds'); + + $deletedSessions = 0; + + foreach ($files as $file) { + $this->files->delete($file->getRealPath()); + $deletedSessions++; + } + + return $deletedSessions; + } +} diff --git a/core/classes/session/SessionHandlerInterface.php b/core/classes/session/SessionHandlerInterface.php new file mode 100644 index 0000000..cf6efac --- /dev/null +++ b/core/classes/session/SessionHandlerInterface.php @@ -0,0 +1,169 @@ + someone visits website through server A + * -> server A creates a new session-id for the visitor and a shared-session-id + * -> saves both into database + * + * later... + * + * #2 + * -> the same device is connected (via loadbalancer) into server B + * -> server B don't have the session-id (send by the client) but gets it from db + * (is session-id do not exist on the db, then this is a new session) + * + * = now both server A and B have the same session-id localy + * + * later... + * + * #3 + * -> some server (let's say B) regenerates the visitors session-id + * -> if later the visitor falls into server A, the A will retrieve the new + * session id through the #2 scenario + * + * this way + * + any server can update/regenerate the visitor's session-id + * + the visitor can be served from both servers randomly + * + all session variables exist on the database (always) + * + all session variables exist on all servers synced-on-demand + * + * + * stucture of session: + * --- + * - shared-session-id: (secret + persistent); exposed between servers **primary + * - session-id: (may change/regenerate/update); exposed to the client **indexed + * - data: serialized array + * - creation_timestamp: + * - last_touched_timestamp: + * - expiration_timestamp: should point to the future or session has expired + * - csrf_token: + * - JWT_token + * - AES-key (this way private data can be kept in browser) + * + * + * what will kept on browser/client (via cookie) + * --- + * - session-name => session-id + * - user info => AES_ectypted(serialized[t=>csfr_token, u=> user_id, s=>SIGNATURE]) + * + * + * Writable file-system shall change + * (local writable file-system tree) + * --- + * /html/storage + * | + * |-- cache : query-caches + * | + * `-- session : FS\sessions + * | + * `-- indexes : share-session-id indexes + * + * + * + * NOTE: Session life-cycle + * + * session_start() * firsts time + * --- + * ::open(path,PHPSESSID) -> (session_id not exist) -> false + * ::create_sid -> '123def' + * ::read('123def') ?-or/and- ::close() + * + * + * session_start() * next times + * --- + * ::open(path, PHPSESSID) -> (session_id exist) -> true + * ::read('123def') -> return data -> will fill $_SESSION[*] + * + * + * $_SESSION['foo'] = 'bar'; + * --- + * ::write('123def', 'foo|s:3:"bar";') -> ['foo' => "bar"] + * ::close() + * + * + * session_regenerate_id(); + * --- + * ::create_sid() -> def123 + * + * + * session_reset() + * --- + * ::open() + * ::read('def123') + * + * + * session_write_close() + * --- + * ::write('123def', 'foo|s:3:"bar";') + * ::close() + * + * + * session_destroy() + * --- + * ::destroy('def123') + * ::close() + * + */ diff --git a/core/cli/cli-cache.php b/core/cli/cli-cache.php new file mode 100644 index 0000000..57d8090 --- /dev/null +++ b/core/cli/cli-cache.php @@ -0,0 +1,65 @@ +#!/usr/local/bin/php + $category) { + + echo textColor( textWidth($category['Title'], 72), NORMAL); + app\controllers\cli\CliContoller::cacheCategoryByUrl($category['FullFriendlyUrl']); + } + + # end of script ------------------------------------------------------------ + +} catch(Exception $e) { // Basic error handling + echo 'Caught exception: ', textColor($e->getMessage(), ERROR), "\n"; + die(); +} + + +$dt = number_format( microtime(true) - $t0 , 0) ."sec"; + +// echo ending... +echo textColor("\nOperation Completed {$dt}\n\n", NORMAL); diff --git a/core/cli/curl-cache.php b/core/cli/curl-cache.php new file mode 100755 index 0000000..6ab2e93 --- /dev/null +++ b/core/cli/curl-cache.php @@ -0,0 +1,155 @@ +#!/usr/local/bin/php +success) { + echo "\033[91mError reading " . $call ."\n\033[39m"; + return false; + } + + return $json; +} + +/** multiple curl get calls + * + * @param $array: an array of urls to be called + * + */ +function multiCurl($array) { + + $multiCurl = array(); // array of curl handlers + $result = array(); // data to be returned + $mh = curl_multi_init(); // multi handler + $module = array(); // array of module names / 1:1 to $result array + + foreach ($array as $i => $category) { + $module[$i] = $category->ID; // guid (module name) + $fetchURL = API_ROOT .'url/'. $category->FullFriendlyUrl; + $multiCurl[$i] = curl_init(); + curl_setopt($multiCurl[$i], CURLOPT_URL,$fetchURL); + curl_setopt($multiCurl[$i], CURLOPT_HEADER,0); + curl_setopt($multiCurl[$i], CURLOPT_RETURNTRANSFER,1); + curl_multi_add_handle($mh, $multiCurl[$i]); + } + + $index=null; + + do { + curl_multi_exec($mh,$index); + } while($index > 0); + + foreach($multiCurl as $k => $ch) { + $result[$k] = curl_multi_getcontent($ch); + curl_multi_remove_handle($mh, $ch); + } + curl_multi_close($mh); // close multi-curl +} + +// force printing string with specified length +function forceLen($str, $len = 72) { + $space = " "; + for($i = 0 ; $i<$len ; $i++) $space .= "."; + return mb_substr($str.$space, 0, $len-1) ." "; +} + + +/** main procedure + * ----------------------------------------------------------------------------- + */ + +echo "\033[39mReading Tree of product categories...\n"; +$tree = singleCurl('tree'); // ask for categories tree + +if ($tree !==false) echo "\033[32mCategories tree is cached\033[39m\n\n"; + +$categories = singleCurl('category'); + +// request every category to create cache if not exist +// send requests in small batches to avoid enormous server-stress +echo "\033[39mRequesting re-Cache for every category\033[39m\n"; +echo "\033[33mPlease Wait...\n\n"; + +$buffer = []; // buffer array +$counter = 0; // counter to track items in buffer +foreach($categories->result as $category) { + + echo "\033[39m". forceLen($category->Title) ."\033[33m"."Sent.\n\033[39m"; + + $counter++; + + // try several batch lengths to establish the ideal one that produces + // results in a smooth quite fase rthm, without stressing the server; + // so that visitors can navigate the production site white the script + // is running; + // for my testing this number is 3-5. I set the cou 3 items give fast results without stressing the server + // so "if ($counter > 2) { ... " is the smoothest option + if ($counter > 2) { + multiCurl($buffer); + $counter = 0; + $buffer = []; + } + + $buffer[] = $category; + +} + +multiCurl($buffer); // run once more for the last non-filled buffer + +$dt = number_format( microtime(true) - $t0 , 1) ."sec"; + +// echo ending... +echo "\n\033[32mOperation Completed \033[39m({$dt})\n\n"; +echo "\033[39m"; // back to default color; + + +// check +// http://www.idein.it/joomla/14-docker-php-apache-with-crontab +?> \ No newline at end of file diff --git a/core/cli/info.md b/core/cli/info.md new file mode 100644 index 0000000..39f8c11 --- /dev/null +++ b/core/cli/info.md @@ -0,0 +1,23 @@ +# cli + +This section is used to setup an anom micro-framework for the cli-interface. +The cli interface is commonly used to support and optimize the whole app. + +A cli script may run manualy, for example using... + + php cli-cache.php + +although... + +it is recommended to run periodically using the cron service: + + 15 * * * * php path/to/cli-cache.php + +or ... + +when a docker instanse of the project fires up; for example including +the following line of code inside the docker/bin/docker-entrypoint.sh +(runs the script after 5 minutes delay to make sure that the instance +is up and running) + + at -f /var/www/core/cli/refresh-cache.php -t now +5 minutes diff --git a/core/cli/micro/setup.php b/core/cli/micro/setup.php new file mode 100644 index 0000000..ca2fd40 --- /dev/null +++ b/core/cli/micro/setup.php @@ -0,0 +1,120 @@ +getMessage(), "\n" +# } \ No newline at end of file diff --git a/core/cli/micro/term_utilities.php b/core/cli/micro/term_utilities.php new file mode 100644 index 0000000..971c36e --- /dev/null +++ b/core/cli/micro/term_utilities.php @@ -0,0 +1,44 @@ + 'text/html; charset=UTF-8', + 'html' => 'text/html; charset=UTF-8', + 'json' => 'application/json; charset=utf-8', + 'js' => 'application/javascript; charset=utf-8', + 'css' => 'text/css', + 'png' => 'image/png', + 'jpeg' => 'image/jpeg', + 'webp' => 'image/web' + ) +); + + +// PROXY_FLAGS +// ----------------------------------------------------------------------------- +// These options represend altered/non-default behaviour +// and will be resolved bitwise, so use powers of 2 +// ----------------------------------------------------------------------------- +define('PROXY_CACHE_ERRORS', 1); // cache result even if error +define('PROXY_IGNORE_CACHE', 2); // ignore cache if exist +define('PROXY_DO_NOT_CACHE', 4); // do not cache result diff --git a/core/config/init.php b/core/config/init.php new file mode 100644 index 0000000..589a486 --- /dev/null +++ b/core/config/init.php @@ -0,0 +1,64 @@ + $basePath) { + + $file = APP_ROOT . $basePath . $classPath .'.php'; + + // if class-file exist, include it + if (file_exists($file)) { + require_once $file; + break; + } + + } + + // // Feel free to extend the fanctionality + // // example: + // // custom (exception) namespaced classes loader + // $customNSclasses = array( + // '\\George\\Class' => 'path/to/George/Class', + // '\\Mary\\Class' => 'path/to/Marias/Class', + // ... + // ); + // if array_key_exists($className) { + // require_once $customNSclasses[$className]; + // } + } +); diff --git a/core/helpers/design_patterns.php b/core/helpers/design_patterns.php new file mode 100644 index 0000000..d66ffa9 --- /dev/null +++ b/core/helpers/design_patterns.php @@ -0,0 +1,104 @@ +get($key); + + } else { $data = false; } // ignore cache? pass false + + + if ($data !== false) { // if exists, serve cached data + + // if cashed data exist + // --------------------------------------------------------------------- + # if (!PRODUCTION) Benchmark::add_spot('proxy-cached'); + if ($data == '_ERROR_') return false; // previous cached error + else return $data; // previous cache () + + + } else { // else (not exist)... + + // no data on cache -> get data from Class::method( arguments ) + // --------------------------------------------------------------------- + $data = call_user_func_array($callable, $args); + + // handle newly created data + // --------------------------------------------------------------------- + + // if faulty data, return false + if ($data == false) { + + if ($cache_errors && !$do_not_cache) { // cache the error + $cache->set($key, '_ERROR_', $ttl); + } + + # if (!PRODUCTION) Benchmark::add_spot('proxy-new-error'); + return false; + } + + // store to cache + if (!$do_not_cache) { + $cache->set($key, $data, $ttl); + } + + # if (!PRODUCTION) Benchmark::add_spot('proxy-new'); + return $data; // serve data + } + +} diff --git a/core/helpers/error_handling.php b/core/helpers/error_handling.php new file mode 100644 index 0000000..95db98c --- /dev/null +++ b/core/helpers/error_handling.php @@ -0,0 +1,119 @@ +getMessage() + . " on file '". $e->getFile() . "' at line {$e->getLine()}"; + + $anom_ERRORS[] = $message; // store the error +} + + +/** + * Catch fatal error work-around + * (set_error_handler not working on fatal errors). + */ +function on_shutdown_check_for_fatal() +{ + global $anom_ERRORS; + + $error = error_get_last(); + + if ($error != null) { // ignore 'normal' system shutdowns [exit|die]() + + if ( $error["type"] == E_ERROR ) { // get as much error-info + handle_error( + $error["type"] ?? -1, + $error["message"] ?? 'unknown error', + $error["file"] ?? 'unknown file', + $error["line"] ?? -1 + ); + } + } + + if (($anom_ERRORS != [])) { + + // TODO: + // Implemetnig the design/layout of the error-messages + // output should not be an error's-handling task, thus + // shall be assigned to the Rendes/View level; + // Same about the log service channels + + // TODO: + // FORWARD ERRORS into LOG-SYSTEM + // ex. into some file... + // file_put_contents("logs/thowable.log", $message_template .PHP_EOL, FILE_APPEND); + // or ...some other channer (Email, Slack, Database etc). + // Channes can be decided according to error severity; + // a file-system error loggin for backup is recommended + // partitioniong of errors onto file-system can be useful + + // TODO: + // 1st: Log Errors + // 2nd: Show Errors + + if (PRODUCTION) { + if (!defined('OUTPUT_STARTED')) { + // throw a prety-error message + Render::view('error/general', ['message' => 'Please forgive me, I know not what I do']); + } + + } else { + + if (!defined('OUTPUT_STARTED')) { + // no template loaded; render erros in general-error template + Render::view('error/general', ['message' => implode("
And:
", $anom_ERRORS)]); + + } else { + echo '
+ Errors:
'. implode("
And:
", $anom_ERRORS) .'
'; + } + } + } +} + + +// register callback on shutdown +register_shutdown_function( "on_shutdown_check_for_fatal" ); + +// register custom error handler callback +set_error_handler( "handle_error" ); + +// set custom exception handler function +set_exception_handler( "handle_exception" ); + +// send errors to stderr (instead of stdout) +ini_set( "display_errors", "off" ); + +// thow out all errors +error_reporting( E_ALL ); + diff --git a/data/.gitkeep b/data/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docker-compose.redis.yml b/docker-compose.redis.yml new file mode 100644 index 0000000..80f8021 --- /dev/null +++ b/docker-compose.redis.yml @@ -0,0 +1,166 @@ +version: "3.7" + +networks: + skeleton-docker.network: + driver: bridge + +services: + web: + container_name: x-anom + build: + context: . + ports: + - "80:8080" + volumes: + - "./public:/var/www/public" + - "./core:/var/www/core" + - "./container:/var/www/container" + environment: + PORT: 8080 + command: sh -c "/usr/local/bin/docker-entrypoint.sh" + php: + build: ./docker/php + restart: always + tty: true + container_name: skeleton-docker.php + # depends_on: + # - mysql + # - postgres + # - redis + # - mariadb + networks: + - skeleton-docker.network + ports: + - "9000:9000" + volumes: + - .:/var/www/app.dev + + # nginx: + # build: ./docker/nginx + # container_name: skeleton-docker.nginx + # restart: always + # tty: true + # depends_on: + # - php + # # - mysql + # # - postgres + # # - redis + # # - mariadb + # ports: + # - "80:80" + # - "433:433" + # volumes: + # - .:/var/www/app.dev + # networks: + # - skeleton-docker.network + + apache: + build: ./docker/apache + restart: always + container_name: skeleton-docker.apache + networks: + - skeleton-docker.network + depends_on: + - php + # - mysql + # - postgres + # - redis + # - mariadb + ports: + - "80:8080" + - "443:443" + volumes: + - "./html:/var/www/public" + - "./core:/var/www/core" + - "./container:/var/www/container" + command: sh -c "/usr/local/bin/docker-entrypoint.sh" + + # mariadb: + # image: mariadb + # tty: true + # restart: always + # container_name: skeleton-docker.mariadb + # networks: + # - skeleton-docker.network + # ports: + # - '3306:3306' + # volumes: + # - './docker/mysql/config/mariadb.cnf:/etc/mysql/conf.d/custom.cnf:ro' + # - './docker/mysql/dbdata:/var/lib/mysql' + # environment: + # - MYSQL_PASSWORD=root + # - MYSQL_ROOT_PASSWORD=root + # - MYSQL_DATABASE=development + + # mysql: + # build: ./docker/mysql + # tty: true + # restart: always + # container_name: skeleton-docker.mysql + # networks: + # - skeleton-docker.network + # ports: + # - "3306:3306" + # volumes: + # - "./docker/mysql/config/mysql.cnf:/etc/mysql/conf.d/custom.cnf:ro" + # - "./docker/mysql/dbdata:/var/lib/mysql" + # environment: + # - MYSQL_PASSWORD=root + # - MYSQL_ROOT_PASSWORD=root + # - MYSQL_DATABASE=development + + # phpmyadmin: + # image: library/phpmyadmin + # container_name: skeleton-docker.phpmyadmin + # tty: true + # networks: + # - skeleton-docker.network + # depends_on: + # - mysql + # - mariadb + # environment: + # PMA_USER: root + # PMA_PASSWORD: root + # PMA_HOSTS: mysql,mariadb + # PMA_PORT: 3306 + # PMA_ARBITRARY: 1 + # ports: + # - '8080:80' + + # postgres: + # build: ./docker/postgres + # container_name: skeleton-docker.postgres + # tty: true + # restart: always + # networks: + # - skeleton-docker.network + # ports: + # - "5432:5432" + # volumes: + # - "./docker/postgres/dbdata:/var/lib/postgresql/data" + # environment: + # - POSTGRES_USER=root + # - POSTGRES_PASSWORD=root + # - POSTGRES_DB=development + + redis: + image: bitnami/redis:latest + restart: always + tty: true + container_name: skeleton-docker.redis + volumes: + - ./docker/redis:/bitnami/redis/data + networks: + - skeleton-docker.network + environment: + - ALLOW_EMPTY_PASSWORD=no + - REDIS_PASSWORD=root + ports: + - "6379:6379" + +# redis-overcommit: +# build: https://github.com/bkuhl/redis-overcommit-on-host.git +# restart: 'no' +# privileged: true +# volumes: +# - /proc/sys/vm:/mnt/vm \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..adf7b7d --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,53 @@ +version: '3.7' + +# networks: +# anom-network: +# driver: bridge + +services: + + web: + container_name: x-anom + build: + context: . + ports: + - "80:8080" + volumes: + - "./public:/var/www/public" + - "./core:/var/www/core" + - "./docker:/var/www/docker" + - "./storage:/var/www/storage" + - "./tests:/var/www/tests" + - "./tests:/var/www/public/files" + environment: + PORT: 8080 + # for xdebug uncomment next subsection + extra_hosts: + - host.docker.internal:host-gateway + # networks: + # - anom-network + command: sh -c "/usr/local/bin/docker-entrypoint.sh" + + # + # memcached: + # container_name: anomemcached + # image: memcached:latest + # ports: + # - "11211:11211" + + # redis: + # container_name: redis + # image: 'redis:5.0.7-alpine' + # ports: + # - 6379:6379 + # networks: + # - anom-network + + # mailhog + # NOTE: this is a fake mail server + # mailhog: + # image: mailhog/mailhog:v1.0.0 + # ports: + # - "1025:1025" + # - "8025:8025" + diff --git a/docker/bin/docker-entrypoint.sh b/docker/bin/docker-entrypoint.sh new file mode 100644 index 0000000..edcce41 --- /dev/null +++ b/docker/bin/docker-entrypoint.sh @@ -0,0 +1,27 @@ +#! /bin/sh + +# setup writable directories +# ------------------------------------------------------------------------------ +# grand to 'cache' directory writable permissions +# set file-cache directory to apache's user ownership +chmod -R 755 /var/www/storage +chown -R www-data:www-data /var/www/storage +chown -R www-data:www-data /var/www/public/files +chmod -R 757 /var/www/public/files + +# copy composer.json to 'www'; then prepare auutoloading +# ------------------------------------------------------------------------------ +cp /var/www/docker/config/composer-naked.json /var/www/composer.json +(cd /var/www ; composer update) +(cd /var/www ; composer dump-autoload) + + +### set -ex +### +### /cloud_sql_proxy -instances=pythia-251711:europe-west4:pythia-db-eu -dir=/cloudsql -credential_file=/cloudsqlproxy.json & +### sleep 3 + +# start apache +# ------------------------------------------------------------------------------ +# End with running the original command +exec /usr/sbin/apache2ctl -D FOREGROUND diff --git a/docker/bin/optimize.sh b/docker/bin/optimize.sh new file mode 100644 index 0000000..02be749 --- /dev/null +++ b/docker/bin/optimize.sh @@ -0,0 +1,17 @@ +!/bin/bash -x + +# optimize performance or development + +if test ["$1" == "PRODUCTION"] ; then + + docker-php-ext-install opcache + # copy OPcache parametres + # etc... + + # install and enable JIT + + # else + # maybe install some development tools + # like XDebug + +fi \ No newline at end of file diff --git a/docker/config/000-default.conf b/docker/config/000-default.conf new file mode 100644 index 0000000..fafa395 --- /dev/null +++ b/docker/config/000-default.conf @@ -0,0 +1,21 @@ + + + # DEFAULT SITE CONFIGURATION + # (optional settings are commented with `##`) + # --- -- -- - - - + + ## ServerName www.example.com + + ServerAdmin webmaster@localhost + DocumentRoot /var/www/public + + ## LogLevel info ssl:warn + + ErrorLog ${APACHE_LOG_DIR}/error.log + CustomLog ${APACHE_LOG_DIR}/access.log combined + + ## Include conf-available/serve-cgi-bin.conf + + + +# vim: syntax=apache ts=4 sw=4 sts=4 sr noet \ No newline at end of file diff --git a/docker/config/composer-naked.json b/docker/config/composer-naked.json new file mode 100644 index 0000000..171c35e --- /dev/null +++ b/docker/config/composer-naked.json @@ -0,0 +1,12 @@ +{ + "require": { + "phpmailer/phpmailer": "^6.8", + "erusev/parsedown": "^1.7" + }, + "autoload": { + "classmap": [ + "/var/www/core/classes", + "/var/www/public/app" + ] + } +} \ No newline at end of file diff --git a/docker/config/composer-redis.json b/docker/config/composer-redis.json new file mode 100644 index 0000000..facb782 --- /dev/null +++ b/docker/config/composer-redis.json @@ -0,0 +1,13 @@ +{ + "require": { + "predis/predis": "^2.0", + "phpmailer/phpmailer": "^6.8", + "erusev/parsedown": "^1.7" + }, + "autoload": { + "classmap": [ + "/var/www/core/classes", + "/var/www/public/app" + ] + } +} \ No newline at end of file diff --git a/docker/config/default-ssl.conf b/docker/config/default-ssl.conf new file mode 100644 index 0000000..deeebc5 --- /dev/null +++ b/docker/config/default-ssl.conf @@ -0,0 +1,131 @@ + + + # DEFAULT _SSL_ SITE CONFIGURATION + # (optional settings are commented with `##`) + # --- -- -- - - - + + ServerAdmin webmaster@localhost + + DocumentRoot /var/www/public + + # Available loglevels: trace8, ..., trace1, debug, info, notice, warn, + # error, crit, alert, emerg. + ## LogLevel info ssl:warn + + ErrorLog ${APACHE_LOG_DIR}/error.log + CustomLog ${APACHE_LOG_DIR}/access.log combined + + ## Include conf-available/serve-cgi-bin.conf + + # SSL Engine Switch: + # Enable/Disable SSL for this virtual host. + SSLEngine on + + # A self-signed (snakeoil) certificate can be created by installing + # the ssl-cert package. See + # /usr/share/doc/apache2/README.Debian.gz for more info. + # If both key and certificate are stored in the same file, only the + # SSLCertificateFile directive is needed. + SSLCertificateFile /etc/ssl/certs/ssl-cert-snakeoil.pem + SSLCertificateKeyFile /etc/ssl/private/ssl-cert-snakeoil.key + + # Server Certificate Chain: + # Point SSLCertificateChainFile at a file containing the + # concatenation of PEM encoded CA certificates which form the + # certificate chain for the server certificate. Alternatively + # the referenced file can be the same as SSLCertificateFile + # when the CA certificates are directly appended to the server + # certificate for convinience. + ## SSLCertificateChainFile /etc/apache2/ssl.crt/server-ca.crt + + # Certificate Authority (CA): + # Set the CA certificate verification path where to find CA + # certificates for client authentication or alternatively one + # huge file containing all of them (file must be PEM encoded) + # Note: Inside SSLCACertificatePath you need hash symlinks + # to point to the certificate files. Use the provided + # Makefile to update the hash symlinks after changes. + ## SSLCACertificatePath /etc/ssl/certs/ + ## SSLCACertificateFile /etc/apache2/ssl.crt/ca-bundle.crt + + # Certificate Revocation Lists (CRL): + # Set the CA revocation path where to find CA CRLs for client + # authentication or alternatively one huge file containing all + # of them (file must be PEM encoded) + # Note: Inside SSLCARevocationPath you need hash symlinks + # to point to the certificate files. Use the provided + # Makefile to update the hash symlinks after changes. + ## SSLCARevocationPath /etc/apache2/ssl.crl/ + ## SSLCARevocationFile /etc/apache2/ssl.crl/ca-bundle.crl + + # Client Authentication (Type): + # Client certificate verification type and depth. Types are + # none, optional, require and optional_no_ca. Depth is a + # number which specifies how deeply to verify the certificate + # issuer chain before deciding the certificate is not valid. + ## SSLVerifyClient require + ## SSLVerifyDepth 10 + + # SSL Engine Options: + # Set various options for the SSL engine. + # o FakeBasicAuth: + # Translate the client X.509 into a Basic Authorisation. This means that + # the standard Auth/DBMAuth methods can be used for access control. The + # user name is the `one line' version of the client's X.509 certificate. + # Note that no password is obtained from the user. Every entry in the user + # file needs this password: `xxj31ZMTZzkVA'. + # o ExportCertData: + # This exports two additional environment variables: SSL_CLIENT_CERT and + # SSL_SERVER_CERT. These contain the PEM-encoded certificates of the + # server (always existing) and the client (only existing when client + # authentication is used). This can be used to import the certificates + # into CGI scripts. + # o StdEnvVars: + # This exports the standard SSL/TLS related `SSL_*' environment variables. + # Per default this exportation is switched off for performance reasons, + # because the extraction step is an expensive operation and is usually + # useless for serving static content. So one usually enables the + # exportation for CGI and SSI requests only. + # o OptRenegotiate: + # This enables optimized SSL connection renegotiation handling when SSL + # directives are used in per-directory context. + ## SSLOptions +FakeBasicAuth +ExportCertData +StrictRequire + + SSLOptions +StdEnvVars + + + SSLOptions +StdEnvVars + + + # SSL Protocol Adjustments: + # The safe and default but still SSL/TLS standard compliant shutdown + # approach is that mod_ssl sends the close notify alert but doesn't wait for + # the close notify alert from client. When you need a different shutdown + # approach you can use one of the following variables: + # o ssl-unclean-shutdown: + # This forces an unclean shutdown when the connection is closed, i.e. no + # SSL close notify alert is send or allowed to received. This violates + # the SSL/TLS standard but is needed for some brain-dead browsers. Use + # this when you receive I/O errors because of the standard approach where + # mod_ssl sends the close notify alert. + # o ssl-accurate-shutdown: + # This forces an accurate shutdown when the connection is closed, i.e. a + # SSL close notify alert is send and mod_ssl waits for the close notify + # alert of the client. This is 100% SSL/TLS standard compliant, but in + # practice often causes hanging connections with brain-dead browsers. Use + # this only for browsers where you know that their SSL implementation + # works correctly. + # Notice: Most problems of broken clients are also related to the HTTP + # keep-alive facility, so you usually additionally want to disable + # keep-alive for those clients, too. Use variable "nokeepalive" for this. + # Similarly, one has to force some clients to use HTTP/1.0 to workaround + # their broken HTTP/1.1 implementation. Use variables "downgrade-1.0" and + # "force-response-1.0" for this. + # BrowserMatch "MSIE [2-6]" \ + # nokeepalive ssl-unclean-shutdown \ + # downgrade-1.0 force-response-1.0 + + + + +# vim: syntax=apache ts=4 sw=4 sts=4 sr noet \ No newline at end of file diff --git a/docker/config/info.md b/docker/config/info.md new file mode 100644 index 0000000..3ca1c81 --- /dev/null +++ b/docker/config/info.md @@ -0,0 +1,69 @@ +## About cron jobs + +(on docker containers) + +Dockerfile #1 + + RUN apt-get update && apt-get install cron -y + RUN crontab / + crontab -l | { cat; echo "10 * * * * /usr/local/bin/php /var/www/core/cli/cache-categories.php"; } | crontab - + CMD cron + + + +--- + + +Dockerfile #2 + + RUN apt-get update && \ + apt-get -y install tzdata cron + + RUN cp /usr/share/zoneinfo/Europe/Rome /etc/localtime && \ + echo "Europe/Rome" > /etc/timezone + + #RUN apt-get -y remove tzdata + RUN rm -rf /var/cache/apk/* + + # Copy cron file to the cron.d directory + COPY cron /etc/cron.d/cron + + # Give execution rights on the cron job + RUN chmod 0644 /etc/cron.d/cron + + # Apply cron job + RUN crontab /etc/cron.d/cron + + # Create the log file to be able to run tail + RUN mkdir -p /var/log/cron + + # Add a command to base-image entrypont scritp + RUN sed -i 's/^exec /service cron start\n\nexec /' /usr/local/bin/apache2-foreground + + +--- + +handle cron status + + service cron status + service cron stop + service cron start + + + +--- + + +docker folder: organizing configuration files + + docker + |-- bin: docker-ettrypoint.sh, optimize.sh + | + `-- config: composer*.json opcache.ini php-overides.ini cron-jobs + + +--- + + +check: +* http://www.idein.it/joomla/14-docker-php-apache-with-crontab \ No newline at end of file diff --git a/docker/config/msmtprc.kill b/docker/config/msmtprc.kill new file mode 100644 index 0000000..5382ecd --- /dev/null +++ b/docker/config/msmtprc.kill @@ -0,0 +1,12 @@ +account default +host mail.roptron.gr +port 587 +tls on +tls_starttls on +tls_trust_file /etc/ssl/certs/ca-certificates.crt +tls_certcheck on +auth on +user my@mail.com +password "YourAwesomeStr0ngP4zzw0rd" +from "my@mail.com" +logfile /var/log/msmtp.log diff --git a/docker/config/opcache.ini b/docker/config/opcache.ini new file mode 100644 index 0000000..6eba8b2 --- /dev/null +++ b/docker/config/opcache.ini @@ -0,0 +1,9 @@ +[opcache] +opcache.enable=1 +opcache.revalidate_freq=0 +opcache.validate_timestamps=0 +opcache.max_accelerated_files=10000 +opcache.memory_consumption=192 +opcache.max_wasted_percentage=10 +opcache.interned_strings_buffer=16 +opcache.fast_shutdown=1 \ No newline at end of file diff --git a/docker/config/php-ext-xdebug.ini b/docker/config/php-ext-xdebug.ini new file mode 100644 index 0000000..4a6a2ed --- /dev/null +++ b/docker/config/php-ext-xdebug.ini @@ -0,0 +1,24 @@ +; [xdebug] + +; uncomment next line to enable xdebug +zend_extension=xdebug + +; modes where xdebug is enabled +xdebug.mode=develop,debug + +; common parametres +xdebug.start_with_request=yes +xdebug.client_port=9003 +; xdebug.remote_handler=dbgp +xdebug.idekey=VSCODE +xdebug.client_host=host.docker.internal +xdebug.log=/usr/local/etc/php/xdebug.log +;error_reporting=E_ALL + + +; Note: +; Remove the comment ; to enable debugging +;zend_extension=xdebug +xdebug.client_host=host.docker.internal +xdebug.start_with_request=yes +xdebug.mode=debug \ No newline at end of file diff --git a/docker/config/php-overrides.ini b/docker/config/php-overrides.ini new file mode 100644 index 0000000..215db1a --- /dev/null +++ b/docker/config/php-overrides.ini @@ -0,0 +1,3 @@ +upload_max_filesize = 10M +post_max_size = 14M +memory_limit = 256M \ No newline at end of file diff --git a/public/.htaccess b/public/.htaccess new file mode 100644 index 0000000..f4ddf81 --- /dev/null +++ b/public/.htaccess @@ -0,0 +1,99 @@ +## Introduce the Server +Header add Origin-Name geone + +### Deny access to specific folders + + ## deny all files too + RedirectMatch 403 ^/app/.*$ + RedirectMatch 403 ^/cache/.*$ + RedirectMatch 403 ^/processed-views/.*$ + + ## deny folders only + RedirectMatch 403 ^/css/?$ + RedirectMatch 403 ^/js/?$ + + + RewriteEngine On + RewriteBase / + + # Remove trailing slash + RewriteRule ^(.*)/$ /$1 [L,R=301,E=limitcache:1] + # Header always set Cache-Control "max-age=5400" env=limitcache + + # Remove index.php from Application Engine URLs + # and set 301 Redirects not indefinitely permanent (cache for 1:30h) + RewriteCond %{THE_REQUEST} ^GET.*index\.php [NC] + RewriteRule (.*?)index\.php/*(.*) /$1$2 [R=301,NE,L,QSA,E=limitcache:1] + + # Define some cache-control max-age for environment = limitcache + # but ExpiresDefault on mod_expires will create a duplicate header_-) + Header always set Cache-Control "max-age=30" env=limitcache + + # Direct all non-file/directory web requests through the site index file_\\) + RewriteCond %{REQUEST_FILENAME} !-f + RewriteCond %{REQUEST_FILENAME} !-d + RewriteRule ^(.*)$ /index.php/$1 [L,QSA] + + + +# +# +# Header set Access-Control-Allow-Origin "*" +# +# + + +### SERVER SIDE OPTIMIZATIONS + +### COMPRESSION + +# COMPRESS text-anything output +AddOutputFilterByType DEFLATE text/plain +AddOutputFilterByType DEFLATE text/html +AddOutputFilterByType DEFLATE text/xml +AddOutputFilterByType DEFLATE text/css +AddOutputFilterByType DEFLATE application/xml +AddOutputFilterByType DEFLATE application/xhtml+xml +AddOutputFilterByType DEFLATE application/rss+xml +AddOutputFilterByType DEFLATE application/javascript +AddOutputFilterByType DEFLATE application/x-javascript +AddOutputFilterByType DEFLATE application/json + +### ### CACHE-CONTROL HEADERS +### +### # CACHE-CONTROL IMAGES 100days +### #: +### +### Header set Cache-Control "max-age=8640000, public" +### +### +### # CACHE-CONTROL CSS and JS 30days +### #: +### #: Header set Cache-Control "max-age=2592000, public" +### #: +### +### ### EXPIRATION DATES +### +### # EXPIRATION DATE BY TYPE +### +### ExpiresActive On +### +### # Default directive +### ExpiresDefault "access plus 0 seconds" +### +### # My Fav-icon and Images +### ExpiresByType image/x-icon "access plus 1 month" +### ExpiresByType image/gif "access plus 1 month" +### ExpiresByType image/png "access plus 1 month" +### ExpiresByType image/jpg "access plus 1 month" +### ExpiresByType image/jpeg "access plus 1 month" +### +### # CSS +### #: ExpiresByType text/css "access plus 1 month" +### # Javascript +### #: ExpiresByType application/javascript "access plus 1 year" +### +### +### +### # Remove Server's Signature +### ServerSignature Off diff --git a/public/app/config/.gitkeep b/public/app/config/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/public/app/config/app_constants.php b/public/app/config/app_constants.php new file mode 100644 index 0000000..b46bc51 --- /dev/null +++ b/public/app/config/app_constants.php @@ -0,0 +1,171 @@ + 'Login', + 2 => 'Account', + 3 => 'Payment' +]); + + + +// ERRORS AND ERROR MESSAGES /////////////////////////////////////////////////// +// ----------------------------------------------------------------------------- + +define('EMPTY_REQUIRED_FIELDS', 'Δεν έχουν συμπληρωθεί όλα τα υποχρεωτικά πεδία'); + + + +// ASSETS: PATHS USED HEAVILY ////////////////////////////////////////////////// +// ----------------------------------------------------------------------------- +// these paths are used in order to easily find and autoload useful elemets + + +// JAVASCRIPT LIBRARY PATHS +// ----------------------------------------------------------------------------- +define('JS_DIR', '/content/'); // path to contruct link +define('JS_LIBRARIES', array( // path after to the actual file + 'jquery' => 'lib/jquery/dist/jquery.min.js', + + 'require/files' => 'lib/require/require.files.js', + 'require/slim' => 'lib/require/require.slim.js', + + 'plugins' => 'lib/plugins/plugins.min.js', + 'plugins/setup' => 'lib/plugins/plugins.setup.js' + ) +); + + +// CSS ASSET PATHS +// ----------------------------------------------------------------------------- +define('CSS_DIR', '/content/css/'); // path to contruct link +define('CSS_FILES', array( // path after to the actual file + 'main' => 'main.min.css', + 'filter' => 'filter.css', + 'overides' => 'overides.css', + ) +); + + +// FONTS +// ----------------------------------------------------------------------------- +define('FONTS_DIR', '/content/fonts/'); +define('FONT_FILES', array( + 'iconfont' => 'iconfont.woff2', + 'CFAstyStdBold' => 'CFAstyStd-Bold.woff2', + 'CFAstyStdBook' => 'CFAstyStd-Book.woff2', + 'CFAstyStdMedium' => 'CFAstyStd-Medium.woff2' + ) +); + + + + +// Mesagges //////////////////////////////////////////////////////////////////// +// ----------------------------------------------------------------------------- + +// 404 +// --- +define('PAGE_404_TITLE', '404: Not Found'); + +// registration +// --- +define('REGISTRATION_SUCCESS', "

Επιτυχής Εγγραφή

+

+ Παρακαλώ ελέγξτε το email σας και ακολουθήστε το σύνδεσμο ενεργοποίησης + που σας 'εχει αποσταλεί, ώστε να έχετε πρόσβαση σε επιπλέον περιεχόμενο του site +

" +); +define ('REGISTRATION_USER_EXISTS', "

Αποτυχία Εγγραφής

+

+ Υπάρχει ήδη χρήστης με αυτό το email. Αν δεν θυμάστε το password + μπορείτε να κάνετε επαναφορά του ακολουθώντας + αυτό το σύνδεσμο. +

" +); + + +// activation +// --- +define('ACCOUNT_ACTIVATED_TITLE', 'Ο λογαριασμός σας έχει ενεργοποιηθεί'); +define('ACCOUNT_ACTIVATED_MESSAGE', 'Τώρα μπορείτε να κάνετε + Είσοδο στο σύστημα + για να δείτε περισσότερο περιεχόμενο'); +define('NOT_VALID_ACTIVATION_TITLE', 'Σφάλμα!'); +define('NOT_VALID_ACTIVATION_MESSAGE', 'Ο κωδικός ενεργοποίησης δεν είναι σωστός. + Μήπως τον έχετε ενεργοποιήσει στο παρελθόν;'); \ No newline at end of file diff --git a/public/app/config/init_routes.php b/public/app/config/init_routes.php new file mode 100644 index 0000000..22d6ec0 --- /dev/null +++ b/public/app/config/init_routes.php @@ -0,0 +1,12 @@ +POST['email']); + + // if no user exists, return false + if ($record === false) return false; + + // user is valid; check user password + // create a user object + $user = (new Classroom_user()) + ->setID($record['id']) + ->setUserName($record['email']) + ->setName($record['first_name'] .' '. $record['last_name']) + ->setPassword($record['password']) + ->setEnabled($record['active']); + + // let user manager to validate user credentials + $userManager = new Classroom_manager(); + + if ($userManager->isPasswordValid($user, $req->POST['password'])) { + + // get user's security attributes + $attributes = User_Model::getUser($record['id']); + $roles = json_decode($attributes['Roles_json']); + $user + ->setRoles($roles) + ->setPrivileges( + array_merge( + json_decode($attributes['RootPrivileges_json']), + self::merge_lists_array( + json_decode($attributes['SubPrivileges_json']) + ) + ) + ); + + // regeneration session ID (prevent session fixation) + session_regenerate_id(); + // set cookie for connected user + setcookie( + 'cluser', + 'connected;'. $user->getName(), + time()+60*60*8, // 8 hours + '/' + ); + + // check if admin (and redirect differently) + $is_admin = (!empty(array_intersect([1,2,3], $roles))); + + // login OK, set Token in session + $userManager->createUserToken($user); + return [ + 'success' => true, + 'goto' => $is_admin ? '/admin/lessons' : '/user/profile', + ]; + + } else { + return false; + } + } + + + /** + * merges an array of lists to one list + */ + private static function merge_lists_array( $list ) + { + $current = []; + foreach($list as $sublist) { + $current = array_merge($current, $sublist); + } + return $current; + } + + + /** activate + * resolves a call like: /account/activate?ticket=ca42d68cfba5fbbafeacc010b8e3a551 + */ + public static function activate() + { + $req = Registry::get('REQUEST'); + + // get the record of the target user + $check = User_model::activate($req->GET['ticket']); + + if ($check == true) { + Render::view('/error/general', [ + 'title' => ACCOUNT_ACTIVATED_TITLE, + 'message' => ACCOUNT_ACTIVATED_MESSAGE + ]); + + } else { + Render::view('/error/general', [ + 'title' => NOT_VALID_ACTIVATION_TITLE, + 'message' => NOT_VALID_ACTIVATION_MESSAGE + ]); + } + + } + + /** register + * + * Method for new user registration + * + */ + public static function register() + { + $userManager = new Classroom_manager(); + $req = Registry::get('REQUEST'); + + // create a salted password hash + $password = $userManager->cryptPassword($req->POST['password']); + + // echo $password; print_r($req->POST); die(); // OK! + + $user = (new Classroom_user()) + ->setUserName($req->POST['email']) + ->setName($req->POST['name'] .' '. $req->POST['surname']) + ->setPassword($password) + ->setRoles([ READER ]) // Role: authorized reader + ->setPrivileges([]); // none privilege until acount confirmation + + // create user record + $activation_code = User_model::registerUser($req->POST, $password); + + // TODO: + // handle error on user registration + // ... + // + // if ($activatopn_code[] == -1) { + // return [ + // 'success' => false, + // 'message' => REGISTRATION_USER_EXISTS + // ]; + // } + + $send_mail = Send_mail::send_activation_code([ + 'email' => $req->POST['email'], + 'name' => $req->POST['name'] .' '. $req->POST['surname'], + 'code' => $activation_code['activation'] + ]); + + // Send replies + if ($send_mail) { + return [ + 'success' => true, + 'message' => REGISTRATION_SUCCESS + ]; + + } else { + return [ + 'success' => false, + 'message' => 'error on sending email' + ]; + } + + } + + /** is_connected + * checks if the user is connected + * + * @return true|false + */ + public static function is_connected() + { + $manager = new Classroom_manager(); + if ($manager->hasUserToken()) { + + // user is connected; + $token = $manager->getUserToken(); + $user = $token->getUser(); + + return $user; + + } else { + // user is not connected; + return false; + } + } + + + /** logout + * + * performs a secure logout; + * regenerates session; deletes cookies; + */ + public static function logout() + { + $userManager = new Classroom_manager(); + $userManager->logout(); + + // regeneration session ID (prevent session fixation) + session_regenerate_id(); + + // remove user-conected cookie + if (isset($_COOKIE['cluser'])) { + unset($_COOKIE['cluser']); + setcookie('cluser', '', -1, '/'); + return true; + + } else { + return false; + } + } + + + + /** hasPermition( PERMIT ) + * + * checks if the user owns the specified permition + * to access the source + * + */ + public static function hasPermition($permit = [0]) + { + if (in_array(0, $permit)) { // permision 0 means public + return true; // permision 0 is always granted + } + + if ($user = self::is_connected() === false) { // if not connected + return false; // then no other permition is granted + } + + if ($user instanceof UserInterface) { + return ( !empty( array_intersect($permit, $user->getPrivileges()) ) ); + } + } + + + /** isAuthenticated() + * + * chechs if the user's roles and permitions + * satisfy the specified requirements + * to access the source + * + * @param $requirements (array of rules-array) + * + * example: + * [ + * [ + * role => [2, 3] + * permition => ['10', '12', '18'] + * ], + * [ + * role => [1 , 4] + * ], + * [ + * permition => [ 3 ] + * ] + * ] + * + * defines (and parses to) a requirements rule of: + * [ + * user should be creator or editor + * _AND_ have permition 10 or 12 or 18 + * ] + * OR + * [ + * user should be an administrator or developer + * ] + * OR + * [ + * user should have permition #3 + * ] + * + * + */ + public static function isAuthorized($requirements) + { + $authorized = false; + foreach($requirements as $required) { + + if (isset($required['role'])) { // if a role is required + if ( (self::isGranted($required['role'])) // authorize both role + && (self::hasPermition($required['permition'] ?? [ 0 ])) ) { // and permition + // $authorized = true; + return true; + } + + } else { // else, if not is not required + if (self::hasPermition($required['permition'] ?? [ 0 ])) { // authorize permition + // $authorized = true; + return true; + } + } + } + return $authorized; + } + + + public static function forgot_pass() + { + } + + + + public static function validate_otp() + { + } + + + /** allowRoles + * + * method filters access for certain roles + * if user is not grented acces, a forbiden message is sent and app ends._ + * otherwise the method returns true (app will continue) + * + * @param $allowed (array) : array of allowed roles + * @return true or die(); + */ + public static function allowRoles($allowed) + { + $manager = new Classroom_manager(); + if ($manager->isGranted($allowed)) { // if valid, return true (continue) + return true; + + } else { // no user, no access; die._ + Render::view('error/general', [ + 'title' => 'Forbidden', + 'message' => 'Access is forbidden' + ]); + die(); + return false; // this line will never run + } + } + + + /** hasValidRole + * like allowRoles() but dowes not stop execution + * + * @return true|false + */ + public static function hasValidRole($allowed) + { + $manager = new Classroom_manager(); + return ($manager->isGranted($allowed)); + } + + + public static function in_admin_group() + { + $manager = new Classroom_manager(); + return ($manager->isGranted([1, 2, 3])); + } + + + +} + + +// NOTE: +// check: https://netcorecloud.com/tutorials/send-an-email-via-gmail-smtp-server-using-php/ \ No newline at end of file diff --git a/public/app/controllers/admin/.gitkeep b/public/app/controllers/admin/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/public/app/controllers/admin/Course_admin.php b/public/app/controllers/admin/Course_admin.php new file mode 100644 index 0000000..fcf38c9 --- /dev/null +++ b/public/app/controllers/admin/Course_admin.php @@ -0,0 +1,676 @@ +query( + "INSERT INTO course (parent_id, label) VALUES (:parent, :label)", + [ + ':parent' => Registry::get('REQUEST')->POST['parent_id'], + ':label' => Registry::get('REQUEST')->POST['label'] + ] + )->lastInsertID(); + + $cache = self::update_courses_cache(); // update category caches + return $cache['breadcrumbs']; // return + } + + + /** update course + * --- + * @param void (get data from POST) + */ + public static function update_course() + { + Registry::use('database')->query( + "UPDATE course SET parent_id = :parent, label = :label + WHERE id = :id", + [ + ':id' => Registry::get('REQUEST')->POST['id'], + ':parent' => Registry::get('REQUEST')->POST['parent_id'], + ':label' => Registry::get('REQUEST')->POST['label'] + ] + ); + $cache = self::update_courses_cache(); + return $cache['breadcrumbs']; + } + + + /** update courses cache + * --- -- -- - - - + * Forces re-caching of courses tree + * Shall run if anything changes to courses + * + * @param void + * @return (array): ['tree' => ..., 'breadcrumbs' => ... ] + */ + public static function update_courses_cache() + { + return Cache_service::courses_struct(PROXY_IGNORE_CACHE); + } + + + ## ------------------------------------------------------------------------- + ## + ## LESSON METHODS + ## + ## ------------------------------------------------------------------------- + + + /** all lessons + * + */ + public static function all_lessons() + { + Render::json(Registry::use('database')->runQuery( + "SELECT lesson.*, lesson_privilege.privilege_id FROM lesson + LEFT JOIN lesson_privilege ON lesson_privilege.lesson_id = lesson.id", + [] + )); + } + + + /** get_lesson + * + * @param $id (int) : lesson's id + */ + public static function get_lesson($id) + { + // get (first) lesson with id = $id; render as json + Render::json(Registry::use('database')->query( + "SELECT lesson.*, + ( SELECT + CONCAT('[', GROUP_CONCAT(JSON_OBJECT( + 'id', media.id, + 'label', media.label, + 'type', media.type, + 'path', media.path )), + ']') + FROM media + LEFT JOIN lesson_media ON lesson_media.media_id = media.id + WHERE lesson_media.lesson_id = :id + ) AS medias_json, + lesson_privilege.privilege_id + FROM lesson + LEFT JOIN lesson_privilege ON lesson_privilege.lesson_id = lesson.id + WHERE lesson.id = :id", + [ ':id' => $id] + )->getFirst()); + } + + + /** add_lesson + * insert a new lesson + * + * all parametres are passed via $_POST array + * + * POST @param title + * POST @param course_id + * POST @param intro + * POST @param body + * POST @param published + */ + public static function add_lesson() + { + $post = Registry::get('REQUEST')->POST; + + // insert lesson content into daabase + $id = Registry::use('database')->query( + "INSERT INTO lesson (title, course_id, intro, `body`, `status`) + VALUES (:title, :courseid, :intro, :body, :status)", + [ + ':title' => $post['title'], + ':courseid' => $post['course_id'], + ':intro' => $post['intro'], + ':body' => $post['body'], + 'status' => $post['status'] + ] + )->lastInsertID(); + + // set lesson privileges to database + Registry::use('database')->runQuery( + "INSERT INTO lesson_privilege (lesson_id, privilege_id) + VALUES (:lesson_id, :privilege_id)", + [ + ':lesson_id' => $id, + ':privilege_id' => $post['privilege_id'] + ] + ); + + if (isset($post['media'])) { + // update media's privileges; NOTE: media table + self::update_medias_privileges($post['media'], $post['privilege_id']); + + // set lesson's media files; NOTE: lesson_media table + // ERROR: self::create_medias_for_lesson($post['media'], $id, $post['privilege_id']); + self::create_medias_for_lesson($post['media'], $id); + + // recache media + Cache_service::files_attributes(PROXY_IGNORE_CACHE); + } + + return true; + } + + + public static function update_lesson() + { + $post = Registry::get('REQUEST')->POST; + + // print_r($post); die(); + + // update lesson's content + Registry::use('database')->query( + "UPDATE lesson + SET title = :title, + course_id = :courseid, + intro = :intro, `body` = :body, + `status` = :status + WHERE id = :id", + [ + ':id' => $post['id'], + ':title' => $post['title'], + ':courseid' => $post['course_id'], + ':intro' => $post['intro'], + ':body' => $post['body'], + ':status' => $post['status'] + ] + ); + + // update lesson's privileges + Registry::use('database')->runQuery( + "UPDATE lesson_privilege SET privilege_id = :privilege_id + WHERE lesson_id = :lesson_id", + [ + ':lesson_id' => $post['id'], + ':privilege_id' => $post['privilege_id'] + ] + ); + + if (isset($post['media'])) { + // update media's privileges + self::update_medias_privileges($post['media'], $post['privilege_id']); + } + + // remove all old lesson's links to media files + Registry::use('database')->runQuery( + "DELETE from lesson_media WHERE lesson_id = :lesson", + [ ':lesson' => $post['id'] ] + ); + + if (isset($post['media'])) { + // update lesson's media files + // ERROR: self::create_medias_for_lesson($post['media'], $post['id'], $post['privilege_id']); + self::create_medias_for_lesson($post['media'], $post['id']); + + // recache media + Cache_service::files_attributes(PROXY_IGNORE_CACHE); + } + + return true; + } + + + ## ------------------------------------------------------------------------- + ## + ## FILE METHODS + ## + ## ------------------------------------------------------------------------- + + + /** files + * echo all files + * + * @return (array) + */ + public static function files() + { + return Cache_service::files_attributes(); + } + + + /** upload_file + * upload the file to the file system + * + * the method reads the POST and FILES array + * to retrieve all needed parametres + * + * FILES @param file + * POST @param folder : lesson's ID or somthing random + * POST @param reference : reference type + * POST @param + */ + public static function upload_file() + { + $request = Registry::get('REQUEST'); + + $uploaded = self::upload_to_fs(); // upload file to file-system + + if ($uploaded['success']) { + + $media_id = self::define_media([ // define media in database; get id + 'title' => $request->POST['title'], + 'type' => $uploaded['type'], + 'path' => $uploaded['path'] + ]); + + Render::json([ // render results as json + 'success' => true, + 'id' => $media_id, + 'title' => $request->POST['title'], + 'path' => $uploaded['path'], + 'type' => $uploaded['type'] + ]); + + } else { + Render::json(['success' => false ]); + } + } + + + /** upload to fs + * upload file to File-System + * + * POST @param folder + * FILES @param file + */ + private static function upload_to_fs() + { + $post = Registry::get('REQUEST')->POST; + $files = Registry::get('REQUEST')->FILES; + + + // Checks before uploading the file + //////////////////////////////////////////////////////////////////////// + + // ** 1: file is upladed to temporary folder --------------------------- + if (! is_uploaded_file($files['file']['tmp_name'])) { + return ['success' => false]; // bye! + } + + // ** 2: File belongs to the allowed MIME types ------------------------ + $allowed_file_types = array( + 'application/pdf', + 'image/png', 'image/jpeg', + 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' + ); + // Recomended MIME type checking via mime_content_type(): + $mime_type = mime_content_type($files['file']['tmp_name']); + if (! in_array($mime_type, $allowed_file_types)) { // File type NOT allowed ... + return ['success' => false]; // bye! + } + + $file_name = $files['file']['name']; + $file_type = $files['file']['type']; // do not take it for granted + $file_size = $files['file']['size']; + $file_tmp = $files['file']['tmp_name']; + + $bare_name = pathinfo($file_name, PATHINFO_FILENAME); + $file_ext = pathinfo($file_name, PATHINFO_EXTENSION); + + + // ** 3: filename or size checks may be added -------------------------- + if ($file_name == "") { + return ['success' => false]; // bye! + } + + + // READY to finaly save/upload the file to CDN ///////////////////////// + + $folder = MEDIA_STORAGE_ROOT . $post['folder']; + if (!file_exists($folder)) { // create folder if not exists + mkdir($folder, 0757, true); + } + + // print_r([ + // 'dir' => $folder, + // 'file' => $bare_name, + // 'ext' => $file_ext, + // 'type' => $file_type + // ]); die(); + + $relative_filename = $post['folder'] + .'/' + . strtolower(self::clear_file_name($bare_name) .'.'. $file_ext); + $store_filename = MEDIA_STORAGE_ROOT . $relative_filename; + + if (move_uploaded_file($files["file"]["tmp_name"], $store_filename)) { + return [ + 'success' => true, + 'path' => $relative_filename, + 'type' => $mime_type + ]; + + } else { return ['success' => false]; } + + } + + + /** clear_file_name + * replace greek characters and strip symbols + */ + private static function clear_file_name($str) + { + $el = mb_split( "ΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨΩαβγδεζηθικλμνξοπρστυφχψωάέήίόύώϊϋς ", ""); + $en = str_split("ABGDEZHUIKLMNJOPRSTYFXCVabgdezhuiklmnjoprstyfxcvaehioyviys-"); + $strip = str_split("!@#$%^&*()+~`[]{};'/<>?=\""); + + return str_replace($strip, '', str_replace($el, $en, $str)); + } + + + /** define_media + * + * create a record in media table + * + * @param $data (array): [title => , path => , type => mime-type] + * @return id (int): id of created media record + */ + private static function define_media($data) + { + $request = Registry::get('REQUEST'); + + $media_id = Registry::use('database')->query( + "INSERT INTO media (label, `type`, `path`) + VALUES (:label, :mimetype, :filepath)", + [ + 'label' => $data['title'], + 'mimetype' => $data['type'], + 'filepath' => $data['path'] + ] + )->lastInsertID(); + return $media_id; + } + + + /** create media for lesson + * + * links lesson to each media-file of the media `id`s array + * + * @param $media (array): a list of media-file `id`s + * @param $lesson_id (int) + */ + private static function create_medias_for_lesson($medias, $lesson_id) + { + foreach($medias as $key => $medi) { + self::link_media_to_lesson($medi, $lesson_id); // link to lesson + } + return true; + } + + + /** create media for page + * + * links page to each media-file of the media `id`s array + * + * @param $media (array): a list of media-file `id`s + * @param $page_id (int) + */ + private static function create_medias_for_page($medias, $page_id) + { + foreach($medias as $key => $medi) { + self::link_media_to_page($medi, $page_id); // link to page + } + return true; + } + + + /** link one media-file to a specific post + * + * NOTE: + * the method does not check if media is linked already + * so be sure that the pair of (media_id,post_id) not exist + * + * @param $media_id (int) + * @param $lesson_id (int) + */ + private static function link_media_to_lesson( $media_id, $lesson_id ) + { + Registry::use('database')->runQuery( + "INSERT INTO lesson_media (lesson_id, media_id) + VALUES (:lesson, :media)", + [ + 'lesson' => $lesson_id, + 'media' => $media_id, + ] + ); + return true; + } + + /** link one media-file to a specific page + * + * NOTE: + * the method does not check if media is linked already + * so be sure that the pair of (media_id, page_id) not exist + * + * @param $media_id (int) + * @param $page_id (int) + */ + private static function link_media_to_page( $media_id, $page_id ) + { + Registry::use('database')->runQuery( + "INSERT INTO page_media (page_id, media_id) + VALUES (:page, :media)", + [ + 'page' => $page_id, + 'media' => $media_id, + ] + ); + return true; + } + + + /** update medias privileges + * + * UPDATE media SET privige WHERE IN {list} + * + * @param $medias (array) : array of media id(s) + * @param $privilege (int) : privilege id + * @return true (always) + */ + private static function update_medias_privileges($medias, $privilege) + { + // create WHERE IN (LIST) holders and params for prepare statement + $params = [ ':privilege' => $privilege]; + $holders = []; + foreach($medias as $key => $media) { + $params[':id'.$key] = $media; + $holders[] = ':id'.$key; + } + $list = '('. implode(',', $holders) .')'; + + // print_r(['params' => $params, 'list' => $list]); die(); + + Registry::use('database')->runQuery( + "UPDATE media SET privilege_id = :privilege + WHERE id IN {$list}", + $params + ); + + return true; + } + + + + + + + ## ------------------------------------------------------------------------- + ## + ## PAGE METHODS + ## + ## ------------------------------------------------------------------------- + + + /** all pages + * + */ + public static function all_pages() + { + Render::json(Registry::use('database')->runQuery( + "SELECT * FROM page", [] + )); + } + + + /** get_page + * + * @param $id (int) : page's id + */ + public static function get_page($id) + { + // get (first) page with id = $id; render as json + Render::json(Registry::use('database')->query( + "SELECT page.*, + ( SELECT + CONCAT('[', GROUP_CONCAT(JSON_OBJECT( + 'id', media.id, + 'label', media.label, + 'type', media.type, + 'path', media.path )), + ']') + FROM media + LEFT JOIN page_media ON page_media.media_id = media.id + WHERE page_media.page_id = :id + ) AS medias_json + FROM page + WHERE page.id = :id", + [ ':id' => $id] + )->getFirst()); + } + + + /** add_page + * insert a new page + * + * all parametres are passed via $_POST array + * + * POST @param title + * POST @param body + * POST @param status (int): 0 = draft , 1 = published + */ + public static function add_page() + { + $post = Registry::get('REQUEST')->POST; + + // insert page content into daabase + $id = Registry::use('database')->query( + "INSERT INTO page (title, `body`, `status`) + VALUES (:title, :body, :status)", + [ + ':title' => $post['title'], + ':body' => $post['body'], + 'status' => $post['status'] + ] + )->lastInsertID(); + + + if (isset($post['media'])) { + // update media's privileges; + // NOTE: media table; pages have public files (privilege_id=0) + self::update_medias_privileges($post['media'], 0); + + // set page's media files + // NOTE: page_media table + self::create_medias_for_page($post['media'], $id); + } + + self::update_pages_cache(); // update pages cache + + return true; + } + + + /** update_page + * + * POST @param id (int) + * POST @param title + * POST @param body + * POST @param status (int): 0 = draft , 1 = published + */ + public static function update_page() + { + $post = Registry::get('REQUEST')->POST; + + // print_r($post); die(); + + // update page's content + Registry::use('database')->query( + "UPDATE page + SET title = :title, `body` = :body, `status` = :status + WHERE id = :id", + [ + ':id' => $post['id'], + ':title' => $post['title'], + ':body' => $post['body'], + ':status' => $post['status'] + ] + ); + + if (isset($post['media'])) { + // update media's privileges + self::update_medias_privileges($post['media'], 0); + } + + // remove all old page's links to media files + Registry::use('database')->runQuery( + "DELETE from page_media WHERE page_id = :page", + [ ':page' => $post['id'] ] + ); + + if (isset($post['media'])) { + // update page's media files + self::create_medias_for_page($post['media'], $post['id']); + } + + self::update_pages_cache(); // update pages cache + + return true; + } + + + /** update pages cache + * --- -- -- - - - + * Forces re-caching of pages tree + * Shall run if anything changes to courses + * + * @param void + * @return (array): ['tree' => ..., 'breadcrumbs' => ... ] + */ + public static function update_pages_cache() + { + return Cache_service::pages_list(PROXY_IGNORE_CACHE); + } + +} \ No newline at end of file diff --git a/public/app/controllers/admin/Users_admin.php b/public/app/controllers/admin/Users_admin.php new file mode 100644 index 0000000..4e335c1 --- /dev/null +++ b/public/app/controllers/admin/Users_admin.php @@ -0,0 +1,42 @@ +runQuery( + "SELECT * FROM privilege",[] + ); + } + + /** edit privileges + * --- + */ + public static function edit_privileges() + { + // TODO: ... + } + + + /** update course + * --- + */ + public static function update_course() + { + // TODO: ... + } + +} \ No newline at end of file diff --git a/public/app/controllers/api/.gitkeep b/public/app/controllers/api/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/public/app/controllers/api/Common_api.php b/public/app/controllers/api/Common_api.php new file mode 100644 index 0000000..a8bd75f --- /dev/null +++ b/public/app/controllers/api/Common_api.php @@ -0,0 +1,281 @@ + data-source] pairs + * where 'label' is a friendly name of the datasource + * (and 'datasource' the actual table/data-source) + */ + public static function APITables() + { + /** allowed tables for api call + * @return: (array) an of api-call arrays + * each api-call array has the folloing form: + * [ label => [ + * 'table' => (string) actual table in database, + * (optional) 'filter' => (array) of allowed filtering fields, + * (optional) 'sort' => (array) of allowed sorting fields, + * ] + * ] + */ + return [ + 'page' => [ + 'table' => 'pages', + 'filter' => ['Title'] + ], + 'product' => [ + 'table' => 'products', + 'filter' => [ 'ID', 'Title' ], + 'sort' => ['ID'] + ], + 'category' => [ + 'table' => 'product_categories', + 'filter' => ['Title', 'Hierarchy', 'Level'] + ] + ]; + } + + + /** select anything on any (allowed table) + * + supports filtering and sorting + * check self::parse_Where_OrderBy() for options; + * sets a limti of 1000 records + */ + public static function table($table) + { + $sources = self::APITables(); + if (array_key_exists($table, $sources)) { + + $query = Registry::get('REQUEST')->QUERY; + + // Get parametres? => parse filters + if ($query !== false) { + + $parsed = self::parse_Where_OrderBy( + $query, + ($sources[$table]['filter'] ?? []), + ($sources[$table][ 'sort' ] ?? []) + ); + $where = $parsed['filter'] ? (' WHERE ' . $parsed['filter']) : ''; + $orderBy = $parsed['sort'] ? (' ORDER BY '. $parsed['sort']) : '' ; + $bindArguments = $parsed['bind']; + + } else { + $where = ''; + $orderBy = ''; + $bindArguments = []; + } + + $db = Registry::use('database'); + $result = $db->runQuery('SELECT * + FROM '. $sources[$table]['table'] + . $where + . $orderBy + .' LIMIT 1000', + $bindArguments + ); + + reply_json([ + 'success' => true, + 'client' => Registry::get('REQUEST')->SIGNATURE, + 'result' => $result + ]); + + } else { + reply_json(['success' => false]); + } + die(); + } + + + /** get record of {table} by ID + * (if table has no ID then return false) + */ + public static function record($table, $id) + { + $sources = self::APITables(); + if (array_key_exists($table, $sources)) { + + $db = Registry::use('database'); + $result = $db->runQuery("SELECT * + FROM ". $sources[$table]['table'] ." + WHERE ID = :id", + [':id' => $id] + ); + reply_json([ + 'success' => true, + 'data' => $result[0] + ]); + + } else { + reply_json(['status' => false]); + + } + die(); + } + + + /** category_by_url + * product categorie by full-friendly-URL + * @param $url (string): full friendly url + */ + public static function category_by_url($url) + { + $category = proxy( + [\app\models\market\ProductCategories_model::class, 'categoryFromUrl'], + [ $url ], CACHE_CATEGORY_TTL + ); + if ($category !== false) { + reply_json([ + 'success' => true, + 'result' => $category + ]); + + } else { + reply_json(['status' => false]); + + } + die(); + } + + /** parse Where & OrderBy + * ------------------------------------------------------------------------- + * parses SAFELY the query string to Where {CONDITIONS} and ORDER BY clauses + * according to the specified rules. + * + * The rules: + * ** filters: ?fieldname=[operator]:value &... + * where operators:[ like | startlike | endlike | eq | gt | gteq | lt |t leq ] + * + * ** Order by: ?_sort=fieldname[:[asc|desc]][,field[,]] + * + * for example: ?id=gt:4&active=1&_sort:reputation:desc,category + * parses to WHERE id > 4 AND active = 4 ORDER BY reputation desc, catetory asc + * + * ------------------------------------------------------------------------- + * arguments: + * @param $query (string): string to be parsed + * @param $filters (array): the allowed fields to apply filters + * @param $sortings (array): the allowed fields to sort the result + * @return array('filter'=>(string) , 'sort'=>(string) , 'bind'=>(array)) + */ + private static function parse_Where_OrderBy($query, $filters=[], $sortings=[]) + { + // break query to [key => value] pairs + parse_str($query, $queryArray); + + $filterClause = []; // array to hold filter/WHERE clauses + $sortClause = []; // array to hold sort/ORDER-BY caluses + $bindings = []; // array to hold variable bindigs + + // parse filers + // --------------------------------------------------------------------- + foreach($queryArray as $filter => $value) { + + if (in_array($filter, $filters)) { + + $parts = explode(':', $value ); // that is => [operator], value + if (count($parts) == 0) { + // forget it + + } else if (count($parts) == 1) { + $filterClause[] = "{$filter} = :{$filter}"; + $bindings[$filter] = $parts[0]; + + } else { + + switch ($parts[0]) { + case 'like': + $filterClause[] = "{$filter} LIKE :{$filter}"; + $bindings[':'.$filter] = '%'.$parts[1].'%'; + break; + + case 'startlike': + $filterClause[] = "{$filter} LIKE :{$filter}"; + $bindings[':'.$filter] = $parts[1].'%'; + break; + + case 'endlike': + $filterClause[] = "{$filter} LIKE :{$filter}"; + $bindings[':'.$filter] = '%'.$parts[1]; + break; + + case 'gt': + $filterClause[] = "{$filter} > :{$filter}"; + $bindings[':'.$filter] = $parts[1]; + break; + + case 'gteq': + $filterClause[] = "{$filter} >= :{$filter}"; + $bindings[':'.$filter] = $parts[1]; + break; + + case 'lt': + $filterClause[] = "{$filter} < :{$filter}"; + $bindings[':'.$filter] = $parts[1]; + break; + + case 'lteq': + $filterClause[] = "{$filter} <= :{$filter}"; + $bindings[':'.$filter] = $parts[1]; + break; + + case 'eq': + default: + $filterClause[] = "{$filter} = :{$filter}"; + $bindings[':'.$filter] = $parts[1]; + } + } + } + } + $whereSQL = ($filterClause == []) + ? false + : implode(' AND ', $filterClause); + + + // parse sort options + // --------------------------------------------------------------------- + if (isset($queryArray['_sort'])) { + $sortTerms = explode(',', $queryArray['_sort']); + + foreach($sortTerms as $term) { + + $parts = explode(':', $term); + if ($parts != [] && in_array($parts[0], $sortings) ) { + $sortClause[] = (count($parts)==1) + ? $parts[0] + : $parts[0] .' '. (($parts[1] == 'desc') ? 'desc' : 'asc'); + } + } + + } + $sortSQL = ($sortClause == []) + ? false + : implode(', ', $sortClause); + + return ([ + 'filter' => $whereSQL, + 'sort' => $sortSQL, + 'bind' => $bindings + ]); + + } + +} \ No newline at end of file diff --git a/public/app/controllers/api/Doc_api.php b/public/app/controllers/api/Doc_api.php new file mode 100644 index 0000000..e893552 --- /dev/null +++ b/public/app/controllers/api/Doc_api.php @@ -0,0 +1,34 @@ + true, 'result' => $tree ]); + die(); + } + + + public static function dbDoc() + { + $j = new Jorge(); + reply_json([ + 'success' => true, + 'result' => $j->databaseDocumentation() + ]); + die(); + + } +} \ No newline at end of file diff --git a/public/app/controllers/cli/CliContoller.php b/public/app/controllers/cli/CliContoller.php new file mode 100644 index 0000000..4549b50 --- /dev/null +++ b/public/app/controllers/cli/CliContoller.php @@ -0,0 +1,55 @@ +GET['type']; // get media-type + $real_path = MEDIA_STORAGE_ROOT . $file_path; // construct real path + + if (!file_exists($real_path)) { + Render::view('error/404'); + + } else { + Render::file($real_path, $media_type); + } + + } else { + Render::view('error/404', [ + 'error_code' => 403, + 'moto' => 'Forbidden', + 'message' => '' + ]); + } + } + + + /** get_file_attributes + * + * returns attributes of a file + * (medias are proxied for speed optimization) + * + * @param $path (string) : file path + * @return $file attributes --or-- false + */ + private static function get_file_attributes($path) + { + $medias = Cache_service::files_attributes(); + + foreach($medias as $key => $medi) { + + if ($medi['path'] == $path) { + + return $medi; + } + } + + return false; + } + + + + /** COURSES + * ------------------------------------------------------------------------- + */ + + /** course + * prepare and render the course view + * + * @param $id : course id + */ + public static function course($id) + { + $id = intval($id); // course id + + // collect all data needed for course view + // --- -- -- - - - + $cache = Cache_service::courses_struct(); + $tree = $cache['tree']; + $breadcrumbs = $cache['breadcrumbs']; + $lessons = Course_model::lessons_of_course(intval($id)); + + // find parent_ids of current category (to open the menu tree) + // --- + $course_path = []; + foreach($breadcrumbs[$id]['parents'] as $key => $parent) { + $course_path[] = intval($parent['id']); + } + $course_path[] = $id; + + // then Render + // --- -- -- - - - + Render::view('templates/course', [ + 'id' => $id, + 'title' => $breadcrumbs[ $id ]['rec']['label'], + 'categories' => $tree, + 'breadcrumbs' => $breadcrumbs, + 'lessons' => $lessons, + 'course_path' => $course_path, + // needed by footer + 'entity' => Cache_service::pages_list() + ]); + } + + + + /** LESSONS + * ------------------------------------------------------------------------ + */ + + + /** lesson + * + * check if user is authorized to view the content; + * if so, prepare and render the lesson view + * + * @param $id : lesson id + */ + public static function lesson($id) + { + $id = intval($id); // lesson id + + $lesson = Course_model::lesson($id); // get lesson + $course_id = $lesson['course_id']; // get course id + $pri_id = $lesson['privilege_id']; // privilege needed + + + if (self::is_admin_or_permited($pri_id)) { + + // collect all data needed for course view + // --- -- -- - - - + $cache = Cache_service::courses_struct(); + $tree = $cache['tree']; + $breadcrumbs = $cache['breadcrumbs']; + + // find parent_ids of current category (to open the menu tree) + // --- + $course_path = []; + foreach($breadcrumbs[$course_id]['parents'] as $key => $parent) { + $course_path[] = intval($parent['id']); + } + $course_path[] = intval($course_id); + + // NOTE: CRITICAL: + // do not pass Class::method directly to the eported variables + // $entity = Cache_service::pages_list(); + + // var_dump($entity); die(); + + // then Render + // --- -- -- - - - + Render::view('templates/lesson', [ + // main content + 'id' => $id, + 'course_id' => $course_id, + 'title' => $lesson['title'], + 'lesson' => $lesson, + // needed for side panel and breadcrunbs + 'categories' => $tree, + 'breadcrumbs' => $breadcrumbs, + 'course_path' => $course_path, + // needed by footer + 'entity' => Cache_service::pages_list() + ]); + + } else { // user is not authorized + Render::view('error/general', [ + 'title' => 'Δεν έχετε πρόσβαση', + 'message' => 'Θα πρέπει να αγοράσετε το πακέτο πρόσβασης ' + . $pri_id + .' για να δείτε το περιεχόμενο! +

+ Αγόρασε τώρα το ' + ]); + } + + } + + + /** lesson + * + * check if user is authorized to view the content; + * if so, prepare and render the lesson view + * + * @param $id : lesson id + */ + public static function page($id) + { + $id = intval($id); // lesson id + + $pages = Cache_service::pages_list(); + $page = $pages[$id]; + + // then Render + // --- -- -- - - - + Render::view('templates/page', [ + // main content + 'id' => $id, + 'page' => $page, + // needed by footer + 'entity' => $pages + ]); + + } + + + + +} \ No newline at end of file diff --git a/public/app/extends/.gitkeep b/public/app/extends/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/public/app/extends/Cache_service.php b/public/app/extends/Cache_service.php new file mode 100644 index 0000000..52311f3 --- /dev/null +++ b/public/app/extends/Cache_service.php @@ -0,0 +1,82 @@ +POST + * + */ + public function register_user() + { + $req = Registry::get('REQUEST'); + $register = User_model::registerUser($req->POST); + + if ($register['success']) { + + //create OTP; + $otp = $this->create_OPT($register['id']); + + // send for account confirmation + + + } else { + print_r($register); + } + + } + + + public function login_user() + { + + } + + + /** forgot password + * + * send an otp + * (then verify) + * + */ + public function forgot_password() + { + + } + + + /** create OTP + * + * create a random OTP + * keep it into User's database Table + * + * @param $id (int): user id + * + */ + public function create_OTP($id) + { + $otp = rand(100000,999999); + User_model::set_OTP($id, $otp); + + return true; + } + + + /** Verify Account + * + * check if posted OTP matched the one sent to the user + * + * @param $identity (array): pair of [index_key => identity_value] + * example: [ 'email' => 'geo@roptron.gr' ] + * + */ + public function verify_otp($identity, $otp) + { + + } + + +} \ No newline at end of file diff --git a/public/app/extends/Classroom_user.php b/public/app/extends/Classroom_user.php new file mode 100644 index 0000000..c27c4bf --- /dev/null +++ b/public/app/extends/Classroom_user.php @@ -0,0 +1,121 @@ +id; + } + + /** setID() + * + * @param int : user id + * + * @return User + */ + public function setID(int $id): self + { + $this->id = $id; + return $this; + } + + + // name + // --- -- -- - - - + + /** getName + * @return int + */ + public function getName(): string + { + return $this->name; + } + + /** setName() + * + * @param string : user's full name + * + * @return User + */ + public function setName(string $name): self + { + $this->name = $name; + return $this; + } + + + // privileges + // --- -- -- - - - + + /** getPrivileges + * + * @return privileges (array) + */ + public function getPrivileges(): array + { + return $this->privileges; + } + + + /** setPrivileges() + * + * @param array $privileges + * + * @return User + */ + public function setPrivileges(array $privileges): self + { + $this->privileges = $privileges; + return $this; + } + + +} \ No newline at end of file diff --git a/public/app/extends/Send_mail.php b/public/app/extends/Send_mail.php new file mode 100644 index 0000000..52eab53 --- /dev/null +++ b/public/app/extends/Send_mail.php @@ -0,0 +1,212 @@ + user email + * name => user full name + * subject => subject + * body => email message body + * ] + * + * then send the email; + * + * @param $activator (array): [ + * email => user email, + * name => user full name, + * code => activation code + * ] + * + * @return success_starus (boolean) + * + * TODO: + * check for email templating: + * + https://stackoverflow.com/questions/2391171/how-to-get-output-from-local-script-in-php + * + https://stackoverflow.com/questions/171318/how-do-i-capture-php-output-into-a-variable + */ + public static function send_activation_code($activator) + { + $activation_url = SITE_URL ."/account/activate?ticket=". $activator['code']; + $envelope = [ + 'email' => $activator['email'], + 'name' => $activator['name'], + 'subject' => 'Εγγραφή στο Classroom', + 'body' => "Χαίρετε,
+ το παρόν αυτοποιημένο email σάς έχει σταλεί + γιατί έχει γίνει αίτημα εγγραφής σας στο Classroom.
+
+ Όνομα επαφής: ". $activator['name'] ."
+ Email : ". $activator['email'] ."
+
+ Για να ενεργοποιήσετε την πρόσβασή σας στο site + θα πρέπει ακολουθήσετε τον παρακάτω σύνδεσμο: + ". $activation_url .". +
+
+ Μετά την ενεργοποίηση θα έχετε πρόσσβαση + στο περιεχόμενο του site.
+ Μην απαντήσετε στο email, + δεν υπάρχει φυσική επαφή η οποία να λαμβάνει τυχόν replies." + ]; + + switch (DEFAULT_MAIL_SERVICE) { + case 'mailjet': + $reply = self::send_mailjet($envelope); + break; + + case 'phpMailer': + $reply = self::send_phpMailer($envelope); + break; + } + + return $reply; + + } + + + /** send phpMailer + * + * send email using phpMailer class; + * optional SMTP server configuration; + * + * @param $envelope + * @return success_starus (boolean) + */ + public static function send_phpMailer($envelope) + { + $mail = new PHPMailer(); + + # // setup smpt + $mail->SMTPDebug = 3; + $mail->IsSMTP(); + $mail->Host = MAIL_HOST; + # $mail->SMPTAuth = false; + $mail->SMTPSecure = MAIL_ENCRYPTION; + # // $mail->Protocol = 'mail'; + + $mail->Mailer = MAIL_MAILER; + $mail->Port = MAIL_PORT; + $mail->Username = MAIL_USERNAME; + $mail->Password = MAIL_PASSWORD; + + // setup format + $mail->CharSet = 'utf-8'; + $mail->IsHTML(true); + + // setup THE mail + // --- -- -- - - - + // It's important not to use the submitter's address as the from address + // as it's forgery, which will cause your messages to fail SPF checks. + // Use an address in your own domain as the from address; + // put the submitter's address in a reply-to + + $mail->setFrom(NO_REPLY_EMAIL, MAIL_FROM_NAME); + $mail->addAddress($envelope['email'], $envelope['name']); + $mail->addReplyTo(REPLY_TO_EMAIL, MAIL_FROM_NAME); + $mail->Subject = $envelope['subject']; + $mail->Body = $envelope['body']; + + return (!$mail->send()) ? false : true; + + } + + + /** send_mailjet + * + * send email using CURL mail-relay of Mailjet + * + * @param $envelope + * @return success_starus (boolean) + */ + public static function send_mailjet($envelope) + { + $body = [ + 'Messages' => [ + [ + 'From' => [ + 'Email' => REPLY_TO_EMAIL, + 'Name' => MAIL_FROM_NAME + ], + 'To' => [ + [ + 'Email' => $envelope['email'], + 'Name' => $envelope['name'] + ] + ], + 'Subject' => $envelope['subject'], + 'HTMLPart' => $envelope['body'] + ] + ] + ]; + + $ch = curl_init(); + + curl_setopt($ch, CURLOPT_URL, "https://api.mailjet.com/v3.1/send"); + curl_setopt($ch, CURLOPT_POST, 1); + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body)); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, array( + 'Content-Type: application/json') + ); + curl_setopt( + $ch, + CURLOPT_USERPWD, + MAILJET_APIKEY. ':'. MAILJET_SECRET + ); + $server_output = curl_exec($ch); + curl_close ($ch); + + $response = json_decode($server_output); + + return ($response->Messages[0]->Status == 'success'); + + } + + + +} + + + + + +/** NOTE: + * ----------------------------------------------------------------------------- + * (brainstorming) + * + +Optimization per concept alternative technologies +--- -- -- - - - + +## Session +File Session +Database Session +Redis Session +Memcached Session + +--- + +## Cache +File Cache +Database Cache +Redis Cache +Memcashed Cache + +--- + +## Log +File Log +Database Log +Log event to Slack +Log event to Email + + * ----------------------------------------------------------------------------- + */ diff --git a/public/app/models/.gitkeep b/public/app/models/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/public/app/models/Jorge.php b/public/app/models/Jorge.php new file mode 100644 index 0000000..5d79007 --- /dev/null +++ b/public/app/models/Jorge.php @@ -0,0 +1,172 @@ + [], + 'relate' => [], + 'fields' => [] + ]; + + public function __construct() + { + $db = Registry::use('database'); + + // get all tables + $tableList = $db->runQuery("SHOW TABLES", []); + + // get all CREATE TABLE statements + foreach($tableList as $tableArray) { + $table = array_shift($tableArray); + $create = $db->runQuery("SHOW CREATE TABLE {$table}", []); + $this->tables['create'][$table] = $create[0]['Create Table']; + } + + // define relations + $this->tables['relate'] = [ + + 'lesson' => [ + 'course' => 'course.id = lesson.course_id', + 'lesson_media' => 'lesson_media.lesson_id = lesson.id', + 'lesson_privilege' => 'lesson_privilege.lesson_id = lesson.id' + ], + + 'page' => [ + 'page_media' => 'page_media.page_id = page.id' + ], + + 'user' => [ + 'user_privilege' => 'user_privilege.user_id = user.id' + ], + + 'privilege' => [ + 'user_privilege' => 'user_privilege.privilege_id = privilege.id' + ], + + ]; + + $this->extractTableFields(); + + // return true; + } + + + /** Create... + * one or more database tables + * --- + * @param $datasource (string|void) : table name or none + * if none then all tables will be created + * @return (boolean) + * + * NOTE: + * for safety reasons this method is not executing SQL; + * it just echoes the SQL that needs to be executed in + * order to crate all database tables. + */ + public function create($datasource = '') + { + // if no datasource passed then datasource is all tables + if ($datasource == '') { + $datasource = array_keys(self::$tables['create']); + + } else { + // if datasource exist, make it an array + if (array_key_exists($datasource, self::$tables['create'])) { + $datasource = [ $datasource ]; + + } else { + // table does not exist + return fasle; + } + } + + $sql = ""; + foreach( $datasource as $table ) { + $sql .= "-- CREATE ". $table .";\n". self::$tables['create'][$table] . "\n\n"; + } + + return ['sql' => $sql]; + } + + + /** Calculate fields of every table + * --- + * Parse every CREATE SQL statement and extract list of fields; + * Algorithm implements linear-parsing (faster than a recursive one) + */ + private function extractTableFields() + { + // words used by SQL that can not be field names + $nonFields = ['PRIMARY', 'KEY', '(', ')', 'CONSTRAINT', 'UNIQUE']; // reduced list (used on CREATE) + + foreach( $this->tables['create'] as $table => $sqlCreate ) { + + $fields = []; // variable to hold the extracted fields + + // get text between first open-parentesis and last close-parentesis + // this is waht lies between 'CREATE TABLE table(' and ') [ENGINE whatever]' + // (including the patenteses) + preg_match_all( + "/\((((?>[^()]+)|(?R))*)\)/", + str_replace("\n", '', $sqlCreate), + $match + ); + // remove 1st+last parentesis + $mainDefinitions = substr($match[0][0], 1, -1); + + // replace comma (,) on sub-parenthesis + // ex: 'decimal(18, 2)' turns to 'decimal(18: 2)' + // or: 'PRIMARY KEY (id1, id2)' turns to 'PRIMARY KEY (id1: id2)' + $sentences = preg_replace( + '/\\(([0-9a-zA-Z_`]*)[ ,]([0-9a-zA-Z_` ]*)\\)/', + '($1:$2)', + $mainDefinitions, + -1 + ); + + // devide the banth of sentences to field definitions + $defines = explode(',', $sentences); + + // now each denine holds a full field definition + // like: `ID` int(11) NOT NULL AUTO_INCREMENT + + foreach($defines as $def) { + // check the first term of each sentence + $terms = explode(' ', trim($def,)); + $term = array_shift($terms); + + if (!in_array($term, $nonFields)) { + $fields[ str_replace('`', '', $term) ] = implode(' ', $terms); + } + } + + $this->tables['fields'][$table] = $fields; + } + + return; + } + + public function databaseDocumentation() + { + return $this->tables['fields']; + } + +} \ No newline at end of file diff --git a/public/app/models/_info.md b/public/app/models/_info.md new file mode 100644 index 0000000..5504523 --- /dev/null +++ b/public/app/models/_info.md @@ -0,0 +1,150 @@ + +# info about the structrure of the models + +According to the former impementation (atcom) the project will need to handle 200+ tables. +The former project includes 209 tables with 5 or more rows (and 325 tables totaly) + + + +## Database schema + +Main tables and recomended constraints: + +``` +SET NAMES utf8; +SET time_zone = '+00:00'; +SET foreign_key_checks = 0; +SET sql_mode = 'NO_AUTO_VALUE_ON_ZERO'; + +SET NAMES utf8mb4; + +DROP TABLE IF EXISTS `course`; +CREATE TABLE `course` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `parent_id` int(11) NOT NULL COMMENT 'if 0 then this is a root course', + `label` varchar(140) COLLATE utf8mb4_unicode_ci NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + + +DROP TABLE IF EXISTS `lesson`; +CREATE TABLE `lesson` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `course_id` int(11) NOT NULL, + `title` varchar(140) COLLATE utf8mb4_unicode_ci NOT NULL, + `body` text COLLATE utf8mb4_unicode_ci NOT NULL, + `published` tinyint(4) NOT NULL DEFAULT '0' COMMENT '0 = unpublished, 1 = published', + PRIMARY KEY (`id`), + KEY `course_id` (`course_id`), + CONSTRAINT `lesson_ibfk_1` FOREIGN KEY (`course_id`) REFERENCES `course` (`id`), + CONSTRAINT `lesson_ibfk_2` FOREIGN KEY (`course_id`) REFERENCES `course` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + + +DROP TABLE IF EXISTS `lesson_media`; +CREATE TABLE `lesson_media` ( + `lesson_id` int(11) NOT NULL, + `media_id` int(11) NOT NULL, + PRIMARY KEY (`lesson_id`,`media_id`), + KEY `media_id` (`media_id`), + CONSTRAINT `lesson_media_ibfk_1` FOREIGN KEY (`lesson_id`) REFERENCES `lesson` (`id`), + CONSTRAINT `lesson_media_ibfk_2` FOREIGN KEY (`lesson_id`) REFERENCES `lesson` (`id`), + CONSTRAINT `lesson_media_ibfk_3` FOREIGN KEY (`lesson_id`) REFERENCES `lesson` (`id`), + CONSTRAINT `lesson_media_ibfk_4` FOREIGN KEY (`media_id`) REFERENCES `media` (`id`), + CONSTRAINT `lesson_media_ibfk_5` FOREIGN KEY (`lesson_id`) REFERENCES `lesson` (`id`) ON DELETE NO ACTION, + CONSTRAINT `lesson_media_ibfk_6` FOREIGN KEY (`media_id`) REFERENCES `media` (`id`) ON DELETE NO ACTION, + CONSTRAINT `lesson_media_ibfk_7` FOREIGN KEY (`lesson_id`) REFERENCES `lesson` (`id`) ON DELETE NO ACTION, + CONSTRAINT `lesson_media_ibfk_8` FOREIGN KEY (`media_id`) REFERENCES `media` (`id`) ON DELETE NO ACTION +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + + +DROP TABLE IF EXISTS `lesson_privilege`; +CREATE TABLE `lesson_privilege` ( + `lesson_id` int(11) NOT NULL, + `privilege_id` int(11) NOT NULL COMMENT 'minimum privilege required to access the lesson', + KEY `lesson_id` (`lesson_id`), + KEY `privilege_id` (`privilege_id`), + CONSTRAINT `lesson_privilege_ibfk_1` FOREIGN KEY (`lesson_id`) REFERENCES `lesson` (`id`), + CONSTRAINT `lesson_privilege_ibfk_2` FOREIGN KEY (`privilege_id`) REFERENCES `privilege` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + + +DROP TABLE IF EXISTS `media`; +CREATE TABLE `media` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `label` varchar(140) COLLATE utf8mb4_unicode_ci NOT NULL, + `type` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL, + `path` varchar(320) COLLATE utf8mb4_unicode_ci NOT NULL, + `referable` tinyint(4) NOT NULL DEFAULT '1' COMMENT '0 = hidden, 1 = referable', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + + +DROP TABLE IF EXISTS `page`; +CREATE TABLE `page` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `title` varchar(140) COLLATE utf8mb4_unicode_ci NOT NULL, + `body` text COLLATE utf8mb4_unicode_ci NOT NULL, + `published` tinyint(4) NOT NULL DEFAULT '0' COMMENT '0 = unpublished; 1 = published', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + + +DROP TABLE IF EXISTS `page_media`; +CREATE TABLE `page_media` ( + `page_id` int(11) NOT NULL, + `media_id` int(11) NOT NULL, + KEY `page_id` (`page_id`), + KEY `media_id` (`media_id`), + CONSTRAINT `page_media_ibfk_1` FOREIGN KEY (`page_id`) REFERENCES `page` (`id`) ON DELETE NO ACTION, + CONSTRAINT `page_media_ibfk_2` FOREIGN KEY (`media_id`) REFERENCES `media` (`id`) ON DELETE NO ACTION +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + + +DROP TABLE IF EXISTS `privilege`; +CREATE TABLE `privilege` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `alias` varchar(8) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'keep it simple; use latin', + `label` varchar(140) COLLATE utf8mb4_unicode_ci NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `alias` (`alias`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + + +DROP TABLE IF EXISTS `privilege_inherit`; +CREATE TABLE `privilege_inherit` ( + `higher_id` int(11) NOT NULL COMMENT 'higher priviledges inherit (include) lower ones', + `lower_id` int(11) NOT NULL, + PRIMARY KEY (`higher_id`,`lower_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + + +DROP TABLE IF EXISTS `user`; +CREATE TABLE `user` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `first_name` int(11) NOT NULL, + `last_name` int(11) NOT NULL, + `email` int(11) NOT NULL, + `expiration` int(11) NOT NULL COMMENT 'account expiration date; 0 = never', + `password` int(11) NOT NULL, + `salt` int(11) NOT NULL, + `otp` int(11) NOT NULL COMMENT 'one time password for reset password', + `otp_expiration` int(11) NOT NULL, + `active` int(11) NOT NULL COMMENT 'account flag; 1=active, 0=inactive', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + + +DROP TABLE IF EXISTS `user_privilege`; +CREATE TABLE `user_privilege` ( + `user_id` int(11) NOT NULL, + `privilege_id` int(11) NOT NULL, + PRIMARY KEY (`user_id`,`privilege_id`), + KEY `privilege_id` (`privilege_id`), + CONSTRAINT `user_privilege_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`), + CONSTRAINT `user_privilege_ibfk_2` FOREIGN KEY (`privilege_id`) REFERENCES `privilege` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +``` + + diff --git a/public/app/models/admin/History_model.php b/public/app/models/admin/History_model.php new file mode 100644 index 0000000..04ce630 --- /dev/null +++ b/public/app/models/admin/History_model.php @@ -0,0 +1,34 @@ +query( + "INSERT INTO history + (user_id, `type`, `message`, `note`, `ip`) + VALUES + (:uid, :type, :msg, :note, :ip)", + [ + ':uid' => $user_id, + ':type' => $type, + ':msg' => $message, + ':note' => $note, + ':ip' => Registry::get('REQUEST')->IP + ] + )->lastInsertID(); + + } + + + +} \ No newline at end of file diff --git a/public/app/models/admin/Privilege_model.php b/public/app/models/admin/Privilege_model.php new file mode 100644 index 0000000..c385484 --- /dev/null +++ b/public/app/models/admin/Privilege_model.php @@ -0,0 +1,42 @@ +runQuery( + "SELECT * FROM privilege",[] + ); + } + + /** edit privileges + * --- + */ + public static function edit_privileges() + { + // TODO: ... + } + + + /** update course + * --- + */ + public static function update_course() + { + // TODO: ... + } + +} \ No newline at end of file diff --git a/public/app/models/admin/User_model.php b/public/app/models/admin/User_model.php new file mode 100644 index 0000000..2885dc1 --- /dev/null +++ b/public/app/models/admin/User_model.php @@ -0,0 +1,299 @@ +query( + "SELECT * FROM user WHERE email = :email AND active = 1", + [ ':email' => $email ] + )->getFirst(); + + // if no user, return false + if ($user === false) return false; + + return $user; + } + + + /** get user (by index key) + * + * user detailed array + * includes all user properties + granted roles + privileges + * + * @param $id (int) : user id + * @param $value (string) + */ + public static function getUser($id) + { + $user = Registry::use('database')->query( + "SELECT user.*, + ( -- construct array (json) of roles granted to user + SELECT CONCAT( + '[', + GROUP_CONCAT(role.id), + ']' + ) + FROM `role` + WHERE role.id IN ( + SELECT user_role.role_id + FROM user_role + WHERE user_role.user_id = :id + ) + ) AS Roles_json, + ( -- construct array of (root-)privileges granted to user + SELECT CONCAT( + '[', + GROUP_CONCAT(privilege.id), + ']' + ) + FROM privilege + WHERE privilege.id IN ( + SELECT user_privilege.privilege_id + FROM user_privilege + WHERE user_privilege.user_id = :id + ) + ) AS RootPrivileges_json, + ( -- construct array of (root-)privileges granted to user + SELECT CONCAT( + '[', + GROUP_CONCAT(privilege.includes), + ']' + ) + FROM privilege + WHERE privilege.id IN ( + SELECT user_privilege.privilege_id + FROM user_privilege + WHERE user_privilege.user_id = :id + ) + ) AS SubPrivileges_json + FROM user + WHERE id = :id", + [ ':id' => $id ] + )->getFirst(); + + + // if no user, return false + if ($user === false) return false; + + + // TODO: + // * merge root+sub privilede lists + // * convert json strings to php arrays + + + // TODO: + // cache user super array + + return $user; + } + + + /** create user + * + * creates user record; + * assigns privileged (usualy defaults); + * creates activation_code + * + * @param $data (array): Request->POST array + * @param $password (string): secure hashed password + * + * @return $activation_code + * + */ + public static function registerUser($data, $password, $privileges = DEFAULT_PRIVILEGES) + { + $required_fields = [ + 'name', + 'surname', + 'email', + 'password' + ]; + + // check required fields + $isOK = true; + foreach($required_fields as $fi) { + if (empty($data[$fi])) $isOK = false; + } + // if empty required fields exists ... return false + if (!$isOK) { + return [ "success" => false, 'error' => EMPTY_REQUIRED_FIELDS ]; + } + + + // TODO: + // check if email exists + // ... + + // create an activation code + $activation_code = md5($data['email'].time().rand(0, 10000)); + + // if isOK go on and... + // create user record + $new_user_id = Registry::use('database')->query( + "INSERT INTO user + (`first_name`, `last_name`, `email`, `password`, `active`, `activation`) + VALUES + (:nam, :surname, :email, :pass, :act, :actcode)", + [ + ':nam' => $data['name'], + ':surname' => $data['surname'], + ':email' => $data['email'], + ':pass' => $password, + ':act' => 0, // needs email confirmation to be activated ... + ':actcode' => $activation_code // ... with the activation code + ] + )->lastInsertID(); + + // set default privileges + self::set_user_privileges($new_user_id, $privileges); + + // set reader role + self::set_user_role($new_user_id, 5); + + // update history + History::trackUserAccess($new_user_id, TRACK_ACCOUNT, 'Create User Account'); + + // return success and user id + return [ + "success" => true, + 'id' => $new_user_id , + 'activation' => $activation_code + ]; + } + + + /** set_user_privileges + * + * @param $privileges (array) + */ + public static function set_user_privileges($user, $privileges) + { + $db = Registry::use('database'); // database connection + foreach($privileges as $pri) { // pri = privilege id + $db->runQuery( + "INSERT INTO user_privilege (user_id, privilege_id) VALUES (:user, :pri)", + [ ':user' => $user, ":pri" => $pri ] + ); + } + return true; + } + + + /** set_user_role + * + * user, role are (int) IDs + */ + public static function set_user_role($user, $role) + { + + Registry::use('database')->runQuery( + "INSERT INTO user_role (user_id, role_id) VALUES (:user, :role)", + [ ':user' => $user, ":role" => $role ] + ); + + return true; + } + + + /** activate + * + * check if activation code is valid; + * if valid, set account active; + * + * @param $ticket (hex/MD5): activation code; + * + */ + public static function activate($ticket) + { + $user = Registry::use('database')->query( + "SELECT * FROM user WHERE activation = :ticket", + [ 'ticket' => $ticket ] + )->getFirst(); + + // if no user with this activation code, return false + if ($user === false) return false; + + // remove activation code from user record + Registry::use('database')->runQuery( + "UPDATE user + SET active = 1, `activation` = NULL + WHERE activation = :ticket", + [ 'ticket' => $ticket ] + ); + + // update history + History::trackUserAccess($user['id'], TRACK_ACCOUNT, 'User Account Activated'); + + return true; + + } + + +} + +/* example query getUser (super-array) +--- -- -- - - - + +SELECT user.*, +( -- array (json) of roles granted to user + SELECT CONCAT('[', GROUP_CONCAT(role.id), ']') + FROM `role` + WHERE role.id IN ( + SELECT user_role.role_id + FROM user_role + WHERE user_role.user_id = 1 + ) +) AS Roles_json, +( + SELECT CONCAT( + '[', + GROUP_CONCAT(privilege.id), + ']' + ) + FROM privilege + WHERE privilege.id IN ( + SELECT user_privilege.privilege_id + FROM user_privilege + WHERE user_privilege.user_id = 1 + ) +) AS RootPrivileges_json, +( + SELECT CONCAT( -- array of array of sub-privileges + '[', + GROUP_CONCAT( -- array (json) of subprivileges + ( + SELECT CONCAT( + '[', + GROUP_CONCAT(included_id), + ']' + ) + FROM privilege_includes + WHERE privilege_id = privilege.id + ) + ), + ']' + ) + FROM privilege + WHERE privilege.id IN ( + SELECT user_privilege.privilege_id + FROM user_privilege + WHERE user_id = 1 + ) +) AS SubPrivileges_json +FROM user +WHERE id = 1 +--- */ \ No newline at end of file diff --git a/public/app/models/cms/Course_model.php b/public/app/models/cms/Course_model.php new file mode 100644 index 0000000..6614993 --- /dev/null +++ b/public/app/models/cms/Course_model.php @@ -0,0 +1,314 @@ +runQuery( + "SELECT * FROM course ORDER BY label", + [] + ); + } + + /** courses struct + * + * a super array with almost any info needed about courses + * + * @return (array) ['tree' => ..., 'breadcrumbs' => ... ] + */ + public static function courses_struct() + { + $tree = self::category_tree(); + return [ + 'tree' => $tree, + 'breadcrumbs' => self::breadcrumbs($tree) + ]; + } + + + /** constuct a category_tree + * + * returns a tree representation of the categories + * + * NOTE: + * category_tree() is an expensive method; + * it calls 2 other methods implementing recursive algorithms + * thus it uses many sources to run (particularly RAM). + * Caching the result is strogly recommended. + * + */ + public static function category_tree() + { + $categories = self::get_categories(); // get all categories + + $tree = self::to_tree($categories); // format to a tree + + $tree_wParents = self::tree_parents($tree); // add parents section for each tree-node + + return $tree_wParents; + } + + + /** to_tree + * + * constructs a tree from raw-table data; + * this is a private method and uses a recursive algorithm + * + * @param $dataset (array): flar array of records with id/parent-id pairs + * @return $root (array): id of root category + * + * (**) each node has 2 parts: + * .... .. rec : all record attributes/data as passed into $dataset + * .... .. childs : array of (children) nodes + */ + private static function to_tree($dataset, $root = 0) + { + $return = []; + + // loop data ; search for direct children of root + foreach($dataset as $key => $rec) { + + $child = $rec['id']; + $parent = $rec['parent_id']; + + if ($parent == $root) { // a direct child is found + + unset($dataset[$key]); // remove item (no need to traverse again) + + // Append the child into result array ; parse its children + $return[] = [ + 'rec' => [ + 'id' => $rec['id'], + 'label' => $rec['label'], + 'order' => $rec['order'], + 'parent' => $rec['parent_id'] + ], + 'childs' => self::to_tree($dataset, $child) // recursively + ]; + } + } + return empty($return) ? [] : $return; + } + + + /** tree_parents + * + * adds a section to each tree node with all parents of each node + * + * @param $tree (array) : nodes array (each node has `rec` and `childs` sections ) + * @param $parents (array); DO NOT SET IT (takes values automaticaly) + * @return array of nodes with an extra node[parents] section + * + */ + private static function tree_parents($tree, $parents = []) + { + $tree_with_parents = []; + + foreach($tree as $key => $node) { + // parents to be pushed for node's children + $push_parents = $parents; // parents so far + $push_parents[] = $node['rec']; // this record will be a new parent + + $tree_with_parents[$key] = [ + 'rec' => $node['rec'], + 'parents' => $parents, + 'childs' => ($node['childs'] == []) + ? [] + : self::tree_parents($node['childs'], $push_parents) + ]; + } + + return $tree_with_parents; + } + + + /** all_breadcrumbs + * ------------------------------------------------------------------------- + * + * returns an array of all breadcrumbs + * where array-key of each record is category[id] + * + * NOTE: + * --- + * Course_model::all_breadcrumbs returns an indexed super-array; + * each array item includes a banch of information: [ + * breadcrumb, + * rec: [ id , title ], + * parents: [ [id, title] , ... ] + * childs: [ [id, title] , ... ], + * level + * ] + * + * Use Cases: + * --- + * as a super-array, the output can be used in many cases + * for example... + * into form elements + * .. while selecting category for a post + * .. or editing a category + * or directry referring to category's parents/childs + * + * Arguments: + * --- + * @param $tree (array) : category tree (with childs and parents parts) + * @param $detimiter (string, optional) : string to split breadcrumb's path-nodes + * @param $exception (int, optional) : id of category to exclude (subcategories shall be excluded too) + * @param $l (int, not-pass) : depth level of the node; DO NOT SET (takes values automaticaly) + * @return array of breadcrumbs + * ------------------------------------------------------------------------- + */ + static public function breadcrumbs($tree, $delimiter = " / ", $exception = 0, $l = 0) + { + $all = []; // results array + + foreach($tree as $node) { // loop through all nodes + + if (intval($node['rec']['id']) != $exception) { // if node is not exception + + // construct breadcrumb html of node + // --- -- -- - - - + $breadcrumb = ""; + foreach($node['parents'] as $par) { // first: join path titles + $breadcrumb .= $par['label'] . $delimiter; + } + $breadcrumb .= $node['rec']['label']; // last: append title + + // make a new super record + // --- -- -- - - - + $all[$node['rec']['id']] = [ // set record is as key + 'breadcrumb' => $breadcrumb, // add breadcrump to results + 'rec' => $node['rec'], // + node info + 'parents' => $node['parents'], // + parents array + 'childs' => self::first_level_childs($node), // + direct childs + 'level' => $l // + level + ]; + + // recursively traverse children nodes + // --- -- -- - - - + if (isset($node['childs']) && $node['childs'] != []) { + $child_breadcrumbs = self::breadcrumbs( + $node['childs'], + $delimiter, + $exception, + $l+1 + ); + + $all = $all + $child_breadcrumbs; // concatenate arrays (keep array-keys) + } + } + + } + return $all; + + } + + /** first_level_childs + * --- -- -- - - - + * used by all_breadcrumbs() + */ + static private function first_level_childs($node) + { + $childs = []; + if ($node['childs'] == []) { + return []; + } + foreach($node['childs'] as $key => $kid) { + $childs[] = [ + 'id' => $kid['rec']['id'], + 'label' => $kid['rec']['label'] + ]; + } + return $childs; + } + + + + + /** LESSONS + * ------------------------------------------------------------------------- + */ + + + /** lessons of course + * + * @param $id (int) : course_id + */ + public static function lessons_of_course($id) + { + // TODO: order results in some way + + return Registry::use('database')->runQuery( + "SELECT lesson.*, lesson_privilege.privilege_id FROM lesson + LEFT JOIN lesson_privilege ON lesson_privilege.lesson_id = lesson.id + WHERE lesson.status = 1 AND lesson.course_id = :id + ORDER BY lesson_privilege.privilege_id ASC", + [':id' => $id] + ); + } + + /** lesson + * + * @param $id (int) : lesson id + */ + public static function lesson($id) + { + // TODO: order results in some way + + return Registry::use('database')->query( + "SELECT lesson.*, lesson_privilege.privilege_id FROM lesson + LEFT JOIN lesson_privilege ON lesson_privilege.lesson_id = lesson.id + WHERE lesson.status = 1 AND lesson.id = :id", + [':id' => $id] + )->getFirst(); + } + + + + /** files + * return all files + * @param void + * @return array + */ + public static function files() + { + return Registry::use('database')->runQuery("SELECT * from media", []); + } + + + /** pages + * + * return all pages as array; + * array key of each item shall be the page-id + * + * @param void + * @return array + */ + public static function pages() + { + $result = []; + $pages = Registry::use('database')->runQuery("SELECT * FROM page", []); + foreach($pages as $key => $page) { + $result[ $page['id'] ] = [ + 'title' => $page['title'], + 'body' => $page['body'], + 'status' => $page['status'] + ]; + } + // print_r($result); die(); + return $result; + } + +} diff --git a/public/app/models/cms/Media_model.php b/public/app/models/cms/Media_model.php new file mode 100644 index 0000000..6a4a681 --- /dev/null +++ b/public/app/models/cms/Media_model.php @@ -0,0 +1,36 @@ +, rights =>, path => ] + + // validate ticket + // ValidateAccess::for($ticket) + + // serve + // header("Content-type: type"); + // passthru('cat $media_path'); + + + return Registry::use('database')->query( + "SELECT * FROM pages + WHERE FullFriendlyUrl = :url AND IsActive = 1", + [ ':url' => $url ] + )->getFirst(); + } + +} + + + +// check: +// https://stackoverflow.com/questions/1353850/serve-image-with-php-script-vs-direct-loading-an-image \ No newline at end of file diff --git a/public/app/models/cms/Page_model.php b/public/app/models/cms/Page_model.php new file mode 100644 index 0000000..fda5fb1 --- /dev/null +++ b/public/app/models/cms/Page_model.php @@ -0,0 +1,18 @@ +query( + "SELECT * FROM pages + WHERE FullFriendlyUrl = :url AND IsActive = 1", + [ ':url' => $url ] + )->getFirst(); + } + +} \ No newline at end of file diff --git a/public/app/models/market/Market_repository.php b/public/app/models/market/Market_repository.php new file mode 100644 index 0000000..1338426 --- /dev/null +++ b/public/app/models/market/Market_repository.php @@ -0,0 +1,235 @@ + "SELECT * FROM cars WHERE age = :Age" + * ... won't make your code sexier nor easier to read; it will just increase + * your label's entropy and make trickier to pick your next sensable label; + * + * repository classes include two methods (inherited by the abstract, so + * you do not need to implement something more than just construct + * the $repository array) + * ** pull: for pulling a query by a friendly name + * ** echo: optional method used by some devolopment tools + * + * TODO: development tool using echo is still in progress + */ +class Market_repository extends Repository +{ + //// use Repository; // repository trait includes + //// // a method to pull an entity of the gathering + //// // and method to (TODO:) ... + + /** repository is an array of Prepare SWL queries; + * each query is attached to the array via a friendly label; + * + * NOTE: (PROPOSAL) + * --- + * about the label: + * 1. keep names short descriptive but not too long + * 2. main entity/entities shall be UPPERCASED + * 3. determinands/adjectives shall be Cappitalized + * 4. words are connected with underscore (_) + * 5. if SQL inludes bind-holderd, label shall be suffixed with '_by:' + * followed by holder names connected with undercores (case-sensitively) + * + * about the data-bind holders (if present): + * 1. all holders are prefixed with ':' + * 2. holder name shall begin with a letter and contain one word. + * + * example: + * 'Active_EMPLOYEES_by:Age_CityID' => "SELECT Em.*, c.* + * FROM employee Em LEFT JOIN city c ON c.id = Em.city_id + * WHERE c.id = :CityID AND Em.age = :Age" + * + * It is recommended to leave a descriptive comment befor each declaration + * and you may also include sql-comments inside the query. + */ + public static $repository = [ + + // PRODUCTS by: storeID, ProductUrl + // picks all product properties + // + default image + price for certain store + 'Active_PRODUCT_by:storeID_ProductUrl' => + "SELECT p.*, + pc.ID AS ProductCategory, + pc.Hierarchy, + pc.FullFriendlyUrl AS `Path`, + prices.ProductPrice_OriginalPrice AS Price, + prices.ProductPrice_DiscountedPrice AS DiscountPrice, + prices.UnitMeasurePrice_OriginalPrice AS PerUnitPrice, + prices.UnitMeasurePrice_DiscountedPrice AS PerUnitDiscountPrice, + b.Title BrandName + FROM products p + LEFT JOIN products_to_product_categories p2pc ON p.ID = p2pc.SimpleProductID + LEFT JOIN product_categories pc ON p2pc.ProductCategoryID = pc.ID + LEFT JOIN prices ON p.SKU = prices.SKU + LEFT JOIN products_to_images p2i ON p2i.SimpleProductID = p.ID + LEFT JOIN assets ON assets.ID = p2i.ImageID + LEFT JOIN brands b ON b.ID = p.BrandID + WHERE pc.IsActive = 1 AND pc.IsCurrentlyActive = 1 + AND p2i.Order = 1 + AND p.IsActive = 1 + AND p.Published = 1 + AND prices.PriceListID = :storeID + AND p.FriendlyUrl = :ProductUrl" + , + + // PRODUCTS by: storeID, ProductID + // picks all product properties + // + default image + price for certain store + 'Active_PRODUCT_by:storeID_ProductID' => + "SELECT p.*, + pc.ID AS ProductCategory, + pc.Hierarchy, + pc.FullFriendlyUrl AS `Path`, + prices.ProductPrice_OriginalPrice AS Price, + prices.ProductPrice_DiscountedPrice AS DiscountPrice, + prices.UnitMeasurePrice_OriginalPrice AS PerUnitPrice, + prices.UnitMeasurePrice_DiscountedPrice AS PerUnitDiscountPrice, + b.Title BrandName + FROM products p + LEFT JOIN products_to_product_categories p2pc ON p.ID = p2pc.SimpleProductID + LEFT JOIN product_categories pc ON p2pc.ProductCategoryID = pc.ID + LEFT JOIN prices ON p.SKU = prices.SKU + LEFT JOIN products_to_images p2i ON p2i.SimpleProductID = p.ID + LEFT JOIN assets ON assets.ID = p2i.ImageID + LEFT JOIN brands b ON b.ID = p.BrandID + WHERE pc.IsActive = 1 AND pc.IsCurrentlyActive = 1 + AND p2i.Order = 1 + AND p.IsActive = 1 + AND p.Published = 1 + AND prices.PriceListID = :storeID + AND p.ID = :ProductID" + , + + // CATEGORY-PRODUCTS by: storeID, likeHierarchy + // traverses all products from category and subcategories + // picke price and default image + 'CATEGORY-PRODUCTS_by:storeID_likeHierarchy' => + "SELECT p.*, + assets.Url AS ImageUrl, + pc.FullFriendlyUrl AS `Path`, + prices.ProductPrice_OriginalPrice AS Price, + prices.ProductPrice_DiscountedPrice AS DiscountPrice, + prices.UnitMeasurePrice_OriginalPrice AS PerUnitPrice, + prices.UnitMeasurePrice_DiscountedPrice AS PerUnitDiscountPrice + FROM products p + LEFT JOIN prices ON p.SKU = prices.SKU + LEFT JOIN products_to_product_categories p2pc ON p2pc.SimpleProductID = p.ID + LEFT JOIN product_categories pc ON pc.ID = p2pc.ProductCategoryID + LEFT JOIN products_to_images p2i ON p2i.SimpleProductID = p.ID + LEFT JOIN assets ON assets.ID = p2i.ImageID + WHERE p2pc.ProductCategoryID IN ( + SELECT `ID` FROM product_categories + WHERE Hierarchy LIKE :likeHierarchy + ) + AND p2i.Order = 1 + AND p.IsActive = 1 + AND p.Published = 1 + AND prices.PriceListID = :storeID" + , + + // Count Products + // of all last-lever Product categories + // by StoreID + // --- + // used on creating product_categories tree + // ProductCategories_model::tree() + 'Count_Last_Level_PRODUCTS_by:storeID' => + "SELECT pc.ID, COUNT(p.ID) as `Counter` + FROM product_categories pc + LEFT JOIN products_to_product_categories p2pc ON pc.ID = p2pc.ProductCategoryID + LEFT JOIN products p ON p2pc.SimpleProductID = p.ID + LEFT JOIN prices ON p.SKU = prices.SKU + LEFT JOIN products_to_images p2i ON p2i.SimpleProductID = p.ID + LEFT JOIN assets ON assets.ID = p2i.ImageID + WHERE pc.IsActive = 1 AND pc.IsCurrentlyActive = 1 + AND p2i.Order = 1 + AND pc.Level = 2 + AND p.IsActive = 1 + AND p.Published = 1 + AND prices.PriceListID = :storeID + GROUP BY pc.ID" + , + + // PRODUCT by: ProductID + // Join images array (json string) + // Join prices for all stores (json string) + 'Active_PRODUCT_complete_by:ProductID' => + "SELECT p.*, pc.ID AS ProductCategory, + pc.Hierarchy, pc.FullFriendlyUrl AS `Path`, + ( -- get all store prices as Json + SELECT JSON_ARRAYAGG(JSON_OBJECT( + 'Store', prices.PriceListID, + 'Price', prices.ProductPrice_OriginalPrice, + 'DiscountPrice', prices.ProductPrice_DiscountedPrice, + 'PerUnitPrice', prices.UnitMeasurePrice_OriginalPrice, + 'PerUnitDiscountPrice', prices.UnitMeasurePrice_DiscountedPrice + )) + FROM prices + WHERE prices.SKU = p.SKU + ) as pricesJson, + ( -- get all images as Json + SELECT JSON_ARRAYAGG(JSON_OBJECT( + 'Order', p2i.Order, + 'Url', assets.Url, + 'Title', assets.Title, + 'Description', assets.Description + )) + FROM products_to_images p2i + LEFT JOIN assets ON assets.ID = p2i.ImageID + WHERE p2i.SimpleProductID = p.ID + ) AS imagesJson, + b.Title BrandName + FROM products p + LEFT JOIN products_to_product_categories p2pc ON p.ID = p2pc.SimpleProductID + LEFT JOIN product_categories pc ON p2pc.ProductCategoryID = pc.ID + LEFT JOIN products_to_images p2i ON p2i.SimpleProductID = p.ID + LEFT JOIN assets ON assets.ID = p2i.ImageID + LEFT JOIN brands b ON b.ID = p.BrandID + WHERE pc.IsActive = 1 AND pc.IsCurrentlyActive = 1 + AND p2i.Order = 1 + AND p.IsActive = 1 + AND p.Published = 1 + AND p.ID = :ProductID" + , + + // Product images by: ProductID + // (sorted by 'order' field) + 'PRODUCT_IMAGES_by:productID' => + "SELECT a.Url, a.Title, a.Description + FROM products p + LEFT JOIN products_to_images p2i ON p2i.SimpleProductID = p.ID + LEFT JOIN assets a ON a.ID = p2i.ImageID + WHERE p.IsActive = 1 + AND p.Published = 1 + AND p.ID = :productID + ORDER BY p2i.Order" + + ]; + + /* no need to impement anything else; + * you can overide if needed the default methods: + * + public static function pull($entity){ } + * + public static function echo($content =false){ } + */ + +} diff --git a/public/app/models/market/Payment_template.txt b/public/app/models/market/Payment_template.txt new file mode 100644 index 0000000..4a66a1a --- /dev/null +++ b/public/app/models/market/Payment_template.txt @@ -0,0 +1,98 @@ +class WC_Piraeusbank_Gateway extends WC_Payment_Gateway { + + public function __construct() + + function pb_get_pages($title = false, $indent = true) + + function pb_get_installments($title = false, $indent = true) + + function generate_piraeusbank_form($order_id) + + function process_payment($order_id) + + function receipt_page($order) + + function check_piraeusbank_response() + + function piraeusbank_message() + + function woocommerce_add_piraeusbank_gateway($methods) + + function piraeusbank_plugin_action_links($links, $file) + +} + + + +class WC_NBG_Gateway extends WC_Payment_Gateway { + + public function __construct() + + public function admin_options() + + function init_form_fields() + + function nbg_get_pages($title = false, $indent = true) + + function nbg_get_installments($title = false, $indent = true) + + function generate_nbg_form($order_id) + + function process_payment($order_id) + + function receipt_page($order) { + + function check_nbg_response() + + function nbg_message() + + function woocommerce_add_nbg_gateway($methods) + + function nbg_plugin_action_links($links, $file) + +} + + + +global $wc; + + $this->id = 'nbg_gateway'; + $this->icon = apply_filters('nbg_icon', plugins_url('assets/nbg.png', __FILE__)); + $this->has_fields = false; + $this->notify_url = WC()->api_request_url('WC_NBG_Gateway'); + $this->method_description = __('National Bank Greece Payment Gateway allows you to accept payment through various channels such as Maestro, Mastercard and Visa cards On your Woocommerce Powered Site.', 'woocommerce-nbg-payment-gateway'); + $this->redirect_page_id = $this->get_option('redirect_page_id'); + $this->method_title = 'National Bank of Greece Gateway'; + + // Load the form fields. + $this->init_form_fields(); + + //dhmioyrgia vashs + + global $wpdb; + + if ($wpdb->get_var("SHOW TABLES LIKE '" . $wpdb->prefix . "nbg_transactions'") === $wpdb->prefix . 'nbg_transactions') { + // The database table exist + } else { + // Table does not exist + $query = 'CREATE TABLE IF NOT EXISTS ' . $wpdb->prefix . 'nbg_transactions (id int(11) unsigned NOT NULL AUTO_INCREMENT,merchantreference varchar(30) not null, reference varchar(100) not null, orderid varchar(100) not null , timestamp datetime default null, PRIMARY KEY (id))'; + $wpdb->query($query); + } + + + // Load the settings. + $this->init_settings(); + + + // Define user set variables + $this->title = $this->get_option('title'); + $this->description = $this->get_option('description'); + $this->nbg_Username = $this->get_option('nbg_Username'); + $this->nbg_Password = $this->get_option('nbg_Password'); + + $this->nbg_description= $this->get_option('nbg_description'); + $this->mode = $this->get_option('mode'); + $this->nbg_installments= $this->get_option('nbg_installments'); + //Actions + add_action('woocommerce_receipt_nbg_gateway', array($this, 'receipt_page')); + add_action('woocommerce_update_options_payment_gateways_' . $this->id, array($this diff --git a/public/app/models/market/ProductCategories_model.php b/public/app/models/market/ProductCategories_model.php new file mode 100644 index 0000000..d8c274e --- /dev/null +++ b/public/app/models/market/ProductCategories_model.php @@ -0,0 +1,199 @@ +query( "SELECT * FROM product_categories + WHERE FullFriendlyUrl = :url AND IsActive = 1", + [ ':url' => $url ] + )->getFirst(); + + if ($category === false) { return false; } // no category? -> false + + // GET PRODUCTS of category + $category['products'] = self::categoryProductsByHierarchy($category['Hierarchy']); + return $category; + } + + + /** Get (product_) category from ID + * --- (self explanatory) + * @param $id (int): Catgory ID (usualy from products) + * @return $category (array); also includes all category products + */ + public static function categoryFromID($id) + { + return Registry::use('database')->query( "SELECT * FROM product_categories + WHERE `ID` = :url AND IsActive = 1", + [':id' => $id] + )->getFirst(); + + } + + + /** categoryProductsByHierarchy + * --- + * This method returns the products belonging to a certain + * category (and all it's the children/sub-categories); + * It uses category.Hierarchy in a WHERE LIKE condition to + * make it fast. + * + * The method is category.Level agnostic + * + * @param $category (array) + * @return $products (array) + */ + public static function categoryProductsByHierarchy($hierarchy, $store = 904) + { + // GET PRODUCTS of category + // NOTE: + // category.Hierarchy is used + + return Registry::use('database')->runQuery( + Market_repository::pull('CATEGORY-PRODUCTS_by:storeID_likeHierarchy'), + [ + ':likeHierarchy' => $hierarchy.'%', + ':storeID' => 904 + ] + ); + } + + + # depricated: + # now Proxy has the responsibility to cache whatever + # --- + # /** GET tree of product_categories + # * --- + # * get from cache or cache it after creation + # */ + # public static function tree($store = 904) + # { + # $cacheKey = get_called_class() . $store .'/tree'; + # + # // IF category CACHED + # $cache = Registry::use('cache'); + # $tree = $cache->get($cacheKey); + # if ($tree !== false) { + # return $tree; + # } + # + # // (not cached) Create and Cache it + # $tree = self::createCategoriesTree(); + # $cache->set($cacheKey, $tree, CACHE_ROOT_TTL); + # + # return $tree; + # } + + + /** CREATE product_categories tree + * NOTE: includes active categories only; + * + * Algorith uses 'Hierarchy' for category path-positioning, + * implementing a linear conctruction of the requested tree; + * (faster and less source-consuming than a recursive algo) + */ + public static function tree($store = 904) + { + $tree = []; // variable to hold results + + $db = Registry::use('database'); + + // get raw categories data --------------------------------------------- + + $raw = $db->runQuery( "SELECT `ID`, Title, `Level`, Hierarchy, + ParentID, `Order`, FullFriendlyUrl + FROM product_categories pc + WHERE IsActive = 1 AND IsCurrentlyActive = 1 + ORDER BY pc.Level asc, pc.Order asc, pc.Hierarchy asc", + [] + ); + + // count products per 3rd level category ------------------------------- + + $countProducts = $db->runQuery( + Market_repository::pull('Count_Last_Level_PRODUCTS_by:storeID'), + [ ':storeID' => $store ] + ); + // map categoryID -> Counter + $mapCounter = []; + foreach($countProducts as $rec) $mapCounter[$rec['ID']] = $rec['Counter']; + + // parse raw data to hierarchical tree (2 pass) ------------------------ + + // 1st pass: parse raw data, construct and fill the $tree array ........ + foreach($raw as $rec) { + + // create category path from Hierarchy (format: .10.10100. ) + // so remove 1st and last dots (.) from Hierarchy string + // then explode via dot-character (.) + $categoryPath = explode('.', substr($rec['Hierarchy'], 1, -1)); + + switch ($rec['Level']) { + case 0: + $tree[$categoryPath[0]] = [ + 'info' => $rec, + 'childs' => [] + ]; break; + + case 1: + $tree[$categoryPath[0]]['childs'][$categoryPath[1]] = [ + 'info' => $rec, + 'childs' => [] + ]; break; + + case 2: + $tree[$categoryPath[0]]['childs'][$categoryPath[1]]['childs'][$categoryPath[2]] = [ + 'info' => $rec, + 'count' => isset($mapCounter[$categoryPath[2]]) ? $mapCounter[$categoryPath[2]] : 0 + ]; break; + + default: + // nothing... + } + + } + + // 2nd pass: remove categories with no products ........................ + foreach($tree as $id0 => $rec0) { + $count0 = 0; + + foreach($rec0['childs'] as $id1 => $rec1) { + $count1 = 0; + + foreach($rec1['childs'] as $id2 => $rec2) { + if ($rec2['count']>0) $count1++; + else unset($tree[$id0]['childs'][$id1]['childs'][$id2]); + } + + if ($count1 > 0) $count0++; + else unset($tree[$id0]['childs'][$id1]); + } + + if (!$count0) unset($tree[$id0]); + } + + return $tree; + } + + +} \ No newline at end of file diff --git a/public/app/models/market/Product_model.php b/public/app/models/market/Product_model.php new file mode 100644 index 0000000..aa554a1 --- /dev/null +++ b/public/app/models/market/Product_model.php @@ -0,0 +1,49 @@ +query( + Market::pull('Active_PRODUCT_by:storeID_ProductUrl'), + [ + ':storeID' => $store, + ':ProductUrl' => $url + ] + )->getFirst(); + + // if no product, return false + if ($product === false) return false; + + // inject product's images + $product['Images'] = self::getProductImages($product['ID']); + + return $product; + } + + + public static function getProductImages($id) + { + ## return Registry::use('database')->runQuery( + ## "SELECT a.Url, a.Title, a.Description + ## FROM products p + ## LEFT JOIN products_to_images p2i ON p2i.SimpleProductID = p.ID + ## LEFT JOIN assets a ON a.ID = p2i.ImageID + ## WHERE p.IsActive = 1 AND p.Published = 1 + ## AND p.ID = :id + ## ORDER BY p2i.Order", + ## [ ':id' => $id ] + ## ); + + return Registry::use('database')->runQuery( + Market::pull('PRODUCT_IMAGES_by:productID'), + ['productID' => $id ] + ); + } + +} \ No newline at end of file diff --git a/public/app/models/todo.md b/public/app/models/todo.md new file mode 100644 index 0000000..defd21e --- /dev/null +++ b/public/app/models/todo.md @@ -0,0 +1,132 @@ + + +Privilege +--- + +Privilege::list_of_privileges( privilege_id ) + +Privilege::privileges_tree() + + + + + +User +--- + +User::get_privileges + + +User::has_privilege( privilege_id ) + + +User::reset_password() + -> create otp + -> send email + + +User::set_privilege( privilege_id ) + + +User::track_action() + -> user_id + -> action : Contoller::method(args) + -> timestamp + + + +Lesson: +--- + +Lesson::create_lesson( POST ) + +Lesson::get_lesson( lesson_id ) + + + +Course +--- + +Course::create_course( POST ) + +Course::courses_tree() + + + +Upgrades (??) +--- + + +A1 + +A2 : includes A1 + +A3 : includes A1, A2 + +A4 : includes A1, A2, A3 + + +A1 100 +A2 200 +A3 350 +A4 500 + +B1 100 +B2 200 +B3 400 +B4 800 + + +:1 -> 0% +:2 -> 15% +:3 -> 20% + + + + +A1 -> A2 +A1 -> A3 +A1 -> A4 + +A2 -> A3 +A2 -> A4 + +A3 -> A4 + +A1 < A2 < A3 < A4 + + + +B1 -> B2 +B1 -> B3 +B1 -> B4 + +B2 -> B3 +B2 -> B4 + +B3 -> B4 + + +A1+B1 + +A2+B2 + +A3+B3 + + +A1+B1 -> A2+B2 +A1+B1 -> A3+B3 +A1+B1 -> A4+B4 + +A2+B2 -> A3+B3 +A2+B2 -> A4+B4 + +A3+B3 -> A4+B4 + + + +UPGRADE PRICE = upper_priv_price - own_priv_price + +buy 1 item : -0% +buy 2 items : -10% +buy 3 items : -15% diff --git a/public/app/routes/.gitkeep b/public/app/routes/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/public/app/routes/api.php b/public/app/routes/api.php new file mode 100644 index 0000000..61e485b --- /dev/null +++ b/public/app/routes/api.php @@ -0,0 +1,81 @@ + 'Διαχείριση Classroom', + 'action' => 'panel', + ]); +}); +Route::add('/admin/panel', function () { // admin panel #2 + Auth::allowRoles([1, 2, 3]); + Render::view('admin/skeleton', [ + 'title' => 'Διαχείριση Classroom', + 'action' => 'panel', + ]); +}); + + +// admin categories (=courses) +//////////////////////////////////////////////////////////////////////////////// + + +Route::add('/admin/categories', function () { // manage categories page + Auth::allowRoles([1, 2, 3]); + Render::view('admin/skeleton', [ + 'title' => 'Διαχείριση Κεφαλαίων', + 'action' => 'categories', + ]); +}); + +Route::add('/admin/api/categories', function () { // ajax: get all courses; with breadcrumbs + Auth::allowRoles([1, 2, 3]); // allow few admin-panel roles + Course_admin::breadcrumbs(); +}); + +Route::add('/admin/api/categories/add', function () { // ajax: add new category (course) + Auth::allowRoles([1, 2, 3]); + Course_admin::add_course(); +}, 'post'); + +Route::add('/admin/api/categories/update', function () { // ajax: edit category (course) + Auth::allowRoles([1, 2, 3]); // allow few admin-panel roles + Course_admin::update_course(); +}, +'post'); + + +// admin privileges +//////////////////////////////////////////////////////////////////////////////// + + +Route::add('/admin/privileges', function () { // manage privileges page + Auth::allowRoles([1, 2, 3]); + Render::view('admin/skeleton', [ + 'title' => 'Διαχείριση Δικαιωμάτων Πρόσβασης', + 'action' => 'privileges', + ]); +}); + +// ajax: get all privileges (courses; with breadcrumbs) +// use proxy (cache results); send as json +Route::add('/admin/api/privileges', function () { + Auth::allowRoles([1, 2, 3]); // allow few admin-panel roles + Render::json( Cache_service::privileges_hierarchy() ); +}); + +// add privilege + +// update privilege + + + +// admin lessons +//////////////////////////////////////////////////////////////////////////////// + +Route::add('/admin/lessons', function () { // request admin-lessons page + Auth::allowRoles([1, 2, 3]); // allow few admin-panel roles + Render::view('admin/skeleton', [ + 'title' => 'Διαχείριση Μαθημάτων', + 'action' => 'lessons', + ]); +}); + +Route::add('/admin/edit_lesson', function () { // new lesson form + Auth::allowRoles([1, 2, 3]); // allow few admin-panel roles + Render::view('admin/skeleton', [ + 'title' => 'Νέο Μάθημα', + 'action' => 'edit_lesson', + ]); +}); + +Route::add('/admin/edit_lesson/([0-9]*)', function ($id) { // edit lesson form + Auth::allowRoles([1, 2, 3]); + Render::view('admin/skeleton', [ + 'title' => 'Επεξεργασία Μαθήματος', + 'action' => 'edit_lesson', + 'id' => intval($id) + ]); +}); + + +Route::add('/admin/api/lesson/([0-9]*)', function ($id) { // ajax: get lesson + Auth::allowRoles([1, 2, 3]); // allow few admin-panel roles + Course_admin::get_lesson($id); +}); + + +Route::add('/admin/api/lesson/add', function () { // ajax: add lesson + Auth::allowRoles([1, 2, 3]); // allow few admin-panel roles + Render::json(['success' => Course_admin::add_lesson()]); +}, 'post'); + + +Route::add('/admin/api/lesson/update', function () { // ajax: update lesson + Auth::allowRoles([1, 2, 3]); // allow few admin-panel roles + Render::json(['success' => Course_admin::update_lesson()]); +}, 'post'); + + +Route::add('/admin/api/lessons', function () { // ajax: get all lessons + Auth::allowRoles([1, 2, 3]); // allow few admin-panel roles + Course_admin::all_lessons(); +}); + + + +// admin files +//////////////////////////////////////////////////////////////////////////////// + +Route::add('/admin/files', function () { // admin-files panel + Auth::allowRoles([1, 2, 3]); + Render::view('admin/skeleton', [ + 'title' => 'Διαχείριση Μέσων (media)', + 'action' => 'files', + ]); +}); + + +Route::add('/admin/api/files', function () { // ajax: get lesson + Auth::allowRoles([1, 2, 3]); + Render::json(Course_admin::files()); +}); + +// ajax: file-upload +// --- +Route::add('/admin/api/file_upload', function () { + Auth::allowRoles([1, 2, 3]); // allow few admin-panel roles + Render::json(Course_admin::upload_file()); +}, 'post'); + + + + +// admin page +//////////////////////////////////////////////////////////////////////////////// + +Route::add('/admin/pages', function () { // request admin-lessons page + Auth::allowRoles([1, 2, 3]); // allow few admin-panel roles + Render::view('admin/skeleton', [ + 'title' => 'Διαχείριση Σελίδων', + 'action' => 'pages', + ]); +}); + + +Route::add('/admin/edit_page', function () { // new page form + Auth::allowRoles([1, 2, 3]); // allow few admin-panel roles + Render::view('admin/skeleton', [ + 'title' => 'Νέα Σελίδα', + 'action' => 'edit_page', + ]); +}); + +Route::add('/admin/edit_page/([0-9]*)', function ($id) { // edit page form + Auth::allowRoles([1, 2, 3]); + Render::view('admin/skeleton', [ + 'title' => 'Επεξεργασία Σελίδας', + 'action' => 'edit_page', + 'id' => intval($id) + ]); +}); + + +Route::add('/admin/api/page/([0-9]*)', function ($id) { // ajax: get page + Auth::allowRoles([1, 2, 3]); // allow few admin-panel roles + Course_admin::get_page($id); +}); + + +Route::add('/admin/api/page/add', function () { // ajax: add page + Auth::allowRoles([1, 2, 3]); // allow few admin-panel roles + Render::json(['success' => Course_admin::add_page()]); +}, 'post'); + + +Route::add('/admin/api/page/update', function () { // ajax: update page + Auth::allowRoles([1, 2, 3]); // allow few admin-panel roles + Render::json(['success' => Course_admin::update_page()]); +}, 'post'); + + +Route::add('/admin/api/pages', function () { // ajax: get all pages + Auth::allowRoles([1, 2, 3]); // allow few admin-panel roles + Course_admin::all_pages(); +}); + diff --git a/public/app/routes/frontend.php b/public/app/routes/frontend.php new file mode 100644 index 0000000..be06b1c --- /dev/null +++ b/public/app/routes/frontend.php @@ -0,0 +1,169 @@ + Cache_service::courses_struct()['tree'], + 'entity' => Cache_service::pages_list() + ]); + // need redirect?... header('Location: /some/default/url', true, 302); +}); + + +// Error pages +//////////////////////////////////////////////////////////////////////////////// + +Route::notFound( function() { // 404 error page + header("HTTP/1.0 404 Not Found"); + Render::view('error/404', ['message' => 'nobody knows it (but you got a secret smile;)']); +}); + + + +// Common content +//////////////////////////////////////////////////////////////////////////////// + + +Route::add('/course/([0-9]*)', // request course by course-id + function($id) { Course::course($id); } +); + +Route::add('/lesson/([0-9]*)', // request lesson by lesson-id + function($id) { Course::lesson($id); } +); + +Route::add('/page/([0-9]*)', // request lesson by lesson-id + function($id) { Course::page($id); } +); + +Route::add('/serve/file/([0-9a-zA-Z-_\.\/]*)', // serve file by path + function ($path) { Course::serve_file($path); } // (also ?type= is sent) +); + + + + + +// Account +/////////////////////////////////////////////////////////////////////////////////////////////// + +// [ok] login +// [ok] register +// [ok] activate +// [..] ask-reset +// [..] reset +// [..] profile +// [..] edit-profile +// [..] subscriptions +// [..] payments +// [..] pay +// [..] order + + + + +/** URL-format Proposals and common Route regex solutions + * ----------------------------------------------------------------------------- + * + * Page (almost-static content): + * /about/{url} +# Route::add('/about/([0-9a-zA-Z-_\/]*)', function($url) { Page::fromUrl($url); }); + * + * Product: + * /{level-1}/{level-2}/{level-3}/{friendly-url}-{id} +# Route::add('/([0-9a-zA-Z-_]*)/([0-9a-zA-Z-_]*)/([0-9a-zA-Z-_]*)/([0-9a-zA-Z-_]*)-([0-9]*)', +# function($category, $subcategory, $group, $label, $id) { +# Product::fromID([$$category, $subcategory, $group, $label], $id); +# } +# ); + * + * Category + * /{category}/[ {subcategory}[ /{group}]] +# Route::add('/([0-9a-zA-Z-_]*)', function($a) { Category::url([$a]); }; +# Route::add('/([0-9a-zA-Z-_]*)/([0-9a-zA-Z-_]*)', function($a, $b) { Category::url([$a, $b]); }); +# Route::add('/([0-9a-zA-Z-_]*)/([0-9a-zA-Z-_]*)/([0-9a-zA-Z-_]*)', function($a, $b, $c) { Category::url([$a, $b, $c]); }); + * + * othet routes used in some user-cases + +# Route::add('/lesson/([0-9a-f]*)/([0-9a-zA-Z-_]*)', +# function($ticket, $lesson) { Lesson::show([$lesson, $ticket]); } +# ); +# +# Route::add('/media/([0-9a-f]*)/([0-9a-zA-Z-_]*)/([0-9]*)/', +# function($ticket, $type, $id) { Media::serve([$type, $id, $ticket]); } +# ); + * + * 4-th level paths belong to products +# Route::add('/about/([0-9a-zA-Z-_]*)/([0-9a-zA-Z-_]*)/([0-9a-zA-Z-_]*)', +# function($a, $b, $c) { Page::show([$a, $b, $c]); } +# ); + * + * + * OTHER EXAMPLES (use it for brainsrtorming) + * ----------------------------------------------------------------------------- + * + * +# // Typical Use +# // --- +# Route::add('/art', function() { Art::blah(); }); +# Route::add('/art/db', function() { Art::fromDatabase(); }); +# Route::add('/art/([0-9]*)', function($id) { Art::getInfo($id); }); + * + * Get-Post route example +# Route::add('/contact-form', function() { +# echo '
'; +# }, 'get'); + + Route::add('/contact-form', function() { + echo 'Hey! The form has been sent:
'; print_r($_POST); + }, 'post'); + * + * Accept number as parameter +# Route::add('/foo/([0-9]*)/bar', function($var1) { +# // echo $var1.' is a number!'; + echo "{$var1} is a number!"; +# }); + * + * About pages +# Route::add('/about/([0-9a-zA-Z-_]*)', function($page) { +# InfoPage::render('about/'.page); +# }); + * + * handle a request like: /foo/123/bar/3254-lefki-zaxari-marata-1kg + * where first-numeric-part of last-uri-section (3254) is the product number +# Route::add('/foo/([0-9]*)/bar/([0-9]*)-([0-9a-zA-Z-_]*)', function($num, $id, $name) { +# echo $num .' is some nomber, id = '. $id .' and label = '. $name; +# }); + * + * handle a request like: /zaxares-glykantika/lefki-zaxari-year-2022-45678 + * where last-numeric-part of last-uri-section (45678) is the product number +# Route::add('/([0-9a-zA-Z-_]*)/([0-9a-zA-Z-_]*)/([0-9a-zA-Z-_]*)/([0-9a-zA-Z-_]*)-([0-9]*)', +# function($category, $subcategory, $group, $label, $id) { +# Shop::product([$$category, $subcategory, $group, $label], $id); +# }); + * + * last chance to resolve (some friendly url) +# Route::add('/([0-9a-zA-Z-_]*)', function($a) { Resolve::uri([$a]); }); +# Route::add('/([0-9a-zA-Z-_]*)/([0-9a-zA-Z-_]*)', function($a, $b) { Resolve::uri([$a, $b]); }); +# Route::add('/([0-9a-zA-Z-_]*)/([0-9a-zA-Z-_]*)/([0-9a-zA-Z-_]*)', function($a, $b, $c) { Resolve::uri([$a, $b, $c]); }); + * + * + * Product page + * handle a request like: /pantopoleio/zaxares/lefki-zaxari/lefki-zaxari-year-2022-45678 + * where last-numeric-part of last-uri-section (45678) is the product number +# Route::add('/([0-9a-zA-Z-_]*)/([0-9a-zA-Z-_]*)/([0-9a-zA-Z-_]*)/([0-9a-zA-Z-_]*)-([0-9]*)', +# function($category, $subcategory, $group, $label, $id) { +# Shop::product([$$category, $subcategory, $group, $label], $id); +# }); + * + * ------------------------------------------------------------------------------ + */ diff --git a/public/app/routes/user.php b/public/app/routes/user.php new file mode 100644 index 0000000..7ad0fa6 --- /dev/null +++ b/public/app/routes/user.php @@ -0,0 +1,84 @@ + then sends to POST:/account/check-login +Route::add('/login', function() { Render::view('user/login'); }); + +// request registration ... -> then sends to POST:/account/register +Route::add('/registration', function() { Render::view('user/registration'); }); + +// request logout +Route::add('/logout', function() { + Auth::logout(); + header('Location: /'); die(); +}); + +// request activation +Route::add('/account/activate', function() { Auth::activate(); } ); + +// request password reset +Route::add('/account/reset_password', function() { Auth::reset_password(); } ); + + + +// Replies to common requests (POST method) +// --- -- -- - - - + +// user sends login form +Route::add('/account/check-login', function() { + $response = Auth::login(); + Render::json(['status' => $response]); + }, + 'post' +); + +// user sends a registration form +Route::add('/account/register', function() { + $response = Auth::register(); + Render::json($response); + }, + 'post' +); + + +// Account Management +// --- -- -- - - - + +// user profile +Route::add('/account/profile', function() { + $user = Auth::is_connected(); + if ($user === false) { + Render::view('error/general', + [ + 'title' => 'Nope!', + 'message' => "

No profile

User is not connected" + ] + ); + + } else { + Render::json(['success' => true, 'status' => 'user is connected']); + } + } +); + + +// temporary (on development) +// --- -- -- - -- +// NOTE: these is only for testing purposes + +// test connection +if (!PRODUCTION) { + Route::add('/check/connection', function() { Auth::is_connected(); }); +} + + + diff --git a/public/app/views/.gitkeep b/public/app/views/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/public/app/views/admin/admin-menu.php b/public/app/views/admin/admin-menu.php new file mode 100644 index 0000000..52e8522 --- /dev/null +++ b/public/app/views/admin/admin-menu.php @@ -0,0 +1,47 @@ + 'posts', + * 'id' => 4 + * ]) + * ----------------------------------------------------------------------------- + */ + +$admin_options = [ + 'panel' => 'Διαχείριση', + 'edit_lesson' => 'Νέο Μάθημα', + 'lessons' => 'Μαθήματα', + 'categories' => 'Κεφάλαια', + 'pages' => 'Σελίδες', + 'files' => 'Αρχεία', + 'privileges' => 'Πρόσβαση', + 'users' => 'Χρήστες' +]; +?> + +
+ + $label) : ?> + + + + + + + + + + + + Logout + +
\ No newline at end of file diff --git a/public/app/views/admin/categories.php b/public/app/views/admin/categories.php new file mode 100644 index 0000000..26fd5a2 --- /dev/null +++ b/public/app/views/admin/categories.php @@ -0,0 +1,91 @@ + +
+
Διαχείριση Κατηγοριών
+
+ + +
+
+ +
+
+
+ +
+ + + + + + +
Κατηγορία/Κεφάλαιο
+ +
+ + +
+
+
+ + + + + + diff --git a/public/app/views/admin/edit_lesson.php b/public/app/views/admin/edit_lesson.php new file mode 100644 index 0000000..ef875b4 --- /dev/null +++ b/public/app/views/admin/edit_lesson.php @@ -0,0 +1,159 @@ + + + +
+
+
+ +
+
+ + +
+
+
+ + +
+ + + + + + +
+ +
+ +
+
+ +
+ +
+ +
+
+ + +
+ +
+ +
+
+ +
+ +
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ + + + +
    +
+ +
+
+ + +
+ +
+
+
+ + +
+ +
+ +
+ +
+
+ + +
+ +
+ +
+ +
+ +
+ +
+ +
+ + + +
+
+ + + + + diff --git a/public/app/views/admin/edit_page.php b/public/app/views/admin/edit_page.php new file mode 100644 index 0000000..46c47bc --- /dev/null +++ b/public/app/views/admin/edit_page.php @@ -0,0 +1,139 @@ + + + +
+
+
+ +
+
+ + +
+
+
+ + +
+ + + + + + +
+ +
+ +
+
+ + +
+ +
+ +
+
+ +
+ +
+ +
+ +
+ +
+
+ +
+ +
+ + + + +
    +
+ +
+
+ + + + + +
+ +
+ +
+ +
+
+ + +
+ +
+ +
+ +
+ +
+ +
+ +
+ + + +
+
+ + + + + diff --git a/public/app/views/admin/files.php b/public/app/views/admin/files.php new file mode 100644 index 0000000..c6d2771 --- /dev/null +++ b/public/app/views/admin/files.php @@ -0,0 +1,96 @@ + +
+
Διαχείριση Επιπέδων Πρόσβασης
+
+ +
+
+ +
+
+
+ +
+ + + + + + + + + + +
α/αΠεριγραφήΔιαδρομήMedia-TypeΠρόσβαση
+ +
+ + +
+
+
+ + + + + + diff --git a/public/app/views/admin/lessons.php b/public/app/views/admin/lessons.php new file mode 100644 index 0000000..d1cd3ae --- /dev/null +++ b/public/app/views/admin/lessons.php @@ -0,0 +1,126 @@ + +
+
Διαχείριση Μαθημάτων
+ +
+ +
+
+
+ +
+ + + + + + + + + +
α/αΤίτλοςΚεφάλαιοΠρόσβαση
+ +
+ +
+
+
+ + + + + + diff --git a/public/app/views/admin/pages.php b/public/app/views/admin/pages.php new file mode 100644 index 0000000..dd8f954 --- /dev/null +++ b/public/app/views/admin/pages.php @@ -0,0 +1,109 @@ + +
+
Διαχείριση Μαθημάτων
+ +
+ +
+
+
+ +
+ + + + + + + +
α/αΤίτλος
+ +
+ +
+
+
+ + + + + + diff --git a/public/app/views/admin/panel.php b/public/app/views/admin/panel.php new file mode 100644 index 0000000..59b8cba --- /dev/null +++ b/public/app/views/admin/panel.php @@ -0,0 +1,4 @@ +
+
+
+

Admin panel

\ No newline at end of file diff --git a/public/app/views/admin/privileges.php b/public/app/views/admin/privileges.php new file mode 100644 index 0000000..587c363 --- /dev/null +++ b/public/app/views/admin/privileges.php @@ -0,0 +1,93 @@ + +
+
Διαχείριση Επιπέδων Πρόσβασης
+
+ + +
+
+ +
+
+
+ +
+ + + + + + + + +
ΚωδικόςΕπίπεδο ΠρόσβασηςΠεριλαμβάνει
+ +
+ + +
+
+
+ + + + + + diff --git a/public/app/views/admin/skeleton.php b/public/app/views/admin/skeleton.php new file mode 100644 index 0000000..927bd2d --- /dev/null +++ b/public/app/views/admin/skeleton.php @@ -0,0 +1,45 @@ + + + + + + Classroom - <?=$title?> + + true] + ); + ?> + + + + + +
+ + $action, + 'id' => $id + ]); + + // render main content + //////////////////////////////////////////////////////////////////// + Render::view("admin/{$action}",[ 'id' => $id ]); //////////////// + ?> +
+ + + + + + + diff --git a/public/app/views/components/breadcrumbs.php b/public/app/views/components/breadcrumbs.php new file mode 100644 index 0000000..59c0fca --- /dev/null +++ b/public/app/views/components/breadcrumbs.php @@ -0,0 +1,30 @@ + + \ No newline at end of file diff --git a/public/app/views/components/courses_menu.php b/public/app/views/components/courses_menu.php new file mode 100644 index 0000000..6539f8c --- /dev/null +++ b/public/app/views/components/courses_menu.php @@ -0,0 +1,97 @@ + $category_tree, + * 'open_path' => [1 ,2] + * ]); + * + * ----------------------------------------------------------------------------- + */ + +/** tree_ul + * creates (recursively) a nested ul-li tree of categories + * --- -- -- - - - + * @param $tree (array) : categories array formated as tree + * @param $open_nodes (array) : array of categor ids that shall be marked open/selected + * @return $html (string) + */ +function tree_li($tree, $open_nodes = [], $l = 0) +{ + $html = ""; + + foreach($tree as $node) { + + // if has childs ... + // write li ; traverse children recursively + if ((isset($node['childs'])) && ( $node['childs']!= [])) { + + if (in_array(intval($node['rec']['id']), $open_nodes)) { + $inner_class = "inner show"; + $sign = "–"; + $a_class = "has-childs selected"; + + } else { + $inner_class = "inner"; + $sign = "+"; + $a_class = "has-childs"; + } + + $html .= "
  • + + {$node['rec']['label']} + {$sign} + +
      " + . tree_li($node['childs'], $open_nodes, $l+1) + ."
    +
  • "; + + + } else { // else just write the li + + if (in_array(intval($node['rec']['id']), $open_nodes)) { + $u_in = ""; + $u_out = ""; + $a_class = "selected"; + + } else { + $u_in = ""; + $u_out = ""; + $a_class = ""; + + } + + $html .= "
  • + + {$node['rec']['label']} + +
  • "; + } + } + + return $html; + +} + + +// draw the menu (hidden on XS screens) +// ----------------------------------------------------------------------------- +echo "

    Διαθέσιμη ύλη

    "; +echo ""; + + + +// TODO: +// draw menu for XS screens (for example, just the 1st-level categories) +// (maybe in an accordion so it won;t take too much area on small screens) diff --git a/public/app/views/components/footer.php b/public/app/views/components/footer.php new file mode 100644 index 0000000..7cd4a5a --- /dev/null +++ b/public/app/views/components/footer.php @@ -0,0 +1,240 @@ +#footer>row` + * + * + * Each sub-structure has 2-5 properties: + * + * @var title (string) : title of the sub-section + * @var entity (string) : type of entity (ex: page/lesson/etc; TODO: method of CMS_model?) + * @var list (array) : list of entity id(s) + * @var template (string) : template that is used to draw the listed pages' info + * @var width (string) : class that relates to sub-structure's width + * + * NOTE: + * designer parsing and rendering can be a quite expensive proccess + * thus it is strongly recommented to use cached entities + * + * + * Example structure: + { + "class" : "row", # container class + "struct" : [ + { # block 1 + "title" : "", + "entity" : "page", + "list" : [2, 1, 3], # pages to be listed + "template" : "list", # list page titles with link to the pages + "width" : "col-md-4" + }, + { + "template" : "null", # list nothing + "width" : "col-md-3" + }, + { + "entity" : "page", + "list" : [4], # list content of page 4; + "template" : "content", # set title to page-title + "width" : "col-md-5" + } + + TODO: (wish) + ,{ + "template": "struct", + "struct" : [ + { + title, entity, list, template, width + }, + ... + ] + } + ] + } + * + * brainstormin: (TODO:) + * use some document-describe stucture like the one on the pdfmake js-library + * ----------------------------------------------------------------------------- + */ + + +/** check imported variables / data + * ----------------------------------------------------------------------------- + */ +if (!isset($designer)) { + + // default designer (and first case-study) + $designer = '{ + "class" : "row", + "struct" : [ + { + "title" : "Πληροφορίες", + "entity" : "page", + "list" : [2, 1, 3, 4], + "template" : "list", + "width" : "col-md-4 no-list-mark" + }, + { + "entity" : "page", + "list" : [6], + "template" : "content", + "width" : "col-md-3 no-list-mark" + }, + { + "entity" : "page", + "list" : [5], + "template" : "content", + "width" : "col-md-5 no-list-mark" + } + ] + }'; +} + +// $entity list shall be passed +// entity link url shall be passed + + +/** templating functions + * ----------------------------------------------------------------------------- + */ + +/** List template + * --- -- -- - - - + * + * lists entity titles with links + * in a `ul>li` html-structure + * + * @param $title + * @param $list + * @param $container + */ +function list_template($title, $list, $container, $entity) { + + $result = '
    +

    '. $title .'

    +
      '; + + foreach($list as $id) { + + if ($entity[$id]['status'] == 1) { // if entity is published + $result .= ' +
    • + + ' . $entity[$id]['title'] .' + +
    • '; + } + + } + return $result . '
    '; +} + +/** content_template + * + * lists whole title and content of entity + * + * @param $list + * @param $container + */ +function content_template($list, $container, $entity) { + + $parsedown = new Parsedown(); + + $result = '
      '; + + foreach($list as $id) { + + if ($entity[$id]['status'] == 1) { // if entity is published + + $result .= ' +
    • +

      '. $entity[$id]['title'] .'

      +
      '. $parsedown->text($entity[$id]['body']) .'
      +
    • '; + } + + } + return $result .'
    '; +} + + +/** ready to start the proccessing + * ----------------------------------------------------------------------------- + */ + +// decode ... +$parse = json_decode($designer); + +/** then start parse-proccessing ... + * ----------------------------------------------------------------------------- + */ +?> + + + +
    + + struct as $section ) { + + switch ($section->template) { + + case 'list': + echo list_template( + $section->title, + $section->list, + $section->width, + $entity + ); + break; + + case 'content': + echo content_template( + $section->list, + $section->width, + $entity + ); + break; + + default: // ex. 'null' + + echo '
    +

    '. $section->title.'

    +
    '; + } + + } + ?> + +
    + + diff --git a/public/app/views/components/header_includes.php b/public/app/views/components/header_includes.php new file mode 100644 index 0000000..62dc616 --- /dev/null +++ b/public/app/views/components/header_includes.php @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/app/views/components/lessons_list.php b/public/app/views/components/lessons_list.php new file mode 100644 index 0000000..a778d59 --- /dev/null +++ b/public/app/views/components/lessons_list.php @@ -0,0 +1,54 @@ + $category_tree, + * 'fieldset' => ['date', 'intro'] + * ]) + * ----------------------------------------------------------------------------- + */ +?> + + + + + +
    + + + + + +
    + +

    + +

    + + +

    (επίπεδο πρόσβασης )

    + + + +

    + + + +

    + + +
    + +
    + + + + diff --git a/public/app/views/components/subcategories.php b/public/app/views/components/subcategories.php new file mode 100644 index 0000000..5c05a94 --- /dev/null +++ b/public/app/views/components/subcategories.php @@ -0,0 +1,19 @@ + + + + +
    Υποκεφάλαια
    + +
    + + $child) : ?> + + + + + + + +
    + + \ No newline at end of file diff --git a/public/app/views/components/top_bar.php b/public/app/views/components/top_bar.php new file mode 100644 index 0000000..eaffe76 --- /dev/null +++ b/public/app/views/components/top_bar.php @@ -0,0 +1,40 @@ + +
    + + $id, + 'label' => $label, + 'parents' => $parents + ]); + ?> + + + +
    \ No newline at end of file diff --git a/public/app/views/components/user-management.php b/public/app/views/components/user-management.php new file mode 100644 index 0000000..636f2ff --- /dev/null +++ b/public/app/views/components/user-management.php @@ -0,0 +1,65 @@ +
    + + + + + +
    + + + diff --git a/public/app/views/error/404.php b/public/app/views/error/404.php new file mode 100644 index 0000000..a085830 --- /dev/null +++ b/public/app/views/error/404.php @@ -0,0 +1,65 @@ + + + + + + 404: Not Found + + + + +
    +
    +

    +
    + +
    +
    + + diff --git a/public/app/views/error/general.php b/public/app/views/error/general.php new file mode 100644 index 0000000..6ed984b --- /dev/null +++ b/public/app/views/error/general.php @@ -0,0 +1,64 @@ + + + + + + <?= isset($title) + ? (SITE_TITLE .": ". $title) + : PAGE_404_TITLE + ?> + + + + + +
    +
    +

    +
    +
    + +
    +
    + + diff --git a/public/app/views/group.php b/public/app/views/group.php new file mode 100644 index 0000000..5d5dea0 --- /dev/null +++ b/public/app/views/group.php @@ -0,0 +1,13 @@ +
    +
    + + + +
    + $item]) ?> +
    + + + +
    +
    diff --git a/public/app/views/item.php b/public/app/views/item.php new file mode 100644 index 0000000..2354c81 --- /dev/null +++ b/public/app/views/item.php @@ -0,0 +1,4 @@ +

    +

    + +

    \ No newline at end of file diff --git a/public/app/views/simple/Model.php b/public/app/views/simple/Model.php new file mode 100644 index 0000000..6ea1e7d --- /dev/null +++ b/public/app/views/simple/Model.php @@ -0,0 +1,887 @@ +pdo_pythia= $db->connect(); + + /// // database for wiki + /// $this->pdo = new PDO('mysql:unix_socket=/cloudsql/pythia-251711:europe-west4:pythia-db-eu;dbname=' . DB_DATABASE . ';charset=utf8', DB_USERNAME, DB_PASSWORD, array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC)); + /// $this->user_id = $_SESSION['user_session']; + + $this->pdo = $GLOBALS['DB_con']; + + } + + + /** Close Instance + * --------------------------------------------------- (probably not needed) + */ + public function close() + { + // Default + // $this->pdo=Database::disconnect(); + } + + + + ## ------------------------------------------------------------------------- + ## + ## CATEGORY METHODS + ## + ## ------------------------------------------------------------------------- + + + /** get all categories + * + * raw table data (simplest SELECT) + * + */ + public function get_categories() + { + $sql = "SELECT * FROM wiki_category ORDER BY title"; + $stmt = $this->pdo->prepare($sql); + $stmt->execute(); + + return $stmt->fetchAll(PDO::FETCH_ASSOC); + } + + + /** constuct a category_tree + * + * returns a tree representation of the categories + * + * NOTE: + * category_tree() is an expensive method; + * it calls 2 other methods implementing recursive algorithms + * thus it uses many sources to run (particularly RAM). + * Caching the result is strogly recommended. + * + */ + public function category_tree() + { + $categories = $this->get_categories(); // get all categories + + $tree = $this->to_tree($categories); // format to a tree + + $tree_wParents = $this->tree_parents($tree); // add parents section for each tree-node + + return $tree_wParents; + } + + + /** to_tree + * + * constructs a tree from raw-table data; + * this is a private method and uses a recursive algorithm + * + * @param $dataset (array): flar array of records with id/parent-id pairs + * @return $root (array): id of root category + * + * (**) each node has 2 parts: + * .... .. rec : all record attributes/data as passed into $dataset + * .... .. childs : array of (children) nodes + */ + public function to_tree($dataset, $root = 0) + { + $return = []; + + // loop data ; search for direct children of root + foreach($dataset as $key => $rec) { + + $child = $rec['id']; + $parent = $rec['parent_id']; + + if ($parent == $root) { // a direct child is found + + unset($dataset[$key]); // remove item (no need to traverse again) + + // Append the child into result array ; parse its children + $return[] = [ + 'rec' => [ + 'id' => $rec['id'], + 'title' => $rec['title'], + 'parent' => $rec['parent_id'] + ], + 'childs' => $this->to_tree($dataset, $child) // recursively + ]; + } + } + return empty($return) ? [] : $return; + } + + + /** tree_parents + * + * adds a section to each tree node with all parents of each node + * + * @param $tree (array) : nodes array (each node has `rec` and `childs` sections ) + * @param $parents (array); DO NOT SET IT (takes values automaticaly) + * @return array of nodes with an extra node[parents] section + * + */ + private function tree_parents($tree, $parents = []) + { + $tree_with_parents = []; + + foreach($tree as $key => $node) { + // parents to be pushed for node's children + $push_parents = $parents; // parents so far + $push_parents[] = $node['rec']; // this record will be a new parent + + $tree_with_parents[$key] = [ + 'rec' => $node['rec'], + 'parents' => $parents, + 'childs' => ($node['childs'] == []) + ? [] + : $this->tree_parents($node['childs'], $push_parents) + ]; + } + + return $tree_with_parents; + } + + + /** add_category() + * + * add new category + * + * @param $recuest (array): [ title => , parent_id => ] + * @return true; + * + * TODO: catch database error -> return succes => false + */ + public function add_category($request) + { + $sql = "INSERT INTO wiki_category (title, parent_id) VALUES (:title, :parent_id)"; + $stmt = $this->pdo->prepare($sql); + $stmt->execute([ + 'title' => $request['title'], + 'parent_id' => $request['parent_id'] + ]); + $inserted_id = $this->pdo->lastInsertId(); + + return [ "success" => true, 'id' => $inserted_id ]; + } + + + /** update_category() + * + * update an existing category + * + * @param $recuest (array): [ id => , title => , parent_id => ] + * @return true; + * + * TODO: catch database error -> return false + */ + public function update_category($request) + { + $sql = "UPDATE wiki_category SET title = :title, parent_id = :parent_id WHERE id = :id"; + $stmt = $this->pdo->prepare($sql); + $stmt->execute([ + 'title' => $request['title'], + 'parent_id' => $request['parent_id'], + 'id' => $request['id'] + ]); ; + + return true; + } + + + + ## ------------------------------------------------------------------------- + ## + ## POST (ARTICLE) METHODS + ## + ## ------------------------------------------------------------------------- + + + /** get_post + * + * get a certain post (given the post's `id`) + * + * @param $id (int) + * @param $only_publised (bool): should the post be published? (default=true) + */ + public function get_post($id, $only_published = true) { + + $where_clause = ($only_published) ? ' AND post.status = 1' : ''; + + // depricated: $sql = "SELECT * FROM post WHERE id = :id {$where_clause}"; + // Select post + // + inject tags json + // TODO: + inject media + // --- + $sql = "SELECT wiki_post.*, + ( SELECT CONCAT('[', GROUP_CONCAT(JSON_OBJECT('id', wiki_tag.id, 'name', wiki_tag.name)), ']') + FROM wiki_tag + WHERE wiki_tag.id IN ( SELECT tag_id FROM wiki_post_tags WHERE post_id = :id ) + ) AS tags_json, + ( SELECT + CONCAT('[', GROUP_CONCAT(JSON_OBJECT( + 'id', wiki_media.id, + 'title', wiki_media.title, + 'type', wiki_media.type, + 'path', wiki_media.path, + 'reference', wiki_post_media.reference )), + ']') + FROM wiki_media + LEFT JOIN wiki_post_media ON wiki_post_media.media_id = wiki_media.id + WHERE wiki_post_media.post_id = :id + ) AS medias_json + FROM wiki_post + WHERE wiki_post.deleted IS NULL AND wiki_post.id = :id"; + + $stmt = $this->pdo->prepare($sql); + $stmt->execute(['id' => $id]); + + $result = $stmt->fetchAll(PDO::FETCH_ASSOC); + + return ($result == []) + ? false // no result? false + : $result[0]; // else return 1st record + } + + + /** add post + * + * insert post to database + * + * @param $data (array): an array with all post-properties + * @return: an array with `status` (bool), `id` (int) given to post + */ + public function add_post($data) + { + $post = [ + 'title' => $data['title'], + 'category_id' => $data['category_id'], + 'body' => $data['body'], + 'intro' => $data['intro'], + 'status' => $data['status'], + 'userid' => $this->user_id + ]; + + $sql = "INSERT INTO wiki_post (`title`, category_id, `body`, intro, `status`, user_id) + VALUES (:title, :category_id, :body, :intro, :status, :userid)"; + $stmt = $this->pdo->prepare($sql); + $stmt->execute($post); + + $inserted_id = $this->pdo->lastInsertId(); + + // create + link tags to the post + // --- -- -- - - - + if (isset($data['tags'])) { // existing tags... + $this->set_post_tags($inserted_id, $data['tags']); + } + if (isset($data['new_tags'])) { // new tags... + $this->create_tags_for_post($data['new_tags'], $inserted_id); + } + + // set medias for post + // --- -- -- - - - + if (isset($data['media'])) { // new tags... + $this->create_medias_for_post($data['media'], $inserted_id); + } + + return [ "success" => true, 'id' => $inserted_id ]; + } + + + /** update post + * + * update post record to database + * + * @param $data (array): an array with all post-properties + */ + public function update_post($data) + { + $post = [ + 'id' => $data['id'], + 'title' => $data['title'], + 'category_id' => $data['category_id'], + 'body' => $data['body'], + 'intro' => $data['intro'], + 'status' => $data['status'] + ]; + $sql = "UPDATE wiki_post + SET `title` = :title, category_id = :category_id, + `body` = :body, intro = :intro, + `status` = :status + WHERE id = :id"; + $stmt = $this->pdo->prepare($sql); + $stmt->execute($post); + + // create + link tags to the post + // --- -- -- - - - + if (isset($data['tags'])) { // old tags... + $this->set_post_tags($post['id'], $data['tags']); + } + if (isset($data['new_tags'])) { // new tags... + $this->create_tags_for_post($data['new_tags'], $post['id']); + } + + // set medias for post + // --- -- -- - - - + if (isset($data['media'])) { + $this->remove_media_from_post($post['id']); // remove old + $this->create_medias_for_post($data['media'], $post['id']); // re-create new + } + + return true; + } + + + /** latest_published_posts + * + * latest PUBLISHED posts of a category (by category_id) + * + * TODO: handle sorting + * + * @param $category_id (int) ; if 0 then no category is specified + * @param $limit (int) ; 0 = no limit + * @param $fieldset (string): list of fields; default all = '*' + */ + public function latest_published_posts($category_id, $limit = 10, $fieldset = '*') + { + + $where_clause = ($category_id) + ? " AND category_id = :categoryid " + : " "; + + $limit_clause = ($limit) + ? " LIMIT ". $limit + : ""; + + $sql = "SELECT {$fieldset} FROM wiki_post + WHERE `status` = 1 AND deleted IS NULL ". $where_clause + ." ORDER BY creation_date DESC ". $limit_clause; + + $stmt = $this->pdo->prepare($sql); + + if (!$category_id) { + $stmt->execute(); + + } else { + $stmt->execute(['categoryid' => $category_id]); + } + + $result = $stmt->fetchAll(PDO::FETCH_ASSOC); + + return ($result === null) + ? false // no result? return false + : $result; // else return results + } + + + /** delete post + * + * NOTE: + * post is *marked* as deleted; + * a cron-job will make the actual deletions in a later time + * + * @param $id (int): post id + */ + public function delete_post($id) + { + if (intval($id) == 0) { // no valid id? + return ["success" => false]; // return false + } + + $sql = "UPDATE wiki_post SET deleted = 1 WHERE id = :id"; + $stmt = $this->pdo->prepare($sql); + $stmt->execute(['id' => $id]); + + return ["success" => true]; + } + + + /** all posts + * published + unpublished ; any category ; no limit + * + * @param $filedset (string): list of fields; default all = '*' + */ + public function all_posts($fieldset = '*') + { + + $sql = "SELECT {$fieldset} FROM wiki_post WHERE deleted IS NULL"; + $stmt = $this->pdo->prepare($sql); + $stmt->execute(); + + $result = $stmt->fetchAll(PDO::FETCH_ASSOC); + + return ($result === null) + ? false // no result? return false + : $result; // else return results + } + + + + ## ------------------------------------------------------------------------- + ## + ## MEDIA METHODS + ## + ## ------------------------------------------------------------------------- + + /** create media for post + * + * links post to each media-file of the media `id`s array + * @param $media (array): a list of media-file `id`s + * @param $post_id (int) + */ + private function create_medias_for_post( $medias, $post_id ) + { + foreach($medias as $medi_string) { + $medi = explode(';', $medi_string); // [0]: media_id, [1]: reference + $this->link_media_to_post($medi[0], $post_id, $medi[1]); // link to post + } + return true; + } + + /** remove_media_from_post + * + * remove all media records/attachments from post + * + * @param $post_id (int) + */ + private function remove_media_from_post( $post_id ) + { + $sql = "DELETE FROM wiki_post_media WHERE post_id = :postid"; + $stmt = $this->pdo->prepare($sql); + $stmt->execute(['postid' => $post_id]); + return true; + } + + /** link one media-file to a specific post + * + * NOTE: + * the method does not check if media is linked already + * so be sure that the pair of (media_id,post_id) not exist + * + * @param $media_id (int) + * @param $post_id (int) + * @param $reference (int|bit) + */ + private function link_media_to_post( $media_id, $post_id, $reference ) + { + $sql = "INSERT INTO wiki_post_media (post_id, media_id, reference) VALUES (:postid, :mediaid, :reference)"; + $stmt = $this->pdo->prepare($sql); + $stmt->execute([ + 'postid' => $post_id, + 'mediaid' => $media_id, + 'reference' => $reference + ]); + return true; + } + + + + + + ## ------------------------------------------------------------------------- + ## + ## FILE METHODS + ## + ## ------------------------------------------------------------------------- + + + /** upload_file_to_folder + * + * upload the file to CloudStorage + * in a virtual folder + * + * @param $folder + */ + public function upload_file_to_folder($folder) + { + // Checks before uploading the file + //////////////////////////////////////////////////////////////////////// + + // ** 1: file is upladed to temporary folder --------------------------- + if (! is_uploaded_file($_FILES['file']['tmp_name'])) { + return false; // bye! + } + + // ** 2: File belongs to the allowed MIME types ------------------------ + $allowed_file_types = array( + 'application/pdf', + 'image/png', 'image/jpeg', + 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' + ); + // Recomended MIME type checking via mime_content_type(): + $mime_type = mime_content_type($_FILES['file']['tmp_name']); + if (! in_array($mime_type, $allowed_file_types)) { // File type NOT allowed ... + return false; // bye! + } + + $file_name = $_FILES['file']['name']; + $file_type = $_FILES['file']['type']; // do not take it for granted + $file_size = $_FILES['file']['size']; + $file_tmp = $_FILES['file']['tmp_name']; + + $bare_name = pathinfo($file_name, PATHINFO_FILENAME); + $file_ext = pathinfo($file_name, PATHINFO_EXTENSION); + + + // ** 3: filename or size checks may be added -------------------------- + if ($file_name == "") { + return false; // bye! + } + + // READY to finaly save/upload the file to CDN ///////////////////////// + + $store_filename = $folder .'/'. strtolower($this->clear_file_name($bare_name) .'.'. $file_ext); + + $file_destination = $this->wiki_files_root .'/'. $store_filename; + + // depricated; it is autoloaded include_once CLOUDSTORAGE; + $cloudstorage = new CloudStorage(); + + if ($cloudstorage->upload_object($file_destination, $file_tmp)) { + + return [ + 'path' => $file_destination, + 'type' => $mime_type + ]; + + } else { return false; } + } + + /** uc_split + * + * this is an mb_str_split polyfill because + * pythia uses a php version < 7.4 + * + * CHECK: https://www.php.net/manual/en/function.mb-str-split.php + */ + private function uc_split($string, $length = 1, $encoding = 'UTF-8' ) + { + if(!empty($string)){ + $split = array(); + $mb_strlen = mb_strlen($string,$encoding); + for($pi = 0; $pi < $mb_strlen; $pi += $length){ + $substr = mb_substr($string, $pi,$length,$encoding); + if( !empty($substr)) { + $split[] = $substr; + } + } + } + return $split; + } + + /** clear_file_name + * replace greek characters and strip symbols + */ + private function clear_file_name($str) + { + $el = $this->uc_split("ΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨΩαβγδεζηθικλμνξοπρστυφχψωάέήίόύώϊϋς "); + $en = str_split( "ABGDEZHUIKLMNJOPRSTYFXCVabgdezhuiklmnjoprstyfxcvaehioyviys-"); + $strip = str_split("!@#$%^&*()+~`[]{};'/<>?=\""); + + return str_replace($strip, '', str_replace($el, $en, $str)); + } + + + /** define_media + * + * create a record in media table + * + * @param $data + * @return id (int): id of created media record + */ + public function define_media($data) + { + $sql = "INSERT INTO wiki_media (`title`, `type`, `path`) + VALUES (:title, :mimetype, :filepath)"; + $stmt = $this->pdo->prepare($sql); + $stmt->execute([ + 'title' => $data['title'], + 'mimetype' => $data['type'], + 'filepath' => $data['path'] + ]); + $inserted_id = $this->pdo->lastInsertId(); + + return $inserted_id; + } + + + ## ------------------------------------------------------------------------- + ## + ## TAG METHODS + ## + ## ------------------------------------------------------------------------- + + /** all tags + * + * @return: (array) all tags + */ + public function all_tags() + { + $sql = "SELECT * FROM wiki_tag"; + $stmt = $this->pdo->prepare($sql); + $stmt->execute(); + + $result = $stmt->fetchAll(PDO::FETCH_ASSOC); + + return ($result === null) + ? false // no result? return false + : $result; // else return results + } + + + /** create_tag + * + * @param $tag (string): the tag label + * @return : id (int) given to tag + */ + public function create_tag($tag) + { + $sql = "INSERT INTO wiki_tag (`name`) VALUES (:label)"; + $stmt = $this->pdo->prepare($sql); + $stmt->execute([ 'label' => $tag ]); + + $tag_id = $this->pdo->lastInsertId(); + + return $tag_id; + } + + /** destroy_tag + * + * @param $tag_id (int) + */ + public function destroy_tag($id) + { + // remove all post-links to this tag + $clean_posts = "DELETE FROM wiki_post_tags WHERE tag_id = :tagid"; + $clean = $this->pdo->prepare($clean_posts); + $clean->execute([ 'tagid' => $id ]); + + // remove tag from tags table + $sql = "DELETE FROM wiki_tag WHERE id = :tagid"; + $stmt = $this->pdo->prepare($sql); + $stmt->execute([ 'tagid' => $id ]); + + return true; + } + + /** posts_of_tag + * + * return posts linked to the specified tag (by tag-id) + * + * @param tag_id + * @param limit + * @param fieldset (string): list of post field-names separated with a comma (,) + * @param only_published (bool) + */ + public function posts_of_tag($tag_id, $limit = 0, $fieldset = '*', $only_published = true) + { + // format post field-names for SELECT SQL + // post_fields will be somethning like... `post.*` + // or... `post.id, post.title, post.into` + // --- -- -- - - - + $fields = explode(',', str_replace(' ', '', $fieldset)); + $fields_arr = []; + foreach($fields as $f) { $fields_arr[] = 'wiki_post.'. $f; } + $post_fields = implode(', ', $fields_arr); + + $limit_clause = ($limit) + ? " LIMIT ". $limit + : ""; + + $filter = ($only_published) + ? " AND post.status = 1 " + : ""; + + $sql = "SELECT {$post_fields} FROM wiki_post + LEFT JOIN wiki_post_tags ON wiki_post_tags.post_id = wiki_post.id + LEFT JOIN wiki_tag ON wiki_tag.id = wiki_post_tags.tag_id + WHERE wiki_post.deleted IS NULL AND wiki_tag.id = :tagid {$filter} + {$limit_clause}"; + + $stmt = $this->pdo->prepare($sql); + $stmt->execute(['tagid' => $tag_id]); + + $result = $stmt->fetchAll(PDO::FETCH_ASSOC); + + return $result; + } + + + /** tag_name + * return tag name (name = label / title / text) + * by tag_id + * + * @param $id (int): given tag-id + * @return (string) + */ + public function tag_name($id) { + $sql = "SELECT `name` FROM wiki_tag WHERE id = :id LIMIT 1"; + $stmt = $this->pdo->prepare($sql); + $stmt->execute(['id' => $id]); + + $result = $stmt->fetch(); + + return $result['name']; + + } + + /** set_tags_to_post() + * + * REMOVE OLD tags from post; + * then SET NEW tags + * + * NOTE: + * make sure that any old tags are removed from post; + * new tags should not be linked in a later cycle + * + */ + private function set_post_tags($post_id, $tags) + { + $this->remove_tags_from_post($post_id); // remobve any old tags + + foreach($tags as $tag) { // set each tag to post + $this->link_tag_to_post($tag, $post_id); + } + } + + /** remove_tags_from_post + * + * @param $post_id (int) + */ + private function remove_tags_from_post( $post_id ) + { + $sql = "DELETE FROM wiki_post_tags WHERE post_id = :postid"; + $stmt = $this->pdo->prepare($sql); + $stmt->execute(['postid' => $post_id]); + return true; + } + + /** set one tag to a specific post + * + * NOTE: + * the method does not check if tag is linked already + * + */ + private function link_tag_to_post( $tag_id, $post_id ) + { + $sql = "INSERT INTO wiki_post_tags (post_id, tag_id) VALUES (:postid, :tagid)"; + $stmt = $this->pdo->prepare($sql); + $stmt->execute([ + 'postid' => $post_id, + 'tagid' => $tag_id + ]); + return true; + } + + /** create tags for post + * + * creates an array of tags; links post to each one; + * @param $tags (array): a list of tags (labels/texts) + * @param $post_id (int) + */ + private function create_tags_for_post( $tags, $post_id ) + { + foreach($tags as $tag) { + $tag_id = $this->create_tag($tag); // create tag + $this->link_tag_to_post($tag_id, $post_id); // link to post + } + return true; + } + + + public function tags_stats() + { + $sql = "SELECT tag.id, tag.name, COUNT(post_tags.post_id) as totals + FROM wiki_tag + LEFT JOIN wiki_post_tags ON wiki_post_tags.tag_id = wiki_tag.id + GROUP BY wiki_tag.id"; + $stmt = $this->pdo->prepare($sql); + $stmt->execute(); + + return $stmt->fetchAll(PDO::FETCH_ASSOC); + } + + + ## ------------------------------------------------------------------------- + ## + ## USER METHODS + ## + ## ------------------------------------------------------------------------- + + /** user_info + * + * no user is passed; user info are exrtacted from Session + * + */ + private function user_info() + { + $user = []; + $keys = [ + 'user_session' => 'user', + 'employee_id' => 'employee', + 'store_id' => 'store', + 'site_id' => 'site', + 'unit_id' => 'unit', + 'employee_rID' => 'rid' + ]; + + foreach($keys as $key => $attribute) { + if (isset($_SESSION[$key]) && ($_SESSION[$key] != '')) { + $user[$attribute] = $_SESSION[$key]; + } + } + return $wiki_user; + } + + + /** update post stats + * + */ + public function update_post_stats($post_id) + { + + $user_id = $_SESSION['user_session']; // get user from session + + $sql = "INSERT INTO wiki_stats + (post_id, user_id, views) VALUES (:post, :user, 1) + ON DUPLICATE KEY UPDATE views = views + 1"; + + $stmt = $this->pdo->prepare($sql); + $stmt->execute([ + 'user' => $user_id, + 'post' => $post_id + ]); + + return true; + + } + +} diff --git a/public/app/views/simple/Render.php b/public/app/views/simple/Render.php new file mode 100644 index 0000000..79a93a0 --- /dev/null +++ b/public/app/views/simple/Render.php @@ -0,0 +1,182 @@ +"; + } + + } + + + /** all_breadcrumbs + * ------------------------------------------------------------------------- + * + * returns an array of all breadcrumbs + * where array-key of each record is category[id] + * + * NOTE: + * --- + * Render::all_breadcrumbs returns an indexed super-array; + * each array item includes a banch of information: [ + * breadcrumb, + * rec: [ id , title ], + * parents: [ [id, title] , ... ] + * childs: [ [id, title] , ... ], + * level + * ] + * + * Use Cases: + * --- + * as a super-array, the output can be used in many cases + * for example... + * into form elements + * .. while selecting category for a post + * .. or editing a category + * or directry referring to category's parents/childs + * + * Arguments: + * --- + * @param $tree (array) : category tree (with childs and parents parts) + * @param $detimiter (string, optional) : string to split breadcrumb's path-nodes + * @param $exception (int, optional) : id of category to exclude (subcategories shall be excluded too) + * @param $l (int, not-pass) : depth level of the node; DO NOT SET (takes values automaticaly) + * @return array of breadcrumbs + * ------------------------------------------------------------------------- + */ + static public function all_breadcrumbs($tree, $delimiter = " / ", $exception = 0, $l = 0) + { + $all = []; // results array + + foreach($tree as $node) { // loop through all nodes + + if (intval($node['rec']['id']) != $exception) { // if node is not exception + + // construct breadcrumb html of node + // --- -- -- - - - + $breadcrumb = ""; + foreach($node['parents'] as $par) { // first: join path titles + $breadcrumb .= $par['title'] . $delimiter; + } + $breadcrumb .= $node['rec']['title']; // last: append title + + // make a new super record + // --- -- -- - - - + $all[$node['rec']['id']] = [ // set record is as key + 'breadcrumb' => $breadcrumb, // add breadcrump to results + 'rec' => $node['rec'], // + node info + 'parents' => $node['parents'], // + parents array + 'childs' => self::first_level_childs($node), // + direct childs + 'level' => $l // + level + ]; + + // recursively traverse children nodes + // --- -- -- - - - + if (isset($node['childs']) && $node['childs'] != []) { + $child_breadcrumbs = self::all_breadcrumbs( + $node['childs'], + $delimiter, + $exception, + $l+1 + ); + + $all = $all + $child_breadcrumbs; // concatenate arrays (keep array-keys) + } + } + + } + return $all; + + } + + /** first_level_childs + * --- -- -- - - - + * used by all_breadcrumbs() + */ + static private function first_level_childs($node) + { + $childs = []; + if ($node['childs'] == []) { + return []; + } + foreach($node['childs'] as $key => $kid) { + $childs[] = [ + 'id' => $kid['rec']['id'], + 'title' => $kid['rec']['title'] + ]; + } + return $childs; + } + + + /** date_box + * creates a pretty date box; outputs greek months + * + * @param $date (string) in 'Y-m-d H:i:s' format + * @return html (string) of the date-box + */ + static public function date_box($date) + { + $months_Gr = ['', + 'Ιαν', 'Φεβ', 'Μαρ', 'Απρ', + 'Μάι', 'Ιουν', 'Ιουλ', 'Αυγ', + 'Σεπ', 'Οκτ', 'Νοε', 'Δεκ' + ]; + $date = DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $date); + $day = $date->format("j"); + $month = $months_Gr[intval($date->format("m"))]; + $year = $date->format("Y"); + + // print_r([$date, $day, $month, $year]); die(); + + return "
    +

    {$day}

    +

    {$month}

    +

    {$year}

    +
    "; + } + + /** date_friendly + * outputs an unformated greek-language date string + * + * @param $date (string) in 'Y-m-d H:i:s' format + */ + static public function date_friendly($date, $display_time = false) + { + $months_Gr = ['', + 'Ιαν', 'Φεβ', 'Μαρ', 'Απρ', 'Μάι', 'Ιουν', + 'Ιουλ', 'Αυγ', 'Σεπ', 'Οκτ', 'Νοε', 'Δεκ' + ]; + $date = DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $date); + $day = $date->format("j"); + $month = $months_Gr[intval($date->format("m"))]; + $year = $date->format("Y"); + $time = ($display_time) ? $date->format("H:i") : ''; + + return "{$day} {$month} {$year} {$time}"; + } + +} diff --git a/public/app/views/simple/category.php b/public/app/views/simple/category.php new file mode 100644 index 0000000..15d8b5e --- /dev/null +++ b/public/app/views/simple/category.php @@ -0,0 +1,156 @@ +category_tree(); +$breadcrumbs = Render::all_breadcrumbs($categories); + +// get ALL posts of category +$posts = $wiki->latest_published_posts($category_id, 0); + + +// special case: +// ----------------------------------------------------------------------------- +// ONE post + NO subcaegories ..then redirect to the only post +if (($breadcrumbs[ $category_id ]['childs'] == []) && (count($posts) == 1)) { + header('Location: /wiki/post?id='. $posts[0]['id'], true, 302); + die(); +} + + +// find parent_ids of current category (to open the menu tree) +$category_path = []; +foreach($breadcrumbs[$category_id]['parents'] as $parent) { + $category_path[] = intval($parent['id']); +} +$category_path[] = intval($category_id); + + +// format $posts array +// --- +$posts_formated = []; +foreach($posts as $post) { + $date = DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $post['creation_date']); + $posts_formated[] = [ + 'id' => $post['id'], + 'title' => $post['title'], + 'intro' => $post['intro'] ?? false, + 'date' => Render::date_box($post['creation_date']) + ]; +} + + +// then render the page ... +// ----------------------------------------------------------------------------- +?> + + + + Wiki + + false ]); ?> + + + + + +
    + +
    + + +
    + +
    + +
    + +
    + +
    + +
    + + +

    + +

    + + + + + + +

    Υποκατηγορίες

    +
    + + $child) : ?> + + + + + + + +
    + + + + + + + + +

    Άρθρα / Δημοσιεύσεις

    + + $posts_formated, + 'fieldset' => ['intro', 'date'] + ]); + //////////////////////////////////////////////////// + ?> + + + +
    + +
    +
    +
    + + + + diff --git a/public/app/views/simple/index.php b/public/app/views/simple/index.php new file mode 100644 index 0000000..9860db0 --- /dev/null +++ b/public/app/views/simple/index.php @@ -0,0 +1,123 @@ +category_tree(); +$breadcrumbs = Render::all_breadcrumbs($categories); +$posts = $wiki->latest_published_posts(0, 7); // 0 = no category ; 7 = posts limit + +// format posts array; inject breadcrumbs +// --- +$posts_formated = []; +foreach($posts as $post) { + $posts_formated[] = [ + 'id' => $post['id'], + 'title' => $post['title'], + 'category' => $breadcrumbs[ $post['category_id'] ]['breadcrumb'], + 'intro' => $post['intro'] ?? false, + 'date' => Render::date_box($post['creation_date']) + ]; +} + +// optional fields to be printed into post-list +// --- +$fieldset = [ + 'date', + 'intro', + 'category' +]; + +// then render the page ... +// ----------------------------------------------------------------------------- +?> + + + + Wiki + + false ]); ?> + + + + + +
    + +
    + + + +
    + +
    + +
    + +
    + + + +
    + + +
    + +

    + Welcome to Wiki! +

    + + +

    Ίσως κάποιο εισαγωγικό κείμενο, στατικό ή δυναμικό. + Pellentesque non mauris ut orci volutpat sollicitudin ut vel ligula. Nullam et leo eu ante ornare ultrices a a leo. Proin non accumsan ex, sit amet finibus purus. +

    + Vestibulum posuere maximus nisl eget consequat. Etiam venenatis lectus sed libero vestibulum viverra. Sed nec nibh ac nulla venenatis tincidunt eu ut diam. Sed tincidunt, arcu sit amet pharetra ornare, libero nunc placerat nunc, at dictum dui diam sit amet metus. Sed venenatis vehicula nisl at hendrerit. +

    + + +

    Τελευταίες Δημοσιεύσεις

    + + $posts_formated, + 'fieldset' => $fieldset + ]); + //////////////////////////////////////////////////////// + ?> + +
    + + +
    + +
    + +
    + + + + diff --git a/public/app/views/simple/lorem-dates.php b/public/app/views/simple/lorem-dates.php new file mode 100644 index 0000000..7a7c000 --- /dev/null +++ b/public/app/views/simple/lorem-dates.php @@ -0,0 +1,18 @@ +setTimestamp(time() - rand(0,30000)*1000 + rand(1,1000) ); + + // return time() - rand(0,30000)*1000 + rand(1,1000); + return $dt->format('Y-m-d H:i:s'); +} + + +include 'pythia.php'; + +$posts = $wiki->latest_posts(0, 900); + +foreach($posts as $post) { + $wiki->set_date($post['id'], some_date()); +} +echo "done!"; \ No newline at end of file diff --git a/public/app/views/simple/lorem-intros.php b/public/app/views/simple/lorem-intros.php new file mode 100644 index 0000000..a3629e1 --- /dev/null +++ b/public/app/views/simple/lorem-intros.php @@ -0,0 +1,721 @@ +latest_published_posts(0, 900, 'id, title, intro'); +$tags = $wiki->all_tags(); + +shuffle($posts); +shuffle($titles); + + +for ($i = 0 ; $i < 500 ; $i++) { + // $wiki->set_intro($posts[$i]['id'], some_lorem_intro()); + // $wiki->set_status($posts[$i]['id'], 0); + // $wiki->set_title($posts[$i]['id'], $titles[$i]); + shuffle($tags); + $c = ($i % 3) + 1; + for ($j = 0; $j < $c; $j ++) { $wiki->link_tag_to_post($tags[$j]['id'], $posts[$i]['id']); } +} +echo "done!"; + + + diff --git a/public/app/views/simple/lorem-posts.php b/public/app/views/simple/lorem-posts.php new file mode 100644 index 0000000..5f7fccb --- /dev/null +++ b/public/app/views/simple/lorem-posts.php @@ -0,0 +1,93 @@ +". $lorem[$i] ."

    "; + } + + return $html; +} + +function some_lorem_title() { + // make an array of + $lorem = explode(".", // sentences only + str_replace(". ", ".", + implode(" ", LOREM) // made from all Lorem-ipsums + ) + ); + shuffle($lorem); + return $lorem[0]; +} + + +include 'pythia.php'; + + +$categories = $wiki->get_categories(); + +$non_root = []; +foreach($categories as $node) { + if (intval($node['parent_id']) > 0) { $non_root[] = $node['id']; } + // if (intval($node['parent_id']) > 5) { $non_root[] = $node['id']; } +} +// print_r($non_root); die(); + +echo "
    ";
    +for( $i = 0 ; $i < 250 ; $i++ ) {
    +
    +    $temp = $non_root;
    +    shuffle($temp);
    +
    +    $post = [
    +        'title' => some_lorem_title(),
    +        'category' => intval($temp[0]),
    +        'body' => some_lorem_html()
    +    ];
    +    print_r($post);
    +
    +    $wiki->add_post($post);
    +}
    +echo "
    "; \ No newline at end of file diff --git a/public/app/views/simple/post.php b/public/app/views/simple/post.php new file mode 100644 index 0000000..9e37cd9 --- /dev/null +++ b/public/app/views/simple/post.php @@ -0,0 +1,236 @@ +get_post($post_id); +$categories = $wiki->category_tree(); +$breadcrumbs = Render::all_breadcrumbs($categories); + +if ($post == false) { // when post not found + $post = [ // create a virtual record with error messages + 'id' => 0, + 'title' => 'Το άρθρο που αναζητήσατε δεν υπάρχει', + 'category_id' => 0, + 'intro' => 'Για να βρειτε το άρθρο που ψάχνετε χρησιμοποιήστε την αναζήτηση + του wiki ή πλοηγηθείτε στο δέντρο των κατηγοριών.', + 'body' => "" + ]; + +} else { // post found; prepare article's data to render + + $category_id = $post['category_id']; + + // find parent_ids of current category (to open the menu tree) + $category_path = []; + foreach($breadcrumbs[$category_id]['parents'] as $parent) { + $category_path[] = intval($parent['id']); + } + $category_path[] = intval($category_id); + +} + + +// format special parts of post +$parsedown = new Parsedown(); +$body_html = $parsedown->text($post['body']); // body: markdown to html + +// decode json output (tags + medias) +if ($post['tags_json'] != null) { $tags = json_decode($post['tags_json']); } +if ($post['medias_json'] != null) { $medias = json_decode($post['medias_json']); } + + +// then render the page ... +// ----------------------------------------------------------------------------- +?> + + + + Wiki + + false ]); ?> + + + + +
    + +
    + + +
    + +
    + +
    + +
    + + + +
    + + +
    + + +

    + + + + + + + + +

    + + + + + + +

    + + + +
    + +
    + + + +
    +

    Συνημμένα:

    +
      + + reference == 1) : ?> +
    1. + + title?> + +
    2. + + +
    +
    + + + + +
    + Επικέτες: + + name?> + +
    + + +
    + +
    + +
    +
    + + + + + + + + + + + + + + diff --git a/public/app/views/templates/course.php b/public/app/views/templates/course.php new file mode 100644 index 0000000..1d212b8 --- /dev/null +++ b/public/app/views/templates/course.php @@ -0,0 +1,93 @@ + + + + + <?=SITE_TITLE?> - <?=$title?> + + + + + + + $id, + 'label' => $breadcrumbs[ $id ]['rec']['label'], + 'parents' => $breadcrumbs[ $id ]['parents'] + ]); + ?> + + +
    +
    + + + +
    + + +

    + +

    + + + + $id, + 'breadcrumbs' => $breadcrumbs + ]) + ?> + + + + + +
    Μαθήματα
    + + $lessons, + 'fieldset' => ['intro', 'level'] // ['intro', 'date'] + ]); + ?> + + + + + +
    + +
    +
    + +
    +
    + + $entity ]); + ?> + +
    +
    + + + + + diff --git a/public/app/views/templates/lesson.php b/public/app/views/templates/lesson.php new file mode 100644 index 0000000..0867cd3 --- /dev/null +++ b/public/app/views/templates/lesson.php @@ -0,0 +1,82 @@ + + + + + <?=SITE_TITLE?> - <?=$title?> + + + + + + + $id, + 'label' => $breadcrumbs[ $course_id ]['rec']['label'], + 'parents' => $breadcrumbs[ $course_id ]['parents'] + ]); + ?> + + +
    +
    + + + +
    + + +

    + +

    + + + +

    + + + +
    + text($lesson['body']); + + ?> +
    + + +
    + +
    +
    + +
    +
    + + $entity ]); + ?> + +
    +
    + + + + + diff --git a/public/app/views/templates/page.php b/public/app/views/templates/page.php new file mode 100644 index 0000000..6afe45e --- /dev/null +++ b/public/app/views/templates/page.php @@ -0,0 +1,64 @@ + + + + + <?=SITE_TITLE?> - <?=$page['title']?> + + + + + + + 0, + 'label' => '', + 'parents' => [] + ]); + ?> + + +
    + + +
    + + +

    + +

    + + +
    + text($page['body']); + + ?> +
    + + +
    + +
    + +
    +
    + + $entity ]); + ?> + +
    +
    + + + + + diff --git a/public/app/views/user/login.php b/public/app/views/user/login.php new file mode 100644 index 0000000..1a59d43 --- /dev/null +++ b/public/app/views/user/login.php @@ -0,0 +1,91 @@ + + + + + <?=SITE_TITLE?> - Είσοδος Χρήστη + + + + + + +
    +
    +

    Είσοδος Χρήστη

    +
    +
    +
    + + +
    +
    + + +
    +
    + +
    +
    +
    + + + + + diff --git a/public/app/views/user/new-password.php b/public/app/views/user/new-password.php new file mode 100644 index 0000000..06e7fac --- /dev/null +++ b/public/app/views/user/new-password.php @@ -0,0 +1,65 @@ + + + + + <?=SITE_TITLE?> - Επαναφορά κωδικού πρόσβασης + + + + + + + + + + + + +
    +
    +

    Επαναφορά κωδικού πρόσβασης

    +
    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + +
    +
    +
    + + diff --git a/public/app/views/user/registration.php b/public/app/views/user/registration.php new file mode 100644 index 0000000..f0a110c --- /dev/null +++ b/public/app/views/user/registration.php @@ -0,0 +1,120 @@ + + + + + <?=SITE_TITLE?> - Εγγραφή + + + + + + +
    +
    +
    +

    Classroom: Εγγραφή

    +
    +
    +
    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + +
    +
    +
    +
    +

    Αν έχετε ήδη λογαρισμό συνδεθείτε.

    +

    Σε κάθε περίπτωση μπορείτε να επιστρέψετε στην αρχική σελίδα.

    +
    +
    +
    + + + + + diff --git a/public/app/views/welcome.php b/public/app/views/welcome.php new file mode 100644 index 0000000..c5552a8 --- /dev/null +++ b/public/app/views/welcome.php @@ -0,0 +1,71 @@ + + + + + <?=SITE_TITLE?> - Welcome! + + + + + + + + + +
    +
    + + + +
    + the content goes here +
    + +
    +
    + + + + + + + + + +
    +
    + + $entity ]); + ?> + +
    +
    + + + + + \ No newline at end of file diff --git a/public/assets/css/admin.css b/public/assets/css/admin.css new file mode 100644 index 0000000..4bd8718 --- /dev/null +++ b/public/assets/css/admin.css @@ -0,0 +1,150 @@ +:root { + --class-btn-blue: #337ab7; + --class-btn-cyan: #5bc0de; + --class-btn-grey: #c9c9c9; + --class-btn-grey-light: #c9c9c999; +} + +.classroom .manage { + width: 100%; + max-width: 1600px; + margin: 0 auto 2em; +} +.classroom .manage.maximized { max-width: 1960px; } +.classroom .manage.minimized { max-width: 960px; } +.classroom .manage.edit-post { max-width: 1420px; } + +.classroom .edit-post textarea[name=intro] { height: 12vh; } +.classroom .edit-post textarea[name=body] { + height: calc(100vh - 492px); + min-height:320px; + font-family: Consolas, 'Ubuntu Mono','Courier New', Courier, monospace; + color: #333; +} + +.classroom .edit-post .post-media button.over-bar { margin: -26px 0 0 0; } +.classroom .edit-post .post-media ul { + border: 1px solid #ccc; + border-radius: 4px; + padding: 6px; +} + +.classroom .edit-post .post-media ul li { + border-bottom: 1px solid #ddd; + list-style-type: none; + padding: 3px 6px; + + display: flex; + justify-content: space-between; + align-items: center; +} +.classroom .edit-post .post-media ul li:last-child { border-bottom: none ;} +.classroom .edit-post .post-media ul li:hover { background: #eee; } + +.classroom .manage h2 { + font-size: 20px; + color: #445; + margin: 20px 0; +} + +.classroom .manage.edit-post textarea { width: 100% !important;} +.classroom .manage.edit-post .select2-selection { min-height: 34px; } + +.classroom .manage.edit-post select#status.published { + background-color: #5cb85c; + border-color: #4cae4c; + color: #fff; + text-align: center; +} +.classroom .manage.edit-post select#status option { + background: #fff; + color: #333; +} + +/* --- optional fields: defaut=hide --- */ + +.classroom .edit-post .optional label { + margin-bottom: 1em; + padding-bottom: .5em; + border-bottom: 1px solid var(--class-btn-grey-light) +} +.classroom .edit-post .optional label.show { + margin-bottom: unset; + border: none; +} + + +.classroom .edit-post .optional label:hover { + cursor: pointer; + color: var(--class-btn-blue); +} +.classroom .edit-post .optional label::after { float: right; content: "+"; } +.classroom .edit-post .optional label.show::after { content: "–"; } +.classroom .edit-post .optional label + div { display: none; } +.classroom .edit-post .optional label.show + div { display: block; } + + +.alert.alert-info.title-bar { min-height: 52px; margin-top: 20px; } +.alert.alert-info.title-bar button { margin-left: 8px; } + +.classroom .title-bar { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 8px 8px 16px; +} + +.classroom .action-btn button { float: right; } + +.classroom .top-bar { padding: 4px 0; } + +.classroom .top-bar .breadcrumb .btn { + margin-left: 0; + margin-right: 8px; + border-radius: 3px; + color: var(--class-btn-blue); +} + +.classroom .top-bar .btn:hover { + background-color: var(--class-btn-grey-light); +} +.classroom .top-bar .btn.selected { + color: #fff; + background-color: var(--class-btn-blue); +} + + +/* --- dataTables --- */ + +.classroom .manage .dataTable tbody tr:hover { background: #f8f8f8; } +.classroom .manage .dataTable .dt-actions { width: 48px; text-align: right } + +/* --- unpublished posts -- */ +.classroom .manage .dataTable .fa-eye-slash { color: #999; } + + +.classroom .js-copy:hover::after { + opacity: 0; + pointer-events: none; + background: #ddd7; + border-radius: 2px; + content: "Copy"; + padding: .25em .75em; + position: absolute; + z-index: 10; + transform-origin: top; + margin-top: -16px; + transform: translate(-50%, 0); +} + +.classroom .js-copy.copied:hover::after { + content: "Αντιγράφηκε"; + opacity: 1; + margin-top: -22px; + transform: translate(-50%, 0); + transition: all 0.18s ease-out 0.18s; + background: #eee; + border: 1px solid #ddd9; +} + + \ No newline at end of file diff --git a/public/assets/css/class.css b/public/assets/css/class.css new file mode 100644 index 0000000..5252975 --- /dev/null +++ b/public/assets/css/class.css @@ -0,0 +1,394 @@ +/** Classroom sections + * (main layout css-classes) + * ----------------------------------------------------------------------------- + * .classroom { + * .top-bar { + * .breadcrumb + * .admin + * } + * .side-bar { + * .search + * .categories + * } + * .content + * } + * ----------------------------------------------------------------------------- + */ + + +/* Variables + * ---------------------------------------------------------------------------- + */ + +:root { + --class-blue: #337ab7; + --class-blue-opaque: #337ab799; + --class-blue-dark: #082e44; + --class-orange: #f47920; + --class-orange-opaque: #f4792099; + --class-grey-background: #eeedeb; + --class-red: #d13; + --light-grey-bgr: #f2f3f8; +} + + + +/* main layout classes + * ---------------------------------------------------------------------------- + */ + +.classroom .top-bar { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 14px; + border-bottom: 1px solid #ddd; + box-shadow: 1px 1px 7px #9997; + background: var(--light-grey-bgr); +} + +.classroom .top-bar .breadcrumb { margin: 0; padding: 0; background: none; } + +.classroom .top-bar .fa, +.classroom .top-bar .fas { + width: 32px; + text-align: center; +} + + + +.classroom .main-content { + padding: 1.5em; + max-width: 1440px; + min-height: calc(100vh - 345px); +} + + +/* category list menu + * ----------------------------------------------------------------------------- + */ + +.classroom .side-bar { padding-right: 1em; } + + +.classroom .side-bar h3 { + font-weight: 600; font-size: 1em; + color: var(--class-blue); +} + + /* --- ul > li > a --- */ + +.classroom .side-bar ul { list-style: none; padding: 0; } +.classroom .side-bar ul li { margin: 0.5em 0; } +.classroom .side-bar ul li a { + display: flex; + padding: .2em; + background: #4442; + border-radius: 6px; + transition: background 0.3s; +} +.classroom .side-bar ul li a:hover, +.classroom .side-bar ul li a:active { + background: #cde; + text-decoration: none; +} + +/* --- li > a > span --- */ + +.classroom .side-bar ul li a span { + display: block; + align-self: center; + padding: .3em; +} +.classroom .side-bar ul li a .label { + font-size: 13px; + color: var(--class-blue-dark); + flex-grow: 1; + text-align: left; + padding-left: 1em; + border-radius: 6px; + white-space: normal; + font-weight: 400; +} + +/* --- inner --- */ + +.classroom .side-bar ul .inner { + padding-left: 1em; + overflow: hidden; + display: none; +} +.classroom .side-bar ul .inner.show { display: block; } + +/* --- has-childs | toggler --- */ + +.classroom .side-bar ul li a.has-childs { background: #9bf4; } +.classroom .side-bar ul li a.has-childs:hover { background: #cde;} + +.classroom .side-bar ul li a.has-childs .label { + font-weight: 600; + border-radius: 6px 0 0 6px; + width: calc(100% - 36px); +} + +.classroom .side-bar ul li a.has-childs .toggler { + text-align: center; + width: 32px; + font-size: 15px; + border-radius: 4px; + background: #7774; + font-weight: 700; +} +.classroom .side-bar ul li a.has-childs .toggler:hover { + background: var(--class-blue); + color: #fff; +} +.classroom .side-bar ul li a.selected { + background: var(--class-orange-opaque); +} +.classroom .side-bar ul li a:hover, +.classroom .side-bar ul li a:active, +.classroom .side-bar ul li a:focus, +.classroom .side-bar ul li a:visited, +.classroom .side-bar ul li a:link, +.classroom .side-bar ul li a span:hover, +.classroom .side-bar ul li a span:active, +.classroom .side-bar ul li a span:focus, +.classroom .side-bar ul li a span:visited, +.classroom .side-bar ul li a span:link, +.classroom .side-bar ul li a span:target { text-decoration: none !important; } + + + +.classroom .lesson-view img { max-width: 100%; } + + + +/** breadcrumb + * ----------------------------------------------------------------------------- + */ + +.classroom .breadcrumb a { counter-reset: var(--class-blue); font-size: 14px; } +.classroom .breadcrumb i.fas { padding-top: 7px; font-size: 12px; } + + + + +/** content + * ----------------------------------------------------------------------------- + */ + +.classroom .content--wrapper { + max-width: 1024px; + width: 100%; + margin: 1em auto; + padding: 0 2em 2em 2em; +} +.classroom .content h2 { + font-weight: 700; + font-size: 24px; + padding: 8px 3em 1em 0; /* 1em 3em 1em 0; */ + color: var(--class-blue-dark); +} +.classroom .content h3 { + font-weight: 600; font-size: 1.4em; + padding: 1em 3em 1em 0; + color: var(--class-blue); + border-bottom: 1px dashed var(--class-blue-opaque); +} +.classroom .content h3 span { font-weight: 400; color: #444; } +.classroom .content p { + line-height: 150%; + padding: 5px 0; +} + +/* --- post: medias -- */ +/* DEPRICATED: not needed +.classroom .content--wrapper .medias { + padding: 2em 0 1em; + border-top: 1px solid var(--class-grey-background); +} +.classroom .content--wrapper .medias ol li { + margin-left: -12px; + padding: 2px 8px; +} +.classroom .content--wrapper .medias ol li::marker { + font-size: 12px; + color: #999; +} +*/ + +/* --- post: tags --- */ + +.classroom .content--wrapper .tags { + padding: 2em 0 1em; + border-top: 1px solid var(--class-grey-background); +} +.classroom .content--wrapper .tags a { + color: #444; + background-color: var(--class-grey-background); + padding: 6px 12px; + margin: 6px; + border-radius: 3px; +} + + + +.classroom .course-view h5 { padding-top: 2em; } + +/** sub-categories on category page + * ----------------------------------------------------------------------------- + */ +/* +.classroom .category-childs { margin: 1em; padding: 1em; } + +.classroom .category-childs a { + display: inline-block; + margin: .5em; + padding: .2em 2em; + border-radius: 2em; + border: 1px solid var(--class-blue); + line-height: 2em; + font-weight: 600; + transition: background 0.3s; +} + +.classroom .category-childs a:hover { + background-color: var(--class-blue); + color: #fffe; + text-decoration: none; +} + +*/ + + + +/** articles-list + * ----------------------------------------------------------------------------- + * 22 | Title of artivle + * July | path / of / categories + * 2023 | * some intro of article + * ----------------------------------------------------------------------------- + */ +.classroom .post-card { + padding-top: .75em; + margin: 0; + border-bottom: 1px solid #7773; + display: flex; +} +.classroom .post-card .post-card--content { + flex-grow: 1; + width: calc(100% - 90px); +} +.classroom .post-card h3 { padding: 0; margin: 0; border: none; font-size: 20px; } +.classroom .post-card h3 a { display: block; } +.classroom .post-card h3 a:hover { color: #f47920; } +.classroom .post-card .category { color: #566; } +.classroom .post-card .intro { + font-style: italic; + font-size: 16px; +} +/* --- date block --- */ +.classroom .date { + display: flex; + flex-direction: column; + width: 90px; +} +.classroom .date p { + padding: 0; margin: 0; + line-height: 115%; + text-align: center; +} +.classroom .date .day { font-size: 24px; color: var(--class-blue-opaque); } +.classroom .date .month { font-size: 16px; color: #555; } +.classroom .date .year { font-size: 12px; color: #666; } + + + + +.classroom .content-form h3 { color: #369; opacity: 0.9; } + + + + +/** post (article) + * ----------------------------------------------------------------------------- + */ + + + +.classroom .lesson-view h2 { padding-bottom: 0; font-size: 24px; } + +.classroom .lesson-view h2 span { float:right; margin-right: -3em;} + +.classroom .lesson-view pre { padding: 1em; background: #f5f5f6;} + +.classroom .lesson-view .intro { + font-style: italic; + font-size: 18px; + color: var(--class-blue-dark); + padding-bottom: 1em; +} + +.classroom .lesson-view .post--date { + padding: 0; + color: var(--class-blue); +} + +.classroom .article-body img { + display: block; + max-width: 100%; + margin: 1em auto; +} +/* +.classroom .article-body a.modal-toggler { + padding: 0 16px; + background: #def; + color: #334; + border-radius: 3px; + text-decoration: none; + border: 1px solid var(--class-blue-opaque); +} +.classroom .article-body a.modal-toggler:hover { background: var(--class-blue); color: #fff; } +*/ +.classroom .article-body table { + margin: 20px auto; + min-width: 67%; + border-spacing: 6px 0; + border-collapse: separate; +} +.classroom .article-body table tr { margin: 4px; } +.classroom .article-body table thead th { + color: var(--class-btn-blue); + border-bottom: 3px solid var(--class-blue); + padding: 0 8px; + text-align: revert-layer; +} +.classroom .article-body table tbody td { + padding: 6px 8px; + border-bottom: 1px solid var(--class-grey-background); +} + + + +.classroom footer { + margin: 2em 0 0 0; + padding: 2.5em 0; + font-size: .87em; + background: #f5f6f7; +} +.classroom footer a { + color: #024; + text-decoration: none; + padding: 2px 36px 2px 4px; + border-radius: 3px; +} +.classroom footer a:hover { color: #fff; background: #369 ; } +.classroom footer h4 { font-size: 20px; } +.classroom footer .no-list-mark ul { padding-left: 0; margin-left: 0; list-style: none; } +.classroom footer .no-list-mark ul li { margin: 0 0 4px 0; } + + +.classroom .page-container { max-width: 1024px; } + +iframe .ytp-chrome-top-buttons { display: none !important; } \ No newline at end of file diff --git a/public/assets/css/email.css b/public/assets/css/email.css new file mode 100644 index 0000000..0bb6d48 --- /dev/null +++ b/public/assets/css/email.css @@ -0,0 +1,478 @@ +p { + margin: 10px 0; + padding: 0; + color: #0E0C0C; + font-family: Arial, Helvetica, sans-serif; + font-style: normal; + font-weight: normal; + font-size: 15px; + line-height: 24px; + letter-spacing: normal; +} + +table { + border-collapse: collapse; +} + +h1, h2, h3, h4, h5, h6 { + display: block; + margin: 0; + padding: 0; +} + +img, a img { + border: 0; + height: auto; + outline: none; + text-decoration: none; +} + +body, #bodyTable, #bodyCell { + height: 100%; + margin: 0; + padding: 0; + width: 100%; +} + +.mcnPreviewText { + display: none !important; +} + +#outlook a { + padding: 0; +} + +img { + -ms-interpolation-mode: bicubic; +} + +table { + mso-table-lspace: 0pt; + mso-table-rspace: 0pt; +} + +.ReadMsgBody { + width: 100%; +} + +.ExternalClass { + width: 100%; +} + +p, a, li, td, blockquote { + mso-line-height-rule: exactly; +} + + a[href^=tel], a[href^=sms] { + color: inherit; + cursor: default; + text-decoration: none; + } + +p, a, li, td, body, table, blockquote { + -ms-text-size-adjust: 100%; + -webkit-text-size-adjust: 100%; +} + +.ExternalClass, .ExternalClass p, .ExternalClass td, .ExternalClass div, .ExternalClass span, .ExternalClass font { + line-height: 100%; +} + +a[x-apple-data-detectors] { + color: inherit !important; + text-decoration: none !important; + font-size: inherit !important; + font-family: inherit !important; + font-weight: inherit !important; + line-height: inherit !important; +} + +.templateContainer { + max-width: 700px !important; +} + +a.mcnButton { + display: block; +} + +.mcnImage, .mcnRetinaImage { + vertical-align: bottom; +} + +.mcnTextContent { + word-break: break-word; +} + + .mcnTextContent img { + height: auto !important; + } + +.mcnDividerBlock { + table-layout: fixed !important; +} + +.noBorder { + border: none !important; +} + +h1 { + color: #333333; + font-family: Arial, Helvetica, sans-serif; + font-size: 16px; + font-style: normal; + font-weight: bold; + line-height: 24px; + letter-spacing: -0.02em; + text-align: center; + padding-bottom: 14px; +} + +h2 { + color: #0E0C0C; + font-family: Arial, Helvetica, sans-serif; + font-size: 22px; + font-style: normal; + font-weight: bold; + line-height: 30px; + letter-spacing: normal; + text-align: center; + text-transform: uppercase; + padding-bottom: 8px; +} + +h3 { + color: #0E0C0C; + font-family: Arial, Helvetica, sans-serif; + font-size: 22px; + font-style: normal; + font-weight: bold; + line-height: 30px; + letter-spacing: normal; + text-align: center; + text-transform: uppercase; + padding-bottom: 8px; +} + +h4 { + color: #0E0C0C; + font-family: Arial, Helvetica, sans-serif; + font-size: 22px; + font-style: normal; + font-weight: bold; + line-height: 30px; + letter-spacing: normal; + text-align: center; + text-transform: uppercase; + padding-bottom: 8px; +} + + h1 span, + h2 span, + h3 span, + h4 span { + font-weight: normal; + text-transform: none; + } + +#templateHeader { + background-color: #ffffff; + background-image: none; + background-repeat: no-repeat; + background-position: center; + background-size: cover; + border-top: 0; + border-bottom: 1px solid #e3e3e3; + padding-top: 0px; + padding-bottom: 20px; +} + + #templateHeader.default { + border: none; + padding: 0; + } + +.headerContainer { + background-color: transparent; + background-image: none; + background-repeat: no-repeat; + background-position: center; + background-size: cover; + border-top: 0; + border-bottom: 0; + padding-top: 0; + padding-bottom: 0; +} + + .headerContainer .mcnTextContent, .headerContainer .mcnTextContent p { + color: #757575; + font-family: Arial, Helvetica, sans-serif; + font-size: 16px; + line-height: 150%; + text-align: left; + } + + .headerContainer .mcnTextContent a, .headerContainer .mcnTextContent p a { + color: #007C89; + font-weight: normal; + text-decoration: underline; + } + +#templateBody { + background-color: #ffffff; + background-image: none; + background-repeat: no-repeat; + background-position: center; + background-size: cover; + border-top: 0; + border-bottom: 0; + padding-top: 24px; + padding-bottom: 32px; +} + + #templateBody a { + color: #F47920; + text-decoration: none; + } + + #templateBody p { + margin: 0; + color: #333333; + font-family: Arial, Helvetica, sans-serif; + font-size: 15px; + font-style: normal; + font-weight: normal; + line-height: 22px; + letter-spacing: -0.02em; + padding-bottom: 5px; + } + +#templateBodyMsg p { + max-width: 586px; +} + +.bodyContainer { + background-color: transparent; + background-image: none; + background-repeat: no-repeat; + background-position: center; + background-size: cover; + border-top: 0; + border-bottom: 0; + padding-top: 0; + padding-bottom: 0; +} + + .bodyContainer .mcnTextContent, + .bodyContainer .mcnTextContent p { + color: #333333; + font-family: Arial, Helvetica, sans-serif; + font-size: 15px; + line-height: 150%; + text-align: left; + } + + .bodyContainer .mcnTextContent a, + .bodyContainer .mcnTextContent p a { + color: #F47920; + font-weight: normal; + text-decoration: underline; + } + +#templateAfterFooter p { + margin: 0; + color: #333333; + font-family: Arial, Helvetica, sans-serif; + font-size: 13px; + font-style: normal; + font-weight: normal; + line-height: 24px; + letter-spacing: 0.02em; +} + + #templateAfterFooter p a { + margin: 0; + color: #F47920; + font-family: Arial, Helvetica, sans-serif; + font-size: 13px; + font-style: normal; + font-weight: normal; + line-height: 24px; + letter-spacing: 0.02em; + } + +#templateFooter { + background-color: #333333; + background-image: none; + background-repeat: no-repeat; + background-position: center; + background-size: cover; + border-top: 0; + border-bottom: 0; + padding-top: 16px; + padding-bottom: 16px; +} + +.footerContainer { + background-color: transparent; + background-image: none; + background-repeat: no-repeat; + background-position: center; + background-size: cover; + border-top: 0; + border-bottom: 0; + padding-top: 0px; + padding-bottom: 0px; + max-width: 700px !important; +} + + .footerContainer .bodyContainer { + padding: 0 30px; + } + + .footerContainer ul { + padding: 0; + margin: 0; + list-style-type: none; + text-align: center; + } + + .footerContainer ul li { + display: inline-block; + margin-right: 30px; + } + + .footerContainer ul li:last-child { + margin-right: 0; + } + + .footerContainer .mcnTextBlockInner, + .footerContainer .mcnTextBlockInner p { + color: #FFF; + font-family: Arial, Helvetica, sans-serif; + font-size: 15px; + line-height: 24px; + letter-spacing: 0.02em; + } + + .footerContainer .mcnTextBlockInner a, + .footerContainer .mcnTextBlockInner p a { + color: #FFF; + font-family: Arial, Helvetica, sans-serif; + font-size: 12px; + line-height: 18px; + text-decoration: none; + } + +.templateHeroContainer { + max-width: 700px; +} + +.heroContainer { + position: relative; +} + + .heroContainer img { + display: block; + width: 100%; + height: auto; + } + + .heroContainer p { + color: #FFF; + font-family: Arial, Helvetica, sans-serif; + font-size: 40px; + line-height: 50px; + letter-spacing: -0.02em; + text-transform: uppercase; + padding: 0 120px; + position: absolute; + left: 0; + bottom: 80px; + width: 100%; + text-align: center; + font-weight: normal; + margin: 0; + } + +#templateProducts { + padding: 0 0 90px; +} + + #templateProducts .mcnTextBlockInner { + padding: 0 20px; + } + + #templateProducts span { + display: block; + font-family: Arial, Helvetica, sans-serif; + font-size: 15px; + line-height: 24px; + font-weight: bold; + text-transform: uppercase; + padding-top: 10px; + } + +.column { + display: inline-block; + vertical-align: top; + width: 50%; +} + +.label { + font-family: Arial, Helvetica, sans-serif; + font-size: 12px; + line-height: 24px; + font-weight: bold; + text-transform: uppercase; + color: #0E0C0C; + padding-bottom: 2px; +} + +strong { + font-family: Arial, Helvetica, sans-serif; + font-weight: bold; +} + +.tdMinWidth { + max-width: 310px; +} + +@media only screen and (max-width:700px) { + .templateContainer { + width: 360px !important; + } + + .bodyContainer { + padding-left: 0 !important; + padding-right: 0 !important; + } + + .footerContainer ul li { + display: block; + margin-right: 0; + margin-bottom: 16px; + } + + .footerContainer ul li:last-child { + margin-bottom: 0; + } + + .heroContainer p { + font-size: 30px; + line-height: 40px; + padding: 0 30px; + bottom: 30px; + } + + .tdMinWidth { + max-width: 200px; + } + + .halfColumn { + padding: 20px 10px 0 !important; + } +} + +/* td { + outline: 1px solid red; +}*/ \ No newline at end of file diff --git a/public/assets/css/main.css b/public/assets/css/main.css new file mode 100644 index 0000000..016e0ab --- /dev/null +++ b/public/assets/css/main.css @@ -0,0 +1,9 @@ +:root { + + --wrapperXXS: 816px; + --wrapperXS: 984px; + --wrapperS: 1142px; + --wrapper: 1240px; + --wrapperL: 1514px; + --wrapperFull: 1920px; +} \ No newline at end of file diff --git a/public/assets/css/normalize.css b/public/assets/css/normalize.css new file mode 100644 index 0000000..192eb9c --- /dev/null +++ b/public/assets/css/normalize.css @@ -0,0 +1,349 @@ +/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ + +/* Document + ========================================================================== */ + +/** + * 1. Correct the line height in all browsers. + * 2. Prevent adjustments of font size after orientation changes in iOS. + */ + +html { + line-height: 1.15; /* 1 */ + -webkit-text-size-adjust: 100%; /* 2 */ +} + +/* Sections + ========================================================================== */ + +/** + * Remove the margin in all browsers. + */ + +body { + margin: 0; +} + +/** + * Render the `main` element consistently in IE. + */ + +main { + display: block; +} + +/** + * Correct the font size and margin on `h1` elements within `section` and + * `article` contexts in Chrome, Firefox, and Safari. + */ + +h1 { + font-size: 2em; + margin: 0.67em 0; +} + +/* Grouping content + ========================================================================== */ + +/** + * 1. Add the correct box sizing in Firefox. + * 2. Show the overflow in Edge and IE. + */ + +hr { + box-sizing: content-box; /* 1 */ + height: 0; /* 1 */ + overflow: visible; /* 2 */ +} + +/** + * 1. Correct the inheritance and scaling of font size in all browsers. + * 2. Correct the odd `em` font sizing in all browsers. + */ + +pre { + font-family: monospace, monospace; /* 1 */ + font-size: 1em; /* 2 */ +} + +/* Text-level semantics + ========================================================================== */ + +/** + * Remove the gray background on active links in IE 10. + */ + +a { + background-color: transparent; +} + +/** + * 1. Remove the bottom border in Chrome 57- + * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. + */ + +abbr[title] { + border-bottom: none; /* 1 */ + text-decoration: underline; /* 2 */ + text-decoration: underline dotted; /* 2 */ +} + +/** + * Add the correct font weight in Chrome, Edge, and Safari. + */ + +b, +strong { + font-weight: bolder; +} + +/** + * 1. Correct the inheritance and scaling of font size in all browsers. + * 2. Correct the odd `em` font sizing in all browsers. + */ + +code, +kbd, +samp { + font-family: monospace, monospace; /* 1 */ + font-size: 1em; /* 2 */ +} + +/** + * Add the correct font size in all browsers. + */ + +small { + font-size: 80%; +} + +/** + * Prevent `sub` and `sup` elements from affecting the line height in + * all browsers. + */ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +/* Embedded content + ========================================================================== */ + +/** + * Remove the border on images inside links in IE 10. + */ + +img { + border-style: none; +} + +/* Forms + ========================================================================== */ + +/** + * 1. Change the font styles in all browsers. + * 2. Remove the margin in Firefox and Safari. + */ + +button, +input, +optgroup, +select, +textarea { + font-family: inherit; /* 1 */ + font-size: 100%; /* 1 */ + line-height: 1.15; /* 1 */ + margin: 0; /* 2 */ +} + +/** + * Show the overflow in IE. + * 1. Show the overflow in Edge. + */ + +button, +input { /* 1 */ + overflow: visible; +} + +/** + * Remove the inheritance of text transform in Edge, Firefox, and IE. + * 1. Remove the inheritance of text transform in Firefox. + */ + +button, +select { /* 1 */ + text-transform: none; +} + +/** + * Correct the inability to style clickable types in iOS and Safari. + */ + +button, +[type="button"], +[type="reset"], +[type="submit"] { + -webkit-appearance: button; +} + +/** + * Remove the inner border and padding in Firefox. + */ + +button::-moz-focus-inner, +[type="button"]::-moz-focus-inner, +[type="reset"]::-moz-focus-inner, +[type="submit"]::-moz-focus-inner { + border-style: none; + padding: 0; +} + +/** + * Restore the focus styles unset by the previous rule. + */ + +button:-moz-focusring, +[type="button"]:-moz-focusring, +[type="reset"]:-moz-focusring, +[type="submit"]:-moz-focusring { + outline: 1px dotted ButtonText; +} + +/** + * Correct the padding in Firefox. + */ + +fieldset { + padding: 0.35em 0.75em 0.625em; +} + +/** + * 1. Correct the text wrapping in Edge and IE. + * 2. Correct the color inheritance from `fieldset` elements in IE. + * 3. Remove the padding so developers are not caught out when they zero out + * `fieldset` elements in all browsers. + */ + +legend { + box-sizing: border-box; /* 1 */ + color: inherit; /* 2 */ + display: table; /* 1 */ + max-width: 100%; /* 1 */ + padding: 0; /* 3 */ + white-space: normal; /* 1 */ +} + +/** + * Add the correct vertical alignment in Chrome, Firefox, and Opera. + */ + +progress { + vertical-align: baseline; +} + +/** + * Remove the default vertical scrollbar in IE 10+. + */ + +textarea { + overflow: auto; +} + +/** + * 1. Add the correct box sizing in IE 10. + * 2. Remove the padding in IE 10. + */ + +[type="checkbox"], +[type="radio"] { + box-sizing: border-box; /* 1 */ + padding: 0; /* 2 */ +} + +/** + * Correct the cursor style of increment and decrement buttons in Chrome. + */ + +[type="number"]::-webkit-inner-spin-button, +[type="number"]::-webkit-outer-spin-button { + height: auto; +} + +/** + * 1. Correct the odd appearance in Chrome and Safari. + * 2. Correct the outline style in Safari. + */ + +[type="search"] { + -webkit-appearance: textfield; /* 1 */ + outline-offset: -2px; /* 2 */ +} + +/** + * Remove the inner padding in Chrome and Safari on macOS. + */ + +[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} + +/** + * 1. Correct the inability to style clickable types in iOS and Safari. + * 2. Change font properties to `inherit` in Safari. + */ + +::-webkit-file-upload-button { + -webkit-appearance: button; /* 1 */ + font: inherit; /* 2 */ +} + +/* Interactive + ========================================================================== */ + +/* + * Add the correct display in Edge, IE 10+, and Firefox. + */ + +details { + display: block; +} + +/* + * Add the correct display in all browsers. + */ + +summary { + display: list-item; +} + +/* Misc + ========================================================================== */ + +/** + * Add the correct display in IE 10+. + */ + +template { + display: none; +} + +/** + * Add the correct display in IE 10. + */ + +[hidden] { + display: none; +} diff --git a/public/assets/css/overides.css b/public/assets/css/overides.css new file mode 100644 index 0000000..f9d351c --- /dev/null +++ b/public/assets/css/overides.css @@ -0,0 +1,148 @@ +:root { + --bs-heading-color: #679; +} + +html, body { font-family: "Roboto Slab", serif; font-weight: 400; } + +.h1, .h2, .h3, .h4, .h5, .h6, h1, h2, h3, h4, h5, h6 { font-weight: 400; }; + +.content { text-align: unset; } + +.form-control { border: 1px solid #7777; } + +.form-label { font-size: .72em; color: var(--bs-heading-color); } + + + + +/* main.min.css */ +.product__img { + /* max-width: 320px; */ + max-width: 240px; +} + +.footer__listLink { + color: #bbb; +} +.footer__listLink:hover { + color: #eee; +} + +.footer__title { color: #bbb7; } + +/* .user .dropdown button { min-width: 256px; } */ +.user .dropdown ul.dropdown-menu { + right: 0 !important; + left: unset !important; + transform: translate(0, 34px) !important; +} + +.admin-container { font-size: .96em; } +.admin-container .top-bar a:hover { background: #ddd; } + +.admin-container .manage { padding: 1em; } + +.admin-container .title-bar { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1.5em 1em 1.5em 1em; + } + +.admin-container .dt-table { font-size: 14px; } + +.admin-container .dt-table .btn-sm { + --bs-btn-padding-y: .2rem; + --bs-btn-padding-x: .35rem; + --bs-btn-font-size: .65rem; + } + +.admin-container .modal .form-group { padding: .35em 0;} +/*.admin-container .modal .form-control { padding: 1px 8px; border-radius: 4px; font-size: 15px; } */ + +.admin-container .table > :not(caption) > * > * { padding: .35rem .35rem; } +.admin-container .table tbody tr:hover { background: #eee; } + +ul.select2-results__options li { font-size: 14px; padding: 4px 8px; } + +.admin-container .modal-footer .form-actions { display: flex; gap: 8px; } + +.admin-container .title-bar h5 { margin-bottom: 0; color: #333; } + +.admin-container table.dataTable { margin-top: 1em !important; margin-bottom: 1em !important;} + +.admin-container .title-bar .btn { border: 1px solid #7772; } +.admin-container .title-bar .btn:hover { background: #ddd; } + +.admin-container form label { padding: 8px 1em 2px 1em; } + +.admin-container .form-control { border-radius: 4px; } + +.admin-container select[data-chosen='one'] { + font-weight: 700; + color: #0d6efd; + border: 1px solid #0d6efd; +} + +/* hack select2 caret styling + * (like bootstrap5 select caret) + * ----------------------------------------------------------------------- start + */ +.select2-container--default .select2-selection--single .select2-selection__arrow b { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e"); + background-color: transparent; + background-size: contain; + border: none !important; + height: 16 !important; + width: 12px !important; + margin: auto !important; + top: 9px !important; + left: auto !important; + height: 14px; +} +.select2-container--default .select2-selection--single .select2-selection__arrow { width: 24px; } +.select2-container--default .select2-selection--single { height: 31px; } +/* ------------------------------------------------------------------------- end +*/ + +.admin-container .edit-post textarea[name=body] { + height: calc(100vh - 420px); /* almost all avialable height */ +} + +.admin-container .edit-page textarea[name=body] { + height: calc(100vh - 380px); /* almost all avialable height */ +} + +.admin-container .edit-post textarea[name=intro] { height: 94px ;} /* almost 3 (half-)lines */ + +.admin-container .edit-post textarea[name=body] { + height: calc(100vh - 420px); /* almost all avialable height */ +} + +.admin-container .post-media button.over-bar { margin: -26px 0 0 0; float: right; } +.admin-container .post-media ul { + border: 1px solid #ccc; + border-radius: 4px; + padding: 6px; +} + +.admin-container .post-media ul li { + border-bottom: 1px solid #ddd; + list-style-type: none; + padding: 3px 6px; + + display: flex; + justify-content: space-between; + align-items: center; +} +.admin-container .post-media ul li:last-child { border-bottom: none ;} +.admin-container .post-media ul li:hover { background: #eee; } + +.admin-container .post-media li .btn-xs { font-size: 14px; width: 24px; height: 24px; padding:0; } + +.classroom .edit-post textarea[name=body], +.classroom .edit-page textarea[name=body] { + min-height:320px; + font-family: Consolas, 'Ubuntu Mono','Courier New', Courier, monospace; + color: #333; + } \ No newline at end of file diff --git a/public/assets/js/DataTables/Buttons-2.3.6/css/buttons.bootstrap.css b/public/assets/js/DataTables/Buttons-2.3.6/css/buttons.bootstrap.css new file mode 100644 index 0000000..c1445d4 --- /dev/null +++ b/public/assets/js/DataTables/Buttons-2.3.6/css/buttons.bootstrap.css @@ -0,0 +1,386 @@ +@keyframes dtb-spinner { + 100% { + transform: rotate(360deg); + } +} +@-o-keyframes dtb-spinner { + 100% { + -o-transform: rotate(360deg); + transform: rotate(360deg); + } +} +@-ms-keyframes dtb-spinner { + 100% { + -ms-transform: rotate(360deg); + transform: rotate(360deg); + } +} +@-webkit-keyframes dtb-spinner { + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } +} +@-moz-keyframes dtb-spinner { + 100% { + -moz-transform: rotate(360deg); + transform: rotate(360deg); + } +} +div.dataTables_wrapper { + position: relative; +} + +div.dt-buttons { + position: initial; +} +div.dt-buttons .dt-button { + overflow: hidden; + text-overflow: ellipsis; +} + +div.dt-button-info { + position: fixed; + top: 50%; + left: 50%; + width: 400px; + margin-top: -100px; + margin-left: -200px; + background-color: white; + border: 2px solid #111; + box-shadow: 3px 4px 10px 1px rgba(0, 0, 0, 0.3); + border-radius: 3px; + text-align: center; + z-index: 21; +} +div.dt-button-info h2 { + padding: 0.5em; + margin: 0; + font-weight: normal; + border-bottom: 1px solid #ddd; + background-color: #f3f3f3; +} +div.dt-button-info > div { + padding: 1em; +} + +div.dtb-popover-close { + position: absolute; + top: 10px; + right: 10px; + width: 22px; + height: 22px; + border: 1px solid #eaeaea; + background-color: #f9f9f9; + text-align: center; + border-radius: 3px; + cursor: pointer; + z-index: 2003; +} + +button.dtb-hide-drop { + display: none !important; +} + +div.dt-button-collection-title { + text-align: center; + padding: 0.3em 0 0.5em; + margin-left: 0.5em; + margin-right: 0.5em; + font-size: 0.9em; +} + +div.dt-button-collection-title:empty { + display: none; +} + +span.dt-button-spacer { + display: inline-block; + margin: 0.5em; + white-space: nowrap; +} +span.dt-button-spacer.bar { + border-left: 1px solid rgba(0, 0, 0, 0.3); + vertical-align: middle; + padding-left: 0.5em; +} +span.dt-button-spacer.bar:empty { + height: 1em; + width: 1px; + padding-left: 0; +} + +div.dt-button-collection span.dt-button-spacer { + width: 100%; + font-size: 0.9em; + text-align: center; + margin: 0.5em 0; +} +div.dt-button-collection span.dt-button-spacer:empty { + height: 0; + width: 100%; +} +div.dt-button-collection span.dt-button-spacer.bar { + border-left: none; + border-bottom: 1px solid rgba(0, 0, 0, 0.3); + padding-left: 0; +} + +div.dt-button-collection { + position: absolute; + z-index: 2001; + background-color: white; + border: 1px solid rgba(0, 0, 0, 0.15); + border-radius: 4px; + box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175); + padding: 0.5rem; + min-width: 200px; +} +div.dt-button-collection div.dropdown-menu, +div.dt-button-collection ul.dropdown-menu { + position: relative; + display: block; + z-index: 2002; + min-width: 100%; + background-color: transparent; + border: none; + box-shadow: none; + margin: 2px 0; + padding: 0; + border-radius: 0; +} +div.dt-button-collection div.dt-btn-split-wrapper { + width: 100%; + display: inline-flex; + padding-left: 5px; + padding-right: 5px; +} +div.dt-button-collection button.dt-btn-split-drop-button { + width: 100%; + border: none; + border-radius: 0px; + margin-left: 0px !important; +} +div.dt-button-collection button.dt-btn-split-drop-button:focus { + border: none; + border-radius: 0px; + outline: none; +} +div.dt-button-collection.fixed { + position: fixed; + display: block; + top: 50%; + left: 50%; + margin-left: -75px; + border-radius: 5px; + background-color: white; +} +div.dt-button-collection.fixed.two-column { + margin-left: -200px; +} +div.dt-button-collection.fixed.three-column { + margin-left: -225px; +} +div.dt-button-collection.fixed.four-column { + margin-left: -300px; +} +div.dt-button-collection.fixed.columns { + margin-left: -409px; +} +@media screen and (max-width: 1024px) { + div.dt-button-collection.fixed.columns { + margin-left: -308px; + } +} +@media screen and (max-width: 640px) { + div.dt-button-collection.fixed.columns { + margin-left: -203px; + } +} +@media screen and (max-width: 460px) { + div.dt-button-collection.fixed.columns { + margin-left: -100px; + } +} +div.dt-button-collection.fixed > :last-child { + max-height: 100vh; + overflow: auto; +} +div.dt-button-collection.two-column > :last-child, div.dt-button-collection.three-column > :last-child, div.dt-button-collection.four-column > :last-child { + display: block !important; + -webkit-column-gap: 8px; + -moz-column-gap: 8px; + -ms-column-gap: 8px; + -o-column-gap: 8px; + column-gap: 8px; +} +div.dt-button-collection.two-column > :last-child > *, div.dt-button-collection.three-column > :last-child > *, div.dt-button-collection.four-column > :last-child > * { + -webkit-column-break-inside: avoid; + break-inside: avoid; +} +div.dt-button-collection.two-column { + width: 400px; +} +div.dt-button-collection.two-column > :last-child { + padding-bottom: 1px; + column-count: 2; +} +div.dt-button-collection.three-column { + width: 450px; +} +div.dt-button-collection.three-column > :last-child { + padding-bottom: 1px; + column-count: 3; +} +div.dt-button-collection.four-column { + width: 600px; +} +div.dt-button-collection.four-column > :last-child { + padding-bottom: 1px; + column-count: 4; +} +div.dt-button-collection .dt-button { + border-radius: 0; +} +div.dt-button-collection.columns { + width: auto; +} +div.dt-button-collection.columns > :last-child { + display: flex; + flex-wrap: wrap; + justify-content: flex-start; + align-items: center; + gap: 6px; + width: 818px; + padding-bottom: 1px; +} +div.dt-button-collection.columns > :last-child .dt-button { + min-width: 200px; + flex: 0 1; + margin: 0; +} +div.dt-button-collection.columns.dtb-b3 > :last-child, div.dt-button-collection.columns.dtb-b2 > :last-child, div.dt-button-collection.columns.dtb-b1 > :last-child { + justify-content: space-between; +} +div.dt-button-collection.columns.dtb-b3 .dt-button { + flex: 1 1 32%; +} +div.dt-button-collection.columns.dtb-b2 .dt-button { + flex: 1 1 48%; +} +div.dt-button-collection.columns.dtb-b1 .dt-button { + flex: 1 1 100%; +} +@media screen and (max-width: 1024px) { + div.dt-button-collection.columns > :last-child { + width: 612px; + } +} +@media screen and (max-width: 640px) { + div.dt-button-collection.columns > :last-child { + width: 406px; + } + div.dt-button-collection.columns.dtb-b3 .dt-button { + flex: 0 1 32%; + } +} +@media screen and (max-width: 460px) { + div.dt-button-collection.columns > :last-child { + width: 200px; + } +} +div.dt-button-collection .dt-button { + min-width: 200px; +} + +div.dt-button-background { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 2001; +} + +@media screen and (max-width: 767px) { + div.dt-buttons { + float: none; + width: 100%; + text-align: center; + margin-bottom: 0.5em; + } + div.dt-buttons a.btn { + float: none; + } +} +div.dt-buttons button.btn.processing, +div.dt-buttons div.btn.processing, +div.dt-buttons a.btn.processing { + color: rgba(0, 0, 0, 0.2); +} +div.dt-buttons button.btn.processing:after, +div.dt-buttons div.btn.processing:after, +div.dt-buttons a.btn.processing:after { + position: absolute; + top: 50%; + left: 50%; + width: 16px; + height: 16px; + margin: -8px 0 0 -8px; + box-sizing: border-box; + display: block; + content: " "; + border: 2px solid rgb(40, 40, 40); + border-radius: 50%; + border-left-color: transparent; + border-right-color: transparent; + animation: dtb-spinner 1500ms infinite linear; + -o-animation: dtb-spinner 1500ms infinite linear; + -ms-animation: dtb-spinner 1500ms infinite linear; + -webkit-animation: dtb-spinner 1500ms infinite linear; + -moz-animation: dtb-spinner 1500ms infinite linear; +} + +div.dt-btn-split-wrapper button.dt-btn-split-drop { + border-top-right-radius: 4px !important; + border-bottom-right-radius: 4px !important; +} +div.dt-btn-split-wrapper:active:not(.disabled) button, div.dt-btn-split-wrapper.active:not(.disabled) button { + background-color: #e6e6e6; + border-color: #adadad; +} +div.dt-btn-split-wrapper:active:not(.disabled) button.dt-btn-split-drop, div.dt-btn-split-wrapper.active:not(.disabled) button.dt-btn-split-drop { + box-shadow: none; + background-color: #fff; + border-color: rgb(173, 173, 173); +} +div.dt-btn-split-wrapper:active:not(.disabled) button:hover, div.dt-btn-split-wrapper.active:not(.disabled) button:hover { + background-color: #e6e6e6; + border-color: #adadad; +} + +span.dt-down-arrow { + color: rgba(70, 70, 70, 0.9); + font-size: 10px; + padding-left: 10px; +} + +div.dataTables_wrapper div.dt-buttons.btn-group button.btn:last-of-type:first-of-type { + border-radius: 4px !important; +} + +span.dt-down-arrow { + display: none; +} + +span.dt-button-spacer { + float: left; +} +span.dt-button-spacer.bar:empty { + height: inherit; +} + +div.dt-button-collection span.dt-button-spacer { + padding-left: 1rem !important; + text-align: left; +} diff --git a/public/assets/js/DataTables/Buttons-2.3.6/css/buttons.bootstrap.min.css b/public/assets/js/DataTables/Buttons-2.3.6/css/buttons.bootstrap.min.css new file mode 100644 index 0000000..54e96b6 --- /dev/null +++ b/public/assets/js/DataTables/Buttons-2.3.6/css/buttons.bootstrap.min.css @@ -0,0 +1 @@ +@keyframes dtb-spinner{100%{transform:rotate(360deg)}}@-o-keyframes dtb-spinner{100%{-o-transform:rotate(360deg);transform:rotate(360deg)}}@-ms-keyframes dtb-spinner{100%{-ms-transform:rotate(360deg);transform:rotate(360deg)}}@-webkit-keyframes dtb-spinner{100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@-moz-keyframes dtb-spinner{100%{-moz-transform:rotate(360deg);transform:rotate(360deg)}}div.dataTables_wrapper{position:relative}div.dt-buttons{position:initial}div.dt-buttons .dt-button{overflow:hidden;text-overflow:ellipsis}div.dt-button-info{position:fixed;top:50%;left:50%;width:400px;margin-top:-100px;margin-left:-200px;background-color:white;border:2px solid #111;box-shadow:3px 4px 10px 1px rgba(0, 0, 0, 0.3);border-radius:3px;text-align:center;z-index:21}div.dt-button-info h2{padding:.5em;margin:0;font-weight:normal;border-bottom:1px solid #ddd;background-color:#f3f3f3}div.dt-button-info>div{padding:1em}div.dtb-popover-close{position:absolute;top:10px;right:10px;width:22px;height:22px;border:1px solid #eaeaea;background-color:#f9f9f9;text-align:center;border-radius:3px;cursor:pointer;z-index:2003}button.dtb-hide-drop{display:none !important}div.dt-button-collection-title{text-align:center;padding:.3em 0 .5em;margin-left:.5em;margin-right:.5em;font-size:.9em}div.dt-button-collection-title:empty{display:none}span.dt-button-spacer{display:inline-block;margin:.5em;white-space:nowrap}span.dt-button-spacer.bar{border-left:1px solid rgba(0, 0, 0, 0.3);vertical-align:middle;padding-left:.5em}span.dt-button-spacer.bar:empty{height:1em;width:1px;padding-left:0}div.dt-button-collection span.dt-button-spacer{width:100%;font-size:.9em;text-align:center;margin:.5em 0}div.dt-button-collection span.dt-button-spacer:empty{height:0;width:100%}div.dt-button-collection span.dt-button-spacer.bar{border-left:none;border-bottom:1px solid rgba(0, 0, 0, 0.3);padding-left:0}div.dt-button-collection{position:absolute;z-index:2001;background-color:white;border:1px solid rgba(0, 0, 0, 0.15);border-radius:4px;box-shadow:0 6px 12px rgba(0, 0, 0, 0.175);padding:.5rem;min-width:200px}div.dt-button-collection div.dropdown-menu,div.dt-button-collection ul.dropdown-menu{position:relative;display:block;z-index:2002;min-width:100%;background-color:transparent;border:none;box-shadow:none;margin:2px 0;padding:0;border-radius:0}div.dt-button-collection div.dt-btn-split-wrapper{width:100%;display:inline-flex;padding-left:5px;padding-right:5px}div.dt-button-collection button.dt-btn-split-drop-button{width:100%;border:none;border-radius:0px;margin-left:0px !important}div.dt-button-collection button.dt-btn-split-drop-button:focus{border:none;border-radius:0px;outline:none}div.dt-button-collection.fixed{position:fixed;display:block;top:50%;left:50%;margin-left:-75px;border-radius:5px;background-color:white}div.dt-button-collection.fixed.two-column{margin-left:-200px}div.dt-button-collection.fixed.three-column{margin-left:-225px}div.dt-button-collection.fixed.four-column{margin-left:-300px}div.dt-button-collection.fixed.columns{margin-left:-409px}@media screen and (max-width: 1024px){div.dt-button-collection.fixed.columns{margin-left:-308px}}@media screen and (max-width: 640px){div.dt-button-collection.fixed.columns{margin-left:-203px}}@media screen and (max-width: 460px){div.dt-button-collection.fixed.columns{margin-left:-100px}}div.dt-button-collection.fixed>:last-child{max-height:100vh;overflow:auto}div.dt-button-collection.two-column>:last-child,div.dt-button-collection.three-column>:last-child,div.dt-button-collection.four-column>:last-child{display:block !important;-webkit-column-gap:8px;-moz-column-gap:8px;-ms-column-gap:8px;-o-column-gap:8px;column-gap:8px}div.dt-button-collection.two-column>:last-child>*,div.dt-button-collection.three-column>:last-child>*,div.dt-button-collection.four-column>:last-child>*{-webkit-column-break-inside:avoid;break-inside:avoid}div.dt-button-collection.two-column{width:400px}div.dt-button-collection.two-column>:last-child{padding-bottom:1px;column-count:2}div.dt-button-collection.three-column{width:450px}div.dt-button-collection.three-column>:last-child{padding-bottom:1px;column-count:3}div.dt-button-collection.four-column{width:600px}div.dt-button-collection.four-column>:last-child{padding-bottom:1px;column-count:4}div.dt-button-collection .dt-button{border-radius:0}div.dt-button-collection.columns{width:auto}div.dt-button-collection.columns>:last-child{display:flex;flex-wrap:wrap;justify-content:flex-start;align-items:center;gap:6px;width:818px;padding-bottom:1px}div.dt-button-collection.columns>:last-child .dt-button{min-width:200px;flex:0 1;margin:0}div.dt-button-collection.columns.dtb-b3>:last-child,div.dt-button-collection.columns.dtb-b2>:last-child,div.dt-button-collection.columns.dtb-b1>:last-child{justify-content:space-between}div.dt-button-collection.columns.dtb-b3 .dt-button{flex:1 1 32%}div.dt-button-collection.columns.dtb-b2 .dt-button{flex:1 1 48%}div.dt-button-collection.columns.dtb-b1 .dt-button{flex:1 1 100%}@media screen and (max-width: 1024px){div.dt-button-collection.columns>:last-child{width:612px}}@media screen and (max-width: 640px){div.dt-button-collection.columns>:last-child{width:406px}div.dt-button-collection.columns.dtb-b3 .dt-button{flex:0 1 32%}}@media screen and (max-width: 460px){div.dt-button-collection.columns>:last-child{width:200px}}div.dt-button-collection .dt-button{min-width:200px}div.dt-button-background{position:fixed;top:0;left:0;width:100%;height:100%;z-index:2001}@media screen and (max-width: 767px){div.dt-buttons{float:none;width:100%;text-align:center;margin-bottom:.5em}div.dt-buttons a.btn{float:none}}div.dt-buttons button.btn.processing,div.dt-buttons div.btn.processing,div.dt-buttons a.btn.processing{color:rgba(0, 0, 0, 0.2)}div.dt-buttons button.btn.processing:after,div.dt-buttons div.btn.processing:after,div.dt-buttons a.btn.processing:after{position:absolute;top:50%;left:50%;width:16px;height:16px;margin:-8px 0 0 -8px;box-sizing:border-box;display:block;content:" ";border:2px solid rgb(40, 40, 40);border-radius:50%;border-left-color:transparent;border-right-color:transparent;animation:dtb-spinner 1500ms infinite linear;-o-animation:dtb-spinner 1500ms infinite linear;-ms-animation:dtb-spinner 1500ms infinite linear;-webkit-animation:dtb-spinner 1500ms infinite linear;-moz-animation:dtb-spinner 1500ms infinite linear}div.dt-btn-split-wrapper button.dt-btn-split-drop{border-top-right-radius:4px !important;border-bottom-right-radius:4px !important}div.dt-btn-split-wrapper:active:not(.disabled) button,div.dt-btn-split-wrapper.active:not(.disabled) button{background-color:#e6e6e6;border-color:#adadad}div.dt-btn-split-wrapper:active:not(.disabled) button.dt-btn-split-drop,div.dt-btn-split-wrapper.active:not(.disabled) button.dt-btn-split-drop{box-shadow:none;background-color:#fff;border-color:rgb(173, 173, 173)}div.dt-btn-split-wrapper:active:not(.disabled) button:hover,div.dt-btn-split-wrapper.active:not(.disabled) button:hover{background-color:#e6e6e6;border-color:#adadad}span.dt-down-arrow{color:rgba(70, 70, 70, 0.9);font-size:10px;padding-left:10px}div.dataTables_wrapper div.dt-buttons.btn-group button.btn:last-of-type:first-of-type{border-radius:4px !important}span.dt-down-arrow{display:none}span.dt-button-spacer{float:left}span.dt-button-spacer.bar:empty{height:inherit}div.dt-button-collection span.dt-button-spacer{padding-left:1rem !important;text-align:left} diff --git a/public/assets/js/DataTables/Buttons-2.3.6/css/buttons.bootstrap4.css b/public/assets/js/DataTables/Buttons-2.3.6/css/buttons.bootstrap4.css new file mode 100644 index 0000000..b5d9244 --- /dev/null +++ b/public/assets/js/DataTables/Buttons-2.3.6/css/buttons.bootstrap4.css @@ -0,0 +1,430 @@ +@keyframes dtb-spinner { + 100% { + transform: rotate(360deg); + } +} +@-o-keyframes dtb-spinner { + 100% { + -o-transform: rotate(360deg); + transform: rotate(360deg); + } +} +@-ms-keyframes dtb-spinner { + 100% { + -ms-transform: rotate(360deg); + transform: rotate(360deg); + } +} +@-webkit-keyframes dtb-spinner { + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } +} +@-moz-keyframes dtb-spinner { + 100% { + -moz-transform: rotate(360deg); + transform: rotate(360deg); + } +} +div.dataTables_wrapper { + position: relative; +} + +div.dt-buttons { + position: initial; +} +div.dt-buttons .dt-button { + overflow: hidden; + text-overflow: ellipsis; +} + +div.dt-button-info { + position: fixed; + top: 50%; + left: 50%; + width: 400px; + margin-top: -100px; + margin-left: -200px; + background-color: white; + border: 2px solid #111; + box-shadow: 3px 4px 10px 1px rgba(0, 0, 0, 0.3); + border-radius: 3px; + text-align: center; + z-index: 21; +} +div.dt-button-info h2 { + padding: 0.5em; + margin: 0; + font-weight: normal; + border-bottom: 1px solid #ddd; + background-color: #f3f3f3; +} +div.dt-button-info > div { + padding: 1em; +} + +div.dtb-popover-close { + position: absolute; + top: 10px; + right: 10px; + width: 22px; + height: 22px; + border: 1px solid #eaeaea; + background-color: #f9f9f9; + text-align: center; + border-radius: 3px; + cursor: pointer; + z-index: 2003; +} + +button.dtb-hide-drop { + display: none !important; +} + +div.dt-button-collection-title { + text-align: center; + padding: 0.3em 0 0.5em; + margin-left: 0.5em; + margin-right: 0.5em; + font-size: 0.9em; +} + +div.dt-button-collection-title:empty { + display: none; +} + +span.dt-button-spacer { + display: inline-block; + margin: 0.5em; + white-space: nowrap; +} +span.dt-button-spacer.bar { + border-left: 1px solid rgba(0, 0, 0, 0.3); + vertical-align: middle; + padding-left: 0.5em; +} +span.dt-button-spacer.bar:empty { + height: 1em; + width: 1px; + padding-left: 0; +} + +div.dt-button-collection span.dt-button-spacer { + width: 100%; + font-size: 0.9em; + text-align: center; + margin: 0.5em 0; +} +div.dt-button-collection span.dt-button-spacer:empty { + height: 0; + width: 100%; +} +div.dt-button-collection span.dt-button-spacer.bar { + border-left: none; + border-bottom: 1px solid rgba(0, 0, 0, 0.3); + padding-left: 0; +} + +div.dt-button-collection { + position: absolute; + z-index: 2001; + background-color: white; + border: 1px solid rgba(0, 0, 0, 0.15); + border-radius: 4px; + box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175); + padding: 0.5rem; + width: 218px; +} +div.dt-button-collection div.dropdown-menu { + position: relative; + display: block; + z-index: 2002; + min-width: 100%; + background-color: transparent; + border: none; + box-shadow: none; + padding: 0; + border-radius: 0; +} +div.dt-button-collection.fixed { + position: fixed; + display: block; + top: 50%; + left: 50%; + margin-left: -75px; + border-radius: 5px; + background-color: white; +} +div.dt-button-collection.fixed.two-column { + margin-left: -200px; +} +div.dt-button-collection.fixed.three-column { + margin-left: -225px; +} +div.dt-button-collection.fixed.four-column { + margin-left: -300px; +} +div.dt-button-collection.fixed.columns { + margin-left: -409px; +} +@media screen and (max-width: 1024px) { + div.dt-button-collection.fixed.columns { + margin-left: -308px; + } +} +@media screen and (max-width: 640px) { + div.dt-button-collection.fixed.columns { + margin-left: -203px; + } +} +@media screen and (max-width: 460px) { + div.dt-button-collection.fixed.columns { + margin-left: -100px; + } +} +div.dt-button-collection.fixed > :last-child { + max-height: 100vh; + overflow: auto; +} +div.dt-button-collection.two-column > :last-child, div.dt-button-collection.three-column > :last-child, div.dt-button-collection.four-column > :last-child { + display: block !important; + -webkit-column-gap: 8px; + -moz-column-gap: 8px; + -ms-column-gap: 8px; + -o-column-gap: 8px; + column-gap: 8px; +} +div.dt-button-collection.two-column > :last-child > *, div.dt-button-collection.three-column > :last-child > *, div.dt-button-collection.four-column > :last-child > * { + -webkit-column-break-inside: avoid; + break-inside: avoid; +} +div.dt-button-collection.two-column { + width: 400px; +} +div.dt-button-collection.two-column > :last-child { + padding-bottom: 1px; + column-count: 2; +} +div.dt-button-collection.three-column { + width: 450px; +} +div.dt-button-collection.three-column > :last-child { + padding-bottom: 1px; + column-count: 3; +} +div.dt-button-collection.four-column { + width: 600px; +} +div.dt-button-collection.four-column > :last-child { + padding-bottom: 1px; + column-count: 4; +} +div.dt-button-collection .dt-button { + border-radius: 0; +} +div.dt-button-collection.columns { + width: auto; +} +div.dt-button-collection.columns > :last-child { + display: flex; + flex-wrap: wrap; + justify-content: flex-start; + align-items: center; + gap: 6px; + width: 818px; + padding-bottom: 1px; +} +div.dt-button-collection.columns > :last-child .dt-button { + min-width: 200px; + flex: 0 1; + margin: 0; +} +div.dt-button-collection.columns.dtb-b3 > :last-child, div.dt-button-collection.columns.dtb-b2 > :last-child, div.dt-button-collection.columns.dtb-b1 > :last-child { + justify-content: space-between; +} +div.dt-button-collection.columns.dtb-b3 .dt-button { + flex: 1 1 32%; +} +div.dt-button-collection.columns.dtb-b2 .dt-button { + flex: 1 1 48%; +} +div.dt-button-collection.columns.dtb-b1 .dt-button { + flex: 1 1 100%; +} +@media screen and (max-width: 1024px) { + div.dt-button-collection.columns > :last-child { + width: 612px; + } +} +@media screen and (max-width: 640px) { + div.dt-button-collection.columns > :last-child { + width: 406px; + } + div.dt-button-collection.columns.dtb-b3 .dt-button { + flex: 0 1 32%; + } +} +@media screen and (max-width: 460px) { + div.dt-button-collection.columns > :last-child { + width: 200px; + } +} +div.dt-button-collection.fixed:before, div.dt-button-collection.fixed:after { + display: none; +} +div.dt-button-collection .btn-group { + flex: 1 1 auto; +} +div.dt-button-collection .dt-button { + min-width: 200px; +} +div.dt-button-collection div.dt-btn-split-wrapper { + width: 100%; + padding-left: 5px; + padding-right: 5px; +} +div.dt-button-collection button.dt-btn-split-drop-button { + width: 100%; + color: #212529; + border: none; + background-color: white; + border-radius: 0px; + margin-left: 0px !important; +} +div.dt-button-collection button.dt-btn-split-drop-button:focus { + border: none; + border-radius: 0px; + outline: none; +} +div.dt-button-collection button.dt-btn-split-drop-button:hover { + background-color: #e9ecef; +} +div.dt-button-collection button.dt-btn-split-drop-button:active { + background-color: #007bff !important; +} + +div.dt-button-background { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 999; +} + +@media screen and (max-width: 767px) { + div.dt-buttons { + float: none; + width: 100%; + text-align: center; + margin-bottom: 0.5em; + } + div.dt-buttons a.btn { + float: none; + } +} +div.dt-buttons button.btn.processing, +div.dt-buttons div.btn.processing, +div.dt-buttons a.btn.processing { + color: rgba(0, 0, 0, 0.2); +} +div.dt-buttons button.btn.processing:after, +div.dt-buttons div.btn.processing:after, +div.dt-buttons a.btn.processing:after { + position: absolute; + top: 50%; + left: 50%; + width: 16px; + height: 16px; + margin: -8px 0 0 -8px; + box-sizing: border-box; + display: block; + content: " "; + border: 2px solid rgb(40, 40, 40); + border-radius: 50%; + border-left-color: transparent; + border-right-color: transparent; + animation: dtb-spinner 1500ms infinite linear; + -o-animation: dtb-spinner 1500ms infinite linear; + -ms-animation: dtb-spinner 1500ms infinite linear; + -webkit-animation: dtb-spinner 1500ms infinite linear; + -moz-animation: dtb-spinner 1500ms infinite linear; +} +div.dt-buttons div.btn-group { + position: initial; +} + +div.dt-btn-split-wrapper:active:not(.disabled) button, div.dt-btn-split-wrapper.active:not(.disabled) button { + background-color: #5a6268; + border-color: #545b62; +} +div.dt-btn-split-wrapper:active:not(.disabled) button.dt-btn-split-drop, div.dt-btn-split-wrapper.active:not(.disabled) button.dt-btn-split-drop { + box-shadow: none; + background-color: #6c757d; + border-color: #6c757d; +} +div.dt-btn-split-wrapper:active:not(.disabled) button:hover, div.dt-btn-split-wrapper.active:not(.disabled) button:hover { + background-color: #5a6268; + border-color: #545b62; +} + +div.dataTables_wrapper div.dt-buttons.btn-group div.btn-group { + border-radius: 4px !important; +} +div.dataTables_wrapper div.dt-buttons.btn-group div.btn-group:last-child { + border-top-left-radius: 0px !important; + border-bottom-left-radius: 0px !important; +} +div.dataTables_wrapper div.dt-buttons.btn-group div.btn-group:first-child { + border-top-right-radius: 0px !important; + border-bottom-right-radius: 0px !important; +} +div.dataTables_wrapper div.dt-buttons.btn-group div.btn-group:last-child:first-child { + border-top-left-radius: 4px !important; + border-bottom-left-radius: 4px !important; + border-top-right-radius: 4px !important; + border-bottom-right-radius: 4px !important; +} +div.dataTables_wrapper div.dt-buttons.btn-group div.btn-group button.dt-btn-split-drop:last-child { + border: 1px solid rgb(108, 117, 125); +} +div.dataTables_wrapper div.dt-buttons.btn-group div.btn-group div.dt-btn-split-wrapper { + border: none; +} + +div.dt-button-collection div.btn-group { + border-radius: 4px !important; +} +div.dt-button-collection div.btn-group button { + border-radius: 4px; +} +div.dt-button-collection div.btn-group button:last-child { + border-top-left-radius: 0px !important; + border-bottom-left-radius: 0px !important; +} +div.dt-button-collection div.btn-group button:first-child { + border-top-right-radius: 0px !important; + border-bottom-right-radius: 0px !important; +} +div.dt-button-collection div.btn-group button:last-child:first-child { + border-top-left-radius: 4px !important; + border-bottom-left-radius: 4px !important; + border-top-right-radius: 4px !important; + border-bottom-right-radius: 4px !important; +} +div.dt-button-collection div.btn-group button.dt-btn-split-drop:last-child { + border: 1px solid rgb(108, 117, 125); +} +div.dt-button-collection div.btn-group div.dt-btn-split-wrapper { + border: none; +} + +span.dt-button-spacer.bar:empty { + height: inherit; +} + +div.dt-button-collection span.dt-button-spacer { + padding-left: 1rem !important; + text-align: left; +} diff --git a/public/assets/js/DataTables/Buttons-2.3.6/css/buttons.bootstrap4.min.css b/public/assets/js/DataTables/Buttons-2.3.6/css/buttons.bootstrap4.min.css new file mode 100644 index 0000000..1d97a24 --- /dev/null +++ b/public/assets/js/DataTables/Buttons-2.3.6/css/buttons.bootstrap4.min.css @@ -0,0 +1 @@ +@keyframes dtb-spinner{100%{transform:rotate(360deg)}}@-o-keyframes dtb-spinner{100%{-o-transform:rotate(360deg);transform:rotate(360deg)}}@-ms-keyframes dtb-spinner{100%{-ms-transform:rotate(360deg);transform:rotate(360deg)}}@-webkit-keyframes dtb-spinner{100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@-moz-keyframes dtb-spinner{100%{-moz-transform:rotate(360deg);transform:rotate(360deg)}}div.dataTables_wrapper{position:relative}div.dt-buttons{position:initial}div.dt-buttons .dt-button{overflow:hidden;text-overflow:ellipsis}div.dt-button-info{position:fixed;top:50%;left:50%;width:400px;margin-top:-100px;margin-left:-200px;background-color:white;border:2px solid #111;box-shadow:3px 4px 10px 1px rgba(0, 0, 0, 0.3);border-radius:3px;text-align:center;z-index:21}div.dt-button-info h2{padding:.5em;margin:0;font-weight:normal;border-bottom:1px solid #ddd;background-color:#f3f3f3}div.dt-button-info>div{padding:1em}div.dtb-popover-close{position:absolute;top:10px;right:10px;width:22px;height:22px;border:1px solid #eaeaea;background-color:#f9f9f9;text-align:center;border-radius:3px;cursor:pointer;z-index:2003}button.dtb-hide-drop{display:none !important}div.dt-button-collection-title{text-align:center;padding:.3em 0 .5em;margin-left:.5em;margin-right:.5em;font-size:.9em}div.dt-button-collection-title:empty{display:none}span.dt-button-spacer{display:inline-block;margin:.5em;white-space:nowrap}span.dt-button-spacer.bar{border-left:1px solid rgba(0, 0, 0, 0.3);vertical-align:middle;padding-left:.5em}span.dt-button-spacer.bar:empty{height:1em;width:1px;padding-left:0}div.dt-button-collection span.dt-button-spacer{width:100%;font-size:.9em;text-align:center;margin:.5em 0}div.dt-button-collection span.dt-button-spacer:empty{height:0;width:100%}div.dt-button-collection span.dt-button-spacer.bar{border-left:none;border-bottom:1px solid rgba(0, 0, 0, 0.3);padding-left:0}div.dt-button-collection{position:absolute;z-index:2001;background-color:white;border:1px solid rgba(0, 0, 0, 0.15);border-radius:4px;box-shadow:0 6px 12px rgba(0, 0, 0, 0.175);padding:.5rem;width:218px}div.dt-button-collection div.dropdown-menu{position:relative;display:block;z-index:2002;min-width:100%;background-color:transparent;border:none;box-shadow:none;padding:0;border-radius:0}div.dt-button-collection.fixed{position:fixed;display:block;top:50%;left:50%;margin-left:-75px;border-radius:5px;background-color:white}div.dt-button-collection.fixed.two-column{margin-left:-200px}div.dt-button-collection.fixed.three-column{margin-left:-225px}div.dt-button-collection.fixed.four-column{margin-left:-300px}div.dt-button-collection.fixed.columns{margin-left:-409px}@media screen and (max-width: 1024px){div.dt-button-collection.fixed.columns{margin-left:-308px}}@media screen and (max-width: 640px){div.dt-button-collection.fixed.columns{margin-left:-203px}}@media screen and (max-width: 460px){div.dt-button-collection.fixed.columns{margin-left:-100px}}div.dt-button-collection.fixed>:last-child{max-height:100vh;overflow:auto}div.dt-button-collection.two-column>:last-child,div.dt-button-collection.three-column>:last-child,div.dt-button-collection.four-column>:last-child{display:block !important;-webkit-column-gap:8px;-moz-column-gap:8px;-ms-column-gap:8px;-o-column-gap:8px;column-gap:8px}div.dt-button-collection.two-column>:last-child>*,div.dt-button-collection.three-column>:last-child>*,div.dt-button-collection.four-column>:last-child>*{-webkit-column-break-inside:avoid;break-inside:avoid}div.dt-button-collection.two-column{width:400px}div.dt-button-collection.two-column>:last-child{padding-bottom:1px;column-count:2}div.dt-button-collection.three-column{width:450px}div.dt-button-collection.three-column>:last-child{padding-bottom:1px;column-count:3}div.dt-button-collection.four-column{width:600px}div.dt-button-collection.four-column>:last-child{padding-bottom:1px;column-count:4}div.dt-button-collection .dt-button{border-radius:0}div.dt-button-collection.columns{width:auto}div.dt-button-collection.columns>:last-child{display:flex;flex-wrap:wrap;justify-content:flex-start;align-items:center;gap:6px;width:818px;padding-bottom:1px}div.dt-button-collection.columns>:last-child .dt-button{min-width:200px;flex:0 1;margin:0}div.dt-button-collection.columns.dtb-b3>:last-child,div.dt-button-collection.columns.dtb-b2>:last-child,div.dt-button-collection.columns.dtb-b1>:last-child{justify-content:space-between}div.dt-button-collection.columns.dtb-b3 .dt-button{flex:1 1 32%}div.dt-button-collection.columns.dtb-b2 .dt-button{flex:1 1 48%}div.dt-button-collection.columns.dtb-b1 .dt-button{flex:1 1 100%}@media screen and (max-width: 1024px){div.dt-button-collection.columns>:last-child{width:612px}}@media screen and (max-width: 640px){div.dt-button-collection.columns>:last-child{width:406px}div.dt-button-collection.columns.dtb-b3 .dt-button{flex:0 1 32%}}@media screen and (max-width: 460px){div.dt-button-collection.columns>:last-child{width:200px}}div.dt-button-collection.fixed:before,div.dt-button-collection.fixed:after{display:none}div.dt-button-collection .btn-group{flex:1 1 auto}div.dt-button-collection .dt-button{min-width:200px}div.dt-button-collection div.dt-btn-split-wrapper{width:100%;padding-left:5px;padding-right:5px}div.dt-button-collection button.dt-btn-split-drop-button{width:100%;color:#212529;border:none;background-color:white;border-radius:0px;margin-left:0px !important}div.dt-button-collection button.dt-btn-split-drop-button:focus{border:none;border-radius:0px;outline:none}div.dt-button-collection button.dt-btn-split-drop-button:hover{background-color:#e9ecef}div.dt-button-collection button.dt-btn-split-drop-button:active{background-color:#007bff !important}div.dt-button-background{position:fixed;top:0;left:0;width:100%;height:100%;z-index:999}@media screen and (max-width: 767px){div.dt-buttons{float:none;width:100%;text-align:center;margin-bottom:.5em}div.dt-buttons a.btn{float:none}}div.dt-buttons button.btn.processing,div.dt-buttons div.btn.processing,div.dt-buttons a.btn.processing{color:rgba(0, 0, 0, 0.2)}div.dt-buttons button.btn.processing:after,div.dt-buttons div.btn.processing:after,div.dt-buttons a.btn.processing:after{position:absolute;top:50%;left:50%;width:16px;height:16px;margin:-8px 0 0 -8px;box-sizing:border-box;display:block;content:" ";border:2px solid rgb(40, 40, 40);border-radius:50%;border-left-color:transparent;border-right-color:transparent;animation:dtb-spinner 1500ms infinite linear;-o-animation:dtb-spinner 1500ms infinite linear;-ms-animation:dtb-spinner 1500ms infinite linear;-webkit-animation:dtb-spinner 1500ms infinite linear;-moz-animation:dtb-spinner 1500ms infinite linear}div.dt-buttons div.btn-group{position:initial}div.dt-btn-split-wrapper:active:not(.disabled) button,div.dt-btn-split-wrapper.active:not(.disabled) button{background-color:#5a6268;border-color:#545b62}div.dt-btn-split-wrapper:active:not(.disabled) button.dt-btn-split-drop,div.dt-btn-split-wrapper.active:not(.disabled) button.dt-btn-split-drop{box-shadow:none;background-color:#6c757d;border-color:#6c757d}div.dt-btn-split-wrapper:active:not(.disabled) button:hover,div.dt-btn-split-wrapper.active:not(.disabled) button:hover{background-color:#5a6268;border-color:#545b62}div.dataTables_wrapper div.dt-buttons.btn-group div.btn-group{border-radius:4px !important}div.dataTables_wrapper div.dt-buttons.btn-group div.btn-group:last-child{border-top-left-radius:0px !important;border-bottom-left-radius:0px !important}div.dataTables_wrapper div.dt-buttons.btn-group div.btn-group:first-child{border-top-right-radius:0px !important;border-bottom-right-radius:0px !important}div.dataTables_wrapper div.dt-buttons.btn-group div.btn-group:last-child:first-child{border-top-left-radius:4px !important;border-bottom-left-radius:4px !important;border-top-right-radius:4px !important;border-bottom-right-radius:4px !important}div.dataTables_wrapper div.dt-buttons.btn-group div.btn-group button.dt-btn-split-drop:last-child{border:1px solid rgb(108, 117, 125)}div.dataTables_wrapper div.dt-buttons.btn-group div.btn-group div.dt-btn-split-wrapper{border:none}div.dt-button-collection div.btn-group{border-radius:4px !important}div.dt-button-collection div.btn-group button{border-radius:4px}div.dt-button-collection div.btn-group button:last-child{border-top-left-radius:0px !important;border-bottom-left-radius:0px !important}div.dt-button-collection div.btn-group button:first-child{border-top-right-radius:0px !important;border-bottom-right-radius:0px !important}div.dt-button-collection div.btn-group button:last-child:first-child{border-top-left-radius:4px !important;border-bottom-left-radius:4px !important;border-top-right-radius:4px !important;border-bottom-right-radius:4px !important}div.dt-button-collection div.btn-group button.dt-btn-split-drop:last-child{border:1px solid rgb(108, 117, 125)}div.dt-button-collection div.btn-group div.dt-btn-split-wrapper{border:none}span.dt-button-spacer.bar:empty{height:inherit}div.dt-button-collection span.dt-button-spacer{padding-left:1rem !important;text-align:left} diff --git a/public/assets/js/DataTables/Buttons-2.3.6/css/buttons.bootstrap5.css b/public/assets/js/DataTables/Buttons-2.3.6/css/buttons.bootstrap5.css new file mode 100644 index 0000000..7e26228 --- /dev/null +++ b/public/assets/js/DataTables/Buttons-2.3.6/css/buttons.bootstrap5.css @@ -0,0 +1,432 @@ +@keyframes dtb-spinner { + 100% { + transform: rotate(360deg); + } +} +@-o-keyframes dtb-spinner { + 100% { + -o-transform: rotate(360deg); + transform: rotate(360deg); + } +} +@-ms-keyframes dtb-spinner { + 100% { + -ms-transform: rotate(360deg); + transform: rotate(360deg); + } +} +@-webkit-keyframes dtb-spinner { + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } +} +@-moz-keyframes dtb-spinner { + 100% { + -moz-transform: rotate(360deg); + transform: rotate(360deg); + } +} +div.dataTables_wrapper { + position: relative; +} + +div.dt-buttons { + position: initial; +} +div.dt-buttons .dt-button { + overflow: hidden; + text-overflow: ellipsis; +} + +div.dt-button-info { + position: fixed; + top: 50%; + left: 50%; + width: 400px; + margin-top: -100px; + margin-left: -200px; + background-color: white; + border: 2px solid #111; + box-shadow: 3px 4px 10px 1px rgba(0, 0, 0, 0.3); + border-radius: 3px; + text-align: center; + z-index: 21; +} +div.dt-button-info h2 { + padding: 0.5em; + margin: 0; + font-weight: normal; + border-bottom: 1px solid #ddd; + background-color: #f3f3f3; +} +div.dt-button-info > div { + padding: 1em; +} + +div.dtb-popover-close { + position: absolute; + top: 10px; + right: 10px; + width: 22px; + height: 22px; + border: 1px solid #eaeaea; + background-color: #f9f9f9; + text-align: center; + border-radius: 3px; + cursor: pointer; + z-index: 2003; +} + +button.dtb-hide-drop { + display: none !important; +} + +div.dt-button-collection-title { + text-align: center; + padding: 0.3em 0 0.5em; + margin-left: 0.5em; + margin-right: 0.5em; + font-size: 0.9em; +} + +div.dt-button-collection-title:empty { + display: none; +} + +span.dt-button-spacer { + display: inline-block; + margin: 0.5em; + white-space: nowrap; +} +span.dt-button-spacer.bar { + border-left: 1px solid rgba(0, 0, 0, 0.3); + vertical-align: middle; + padding-left: 0.5em; +} +span.dt-button-spacer.bar:empty { + height: 1em; + width: 1px; + padding-left: 0; +} + +div.dt-button-collection span.dt-button-spacer { + width: 100%; + font-size: 0.9em; + text-align: center; + margin: 0.5em 0; +} +div.dt-button-collection span.dt-button-spacer:empty { + height: 0; + width: 100%; +} +div.dt-button-collection span.dt-button-spacer.bar { + border-left: none; + border-bottom: 1px solid rgba(0, 0, 0, 0.3); + padding-left: 0; +} + +div.dt-button-collection { + position: absolute; + z-index: 2001; + background-color: white; + border: 1px solid rgba(0, 0, 0, 0.15); + border-radius: 4px; + box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175); + padding: 0.5rem; + width: 218px; +} +div.dt-button-collection div.dropdown-menu { + position: relative; + display: block; + background-color: transparent; + border: none; + box-shadow: none; + padding: 0; + border-radius: 0; + z-index: 2002; + min-width: 100%; +} +div.dt-button-collection.fixed { + position: fixed; + display: block; + top: 50%; + left: 50%; + margin-left: -75px; + border-radius: 5px; + background-color: white; +} +div.dt-button-collection.fixed.two-column { + margin-left: -200px; +} +div.dt-button-collection.fixed.three-column { + margin-left: -225px; +} +div.dt-button-collection.fixed.four-column { + margin-left: -300px; +} +div.dt-button-collection.fixed.columns { + margin-left: -409px; +} +@media screen and (max-width: 1024px) { + div.dt-button-collection.fixed.columns { + margin-left: -308px; + } +} +@media screen and (max-width: 640px) { + div.dt-button-collection.fixed.columns { + margin-left: -203px; + } +} +@media screen and (max-width: 460px) { + div.dt-button-collection.fixed.columns { + margin-left: -100px; + } +} +div.dt-button-collection.fixed > :last-child { + max-height: 100vh; + overflow: auto; +} +div.dt-button-collection.two-column > :last-child, div.dt-button-collection.three-column > :last-child, div.dt-button-collection.four-column > :last-child { + display: block !important; + -webkit-column-gap: 8px; + -moz-column-gap: 8px; + -ms-column-gap: 8px; + -o-column-gap: 8px; + column-gap: 8px; +} +div.dt-button-collection.two-column > :last-child > *, div.dt-button-collection.three-column > :last-child > *, div.dt-button-collection.four-column > :last-child > * { + -webkit-column-break-inside: avoid; + break-inside: avoid; +} +div.dt-button-collection.two-column { + width: 400px; +} +div.dt-button-collection.two-column > :last-child { + padding-bottom: 1px; + column-count: 2; +} +div.dt-button-collection.three-column { + width: 450px; +} +div.dt-button-collection.three-column > :last-child { + padding-bottom: 1px; + column-count: 3; +} +div.dt-button-collection.four-column { + width: 600px; +} +div.dt-button-collection.four-column > :last-child { + padding-bottom: 1px; + column-count: 4; +} +div.dt-button-collection .dt-button { + border-radius: 0; +} +div.dt-button-collection.columns { + width: auto; +} +div.dt-button-collection.columns > :last-child { + display: flex; + flex-wrap: wrap; + justify-content: flex-start; + align-items: center; + gap: 6px; + width: 818px; + padding-bottom: 1px; +} +div.dt-button-collection.columns > :last-child .dt-button { + min-width: 200px; + flex: 0 1; + margin: 0; +} +div.dt-button-collection.columns.dtb-b3 > :last-child, div.dt-button-collection.columns.dtb-b2 > :last-child, div.dt-button-collection.columns.dtb-b1 > :last-child { + justify-content: space-between; +} +div.dt-button-collection.columns.dtb-b3 .dt-button { + flex: 1 1 32%; +} +div.dt-button-collection.columns.dtb-b2 .dt-button { + flex: 1 1 48%; +} +div.dt-button-collection.columns.dtb-b1 .dt-button { + flex: 1 1 100%; +} +@media screen and (max-width: 1024px) { + div.dt-button-collection.columns > :last-child { + width: 612px; + } +} +@media screen and (max-width: 640px) { + div.dt-button-collection.columns > :last-child { + width: 406px; + } + div.dt-button-collection.columns.dtb-b3 .dt-button { + flex: 0 1 32%; + } +} +@media screen and (max-width: 460px) { + div.dt-button-collection.columns > :last-child { + width: 200px; + } +} +div.dt-button-collection.fixed:before, div.dt-button-collection.fixed:after { + display: none; +} +div.dt-button-collection .btn-group { + flex: 1 1 auto; +} +div.dt-button-collection .dt-button:not(.dt-btn-split-drop) { + min-width: 200px; +} +div.dt-button-collection div.dt-btn-split-wrapper { + width: 100%; +} +div.dt-button-collection button.dt-btn-split-drop-button { + width: 100%; + color: #212529; + border: none; + background-color: white; + border-radius: 0px; + margin-left: 0px !important; +} +div.dt-button-collection button.dt-btn-split-drop-button:focus { + border: none; + border-radius: 0px; + outline: none; +} +div.dt-button-collection button.dt-btn-split-drop-button:hover { + background-color: #e9ecef; +} +div.dt-button-collection button.dt-btn-split-drop-button:active { + background-color: #007bff !important; +} + +div.dt-button-background { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 999; +} + +@media screen and (max-width: 767px) { + div.dt-buttons { + float: none; + width: 100%; + text-align: center; + margin-bottom: 0.5em; + } + div.dt-buttons a.btn { + float: none; + } +} +div.dt-buttons button.btn.processing, +div.dt-buttons div.btn.processing, +div.dt-buttons a.btn.processing { + color: rgba(0, 0, 0, 0.2); +} +div.dt-buttons button.btn.processing:after, +div.dt-buttons div.btn.processing:after, +div.dt-buttons a.btn.processing:after { + position: absolute; + top: 50%; + left: 50%; + width: 16px; + height: 16px; + margin: -8px 0 0 -8px; + box-sizing: border-box; + display: block; + content: " "; + border: 2px solid rgb(40, 40, 40); + border-radius: 50%; + border-left-color: transparent; + border-right-color: transparent; + animation: dtb-spinner 1500ms infinite linear; + -o-animation: dtb-spinner 1500ms infinite linear; + -ms-animation: dtb-spinner 1500ms infinite linear; + -webkit-animation: dtb-spinner 1500ms infinite linear; + -moz-animation: dtb-spinner 1500ms infinite linear; +} +div.dt-buttons div.btn-group { + position: initial; +} + +div.dt-btn-split-wrapper button.dt-btn-split-drop { + border-top-right-radius: 0.25rem !important; + border-bottom-right-radius: 0.25rem !important; +} +div.dt-btn-split-wrapper:active:not(.disabled) button, div.dt-btn-split-wrapper.active:not(.disabled) button { + background-color: #5a6268; + border-color: #545b62; +} +div.dt-btn-split-wrapper:active:not(.disabled) button.dt-btn-split-drop, div.dt-btn-split-wrapper.active:not(.disabled) button.dt-btn-split-drop { + box-shadow: none; + background-color: #6c757d; + border-color: #6c757d; +} +div.dt-btn-split-wrapper:active:not(.disabled) button:hover, div.dt-btn-split-wrapper.active:not(.disabled) button:hover { + background-color: #5a6268; + border-color: #545b62; +} + +div.dataTables_wrapper div.dt-buttons.btn-group div.btn-group { + border-radius: 4px !important; +} +div.dataTables_wrapper div.dt-buttons.btn-group div.btn-group:last-child { + border-top-left-radius: 0px !important; + border-bottom-left-radius: 0px !important; +} +div.dataTables_wrapper div.dt-buttons.btn-group div.btn-group:first-child { + border-top-right-radius: 0px !important; + border-bottom-right-radius: 0px !important; +} +div.dataTables_wrapper div.dt-buttons.btn-group div.btn-group:last-child:first-child { + border-top-left-radius: 4px !important; + border-bottom-left-radius: 4px !important; + border-top-right-radius: 4px !important; + border-bottom-right-radius: 4px !important; +} +div.dataTables_wrapper div.dt-buttons.btn-group div.btn-group button.dt-btn-split-drop:last-child { + border: 1px solid rgb(108, 117, 125); +} +div.dataTables_wrapper div.dt-buttons.btn-group div.btn-group div.dt-btn-split-wrapper { + border: none; +} + +div.dt-button-collection div.btn-group { + border-radius: 4px !important; +} +div.dt-button-collection div.btn-group button { + border-radius: 4px; +} +div.dt-button-collection div.btn-group button:last-child { + border-top-left-radius: 0px !important; + border-bottom-left-radius: 0px !important; +} +div.dt-button-collection div.btn-group button:first-child { + border-top-right-radius: 0px !important; + border-bottom-right-radius: 0px !important; +} +div.dt-button-collection div.btn-group button:last-child:first-child { + border-top-left-radius: 4px !important; + border-bottom-left-radius: 4px !important; + border-top-right-radius: 4px !important; + border-bottom-right-radius: 4px !important; +} +div.dt-button-collection div.btn-group button.dt-btn-split-drop:last-child { + border: 1px solid rgb(108, 117, 125); +} +div.dt-button-collection div.btn-group div.dt-btn-split-wrapper { + border: none; +} + +span.dt-button-spacer.bar:empty { + height: inherit; +} + +div.dt-button-collection span.dt-button-spacer { + padding-left: 1rem !important; + text-align: left; +} diff --git a/public/assets/js/DataTables/Buttons-2.3.6/css/buttons.bootstrap5.min.css b/public/assets/js/DataTables/Buttons-2.3.6/css/buttons.bootstrap5.min.css new file mode 100644 index 0000000..3103eef --- /dev/null +++ b/public/assets/js/DataTables/Buttons-2.3.6/css/buttons.bootstrap5.min.css @@ -0,0 +1 @@ +@keyframes dtb-spinner{100%{transform:rotate(360deg)}}@-o-keyframes dtb-spinner{100%{-o-transform:rotate(360deg);transform:rotate(360deg)}}@-ms-keyframes dtb-spinner{100%{-ms-transform:rotate(360deg);transform:rotate(360deg)}}@-webkit-keyframes dtb-spinner{100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@-moz-keyframes dtb-spinner{100%{-moz-transform:rotate(360deg);transform:rotate(360deg)}}div.dataTables_wrapper{position:relative}div.dt-buttons{position:initial}div.dt-buttons .dt-button{overflow:hidden;text-overflow:ellipsis}div.dt-button-info{position:fixed;top:50%;left:50%;width:400px;margin-top:-100px;margin-left:-200px;background-color:white;border:2px solid #111;box-shadow:3px 4px 10px 1px rgba(0, 0, 0, 0.3);border-radius:3px;text-align:center;z-index:21}div.dt-button-info h2{padding:.5em;margin:0;font-weight:normal;border-bottom:1px solid #ddd;background-color:#f3f3f3}div.dt-button-info>div{padding:1em}div.dtb-popover-close{position:absolute;top:10px;right:10px;width:22px;height:22px;border:1px solid #eaeaea;background-color:#f9f9f9;text-align:center;border-radius:3px;cursor:pointer;z-index:2003}button.dtb-hide-drop{display:none !important}div.dt-button-collection-title{text-align:center;padding:.3em 0 .5em;margin-left:.5em;margin-right:.5em;font-size:.9em}div.dt-button-collection-title:empty{display:none}span.dt-button-spacer{display:inline-block;margin:.5em;white-space:nowrap}span.dt-button-spacer.bar{border-left:1px solid rgba(0, 0, 0, 0.3);vertical-align:middle;padding-left:.5em}span.dt-button-spacer.bar:empty{height:1em;width:1px;padding-left:0}div.dt-button-collection span.dt-button-spacer{width:100%;font-size:.9em;text-align:center;margin:.5em 0}div.dt-button-collection span.dt-button-spacer:empty{height:0;width:100%}div.dt-button-collection span.dt-button-spacer.bar{border-left:none;border-bottom:1px solid rgba(0, 0, 0, 0.3);padding-left:0}div.dt-button-collection{position:absolute;z-index:2001;background-color:white;border:1px solid rgba(0, 0, 0, 0.15);border-radius:4px;box-shadow:0 6px 12px rgba(0, 0, 0, 0.175);padding:.5rem;width:218px}div.dt-button-collection div.dropdown-menu{position:relative;display:block;background-color:transparent;border:none;box-shadow:none;padding:0;border-radius:0;z-index:2002;min-width:100%}div.dt-button-collection.fixed{position:fixed;display:block;top:50%;left:50%;margin-left:-75px;border-radius:5px;background-color:white}div.dt-button-collection.fixed.two-column{margin-left:-200px}div.dt-button-collection.fixed.three-column{margin-left:-225px}div.dt-button-collection.fixed.four-column{margin-left:-300px}div.dt-button-collection.fixed.columns{margin-left:-409px}@media screen and (max-width: 1024px){div.dt-button-collection.fixed.columns{margin-left:-308px}}@media screen and (max-width: 640px){div.dt-button-collection.fixed.columns{margin-left:-203px}}@media screen and (max-width: 460px){div.dt-button-collection.fixed.columns{margin-left:-100px}}div.dt-button-collection.fixed>:last-child{max-height:100vh;overflow:auto}div.dt-button-collection.two-column>:last-child,div.dt-button-collection.three-column>:last-child,div.dt-button-collection.four-column>:last-child{display:block !important;-webkit-column-gap:8px;-moz-column-gap:8px;-ms-column-gap:8px;-o-column-gap:8px;column-gap:8px}div.dt-button-collection.two-column>:last-child>*,div.dt-button-collection.three-column>:last-child>*,div.dt-button-collection.four-column>:last-child>*{-webkit-column-break-inside:avoid;break-inside:avoid}div.dt-button-collection.two-column{width:400px}div.dt-button-collection.two-column>:last-child{padding-bottom:1px;column-count:2}div.dt-button-collection.three-column{width:450px}div.dt-button-collection.three-column>:last-child{padding-bottom:1px;column-count:3}div.dt-button-collection.four-column{width:600px}div.dt-button-collection.four-column>:last-child{padding-bottom:1px;column-count:4}div.dt-button-collection .dt-button{border-radius:0}div.dt-button-collection.columns{width:auto}div.dt-button-collection.columns>:last-child{display:flex;flex-wrap:wrap;justify-content:flex-start;align-items:center;gap:6px;width:818px;padding-bottom:1px}div.dt-button-collection.columns>:last-child .dt-button{min-width:200px;flex:0 1;margin:0}div.dt-button-collection.columns.dtb-b3>:last-child,div.dt-button-collection.columns.dtb-b2>:last-child,div.dt-button-collection.columns.dtb-b1>:last-child{justify-content:space-between}div.dt-button-collection.columns.dtb-b3 .dt-button{flex:1 1 32%}div.dt-button-collection.columns.dtb-b2 .dt-button{flex:1 1 48%}div.dt-button-collection.columns.dtb-b1 .dt-button{flex:1 1 100%}@media screen and (max-width: 1024px){div.dt-button-collection.columns>:last-child{width:612px}}@media screen and (max-width: 640px){div.dt-button-collection.columns>:last-child{width:406px}div.dt-button-collection.columns.dtb-b3 .dt-button{flex:0 1 32%}}@media screen and (max-width: 460px){div.dt-button-collection.columns>:last-child{width:200px}}div.dt-button-collection.fixed:before,div.dt-button-collection.fixed:after{display:none}div.dt-button-collection .btn-group{flex:1 1 auto}div.dt-button-collection .dt-button:not(.dt-btn-split-drop){min-width:200px}div.dt-button-collection div.dt-btn-split-wrapper{width:100%}div.dt-button-collection button.dt-btn-split-drop-button{width:100%;color:#212529;border:none;background-color:white;border-radius:0px;margin-left:0px !important}div.dt-button-collection button.dt-btn-split-drop-button:focus{border:none;border-radius:0px;outline:none}div.dt-button-collection button.dt-btn-split-drop-button:hover{background-color:#e9ecef}div.dt-button-collection button.dt-btn-split-drop-button:active{background-color:#007bff !important}div.dt-button-background{position:fixed;top:0;left:0;width:100%;height:100%;z-index:999}@media screen and (max-width: 767px){div.dt-buttons{float:none;width:100%;text-align:center;margin-bottom:.5em}div.dt-buttons a.btn{float:none}}div.dt-buttons button.btn.processing,div.dt-buttons div.btn.processing,div.dt-buttons a.btn.processing{color:rgba(0, 0, 0, 0.2)}div.dt-buttons button.btn.processing:after,div.dt-buttons div.btn.processing:after,div.dt-buttons a.btn.processing:after{position:absolute;top:50%;left:50%;width:16px;height:16px;margin:-8px 0 0 -8px;box-sizing:border-box;display:block;content:" ";border:2px solid rgb(40, 40, 40);border-radius:50%;border-left-color:transparent;border-right-color:transparent;animation:dtb-spinner 1500ms infinite linear;-o-animation:dtb-spinner 1500ms infinite linear;-ms-animation:dtb-spinner 1500ms infinite linear;-webkit-animation:dtb-spinner 1500ms infinite linear;-moz-animation:dtb-spinner 1500ms infinite linear}div.dt-buttons div.btn-group{position:initial}div.dt-btn-split-wrapper button.dt-btn-split-drop{border-top-right-radius:.25rem !important;border-bottom-right-radius:.25rem !important}div.dt-btn-split-wrapper:active:not(.disabled) button,div.dt-btn-split-wrapper.active:not(.disabled) button{background-color:#5a6268;border-color:#545b62}div.dt-btn-split-wrapper:active:not(.disabled) button.dt-btn-split-drop,div.dt-btn-split-wrapper.active:not(.disabled) button.dt-btn-split-drop{box-shadow:none;background-color:#6c757d;border-color:#6c757d}div.dt-btn-split-wrapper:active:not(.disabled) button:hover,div.dt-btn-split-wrapper.active:not(.disabled) button:hover{background-color:#5a6268;border-color:#545b62}div.dataTables_wrapper div.dt-buttons.btn-group div.btn-group{border-radius:4px !important}div.dataTables_wrapper div.dt-buttons.btn-group div.btn-group:last-child{border-top-left-radius:0px !important;border-bottom-left-radius:0px !important}div.dataTables_wrapper div.dt-buttons.btn-group div.btn-group:first-child{border-top-right-radius:0px !important;border-bottom-right-radius:0px !important}div.dataTables_wrapper div.dt-buttons.btn-group div.btn-group:last-child:first-child{border-top-left-radius:4px !important;border-bottom-left-radius:4px !important;border-top-right-radius:4px !important;border-bottom-right-radius:4px !important}div.dataTables_wrapper div.dt-buttons.btn-group div.btn-group button.dt-btn-split-drop:last-child{border:1px solid rgb(108, 117, 125)}div.dataTables_wrapper div.dt-buttons.btn-group div.btn-group div.dt-btn-split-wrapper{border:none}div.dt-button-collection div.btn-group{border-radius:4px !important}div.dt-button-collection div.btn-group button{border-radius:4px}div.dt-button-collection div.btn-group button:last-child{border-top-left-radius:0px !important;border-bottom-left-radius:0px !important}div.dt-button-collection div.btn-group button:first-child{border-top-right-radius:0px !important;border-bottom-right-radius:0px !important}div.dt-button-collection div.btn-group button:last-child:first-child{border-top-left-radius:4px !important;border-bottom-left-radius:4px !important;border-top-right-radius:4px !important;border-bottom-right-radius:4px !important}div.dt-button-collection div.btn-group button.dt-btn-split-drop:last-child{border:1px solid rgb(108, 117, 125)}div.dt-button-collection div.btn-group div.dt-btn-split-wrapper{border:none}span.dt-button-spacer.bar:empty{height:inherit}div.dt-button-collection span.dt-button-spacer{padding-left:1rem !important;text-align:left} diff --git a/public/assets/js/DataTables/Buttons-2.3.6/css/buttons.bulma.css b/public/assets/js/DataTables/Buttons-2.3.6/css/buttons.bulma.css new file mode 100644 index 0000000..9d711c5 --- /dev/null +++ b/public/assets/js/DataTables/Buttons-2.3.6/css/buttons.bulma.css @@ -0,0 +1,429 @@ +@keyframes dtb-spinner { + 100% { + transform: rotate(360deg); + } +} +@-o-keyframes dtb-spinner { + 100% { + -o-transform: rotate(360deg); + transform: rotate(360deg); + } +} +@-ms-keyframes dtb-spinner { + 100% { + -ms-transform: rotate(360deg); + transform: rotate(360deg); + } +} +@-webkit-keyframes dtb-spinner { + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } +} +@-moz-keyframes dtb-spinner { + 100% { + -moz-transform: rotate(360deg); + transform: rotate(360deg); + } +} +div.dataTables_wrapper { + position: relative; +} + +div.dt-buttons { + position: initial; +} +div.dt-buttons .dt-button { + overflow: hidden; + text-overflow: ellipsis; +} + +div.dt-button-info { + position: fixed; + top: 50%; + left: 50%; + width: 400px; + margin-top: -100px; + margin-left: -200px; + background-color: white; + border: 2px solid #111; + box-shadow: 3px 4px 10px 1px rgba(0, 0, 0, 0.3); + border-radius: 3px; + text-align: center; + z-index: 21; +} +div.dt-button-info h2 { + padding: 0.5em; + margin: 0; + font-weight: normal; + border-bottom: 1px solid #ddd; + background-color: #f3f3f3; +} +div.dt-button-info > div { + padding: 1em; +} + +div.dtb-popover-close { + position: absolute; + top: 10px; + right: 10px; + width: 22px; + height: 22px; + border: 1px solid #eaeaea; + background-color: #f9f9f9; + text-align: center; + border-radius: 3px; + cursor: pointer; + z-index: 2003; +} + +button.dtb-hide-drop { + display: none !important; +} + +div.dt-button-collection-title { + text-align: center; + padding: 0.3em 0 0.5em; + margin-left: 0.5em; + margin-right: 0.5em; + font-size: 0.9em; +} + +div.dt-button-collection-title:empty { + display: none; +} + +span.dt-button-spacer { + display: inline-block; + margin: 0.5em; + white-space: nowrap; +} +span.dt-button-spacer.bar { + border-left: 1px solid rgba(0, 0, 0, 0.3); + vertical-align: middle; + padding-left: 0.5em; +} +span.dt-button-spacer.bar:empty { + height: 1em; + width: 1px; + padding-left: 0; +} + +div.dt-button-collection span.dt-button-spacer { + width: 100%; + font-size: 0.9em; + text-align: center; + margin: 0.5em 0; +} +div.dt-button-collection span.dt-button-spacer:empty { + height: 0; + width: 100%; +} +div.dt-button-collection span.dt-button-spacer.bar { + border-left: none; + border-bottom: 1px solid rgba(0, 0, 0, 0.3); + padding-left: 0; +} + +div.dt-button-collection { + position: absolute; + z-index: 2001; + min-width: 200px; + background: white; + max-width: none; + display: block; + box-shadow: 0 0.5em 1em -0.125em rgba(10, 10, 10, 0.1), 0 0 0 1px rgba(10, 10, 10, 0.02); + border-radius: 4; + padding: 0.5rem; +} +div.dt-button-collection div.dropdown-menu { + display: block; + z-index: 2002; + min-width: 100%; +} +div.dt-button-collection div.dt-btn-split-wrapper { + width: 100%; + padding-left: 5px; + padding-right: 5px; + margin-bottom: 0px; + display: flex; + flex-direction: row; + flex-wrap: wrap; + justify-content: flex-start; + align-content: flex-start; + align-items: stretch; +} +div.dt-button-collection div.dt-btn-split-wrapper button { + margin-right: 0px; + display: inline-block; + width: 0; + flex-grow: 1; + flex-shrink: 0; + flex-basis: 50px; + margin-top: 0px; + border-bottom-left-radius: 3px; + border-top-left-radius: 3px; + border-top-right-radius: 0px; + border-bottom-right-radius: 0px; + overflow: hidden; + text-overflow: ellipsis; +} +div.dt-button-collection div.dt-btn-split-wrapper button.dt-button { + min-width: 30px; + margin-left: -1px; + flex-grow: 0; + flex-shrink: 0; + flex-basis: 0; + border-bottom-left-radius: 0px; + border-top-left-radius: 0px; + border-top-right-radius: 3px; + border-bottom-right-radius: 3px; + padding: 0px; +} +div.dt-button-collection.fixed { + position: fixed; + display: block; + top: 50%; + left: 50%; + margin-left: -75px; + border-radius: 5px; + background-color: white; +} +div.dt-button-collection.fixed.two-column { + margin-left: -200px; +} +div.dt-button-collection.fixed.three-column { + margin-left: -225px; +} +div.dt-button-collection.fixed.four-column { + margin-left: -300px; +} +div.dt-button-collection.fixed.columns { + margin-left: -409px; +} +@media screen and (max-width: 1024px) { + div.dt-button-collection.fixed.columns { + margin-left: -308px; + } +} +@media screen and (max-width: 640px) { + div.dt-button-collection.fixed.columns { + margin-left: -203px; + } +} +@media screen and (max-width: 460px) { + div.dt-button-collection.fixed.columns { + margin-left: -100px; + } +} +div.dt-button-collection.fixed > :last-child { + max-height: 100vh; + overflow: auto; +} +div.dt-button-collection.two-column > :last-child, div.dt-button-collection.three-column > :last-child, div.dt-button-collection.four-column > :last-child { + display: block !important; + -webkit-column-gap: 8px; + -moz-column-gap: 8px; + -ms-column-gap: 8px; + -o-column-gap: 8px; + column-gap: 8px; +} +div.dt-button-collection.two-column > :last-child > *, div.dt-button-collection.three-column > :last-child > *, div.dt-button-collection.four-column > :last-child > * { + -webkit-column-break-inside: avoid; + break-inside: avoid; +} +div.dt-button-collection.two-column { + width: 400px; +} +div.dt-button-collection.two-column > :last-child { + padding-bottom: 1px; + column-count: 2; +} +div.dt-button-collection.three-column { + width: 450px; +} +div.dt-button-collection.three-column > :last-child { + padding-bottom: 1px; + column-count: 3; +} +div.dt-button-collection.four-column { + width: 600px; +} +div.dt-button-collection.four-column > :last-child { + padding-bottom: 1px; + column-count: 4; +} +div.dt-button-collection .dt-button { + border-radius: 0; +} +div.dt-button-collection.columns { + width: auto; +} +div.dt-button-collection.columns > :last-child { + display: flex; + flex-wrap: wrap; + justify-content: flex-start; + align-items: center; + gap: 6px; + width: 818px; + padding-bottom: 1px; +} +div.dt-button-collection.columns > :last-child .dt-button { + min-width: 200px; + flex: 0 1; + margin: 0; +} +div.dt-button-collection.columns.dtb-b3 > :last-child, div.dt-button-collection.columns.dtb-b2 > :last-child, div.dt-button-collection.columns.dtb-b1 > :last-child { + justify-content: space-between; +} +div.dt-button-collection.columns.dtb-b3 .dt-button { + flex: 1 1 32%; +} +div.dt-button-collection.columns.dtb-b2 .dt-button { + flex: 1 1 48%; +} +div.dt-button-collection.columns.dtb-b1 .dt-button { + flex: 1 1 100%; +} +@media screen and (max-width: 1024px) { + div.dt-button-collection.columns > :last-child { + width: 612px; + } +} +@media screen and (max-width: 640px) { + div.dt-button-collection.columns > :last-child { + width: 406px; + } + div.dt-button-collection.columns.dtb-b3 .dt-button { + flex: 0 1 32%; + } +} +@media screen and (max-width: 460px) { + div.dt-button-collection.columns > :last-child { + width: 200px; + } +} +div.dt-button-collection .dropdown-content { + box-shadow: none; + padding-top: 0; + border-radius: 0; +} +div.dt-button-collection.fixed:before, div.dt-button-collection.fixed:after { + display: none; +} + +div.dt-button-background { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 999; +} + +@media screen and (max-width: 767px) { + div.dt-buttons { + float: none; + width: 100%; + text-align: center; + margin-bottom: 0.5em; + } + div.dt-buttons a.btn { + float: none; + } +} +div.dt-buttons button.btn.processing, +div.dt-buttons div.btn.processing, +div.dt-buttons a.btn.processing { + color: rgba(0, 0, 0, 0.2); +} +div.dt-buttons button.btn.processing:after, +div.dt-buttons div.btn.processing:after, +div.dt-buttons a.btn.processing:after { + position: absolute; + top: 50%; + left: 50%; + width: 16px; + height: 16px; + margin: -8px 0 0 -8px; + box-sizing: border-box; + display: block; + content: " "; + border: 2px solid rgb(40, 40, 40); + border-radius: 50%; + border-left-color: transparent; + border-right-color: transparent; + animation: dtb-spinner 1500ms infinite linear; + -o-animation: dtb-spinner 1500ms infinite linear; + -ms-animation: dtb-spinner 1500ms infinite linear; + -webkit-animation: dtb-spinner 1500ms infinite linear; + -moz-animation: dtb-spinner 1500ms infinite linear; +} +div.dt-buttons button.button { + margin-left: 5px; +} +div.dt-buttons button.button:first-child { + margin-left: 0px; +} + +span.dt-down-arrow { + display: none; +} + +span.dt-button-spacer { + display: inline-flex; + margin: 0.5em; + white-space: nowrap; + align-items: center; + font-size: 1rem; +} +span.dt-button-spacer.bar:empty { + height: inherit; +} + +div.dt-button-collection span.dt-button-spacer { + text-align: left; + font-size: 0.875rem; + padding-left: 1rem !important; +} + +div.dt-btn-split-wrapper { + padding-left: 5px; + padding-right: 5px; + margin-bottom: 0px; + margin-bottom: 0px !important; +} +div.dt-btn-split-wrapper button { + margin-right: 0px; + display: inline-block; + margin-top: 0px; + border-bottom-left-radius: 3px; + border-top-left-radius: 3px; + border-top-right-radius: 0px; + border-bottom-right-radius: 0px; + overflow: hidden; + text-overflow: ellipsis; +} +div.dt-btn-split-wrapper button.dt-button { + min-width: 30px; + margin-left: -1px; + border-bottom-left-radius: 0px; + border-top-left-radius: 0px; + border-top-right-radius: 3px; + border-bottom-right-radius: 3px; + padding: 0px; +} +div.dt-btn-split-wrapper:active:not(.disabled) button, div.dt-btn-split-wrapper.active:not(.disabled) button, div.dt-btn-split-wrapper.is-active:not(.disabled) button { + background-color: #eee; + border-color: transparent; +} +div.dt-btn-split-wrapper:active:not(.disabled) button.dt-button, div.dt-btn-split-wrapper.active:not(.disabled) button.dt-button, div.dt-btn-split-wrapper.is-active:not(.disabled) button.dt-button { + box-shadow: none; + background-color: rgb(245, 245, 245); + border-color: transparent; +} +div.dt-btn-split-wrapper:active:not(.disabled) button:hover, div.dt-btn-split-wrapper.active:not(.disabled) button:hover, div.dt-btn-split-wrapper.is-active:not(.disabled) button:hover { + background-color: #eee; + border-color: transparent; +} diff --git a/public/assets/js/DataTables/Buttons-2.3.6/css/buttons.bulma.min.css b/public/assets/js/DataTables/Buttons-2.3.6/css/buttons.bulma.min.css new file mode 100644 index 0000000..c4a08cd --- /dev/null +++ b/public/assets/js/DataTables/Buttons-2.3.6/css/buttons.bulma.min.css @@ -0,0 +1 @@ +@keyframes dtb-spinner{100%{transform:rotate(360deg)}}@-o-keyframes dtb-spinner{100%{-o-transform:rotate(360deg);transform:rotate(360deg)}}@-ms-keyframes dtb-spinner{100%{-ms-transform:rotate(360deg);transform:rotate(360deg)}}@-webkit-keyframes dtb-spinner{100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@-moz-keyframes dtb-spinner{100%{-moz-transform:rotate(360deg);transform:rotate(360deg)}}div.dataTables_wrapper{position:relative}div.dt-buttons{position:initial}div.dt-buttons .dt-button{overflow:hidden;text-overflow:ellipsis}div.dt-button-info{position:fixed;top:50%;left:50%;width:400px;margin-top:-100px;margin-left:-200px;background-color:white;border:2px solid #111;box-shadow:3px 4px 10px 1px rgba(0, 0, 0, 0.3);border-radius:3px;text-align:center;z-index:21}div.dt-button-info h2{padding:.5em;margin:0;font-weight:normal;border-bottom:1px solid #ddd;background-color:#f3f3f3}div.dt-button-info>div{padding:1em}div.dtb-popover-close{position:absolute;top:10px;right:10px;width:22px;height:22px;border:1px solid #eaeaea;background-color:#f9f9f9;text-align:center;border-radius:3px;cursor:pointer;z-index:2003}button.dtb-hide-drop{display:none !important}div.dt-button-collection-title{text-align:center;padding:.3em 0 .5em;margin-left:.5em;margin-right:.5em;font-size:.9em}div.dt-button-collection-title:empty{display:none}span.dt-button-spacer{display:inline-block;margin:.5em;white-space:nowrap}span.dt-button-spacer.bar{border-left:1px solid rgba(0, 0, 0, 0.3);vertical-align:middle;padding-left:.5em}span.dt-button-spacer.bar:empty{height:1em;width:1px;padding-left:0}div.dt-button-collection span.dt-button-spacer{width:100%;font-size:.9em;text-align:center;margin:.5em 0}div.dt-button-collection span.dt-button-spacer:empty{height:0;width:100%}div.dt-button-collection span.dt-button-spacer.bar{border-left:none;border-bottom:1px solid rgba(0, 0, 0, 0.3);padding-left:0}div.dt-button-collection{position:absolute;z-index:2001;min-width:200px;background:white;max-width:none;display:block;box-shadow:0 .5em 1em -0.125em rgba(10, 10, 10, 0.1),0 0 0 1px rgba(10, 10, 10, 0.02);border-radius:4;padding:.5rem}div.dt-button-collection div.dropdown-menu{display:block;z-index:2002;min-width:100%}div.dt-button-collection div.dt-btn-split-wrapper{width:100%;padding-left:5px;padding-right:5px;margin-bottom:0px;display:flex;flex-direction:row;flex-wrap:wrap;justify-content:flex-start;align-content:flex-start;align-items:stretch}div.dt-button-collection div.dt-btn-split-wrapper button{margin-right:0px;display:inline-block;width:0;flex-grow:1;flex-shrink:0;flex-basis:50px;margin-top:0px;border-bottom-left-radius:3px;border-top-left-radius:3px;border-top-right-radius:0px;border-bottom-right-radius:0px;overflow:hidden;text-overflow:ellipsis}div.dt-button-collection div.dt-btn-split-wrapper button.dt-button{min-width:30px;margin-left:-1px;flex-grow:0;flex-shrink:0;flex-basis:0;border-bottom-left-radius:0px;border-top-left-radius:0px;border-top-right-radius:3px;border-bottom-right-radius:3px;padding:0px}div.dt-button-collection.fixed{position:fixed;display:block;top:50%;left:50%;margin-left:-75px;border-radius:5px;background-color:white}div.dt-button-collection.fixed.two-column{margin-left:-200px}div.dt-button-collection.fixed.three-column{margin-left:-225px}div.dt-button-collection.fixed.four-column{margin-left:-300px}div.dt-button-collection.fixed.columns{margin-left:-409px}@media screen and (max-width: 1024px){div.dt-button-collection.fixed.columns{margin-left:-308px}}@media screen and (max-width: 640px){div.dt-button-collection.fixed.columns{margin-left:-203px}}@media screen and (max-width: 460px){div.dt-button-collection.fixed.columns{margin-left:-100px}}div.dt-button-collection.fixed>:last-child{max-height:100vh;overflow:auto}div.dt-button-collection.two-column>:last-child,div.dt-button-collection.three-column>:last-child,div.dt-button-collection.four-column>:last-child{display:block !important;-webkit-column-gap:8px;-moz-column-gap:8px;-ms-column-gap:8px;-o-column-gap:8px;column-gap:8px}div.dt-button-collection.two-column>:last-child>*,div.dt-button-collection.three-column>:last-child>*,div.dt-button-collection.four-column>:last-child>*{-webkit-column-break-inside:avoid;break-inside:avoid}div.dt-button-collection.two-column{width:400px}div.dt-button-collection.two-column>:last-child{padding-bottom:1px;column-count:2}div.dt-button-collection.three-column{width:450px}div.dt-button-collection.three-column>:last-child{padding-bottom:1px;column-count:3}div.dt-button-collection.four-column{width:600px}div.dt-button-collection.four-column>:last-child{padding-bottom:1px;column-count:4}div.dt-button-collection .dt-button{border-radius:0}div.dt-button-collection.columns{width:auto}div.dt-button-collection.columns>:last-child{display:flex;flex-wrap:wrap;justify-content:flex-start;align-items:center;gap:6px;width:818px;padding-bottom:1px}div.dt-button-collection.columns>:last-child .dt-button{min-width:200px;flex:0 1;margin:0}div.dt-button-collection.columns.dtb-b3>:last-child,div.dt-button-collection.columns.dtb-b2>:last-child,div.dt-button-collection.columns.dtb-b1>:last-child{justify-content:space-between}div.dt-button-collection.columns.dtb-b3 .dt-button{flex:1 1 32%}div.dt-button-collection.columns.dtb-b2 .dt-button{flex:1 1 48%}div.dt-button-collection.columns.dtb-b1 .dt-button{flex:1 1 100%}@media screen and (max-width: 1024px){div.dt-button-collection.columns>:last-child{width:612px}}@media screen and (max-width: 640px){div.dt-button-collection.columns>:last-child{width:406px}div.dt-button-collection.columns.dtb-b3 .dt-button{flex:0 1 32%}}@media screen and (max-width: 460px){div.dt-button-collection.columns>:last-child{width:200px}}div.dt-button-collection .dropdown-content{box-shadow:none;padding-top:0;border-radius:0}div.dt-button-collection.fixed:before,div.dt-button-collection.fixed:after{display:none}div.dt-button-background{position:fixed;top:0;left:0;width:100%;height:100%;z-index:999}@media screen and (max-width: 767px){div.dt-buttons{float:none;width:100%;text-align:center;margin-bottom:.5em}div.dt-buttons a.btn{float:none}}div.dt-buttons button.btn.processing,div.dt-buttons div.btn.processing,div.dt-buttons a.btn.processing{color:rgba(0, 0, 0, 0.2)}div.dt-buttons button.btn.processing:after,div.dt-buttons div.btn.processing:after,div.dt-buttons a.btn.processing:after{position:absolute;top:50%;left:50%;width:16px;height:16px;margin:-8px 0 0 -8px;box-sizing:border-box;display:block;content:" ";border:2px solid rgb(40, 40, 40);border-radius:50%;border-left-color:transparent;border-right-color:transparent;animation:dtb-spinner 1500ms infinite linear;-o-animation:dtb-spinner 1500ms infinite linear;-ms-animation:dtb-spinner 1500ms infinite linear;-webkit-animation:dtb-spinner 1500ms infinite linear;-moz-animation:dtb-spinner 1500ms infinite linear}div.dt-buttons button.button{margin-left:5px}div.dt-buttons button.button:first-child{margin-left:0px}span.dt-down-arrow{display:none}span.dt-button-spacer{display:inline-flex;margin:.5em;white-space:nowrap;align-items:center;font-size:1rem}span.dt-button-spacer.bar:empty{height:inherit}div.dt-button-collection span.dt-button-spacer{text-align:left;font-size:.875rem;padding-left:1rem !important}div.dt-btn-split-wrapper{padding-left:5px;padding-right:5px;margin-bottom:0px;margin-bottom:0px !important}div.dt-btn-split-wrapper button{margin-right:0px;display:inline-block;margin-top:0px;border-bottom-left-radius:3px;border-top-left-radius:3px;border-top-right-radius:0px;border-bottom-right-radius:0px;overflow:hidden;text-overflow:ellipsis}div.dt-btn-split-wrapper button.dt-button{min-width:30px;margin-left:-1px;border-bottom-left-radius:0px;border-top-left-radius:0px;border-top-right-radius:3px;border-bottom-right-radius:3px;padding:0px}div.dt-btn-split-wrapper:active:not(.disabled) button,div.dt-btn-split-wrapper.active:not(.disabled) button,div.dt-btn-split-wrapper.is-active:not(.disabled) button{background-color:#eee;border-color:transparent}div.dt-btn-split-wrapper:active:not(.disabled) button.dt-button,div.dt-btn-split-wrapper.active:not(.disabled) button.dt-button,div.dt-btn-split-wrapper.is-active:not(.disabled) button.dt-button{box-shadow:none;background-color:rgb(245, 245, 245);border-color:transparent}div.dt-btn-split-wrapper:active:not(.disabled) button:hover,div.dt-btn-split-wrapper.active:not(.disabled) button:hover,div.dt-btn-split-wrapper.is-active:not(.disabled) button:hover{background-color:#eee;border-color:transparent} diff --git a/public/assets/js/DataTables/Buttons-2.3.6/css/buttons.dataTables.css b/public/assets/js/DataTables/Buttons-2.3.6/css/buttons.dataTables.css new file mode 100644 index 0000000..adf98f2 --- /dev/null +++ b/public/assets/js/DataTables/Buttons-2.3.6/css/buttons.dataTables.css @@ -0,0 +1,560 @@ +@keyframes dtb-spinner { + 100% { + transform: rotate(360deg); + } +} +@-o-keyframes dtb-spinner { + 100% { + -o-transform: rotate(360deg); + transform: rotate(360deg); + } +} +@-ms-keyframes dtb-spinner { + 100% { + -ms-transform: rotate(360deg); + transform: rotate(360deg); + } +} +@-webkit-keyframes dtb-spinner { + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } +} +@-moz-keyframes dtb-spinner { + 100% { + -moz-transform: rotate(360deg); + transform: rotate(360deg); + } +} +div.dataTables_wrapper { + position: relative; +} + +div.dt-buttons { + position: initial; +} +div.dt-buttons .dt-button { + overflow: hidden; + text-overflow: ellipsis; +} + +div.dt-button-info { + position: fixed; + top: 50%; + left: 50%; + width: 400px; + margin-top: -100px; + margin-left: -200px; + background-color: white; + border: 2px solid #111; + box-shadow: 3px 4px 10px 1px rgba(0, 0, 0, 0.3); + border-radius: 3px; + text-align: center; + z-index: 21; +} +div.dt-button-info h2 { + padding: 0.5em; + margin: 0; + font-weight: normal; + border-bottom: 1px solid #ddd; + background-color: #f3f3f3; +} +div.dt-button-info > div { + padding: 1em; +} + +div.dtb-popover-close { + position: absolute; + top: 10px; + right: 10px; + width: 22px; + height: 22px; + border: 1px solid #eaeaea; + background-color: #f9f9f9; + text-align: center; + border-radius: 3px; + cursor: pointer; + z-index: 2003; +} + +button.dtb-hide-drop { + display: none !important; +} + +div.dt-button-collection-title { + text-align: center; + padding: 0.3em 0 0.5em; + margin-left: 0.5em; + margin-right: 0.5em; + font-size: 0.9em; +} + +div.dt-button-collection-title:empty { + display: none; +} + +span.dt-button-spacer { + display: inline-block; + margin: 0.5em; + white-space: nowrap; +} +span.dt-button-spacer.bar { + border-left: 1px solid rgba(0, 0, 0, 0.3); + vertical-align: middle; + padding-left: 0.5em; +} +span.dt-button-spacer.bar:empty { + height: 1em; + width: 1px; + padding-left: 0; +} + +div.dt-button-collection span.dt-button-spacer { + width: 100%; + font-size: 0.9em; + text-align: center; + margin: 0.5em 0; +} +div.dt-button-collection span.dt-button-spacer:empty { + height: 0; + width: 100%; +} +div.dt-button-collection span.dt-button-spacer.bar { + border-left: none; + border-bottom: 1px solid rgba(0, 0, 0, 0.3); + padding-left: 0; +} + +button.dt-button, +div.dt-button, +a.dt-button, +input.dt-button { + position: relative; + display: inline-block; + box-sizing: border-box; + margin-left: 0.167em; + margin-right: 0.167em; + margin-bottom: 0.333em; + padding: 0.5em 1em; + border: 1px solid rgba(0, 0, 0, 0.3); + border-radius: 2px; + cursor: pointer; + font-size: 0.88em; + line-height: 1.6em; + color: black; + white-space: nowrap; + overflow: hidden; + background-color: rgba(0, 0, 0, 0.1); /* Fallback */ + background: linear-gradient(to bottom, rgba(230, 230, 230, 0.1) 0%, rgba(0, 0, 0, 0.1) 100%); + filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr="rgba(230, 230, 230, 0.1)", EndColorStr="rgba(0, 0, 0, 0.1)"); + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + text-decoration: none; + outline: none; + text-overflow: ellipsis; +} +button.dt-button:first-child, +div.dt-button:first-child, +a.dt-button:first-child, +input.dt-button:first-child { + margin-left: 0; +} +button.dt-button.disabled, +div.dt-button.disabled, +a.dt-button.disabled, +input.dt-button.disabled { + cursor: default; + opacity: 0.4; +} +button.dt-button.active:not(.disabled), +div.dt-button.active:not(.disabled), +a.dt-button.active:not(.disabled), +input.dt-button.active:not(.disabled) { + background-color: rgba(0, 0, 0, 0.1); /* Fallback */ + background: linear-gradient(to bottom, rgba(179, 179, 179, 0.1) 0%, rgba(0, 0, 0, 0.1) 100%); + filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr="rgba(179, 179, 179, 0.1)", EndColorStr="rgba(0, 0, 0, 0.1)"); + box-shadow: inset 1px 1px 3px #999999; +} +button.dt-button.active:not(.disabled):hover:not(.disabled), +div.dt-button.active:not(.disabled):hover:not(.disabled), +a.dt-button.active:not(.disabled):hover:not(.disabled), +input.dt-button.active:not(.disabled):hover:not(.disabled) { + box-shadow: inset 1px 1px 3px #999999; + background-color: rgba(0, 0, 0, 0.1); /* Fallback */ + background: linear-gradient(to bottom, rgba(128, 128, 128, 0.1) 0%, rgba(0, 0, 0, 0.1) 100%); + filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr="rgba(128, 128, 128, 0.1)", EndColorStr="rgba(0, 0, 0, 0.1)"); +} +button.dt-button:hover, +div.dt-button:hover, +a.dt-button:hover, +input.dt-button:hover { + text-decoration: none; +} +button.dt-button:hover:not(.disabled), +div.dt-button:hover:not(.disabled), +a.dt-button:hover:not(.disabled), +input.dt-button:hover:not(.disabled) { + border: 1px solid #666; + background-color: rgba(0, 0, 0, 0.1); /* Fallback */ + background: linear-gradient(to bottom, rgba(153, 153, 153, 0.1) 0%, rgba(0, 0, 0, 0.1) 100%); + filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr="rgba(153, 153, 153, 0.1)", EndColorStr="rgba(0, 0, 0, 0.1)"); +} +button.dt-button:focus:not(.disabled), +div.dt-button:focus:not(.disabled), +a.dt-button:focus:not(.disabled), +input.dt-button:focus:not(.disabled) { + border: 1px solid #426c9e; + text-shadow: 0 1px 0 #c4def1; + outline: none; + background-color: rgb(121, 172, 233); /* Fallback */ + background: linear-gradient(to bottom, #d1e2f7 0%, rgb(121, 172, 233) 100%); + filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr="#d1e2f7", EndColorStr="rgb(121, 172, 233)"); +} +button.dt-button.active:focus:not(.disabled), +div.dt-button.active:focus:not(.disabled), +a.dt-button.active:focus:not(.disabled), +input.dt-button.active:focus:not(.disabled) { + background: linear-gradient(to bottom, #d1e2f7 0%, rgb(121, 172, 233) 100%) !important; +} +button.dt-button span.dt-down-arrow, +div.dt-button span.dt-down-arrow, +a.dt-button span.dt-down-arrow, +input.dt-button span.dt-down-arrow { + position: relative; + top: -2px; + color: rgba(70, 70, 70, 0.75); + font-size: 8px; + padding-left: 10px; + line-height: 1em; +} + +.dt-button embed { + outline: none; +} + +div.dt-buttons { + float: left; +} +div.dt-buttons.buttons-right { + float: right; +} + +div.dataTables_layout_cell div.dt-buttons { + float: none; +} +div.dataTables_layout_cell div.dt-buttons.buttons-right { + float: none; +} + +div.dt-btn-split-wrapper { + display: inline-block; +} + +div.dt-button-collection { + position: absolute; + top: 0; + left: 0; + width: 200px; + margin-top: 3px; + margin-bottom: 3px; + padding: 4px 4px 2px 4px; + border: 1px solid #ccc; + border: 1px solid rgba(0, 0, 0, 0.4); + background-color: white; + overflow: hidden; + z-index: 2002; + border-radius: 5px; + box-shadow: 3px 4px 10px 1px rgba(0, 0, 0, 0.3); + box-sizing: border-box; +} +div.dt-button-collection button.dt-button, +div.dt-button-collection div.dt-button, +div.dt-button-collection a.dt-button { + position: relative; + left: 0; + right: 0; + width: 100%; + display: block; + float: none; + margin: 4px 0 2px 0; +} +div.dt-button-collection button.dt-button:active:not(.disabled), div.dt-button-collection button.dt-button.active:not(.disabled), +div.dt-button-collection div.dt-button:active:not(.disabled), +div.dt-button-collection div.dt-button.active:not(.disabled), +div.dt-button-collection a.dt-button:active:not(.disabled), +div.dt-button-collection a.dt-button.active:not(.disabled) { + background-color: #dadada; /* Fallback */ + background: linear-gradient(to bottom, #f0f0f0 0%, #dadada 100%); + filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr="#f0f0f0", EndColorStr="#dadada"); + box-shadow: inset 1px 1px 3px #666; +} +div.dt-button-collection button.dt-button:first-child, +div.dt-button-collection div.dt-button:first-child, +div.dt-button-collection a.dt-button:first-child { + margin-top: 0; + border-top-left-radius: 3px; + border-top-right-radius: 3px; +} +div.dt-button-collection button.dt-button:last-child, +div.dt-button-collection div.dt-button:last-child, +div.dt-button-collection a.dt-button:last-child { + border-bottom-left-radius: 3px; + border-bottom-right-radius: 3px; +} +div.dt-button-collection div.dt-btn-split-wrapper { + display: flex; + flex-direction: row; + flex-wrap: wrap; + justify-content: flex-start; + align-content: flex-start; + align-items: stretch; + margin: 4px 0 2px 0; +} +div.dt-button-collection div.dt-btn-split-wrapper button.dt-button { + margin: 0; + display: inline-block; + width: 0; + flex-grow: 1; + flex-shrink: 0; + flex-basis: 50px; + border-radius: 0; +} +div.dt-button-collection div.dt-btn-split-wrapper button.dt-btn-split-drop { + min-width: 20px; + flex-grow: 0; + flex-shrink: 0; + flex-basis: 0; +} +div.dt-button-collection div.dt-btn-split-wrapper:first-child { + margin-top: 0; +} +div.dt-button-collection div.dt-btn-split-wrapper:first-child button.dt-button { + border-top-left-radius: 3px; +} +div.dt-button-collection div.dt-btn-split-wrapper:first-child button.dt-btn-split-drop { + border-top-right-radius: 3px; +} +div.dt-button-collection div.dt-btn-split-wrapper:last-child button.dt-button { + border-bottom-left-radius: 3px; +} +div.dt-button-collection div.dt-btn-split-wrapper:last-child button.dt-btn-split-drop { + border-bottom-right-radius: 3px; +} +div.dt-button-collection div.dt-btn-split-wrapper:active:not(.disabled) button.dt-button, div.dt-button-collection div.dt-btn-split-wrapper.active:not(.disabled) button.dt-button { + background-color: #dadada; /* Fallback */ + background: linear-gradient(to bottom, #f0f0f0 0%, #dadada 100%); + filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr="#f0f0f0", EndColorStr="#dadada"); + box-shadow: inset 0px 0px 4px #666; +} +div.dt-button-collection div.dt-btn-split-wrapper:active:not(.disabled) button.dt-btn-split-drop, div.dt-button-collection div.dt-btn-split-wrapper.active:not(.disabled) button.dt-btn-split-drop { + box-shadow: none; +} +div.dt-button-collection.fixed .dt-button:first-child { + margin-top: 0; + border-top-left-radius: 0; + border-top-right-radius: 0; +} +div.dt-button-collection.fixed .dt-button:last-child { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; +} +div.dt-button-collection.fixed { + position: fixed; + display: block; + top: 50%; + left: 50%; + margin-left: -75px; + border-radius: 5px; + background-color: white; +} +div.dt-button-collection.fixed.two-column { + margin-left: -200px; +} +div.dt-button-collection.fixed.three-column { + margin-left: -225px; +} +div.dt-button-collection.fixed.four-column { + margin-left: -300px; +} +div.dt-button-collection.fixed.columns { + margin-left: -409px; +} +@media screen and (max-width: 1024px) { + div.dt-button-collection.fixed.columns { + margin-left: -308px; + } +} +@media screen and (max-width: 640px) { + div.dt-button-collection.fixed.columns { + margin-left: -203px; + } +} +@media screen and (max-width: 460px) { + div.dt-button-collection.fixed.columns { + margin-left: -100px; + } +} +div.dt-button-collection.fixed > :last-child { + max-height: 100vh; + overflow: auto; +} +div.dt-button-collection.two-column > :last-child, div.dt-button-collection.three-column > :last-child, div.dt-button-collection.four-column > :last-child { + display: block !important; + -webkit-column-gap: 8px; + -moz-column-gap: 8px; + -ms-column-gap: 8px; + -o-column-gap: 8px; + column-gap: 8px; +} +div.dt-button-collection.two-column > :last-child > *, div.dt-button-collection.three-column > :last-child > *, div.dt-button-collection.four-column > :last-child > * { + -webkit-column-break-inside: avoid; + break-inside: avoid; +} +div.dt-button-collection.two-column { + width: 400px; +} +div.dt-button-collection.two-column > :last-child { + padding-bottom: 1px; + column-count: 2; +} +div.dt-button-collection.three-column { + width: 450px; +} +div.dt-button-collection.three-column > :last-child { + padding-bottom: 1px; + column-count: 3; +} +div.dt-button-collection.four-column { + width: 600px; +} +div.dt-button-collection.four-column > :last-child { + padding-bottom: 1px; + column-count: 4; +} +div.dt-button-collection .dt-button { + border-radius: 0; +} +div.dt-button-collection.columns { + width: auto; +} +div.dt-button-collection.columns > :last-child { + display: flex; + flex-wrap: wrap; + justify-content: flex-start; + align-items: center; + gap: 6px; + width: 818px; + padding-bottom: 1px; +} +div.dt-button-collection.columns > :last-child .dt-button { + min-width: 200px; + flex: 0 1; + margin: 0; +} +div.dt-button-collection.columns.dtb-b3 > :last-child, div.dt-button-collection.columns.dtb-b2 > :last-child, div.dt-button-collection.columns.dtb-b1 > :last-child { + justify-content: space-between; +} +div.dt-button-collection.columns.dtb-b3 .dt-button { + flex: 1 1 32%; +} +div.dt-button-collection.columns.dtb-b2 .dt-button { + flex: 1 1 48%; +} +div.dt-button-collection.columns.dtb-b1 .dt-button { + flex: 1 1 100%; +} +@media screen and (max-width: 1024px) { + div.dt-button-collection.columns > :last-child { + width: 612px; + } +} +@media screen and (max-width: 640px) { + div.dt-button-collection.columns > :last-child { + width: 406px; + } + div.dt-button-collection.columns.dtb-b3 .dt-button { + flex: 0 1 32%; + } +} +@media screen and (max-width: 460px) { + div.dt-button-collection.columns > :last-child { + width: 200px; + } +} + +div.dt-button-background { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.7); /* Fallback */ + background: radial-gradient(ellipse farthest-corner at center, rgba(0, 0, 0, 0.3) 0%, rgba(0, 0, 0, 0.7) 100%); /* W3C Markup, IE10 Release Preview */ + z-index: 2001; +} + +@media screen and (max-width: 640px) { + div.dt-buttons { + float: none !important; + text-align: center; + } +} +button.dt-button.processing, +div.dt-button.processing, +a.dt-button.processing { + color: rgba(0, 0, 0, 0.2); +} +button.dt-button.processing:after, +div.dt-button.processing:after, +a.dt-button.processing:after { + position: absolute; + top: 50%; + left: 50%; + width: 16px; + height: 16px; + margin: -8px 0 0 -8px; + box-sizing: border-box; + display: block; + content: " "; + border: 2px solid rgb(40, 40, 40); + border-radius: 50%; + border-left-color: transparent; + border-right-color: transparent; + animation: dtb-spinner 1500ms infinite linear; + -o-animation: dtb-spinner 1500ms infinite linear; + -ms-animation: dtb-spinner 1500ms infinite linear; + -webkit-animation: dtb-spinner 1500ms infinite linear; + -moz-animation: dtb-spinner 1500ms infinite linear; +} + +button.dt-btn-split-drop { + margin-left: calc(-1px - 0.333em); + padding-bottom: calc(0.5em - 1px); + border-radius: 0px 1px 1px 0px; + color: rgba(70, 70, 70, 0.9); + border-left: none; +} +button.dt-btn-split-drop span.dt-btn-split-drop-arrow { + position: relative; + top: -1px; + left: -2px; + font-size: 8px; +} +button.dt-btn-split-drop:hover { + z-index: 2; +} + +button.buttons-split { + border-right: 1px solid rgba(70, 70, 70, 0); + border-radius: 1px 0px 0px 1px; +} + +button.dt-btn-split-drop-button { + background-color: rgb(255, 255, 255); +} +button.dt-btn-split-drop-button:hover { + background-color: rgb(255, 255, 255); +} diff --git a/public/assets/js/DataTables/Buttons-2.3.6/css/buttons.dataTables.min.css b/public/assets/js/DataTables/Buttons-2.3.6/css/buttons.dataTables.min.css new file mode 100644 index 0000000..02094ef --- /dev/null +++ b/public/assets/js/DataTables/Buttons-2.3.6/css/buttons.dataTables.min.css @@ -0,0 +1 @@ +@keyframes dtb-spinner{100%{transform:rotate(360deg)}}@-o-keyframes dtb-spinner{100%{-o-transform:rotate(360deg);transform:rotate(360deg)}}@-ms-keyframes dtb-spinner{100%{-ms-transform:rotate(360deg);transform:rotate(360deg)}}@-webkit-keyframes dtb-spinner{100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@-moz-keyframes dtb-spinner{100%{-moz-transform:rotate(360deg);transform:rotate(360deg)}}div.dataTables_wrapper{position:relative}div.dt-buttons{position:initial}div.dt-buttons .dt-button{overflow:hidden;text-overflow:ellipsis}div.dt-button-info{position:fixed;top:50%;left:50%;width:400px;margin-top:-100px;margin-left:-200px;background-color:white;border:2px solid #111;box-shadow:3px 4px 10px 1px rgba(0, 0, 0, 0.3);border-radius:3px;text-align:center;z-index:21}div.dt-button-info h2{padding:.5em;margin:0;font-weight:normal;border-bottom:1px solid #ddd;background-color:#f3f3f3}div.dt-button-info>div{padding:1em}div.dtb-popover-close{position:absolute;top:10px;right:10px;width:22px;height:22px;border:1px solid #eaeaea;background-color:#f9f9f9;text-align:center;border-radius:3px;cursor:pointer;z-index:2003}button.dtb-hide-drop{display:none !important}div.dt-button-collection-title{text-align:center;padding:.3em 0 .5em;margin-left:.5em;margin-right:.5em;font-size:.9em}div.dt-button-collection-title:empty{display:none}span.dt-button-spacer{display:inline-block;margin:.5em;white-space:nowrap}span.dt-button-spacer.bar{border-left:1px solid rgba(0, 0, 0, 0.3);vertical-align:middle;padding-left:.5em}span.dt-button-spacer.bar:empty{height:1em;width:1px;padding-left:0}div.dt-button-collection span.dt-button-spacer{width:100%;font-size:.9em;text-align:center;margin:.5em 0}div.dt-button-collection span.dt-button-spacer:empty{height:0;width:100%}div.dt-button-collection span.dt-button-spacer.bar{border-left:none;border-bottom:1px solid rgba(0, 0, 0, 0.3);padding-left:0}button.dt-button,div.dt-button,a.dt-button,input.dt-button{position:relative;display:inline-block;box-sizing:border-box;margin-left:.167em;margin-right:.167em;margin-bottom:.333em;padding:.5em 1em;border:1px solid rgba(0, 0, 0, 0.3);border-radius:2px;cursor:pointer;font-size:.88em;line-height:1.6em;color:black;white-space:nowrap;overflow:hidden;background-color:rgba(0, 0, 0, 0.1);background:linear-gradient(to bottom, rgba(230, 230, 230, 0.1) 0%, rgba(0, 0, 0, 0.1) 100%);filter:progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr="rgba(230, 230, 230, 0.1)", EndColorStr="rgba(0, 0, 0, 0.1)");-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;text-decoration:none;outline:none;text-overflow:ellipsis}button.dt-button:first-child,div.dt-button:first-child,a.dt-button:first-child,input.dt-button:first-child{margin-left:0}button.dt-button.disabled,div.dt-button.disabled,a.dt-button.disabled,input.dt-button.disabled{cursor:default;opacity:.4}button.dt-button.active:not(.disabled),div.dt-button.active:not(.disabled),a.dt-button.active:not(.disabled),input.dt-button.active:not(.disabled){background-color:rgba(0, 0, 0, 0.1);background:linear-gradient(to bottom, rgba(179, 179, 179, 0.1) 0%, rgba(0, 0, 0, 0.1) 100%);filter:progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr="rgba(179, 179, 179, 0.1)", EndColorStr="rgba(0, 0, 0, 0.1)");box-shadow:inset 1px 1px 3px #999}button.dt-button.active:not(.disabled):hover:not(.disabled),div.dt-button.active:not(.disabled):hover:not(.disabled),a.dt-button.active:not(.disabled):hover:not(.disabled),input.dt-button.active:not(.disabled):hover:not(.disabled){box-shadow:inset 1px 1px 3px #999;background-color:rgba(0, 0, 0, 0.1);background:linear-gradient(to bottom, rgba(128, 128, 128, 0.1) 0%, rgba(0, 0, 0, 0.1) 100%);filter:progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr="rgba(128, 128, 128, 0.1)", EndColorStr="rgba(0, 0, 0, 0.1)")}button.dt-button:hover,div.dt-button:hover,a.dt-button:hover,input.dt-button:hover{text-decoration:none}button.dt-button:hover:not(.disabled),div.dt-button:hover:not(.disabled),a.dt-button:hover:not(.disabled),input.dt-button:hover:not(.disabled){border:1px solid #666;background-color:rgba(0, 0, 0, 0.1);background:linear-gradient(to bottom, rgba(153, 153, 153, 0.1) 0%, rgba(0, 0, 0, 0.1) 100%);filter:progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr="rgba(153, 153, 153, 0.1)", EndColorStr="rgba(0, 0, 0, 0.1)")}button.dt-button:focus:not(.disabled),div.dt-button:focus:not(.disabled),a.dt-button:focus:not(.disabled),input.dt-button:focus:not(.disabled){border:1px solid #426c9e;text-shadow:0 1px 0 #c4def1;outline:none;background-color:rgb(121, 172, 233);background:linear-gradient(to bottom, #d1e2f7 0%, rgb(121, 172, 233) 100%);filter:progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr="#d1e2f7", EndColorStr="rgb(121, 172, 233)")}button.dt-button.active:focus:not(.disabled),div.dt-button.active:focus:not(.disabled),a.dt-button.active:focus:not(.disabled),input.dt-button.active:focus:not(.disabled){background:linear-gradient(to bottom, #d1e2f7 0%, rgb(121, 172, 233) 100%) !important}button.dt-button span.dt-down-arrow,div.dt-button span.dt-down-arrow,a.dt-button span.dt-down-arrow,input.dt-button span.dt-down-arrow{position:relative;top:-2px;color:rgba(70, 70, 70, 0.75);font-size:8px;padding-left:10px;line-height:1em}.dt-button embed{outline:none}div.dt-buttons{float:left}div.dt-buttons.buttons-right{float:right}div.dataTables_layout_cell div.dt-buttons{float:none}div.dataTables_layout_cell div.dt-buttons.buttons-right{float:none}div.dt-btn-split-wrapper{display:inline-block}div.dt-button-collection{position:absolute;top:0;left:0;width:200px;margin-top:3px;margin-bottom:3px;padding:4px 4px 2px 4px;border:1px solid #ccc;border:1px solid rgba(0, 0, 0, 0.4);background-color:white;overflow:hidden;z-index:2002;border-radius:5px;box-shadow:3px 4px 10px 1px rgba(0, 0, 0, 0.3);box-sizing:border-box}div.dt-button-collection button.dt-button,div.dt-button-collection div.dt-button,div.dt-button-collection a.dt-button{position:relative;left:0;right:0;width:100%;display:block;float:none;margin:4px 0 2px 0}div.dt-button-collection button.dt-button:active:not(.disabled),div.dt-button-collection button.dt-button.active:not(.disabled),div.dt-button-collection div.dt-button:active:not(.disabled),div.dt-button-collection div.dt-button.active:not(.disabled),div.dt-button-collection a.dt-button:active:not(.disabled),div.dt-button-collection a.dt-button.active:not(.disabled){background-color:#dadada;background:linear-gradient(to bottom, #f0f0f0 0%, #dadada 100%);filter:progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr="#f0f0f0", EndColorStr="#dadada");box-shadow:inset 1px 1px 3px #666}div.dt-button-collection button.dt-button:first-child,div.dt-button-collection div.dt-button:first-child,div.dt-button-collection a.dt-button:first-child{margin-top:0;border-top-left-radius:3px;border-top-right-radius:3px}div.dt-button-collection button.dt-button:last-child,div.dt-button-collection div.dt-button:last-child,div.dt-button-collection a.dt-button:last-child{border-bottom-left-radius:3px;border-bottom-right-radius:3px}div.dt-button-collection div.dt-btn-split-wrapper{display:flex;flex-direction:row;flex-wrap:wrap;justify-content:flex-start;align-content:flex-start;align-items:stretch;margin:4px 0 2px 0}div.dt-button-collection div.dt-btn-split-wrapper button.dt-button{margin:0;display:inline-block;width:0;flex-grow:1;flex-shrink:0;flex-basis:50px;border-radius:0}div.dt-button-collection div.dt-btn-split-wrapper button.dt-btn-split-drop{min-width:20px;flex-grow:0;flex-shrink:0;flex-basis:0}div.dt-button-collection div.dt-btn-split-wrapper:first-child{margin-top:0}div.dt-button-collection div.dt-btn-split-wrapper:first-child button.dt-button{border-top-left-radius:3px}div.dt-button-collection div.dt-btn-split-wrapper:first-child button.dt-btn-split-drop{border-top-right-radius:3px}div.dt-button-collection div.dt-btn-split-wrapper:last-child button.dt-button{border-bottom-left-radius:3px}div.dt-button-collection div.dt-btn-split-wrapper:last-child button.dt-btn-split-drop{border-bottom-right-radius:3px}div.dt-button-collection div.dt-btn-split-wrapper:active:not(.disabled) button.dt-button,div.dt-button-collection div.dt-btn-split-wrapper.active:not(.disabled) button.dt-button{background-color:#dadada;background:linear-gradient(to bottom, #f0f0f0 0%, #dadada 100%);filter:progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr="#f0f0f0", EndColorStr="#dadada");box-shadow:inset 0px 0px 4px #666}div.dt-button-collection div.dt-btn-split-wrapper:active:not(.disabled) button.dt-btn-split-drop,div.dt-button-collection div.dt-btn-split-wrapper.active:not(.disabled) button.dt-btn-split-drop{box-shadow:none}div.dt-button-collection.fixed .dt-button:first-child{margin-top:0;border-top-left-radius:0;border-top-right-radius:0}div.dt-button-collection.fixed .dt-button:last-child{border-bottom-left-radius:0;border-bottom-right-radius:0}div.dt-button-collection.fixed{position:fixed;display:block;top:50%;left:50%;margin-left:-75px;border-radius:5px;background-color:white}div.dt-button-collection.fixed.two-column{margin-left:-200px}div.dt-button-collection.fixed.three-column{margin-left:-225px}div.dt-button-collection.fixed.four-column{margin-left:-300px}div.dt-button-collection.fixed.columns{margin-left:-409px}@media screen and (max-width: 1024px){div.dt-button-collection.fixed.columns{margin-left:-308px}}@media screen and (max-width: 640px){div.dt-button-collection.fixed.columns{margin-left:-203px}}@media screen and (max-width: 460px){div.dt-button-collection.fixed.columns{margin-left:-100px}}div.dt-button-collection.fixed>:last-child{max-height:100vh;overflow:auto}div.dt-button-collection.two-column>:last-child,div.dt-button-collection.three-column>:last-child,div.dt-button-collection.four-column>:last-child{display:block !important;-webkit-column-gap:8px;-moz-column-gap:8px;-ms-column-gap:8px;-o-column-gap:8px;column-gap:8px}div.dt-button-collection.two-column>:last-child>*,div.dt-button-collection.three-column>:last-child>*,div.dt-button-collection.four-column>:last-child>*{-webkit-column-break-inside:avoid;break-inside:avoid}div.dt-button-collection.two-column{width:400px}div.dt-button-collection.two-column>:last-child{padding-bottom:1px;column-count:2}div.dt-button-collection.three-column{width:450px}div.dt-button-collection.three-column>:last-child{padding-bottom:1px;column-count:3}div.dt-button-collection.four-column{width:600px}div.dt-button-collection.four-column>:last-child{padding-bottom:1px;column-count:4}div.dt-button-collection .dt-button{border-radius:0}div.dt-button-collection.columns{width:auto}div.dt-button-collection.columns>:last-child{display:flex;flex-wrap:wrap;justify-content:flex-start;align-items:center;gap:6px;width:818px;padding-bottom:1px}div.dt-button-collection.columns>:last-child .dt-button{min-width:200px;flex:0 1;margin:0}div.dt-button-collection.columns.dtb-b3>:last-child,div.dt-button-collection.columns.dtb-b2>:last-child,div.dt-button-collection.columns.dtb-b1>:last-child{justify-content:space-between}div.dt-button-collection.columns.dtb-b3 .dt-button{flex:1 1 32%}div.dt-button-collection.columns.dtb-b2 .dt-button{flex:1 1 48%}div.dt-button-collection.columns.dtb-b1 .dt-button{flex:1 1 100%}@media screen and (max-width: 1024px){div.dt-button-collection.columns>:last-child{width:612px}}@media screen and (max-width: 640px){div.dt-button-collection.columns>:last-child{width:406px}div.dt-button-collection.columns.dtb-b3 .dt-button{flex:0 1 32%}}@media screen and (max-width: 460px){div.dt-button-collection.columns>:last-child{width:200px}}div.dt-button-background{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0, 0, 0, 0.7);background:radial-gradient(ellipse farthest-corner at center, rgba(0, 0, 0, 0.3) 0%, rgba(0, 0, 0, 0.7) 100%);z-index:2001}@media screen and (max-width: 640px){div.dt-buttons{float:none !important;text-align:center}}button.dt-button.processing,div.dt-button.processing,a.dt-button.processing{color:rgba(0, 0, 0, 0.2)}button.dt-button.processing:after,div.dt-button.processing:after,a.dt-button.processing:after{position:absolute;top:50%;left:50%;width:16px;height:16px;margin:-8px 0 0 -8px;box-sizing:border-box;display:block;content:" ";border:2px solid rgb(40, 40, 40);border-radius:50%;border-left-color:transparent;border-right-color:transparent;animation:dtb-spinner 1500ms infinite linear;-o-animation:dtb-spinner 1500ms infinite linear;-ms-animation:dtb-spinner 1500ms infinite linear;-webkit-animation:dtb-spinner 1500ms infinite linear;-moz-animation:dtb-spinner 1500ms infinite linear}button.dt-btn-split-drop{margin-left:calc(-1px - .333em);padding-bottom:calc(.5em - 1px);border-radius:0px 1px 1px 0px;color:rgba(70, 70, 70, 0.9);border-left:none}button.dt-btn-split-drop span.dt-btn-split-drop-arrow{position:relative;top:-1px;left:-2px;font-size:8px}button.dt-btn-split-drop:hover{z-index:2}button.buttons-split{border-right:1px solid rgba(70, 70, 70, 0);border-radius:1px 0px 0px 1px}button.dt-btn-split-drop-button{background-color:rgb(255, 255, 255)}button.dt-btn-split-drop-button:hover{background-color:rgb(255, 255, 255)} diff --git a/public/assets/js/DataTables/Buttons-2.3.6/css/buttons.foundation.css b/public/assets/js/DataTables/Buttons-2.3.6/css/buttons.foundation.css new file mode 100644 index 0000000..d68021c --- /dev/null +++ b/public/assets/js/DataTables/Buttons-2.3.6/css/buttons.foundation.css @@ -0,0 +1,371 @@ +@keyframes dtb-spinner { + 100% { + transform: rotate(360deg); + } +} +@-o-keyframes dtb-spinner { + 100% { + -o-transform: rotate(360deg); + transform: rotate(360deg); + } +} +@-ms-keyframes dtb-spinner { + 100% { + -ms-transform: rotate(360deg); + transform: rotate(360deg); + } +} +@-webkit-keyframes dtb-spinner { + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } +} +@-moz-keyframes dtb-spinner { + 100% { + -moz-transform: rotate(360deg); + transform: rotate(360deg); + } +} +div.dataTables_wrapper { + position: relative; +} + +div.dt-buttons { + position: initial; +} +div.dt-buttons .dt-button { + overflow: hidden; + text-overflow: ellipsis; +} + +div.dt-button-info { + position: fixed; + top: 50%; + left: 50%; + width: 400px; + margin-top: -100px; + margin-left: -200px; + background-color: white; + border: 2px solid #111; + box-shadow: 3px 4px 10px 1px rgba(0, 0, 0, 0.3); + border-radius: 3px; + text-align: center; + z-index: 21; +} +div.dt-button-info h2 { + padding: 0.5em; + margin: 0; + font-weight: normal; + border-bottom: 1px solid #ddd; + background-color: #f3f3f3; +} +div.dt-button-info > div { + padding: 1em; +} + +div.dtb-popover-close { + position: absolute; + top: 10px; + right: 10px; + width: 22px; + height: 22px; + border: 1px solid #eaeaea; + background-color: #f9f9f9; + text-align: center; + border-radius: 3px; + cursor: pointer; + z-index: 2003; +} + +button.dtb-hide-drop { + display: none !important; +} + +div.dt-button-collection-title { + text-align: center; + padding: 0.3em 0 0.5em; + margin-left: 0.5em; + margin-right: 0.5em; + font-size: 0.9em; +} + +div.dt-button-collection-title:empty { + display: none; +} + +span.dt-button-spacer { + display: inline-block; + margin: 0.5em; + white-space: nowrap; +} +span.dt-button-spacer.bar { + border-left: 1px solid rgba(0, 0, 0, 0.3); + vertical-align: middle; + padding-left: 0.5em; +} +span.dt-button-spacer.bar:empty { + height: 1em; + width: 1px; + padding-left: 0; +} + +div.dt-button-collection span.dt-button-spacer { + width: 100%; + font-size: 0.9em; + text-align: center; + margin: 0.5em 0; +} +div.dt-button-collection span.dt-button-spacer:empty { + height: 0; + width: 100%; +} +div.dt-button-collection span.dt-button-spacer.bar { + border-left: none; + border-bottom: 1px solid rgba(0, 0, 0, 0.3); + padding-left: 0; +} + +ul.dt-buttons li { + margin: 0; +} +ul.dt-buttons li.active a { + box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.6); +} + +ul.dt-buttons.button-group a { + margin-bottom: 0; +} + +div.dt-button-collection { + position: absolute; + z-index: 2002; + max-width: none; + border: 1px solid #cacaca; + padding: 0.5rem; + background-color: white; +} +div.dt-button-collection.fixed { + position: fixed; + display: block; + top: 50%; + left: 50%; + margin-left: -75px; + border-radius: 5px; + background-color: white; +} +div.dt-button-collection.fixed.two-column { + margin-left: -200px; +} +div.dt-button-collection.fixed.three-column { + margin-left: -225px; +} +div.dt-button-collection.fixed.four-column { + margin-left: -300px; +} +div.dt-button-collection.fixed.columns { + margin-left: -409px; +} +@media screen and (max-width: 1024px) { + div.dt-button-collection.fixed.columns { + margin-left: -308px; + } +} +@media screen and (max-width: 640px) { + div.dt-button-collection.fixed.columns { + margin-left: -203px; + } +} +@media screen and (max-width: 460px) { + div.dt-button-collection.fixed.columns { + margin-left: -100px; + } +} +div.dt-button-collection.fixed > :last-child { + max-height: 100vh; + overflow: auto; +} +div.dt-button-collection.two-column > :last-child, div.dt-button-collection.three-column > :last-child, div.dt-button-collection.four-column > :last-child { + display: block !important; + -webkit-column-gap: 8px; + -moz-column-gap: 8px; + -ms-column-gap: 8px; + -o-column-gap: 8px; + column-gap: 8px; +} +div.dt-button-collection.two-column > :last-child > *, div.dt-button-collection.three-column > :last-child > *, div.dt-button-collection.four-column > :last-child > * { + -webkit-column-break-inside: avoid; + break-inside: avoid; +} +div.dt-button-collection.two-column { + width: 400px; +} +div.dt-button-collection.two-column > :last-child { + padding-bottom: 1px; + column-count: 2; +} +div.dt-button-collection.three-column { + width: 450px; +} +div.dt-button-collection.three-column > :last-child { + padding-bottom: 1px; + column-count: 3; +} +div.dt-button-collection.four-column { + width: 600px; +} +div.dt-button-collection.four-column > :last-child { + padding-bottom: 1px; + column-count: 4; +} +div.dt-button-collection .dt-button { + border-radius: 0; +} +div.dt-button-collection.columns { + width: auto; +} +div.dt-button-collection.columns > :last-child { + display: flex; + flex-wrap: wrap; + justify-content: flex-start; + align-items: center; + gap: 6px; + width: 818px; + padding-bottom: 1px; +} +div.dt-button-collection.columns > :last-child .dt-button { + min-width: 200px; + flex: 0 1; + margin: 0; +} +div.dt-button-collection.columns.dtb-b3 > :last-child, div.dt-button-collection.columns.dtb-b2 > :last-child, div.dt-button-collection.columns.dtb-b1 > :last-child { + justify-content: space-between; +} +div.dt-button-collection.columns.dtb-b3 .dt-button { + flex: 1 1 32%; +} +div.dt-button-collection.columns.dtb-b2 .dt-button { + flex: 1 1 48%; +} +div.dt-button-collection.columns.dtb-b1 .dt-button { + flex: 1 1 100%; +} +@media screen and (max-width: 1024px) { + div.dt-button-collection.columns > :last-child { + width: 612px; + } +} +@media screen and (max-width: 640px) { + div.dt-button-collection.columns > :last-child { + width: 406px; + } + div.dt-button-collection.columns.dtb-b3 .dt-button { + flex: 0 1 32%; + } +} +@media screen and (max-width: 460px) { + div.dt-button-collection.columns > :last-child { + width: 200px; + } +} +div.dt-button-collection .button-group.stacked { + position: relative; + border: none; + padding: 0; + margin: 0; +} +div.dt-button-collection.columns .button-group.stacked { + flex-direction: row; + padding: 0; +} +div.dt-button-collection.columns .dt-button { + flex-basis: 200px; +} +div.dt-button-collection div.dt-btn-split-wrapper a.button { + flex-grow: 1; +} +div.dt-button-collection div.dt-btn-split-wrapper a.button, +div.dt-button-collection div.dt-btn-split-wrapper button.button { + display: inline-block !important; + white-space: nowrap; + height: 40px; + flex-basis: auto; + overflow: hidden; + text-overflow: ellipsis; +} + +div.dt-button-background { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 88; +} + +@media screen and (max-width: 767px) { + ul.dt-buttons { + float: none; + width: 100%; + text-align: center; + margin-bottom: 0.5rem; + } + ul.dt-buttons li { + float: none; + } +} +div.button-group.stacked.dropdown-pane { + margin-top: 2px; + padding: 1px; + z-index: 89; +} +div.button-group.stacked.dropdown-pane a.button { + display: block; + margin-bottom: 1px; + border-right: none; +} +div.button-group.stacked.dropdown-pane a.button:last-child { + margin-bottom: 0; + margin-right: 1px; +} + +div.dt-buttons button.button.processing, +div.dt-buttons div.button.processing, +div.dt-buttons a.button.processing { + color: rgba(0, 0, 0, 0.2); + color: rgba(255, 255, 255, 0.2); + border-top-color: white; + border-bottom-color: white; +} +div.dt-buttons button.button.processing:after, +div.dt-buttons div.button.processing:after, +div.dt-buttons a.button.processing:after { + position: absolute; + top: 50%; + left: 50%; + width: 16px; + height: 16px; + margin: -8px 0 0 -8px; + box-sizing: border-box; + display: block; + content: " "; + border: 2px solid rgb(40, 40, 40); + border-radius: 50%; + border-left-color: transparent; + border-right-color: transparent; + animation: dtb-spinner 1500ms infinite linear; + -o-animation: dtb-spinner 1500ms infinite linear; + -ms-animation: dtb-spinner 1500ms infinite linear; + -webkit-animation: dtb-spinner 1500ms infinite linear; + -moz-animation: dtb-spinner 1500ms infinite linear; +} + +div.dt-btn-split-wrapper:active:not(.disabled) button.dt-btn-split-drop, div.dt-btn-split-wrapper.secondary:not(.disabled) button.dt-btn-split-drop { + box-shadow: none; + background-color: #1779ba; + border-color: transparent; +} +div.dt-btn-split-wrapper:active:not(.disabled) button.dt-btn-split-drop:hover, div.dt-btn-split-wrapper.secondary:not(.disabled) button.dt-btn-split-drop:hover { + background-color: #14679e; + border-color: transparent; +} diff --git a/public/assets/js/DataTables/Buttons-2.3.6/css/buttons.foundation.min.css b/public/assets/js/DataTables/Buttons-2.3.6/css/buttons.foundation.min.css new file mode 100644 index 0000000..19a0bd0 --- /dev/null +++ b/public/assets/js/DataTables/Buttons-2.3.6/css/buttons.foundation.min.css @@ -0,0 +1 @@ +@keyframes dtb-spinner{100%{transform:rotate(360deg)}}@-o-keyframes dtb-spinner{100%{-o-transform:rotate(360deg);transform:rotate(360deg)}}@-ms-keyframes dtb-spinner{100%{-ms-transform:rotate(360deg);transform:rotate(360deg)}}@-webkit-keyframes dtb-spinner{100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@-moz-keyframes dtb-spinner{100%{-moz-transform:rotate(360deg);transform:rotate(360deg)}}div.dataTables_wrapper{position:relative}div.dt-buttons{position:initial}div.dt-buttons .dt-button{overflow:hidden;text-overflow:ellipsis}div.dt-button-info{position:fixed;top:50%;left:50%;width:400px;margin-top:-100px;margin-left:-200px;background-color:white;border:2px solid #111;box-shadow:3px 4px 10px 1px rgba(0, 0, 0, 0.3);border-radius:3px;text-align:center;z-index:21}div.dt-button-info h2{padding:.5em;margin:0;font-weight:normal;border-bottom:1px solid #ddd;background-color:#f3f3f3}div.dt-button-info>div{padding:1em}div.dtb-popover-close{position:absolute;top:10px;right:10px;width:22px;height:22px;border:1px solid #eaeaea;background-color:#f9f9f9;text-align:center;border-radius:3px;cursor:pointer;z-index:2003}button.dtb-hide-drop{display:none !important}div.dt-button-collection-title{text-align:center;padding:.3em 0 .5em;margin-left:.5em;margin-right:.5em;font-size:.9em}div.dt-button-collection-title:empty{display:none}span.dt-button-spacer{display:inline-block;margin:.5em;white-space:nowrap}span.dt-button-spacer.bar{border-left:1px solid rgba(0, 0, 0, 0.3);vertical-align:middle;padding-left:.5em}span.dt-button-spacer.bar:empty{height:1em;width:1px;padding-left:0}div.dt-button-collection span.dt-button-spacer{width:100%;font-size:.9em;text-align:center;margin:.5em 0}div.dt-button-collection span.dt-button-spacer:empty{height:0;width:100%}div.dt-button-collection span.dt-button-spacer.bar{border-left:none;border-bottom:1px solid rgba(0, 0, 0, 0.3);padding-left:0}ul.dt-buttons li{margin:0}ul.dt-buttons li.active a{box-shadow:inset 0 0 10px rgba(0, 0, 0, 0.6)}ul.dt-buttons.button-group a{margin-bottom:0}div.dt-button-collection{position:absolute;z-index:2002;max-width:none;border:1px solid #cacaca;padding:.5rem;background-color:white}div.dt-button-collection.fixed{position:fixed;display:block;top:50%;left:50%;margin-left:-75px;border-radius:5px;background-color:white}div.dt-button-collection.fixed.two-column{margin-left:-200px}div.dt-button-collection.fixed.three-column{margin-left:-225px}div.dt-button-collection.fixed.four-column{margin-left:-300px}div.dt-button-collection.fixed.columns{margin-left:-409px}@media screen and (max-width: 1024px){div.dt-button-collection.fixed.columns{margin-left:-308px}}@media screen and (max-width: 640px){div.dt-button-collection.fixed.columns{margin-left:-203px}}@media screen and (max-width: 460px){div.dt-button-collection.fixed.columns{margin-left:-100px}}div.dt-button-collection.fixed>:last-child{max-height:100vh;overflow:auto}div.dt-button-collection.two-column>:last-child,div.dt-button-collection.three-column>:last-child,div.dt-button-collection.four-column>:last-child{display:block !important;-webkit-column-gap:8px;-moz-column-gap:8px;-ms-column-gap:8px;-o-column-gap:8px;column-gap:8px}div.dt-button-collection.two-column>:last-child>*,div.dt-button-collection.three-column>:last-child>*,div.dt-button-collection.four-column>:last-child>*{-webkit-column-break-inside:avoid;break-inside:avoid}div.dt-button-collection.two-column{width:400px}div.dt-button-collection.two-column>:last-child{padding-bottom:1px;column-count:2}div.dt-button-collection.three-column{width:450px}div.dt-button-collection.three-column>:last-child{padding-bottom:1px;column-count:3}div.dt-button-collection.four-column{width:600px}div.dt-button-collection.four-column>:last-child{padding-bottom:1px;column-count:4}div.dt-button-collection .dt-button{border-radius:0}div.dt-button-collection.columns{width:auto}div.dt-button-collection.columns>:last-child{display:flex;flex-wrap:wrap;justify-content:flex-start;align-items:center;gap:6px;width:818px;padding-bottom:1px}div.dt-button-collection.columns>:last-child .dt-button{min-width:200px;flex:0 1;margin:0}div.dt-button-collection.columns.dtb-b3>:last-child,div.dt-button-collection.columns.dtb-b2>:last-child,div.dt-button-collection.columns.dtb-b1>:last-child{justify-content:space-between}div.dt-button-collection.columns.dtb-b3 .dt-button{flex:1 1 32%}div.dt-button-collection.columns.dtb-b2 .dt-button{flex:1 1 48%}div.dt-button-collection.columns.dtb-b1 .dt-button{flex:1 1 100%}@media screen and (max-width: 1024px){div.dt-button-collection.columns>:last-child{width:612px}}@media screen and (max-width: 640px){div.dt-button-collection.columns>:last-child{width:406px}div.dt-button-collection.columns.dtb-b3 .dt-button{flex:0 1 32%}}@media screen and (max-width: 460px){div.dt-button-collection.columns>:last-child{width:200px}}div.dt-button-collection .button-group.stacked{position:relative;border:none;padding:0;margin:0}div.dt-button-collection.columns .button-group.stacked{flex-direction:row;padding:0}div.dt-button-collection.columns .dt-button{flex-basis:200px}div.dt-button-collection div.dt-btn-split-wrapper a.button{flex-grow:1}div.dt-button-collection div.dt-btn-split-wrapper a.button,div.dt-button-collection div.dt-btn-split-wrapper button.button{display:inline-block !important;white-space:nowrap;height:40px;flex-basis:auto;overflow:hidden;text-overflow:ellipsis}div.dt-button-background{position:fixed;top:0;left:0;width:100%;height:100%;z-index:88}@media screen and (max-width: 767px){ul.dt-buttons{float:none;width:100%;text-align:center;margin-bottom:.5rem}ul.dt-buttons li{float:none}}div.button-group.stacked.dropdown-pane{margin-top:2px;padding:1px;z-index:89}div.button-group.stacked.dropdown-pane a.button{display:block;margin-bottom:1px;border-right:none}div.button-group.stacked.dropdown-pane a.button:last-child{margin-bottom:0;margin-right:1px}div.dt-buttons button.button.processing,div.dt-buttons div.button.processing,div.dt-buttons a.button.processing{color:rgba(0, 0, 0, 0.2);color:rgba(255, 255, 255, 0.2);border-top-color:white;border-bottom-color:white}div.dt-buttons button.button.processing:after,div.dt-buttons div.button.processing:after,div.dt-buttons a.button.processing:after{position:absolute;top:50%;left:50%;width:16px;height:16px;margin:-8px 0 0 -8px;box-sizing:border-box;display:block;content:" ";border:2px solid rgb(40, 40, 40);border-radius:50%;border-left-color:transparent;border-right-color:transparent;animation:dtb-spinner 1500ms infinite linear;-o-animation:dtb-spinner 1500ms infinite linear;-ms-animation:dtb-spinner 1500ms infinite linear;-webkit-animation:dtb-spinner 1500ms infinite linear;-moz-animation:dtb-spinner 1500ms infinite linear}div.dt-btn-split-wrapper:active:not(.disabled) button.dt-btn-split-drop,div.dt-btn-split-wrapper.secondary:not(.disabled) button.dt-btn-split-drop{box-shadow:none;background-color:#1779ba;border-color:transparent}div.dt-btn-split-wrapper:active:not(.disabled) button.dt-btn-split-drop:hover,div.dt-btn-split-wrapper.secondary:not(.disabled) button.dt-btn-split-drop:hover{background-color:#14679e;border-color:transparent} diff --git a/public/assets/js/DataTables/Buttons-2.3.6/css/buttons.jqueryui.css b/public/assets/js/DataTables/Buttons-2.3.6/css/buttons.jqueryui.css new file mode 100644 index 0000000..70e1734 --- /dev/null +++ b/public/assets/js/DataTables/Buttons-2.3.6/css/buttons.jqueryui.css @@ -0,0 +1,387 @@ +@keyframes dtb-spinner { + 100% { + transform: rotate(360deg); + } +} +@-o-keyframes dtb-spinner { + 100% { + -o-transform: rotate(360deg); + transform: rotate(360deg); + } +} +@-ms-keyframes dtb-spinner { + 100% { + -ms-transform: rotate(360deg); + transform: rotate(360deg); + } +} +@-webkit-keyframes dtb-spinner { + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } +} +@-moz-keyframes dtb-spinner { + 100% { + -moz-transform: rotate(360deg); + transform: rotate(360deg); + } +} +div.dataTables_wrapper { + position: relative; +} + +div.dt-buttons { + position: initial; +} +div.dt-buttons .dt-button { + overflow: hidden; + text-overflow: ellipsis; +} + +div.dt-button-info { + position: fixed; + top: 50%; + left: 50%; + width: 400px; + margin-top: -100px; + margin-left: -200px; + background-color: white; + border: 2px solid #111; + box-shadow: 3px 4px 10px 1px rgba(0, 0, 0, 0.3); + border-radius: 3px; + text-align: center; + z-index: 21; +} +div.dt-button-info h2 { + padding: 0.5em; + margin: 0; + font-weight: normal; + border-bottom: 1px solid #ddd; + background-color: #f3f3f3; +} +div.dt-button-info > div { + padding: 1em; +} + +div.dtb-popover-close { + position: absolute; + top: 10px; + right: 10px; + width: 22px; + height: 22px; + border: 1px solid #eaeaea; + background-color: #f9f9f9; + text-align: center; + border-radius: 3px; + cursor: pointer; + z-index: 2003; +} + +button.dtb-hide-drop { + display: none !important; +} + +div.dt-button-collection-title { + text-align: center; + padding: 0.3em 0 0.5em; + margin-left: 0.5em; + margin-right: 0.5em; + font-size: 0.9em; +} + +div.dt-button-collection-title:empty { + display: none; +} + +span.dt-button-spacer { + display: inline-block; + margin: 0.5em; + white-space: nowrap; +} +span.dt-button-spacer.bar { + border-left: 1px solid rgba(0, 0, 0, 0.3); + vertical-align: middle; + padding-left: 0.5em; +} +span.dt-button-spacer.bar:empty { + height: 1em; + width: 1px; + padding-left: 0; +} + +div.dt-button-collection span.dt-button-spacer { + width: 100%; + font-size: 0.9em; + text-align: center; + margin: 0.5em 0; +} +div.dt-button-collection span.dt-button-spacer:empty { + height: 0; + width: 100%; +} +div.dt-button-collection span.dt-button-spacer.bar { + border-left: none; + border-bottom: 1px solid rgba(0, 0, 0, 0.3); + padding-left: 0; +} + +div.dt-buttons { + position: relative; + float: left; +} +div.dt-buttons .dt-button { + margin-right: 0; +} +div.dt-buttons .dt-button span.ui-icon { + display: inline-block; + vertical-align: middle; + margin-top: -2px; +} +div.dt-buttons .dt-button:active { + outline: none; +} +div.dt-buttons .dt-button:hover > span { + background-color: rgba(0, 0, 0, 0.05); +} + +div.dt-button-collection { + position: absolute; + top: 0; + left: 0; + width: 150px; + margin-top: 3px; + padding: 8px 8px 4px 8px; + border: 1px solid #ccc; + border: 1px solid rgba(0, 0, 0, 0.4); + background-color: #f3f3f3; + overflow: hidden; + z-index: 2002; + border-radius: 5px; + box-shadow: 3px 3px 5px rgba(0, 0, 0, 0.3); + z-index: 2002; + -webkit-column-gap: 0; + -moz-column-gap: 0; + -ms-column-gap: 0; + -o-column-gap: 0; + column-gap: 0; +} +div.dt-button-collection .dt-button { + position: relative; + left: 0; + right: 0; + width: 100%; + box-sizing: border-box; + display: block; + float: none; + margin-right: 0; + margin-bottom: 4px; +} +div.dt-button-collection .dt-button:hover > span { + background-color: rgba(0, 0, 0, 0.05); +} +div.dt-button-collection.fixed { + position: fixed; + display: block; + top: 50%; + left: 50%; + margin-left: -75px; + border-radius: 5px; + background-color: white; +} +div.dt-button-collection.fixed.two-column { + margin-left: -200px; +} +div.dt-button-collection.fixed.three-column { + margin-left: -225px; +} +div.dt-button-collection.fixed.four-column { + margin-left: -300px; +} +div.dt-button-collection.fixed.columns { + margin-left: -409px; +} +@media screen and (max-width: 1024px) { + div.dt-button-collection.fixed.columns { + margin-left: -308px; + } +} +@media screen and (max-width: 640px) { + div.dt-button-collection.fixed.columns { + margin-left: -203px; + } +} +@media screen and (max-width: 460px) { + div.dt-button-collection.fixed.columns { + margin-left: -100px; + } +} +div.dt-button-collection.fixed > :last-child { + max-height: 100vh; + overflow: auto; +} +div.dt-button-collection.two-column > :last-child, div.dt-button-collection.three-column > :last-child, div.dt-button-collection.four-column > :last-child { + display: block !important; + -webkit-column-gap: 8px; + -moz-column-gap: 8px; + -ms-column-gap: 8px; + -o-column-gap: 8px; + column-gap: 8px; +} +div.dt-button-collection.two-column > :last-child > *, div.dt-button-collection.three-column > :last-child > *, div.dt-button-collection.four-column > :last-child > * { + -webkit-column-break-inside: avoid; + break-inside: avoid; +} +div.dt-button-collection.two-column { + width: 400px; +} +div.dt-button-collection.two-column > :last-child { + padding-bottom: 1px; + column-count: 2; +} +div.dt-button-collection.three-column { + width: 450px; +} +div.dt-button-collection.three-column > :last-child { + padding-bottom: 1px; + column-count: 3; +} +div.dt-button-collection.four-column { + width: 600px; +} +div.dt-button-collection.four-column > :last-child { + padding-bottom: 1px; + column-count: 4; +} +div.dt-button-collection .dt-button { + border-radius: 0; +} +div.dt-button-collection.columns { + width: auto; +} +div.dt-button-collection.columns > :last-child { + display: flex; + flex-wrap: wrap; + justify-content: flex-start; + align-items: center; + gap: 6px; + width: 818px; + padding-bottom: 1px; +} +div.dt-button-collection.columns > :last-child .dt-button { + min-width: 200px; + flex: 0 1; + margin: 0; +} +div.dt-button-collection.columns.dtb-b3 > :last-child, div.dt-button-collection.columns.dtb-b2 > :last-child, div.dt-button-collection.columns.dtb-b1 > :last-child { + justify-content: space-between; +} +div.dt-button-collection.columns.dtb-b3 .dt-button { + flex: 1 1 32%; +} +div.dt-button-collection.columns.dtb-b2 .dt-button { + flex: 1 1 48%; +} +div.dt-button-collection.columns.dtb-b1 .dt-button { + flex: 1 1 100%; +} +@media screen and (max-width: 1024px) { + div.dt-button-collection.columns > :last-child { + width: 612px; + } +} +@media screen and (max-width: 640px) { + div.dt-button-collection.columns > :last-child { + width: 406px; + } + div.dt-button-collection.columns.dtb-b3 .dt-button { + flex: 0 1 32%; + } +} +@media screen and (max-width: 460px) { + div.dt-button-collection.columns > :last-child { + width: 200px; + } +} + +div.dt-btn-split-wrapper { + padding: 0px !important; + background-color: transparent !important; + display: flex; + border: none !important; + margin: 0px; +} +div.dt-btn-split-wrapper:hover { + border: none; +} +div.dt-btn-split-wrapper button.dt-btn-split-drop { + width: 24px; + padding-left: 6px; + padding-right: 6px; + font-size: 10px; + height: 29.5px; + border-radius: 0px; + margin-left: -1px; +} +div.dt-btn-split-wrapper:active:not(.disabled) button.dt-button, div.dt-btn-split-wrapper.ui-state-active:not(.disabled) button.dt-button, div.dt-btn-split-wrapper.is-active:not(.disabled) button.dt-button { + background-color: #007fff; + border-color: #003eff; +} +div.dt-btn-split-wrapper:active:not(.disabled) button.dt-btn-split-drop, div.dt-btn-split-wrapper.ui-state-active:not(.disabled) button.dt-btn-split-drop, div.dt-btn-split-wrapper.is-active:not(.disabled) button.dt-btn-split-drop { + box-shadow: none; + background-color: #f6f6f6; + border-color: #c5c5c5; +} +div.dt-btn-split-wrapper:active:not(.disabled) button:hover, div.dt-btn-split-wrapper.ui-state-active:not(.disabled) button:hover, div.dt-btn-split-wrapper.is-active:not(.disabled) button:hover { + background-color: #ededed; + border-color: #cccccc; +} + +div.dt-button-background { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.7); /* Fallback */ + background: radial-gradient(ellipse farthest-corner at center, rgba(0, 0, 0, 0.3) 0%, rgba(0, 0, 0, 0.7) 100%); /* W3C Markup, IE10 Release Preview */ + z-index: 2001; +} + +@media screen and (max-width: 640px) { + div.dt-buttons { + float: none !important; + text-align: center; + } +} +button.dt-button.processing, +div.dt-button.processing, +a.dt-button.processing { + color: rgba(0, 0, 0, 0.2); +} +button.dt-button.processing:after, +div.dt-button.processing:after, +a.dt-button.processing:after { + position: absolute; + top: 50%; + left: 50%; + width: 16px; + height: 16px; + margin: -8px 0 0 -8px; + box-sizing: border-box; + display: block; + content: " "; + border: 2px solid rgb(40, 40, 40); + border-radius: 50%; + border-left-color: transparent; + border-right-color: transparent; + animation: dtb-spinner 1500ms infinite linear; + -o-animation: dtb-spinner 1500ms infinite linear; + -ms-animation: dtb-spinner 1500ms infinite linear; + -webkit-animation: dtb-spinner 1500ms infinite linear; + -moz-animation: dtb-spinner 1500ms infinite linear; +} + +span.dt-down-arrow { + display: none; +} diff --git a/public/assets/js/DataTables/Buttons-2.3.6/css/buttons.jqueryui.min.css b/public/assets/js/DataTables/Buttons-2.3.6/css/buttons.jqueryui.min.css new file mode 100644 index 0000000..42ab77e --- /dev/null +++ b/public/assets/js/DataTables/Buttons-2.3.6/css/buttons.jqueryui.min.css @@ -0,0 +1 @@ +@keyframes dtb-spinner{100%{transform:rotate(360deg)}}@-o-keyframes dtb-spinner{100%{-o-transform:rotate(360deg);transform:rotate(360deg)}}@-ms-keyframes dtb-spinner{100%{-ms-transform:rotate(360deg);transform:rotate(360deg)}}@-webkit-keyframes dtb-spinner{100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@-moz-keyframes dtb-spinner{100%{-moz-transform:rotate(360deg);transform:rotate(360deg)}}div.dataTables_wrapper{position:relative}div.dt-buttons{position:initial}div.dt-buttons .dt-button{overflow:hidden;text-overflow:ellipsis}div.dt-button-info{position:fixed;top:50%;left:50%;width:400px;margin-top:-100px;margin-left:-200px;background-color:white;border:2px solid #111;box-shadow:3px 4px 10px 1px rgba(0, 0, 0, 0.3);border-radius:3px;text-align:center;z-index:21}div.dt-button-info h2{padding:.5em;margin:0;font-weight:normal;border-bottom:1px solid #ddd;background-color:#f3f3f3}div.dt-button-info>div{padding:1em}div.dtb-popover-close{position:absolute;top:10px;right:10px;width:22px;height:22px;border:1px solid #eaeaea;background-color:#f9f9f9;text-align:center;border-radius:3px;cursor:pointer;z-index:2003}button.dtb-hide-drop{display:none !important}div.dt-button-collection-title{text-align:center;padding:.3em 0 .5em;margin-left:.5em;margin-right:.5em;font-size:.9em}div.dt-button-collection-title:empty{display:none}span.dt-button-spacer{display:inline-block;margin:.5em;white-space:nowrap}span.dt-button-spacer.bar{border-left:1px solid rgba(0, 0, 0, 0.3);vertical-align:middle;padding-left:.5em}span.dt-button-spacer.bar:empty{height:1em;width:1px;padding-left:0}div.dt-button-collection span.dt-button-spacer{width:100%;font-size:.9em;text-align:center;margin:.5em 0}div.dt-button-collection span.dt-button-spacer:empty{height:0;width:100%}div.dt-button-collection span.dt-button-spacer.bar{border-left:none;border-bottom:1px solid rgba(0, 0, 0, 0.3);padding-left:0}div.dt-buttons{position:relative;float:left}div.dt-buttons .dt-button{margin-right:0}div.dt-buttons .dt-button span.ui-icon{display:inline-block;vertical-align:middle;margin-top:-2px}div.dt-buttons .dt-button:active{outline:none}div.dt-buttons .dt-button:hover>span{background-color:rgba(0, 0, 0, 0.05)}div.dt-button-collection{position:absolute;top:0;left:0;width:150px;margin-top:3px;padding:8px 8px 4px 8px;border:1px solid #ccc;border:1px solid rgba(0, 0, 0, 0.4);background-color:#f3f3f3;overflow:hidden;z-index:2002;border-radius:5px;box-shadow:3px 3px 5px rgba(0, 0, 0, 0.3);z-index:2002;-webkit-column-gap:0;-moz-column-gap:0;-ms-column-gap:0;-o-column-gap:0;column-gap:0}div.dt-button-collection .dt-button{position:relative;left:0;right:0;width:100%;box-sizing:border-box;display:block;float:none;margin-right:0;margin-bottom:4px}div.dt-button-collection .dt-button:hover>span{background-color:rgba(0, 0, 0, 0.05)}div.dt-button-collection.fixed{position:fixed;display:block;top:50%;left:50%;margin-left:-75px;border-radius:5px;background-color:white}div.dt-button-collection.fixed.two-column{margin-left:-200px}div.dt-button-collection.fixed.three-column{margin-left:-225px}div.dt-button-collection.fixed.four-column{margin-left:-300px}div.dt-button-collection.fixed.columns{margin-left:-409px}@media screen and (max-width: 1024px){div.dt-button-collection.fixed.columns{margin-left:-308px}}@media screen and (max-width: 640px){div.dt-button-collection.fixed.columns{margin-left:-203px}}@media screen and (max-width: 460px){div.dt-button-collection.fixed.columns{margin-left:-100px}}div.dt-button-collection.fixed>:last-child{max-height:100vh;overflow:auto}div.dt-button-collection.two-column>:last-child,div.dt-button-collection.three-column>:last-child,div.dt-button-collection.four-column>:last-child{display:block !important;-webkit-column-gap:8px;-moz-column-gap:8px;-ms-column-gap:8px;-o-column-gap:8px;column-gap:8px}div.dt-button-collection.two-column>:last-child>*,div.dt-button-collection.three-column>:last-child>*,div.dt-button-collection.four-column>:last-child>*{-webkit-column-break-inside:avoid;break-inside:avoid}div.dt-button-collection.two-column{width:400px}div.dt-button-collection.two-column>:last-child{padding-bottom:1px;column-count:2}div.dt-button-collection.three-column{width:450px}div.dt-button-collection.three-column>:last-child{padding-bottom:1px;column-count:3}div.dt-button-collection.four-column{width:600px}div.dt-button-collection.four-column>:last-child{padding-bottom:1px;column-count:4}div.dt-button-collection .dt-button{border-radius:0}div.dt-button-collection.columns{width:auto}div.dt-button-collection.columns>:last-child{display:flex;flex-wrap:wrap;justify-content:flex-start;align-items:center;gap:6px;width:818px;padding-bottom:1px}div.dt-button-collection.columns>:last-child .dt-button{min-width:200px;flex:0 1;margin:0}div.dt-button-collection.columns.dtb-b3>:last-child,div.dt-button-collection.columns.dtb-b2>:last-child,div.dt-button-collection.columns.dtb-b1>:last-child{justify-content:space-between}div.dt-button-collection.columns.dtb-b3 .dt-button{flex:1 1 32%}div.dt-button-collection.columns.dtb-b2 .dt-button{flex:1 1 48%}div.dt-button-collection.columns.dtb-b1 .dt-button{flex:1 1 100%}@media screen and (max-width: 1024px){div.dt-button-collection.columns>:last-child{width:612px}}@media screen and (max-width: 640px){div.dt-button-collection.columns>:last-child{width:406px}div.dt-button-collection.columns.dtb-b3 .dt-button{flex:0 1 32%}}@media screen and (max-width: 460px){div.dt-button-collection.columns>:last-child{width:200px}}div.dt-btn-split-wrapper{padding:0px !important;background-color:transparent !important;display:flex;border:none !important;margin:0px}div.dt-btn-split-wrapper:hover{border:none}div.dt-btn-split-wrapper button.dt-btn-split-drop{width:24px;padding-left:6px;padding-right:6px;font-size:10px;height:29.5px;border-radius:0px;margin-left:-1px}div.dt-btn-split-wrapper:active:not(.disabled) button.dt-button,div.dt-btn-split-wrapper.ui-state-active:not(.disabled) button.dt-button,div.dt-btn-split-wrapper.is-active:not(.disabled) button.dt-button{background-color:#007fff;border-color:#003eff}div.dt-btn-split-wrapper:active:not(.disabled) button.dt-btn-split-drop,div.dt-btn-split-wrapper.ui-state-active:not(.disabled) button.dt-btn-split-drop,div.dt-btn-split-wrapper.is-active:not(.disabled) button.dt-btn-split-drop{box-shadow:none;background-color:#f6f6f6;border-color:#c5c5c5}div.dt-btn-split-wrapper:active:not(.disabled) button:hover,div.dt-btn-split-wrapper.ui-state-active:not(.disabled) button:hover,div.dt-btn-split-wrapper.is-active:not(.disabled) button:hover{background-color:#ededed;border-color:#ccc}div.dt-button-background{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0, 0, 0, 0.7);background:radial-gradient(ellipse farthest-corner at center, rgba(0, 0, 0, 0.3) 0%, rgba(0, 0, 0, 0.7) 100%);z-index:2001}@media screen and (max-width: 640px){div.dt-buttons{float:none !important;text-align:center}}button.dt-button.processing,div.dt-button.processing,a.dt-button.processing{color:rgba(0, 0, 0, 0.2)}button.dt-button.processing:after,div.dt-button.processing:after,a.dt-button.processing:after{position:absolute;top:50%;left:50%;width:16px;height:16px;margin:-8px 0 0 -8px;box-sizing:border-box;display:block;content:" ";border:2px solid rgb(40, 40, 40);border-radius:50%;border-left-color:transparent;border-right-color:transparent;animation:dtb-spinner 1500ms infinite linear;-o-animation:dtb-spinner 1500ms infinite linear;-ms-animation:dtb-spinner 1500ms infinite linear;-webkit-animation:dtb-spinner 1500ms infinite linear;-moz-animation:dtb-spinner 1500ms infinite linear}span.dt-down-arrow{display:none} diff --git a/public/assets/js/DataTables/Buttons-2.3.6/css/buttons.semanticui.css b/public/assets/js/DataTables/Buttons-2.3.6/css/buttons.semanticui.css new file mode 100644 index 0000000..7549ea2 --- /dev/null +++ b/public/assets/js/DataTables/Buttons-2.3.6/css/buttons.semanticui.css @@ -0,0 +1,409 @@ +@keyframes dtb-spinner { + 100% { + transform: rotate(360deg); + } +} +@-o-keyframes dtb-spinner { + 100% { + -o-transform: rotate(360deg); + transform: rotate(360deg); + } +} +@-ms-keyframes dtb-spinner { + 100% { + -ms-transform: rotate(360deg); + transform: rotate(360deg); + } +} +@-webkit-keyframes dtb-spinner { + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } +} +@-moz-keyframes dtb-spinner { + 100% { + -moz-transform: rotate(360deg); + transform: rotate(360deg); + } +} +div.dataTables_wrapper { + position: relative; +} + +div.dt-buttons { + position: initial; +} +div.dt-buttons .dt-button { + overflow: hidden; + text-overflow: ellipsis; +} + +div.dt-button-info { + position: fixed; + top: 50%; + left: 50%; + width: 400px; + margin-top: -100px; + margin-left: -200px; + background-color: white; + border: 2px solid #111; + box-shadow: 3px 4px 10px 1px rgba(0, 0, 0, 0.3); + border-radius: 3px; + text-align: center; + z-index: 21; +} +div.dt-button-info h2 { + padding: 0.5em; + margin: 0; + font-weight: normal; + border-bottom: 1px solid #ddd; + background-color: #f3f3f3; +} +div.dt-button-info > div { + padding: 1em; +} + +div.dtb-popover-close { + position: absolute; + top: 10px; + right: 10px; + width: 22px; + height: 22px; + border: 1px solid #eaeaea; + background-color: #f9f9f9; + text-align: center; + border-radius: 3px; + cursor: pointer; + z-index: 2003; +} + +button.dtb-hide-drop { + display: none !important; +} + +div.dt-button-collection-title { + text-align: center; + padding: 0.3em 0 0.5em; + margin-left: 0.5em; + margin-right: 0.5em; + font-size: 0.9em; +} + +div.dt-button-collection-title:empty { + display: none; +} + +span.dt-button-spacer { + display: inline-block; + margin: 0.5em; + white-space: nowrap; +} +span.dt-button-spacer.bar { + border-left: 1px solid rgba(0, 0, 0, 0.3); + vertical-align: middle; + padding-left: 0.5em; +} +span.dt-button-spacer.bar:empty { + height: 1em; + width: 1px; + padding-left: 0; +} + +div.dt-button-collection span.dt-button-spacer { + width: 100%; + font-size: 0.9em; + text-align: center; + margin: 0.5em 0; +} +div.dt-button-collection span.dt-button-spacer:empty { + height: 0; + width: 100%; +} +div.dt-button-collection span.dt-button-spacer.bar { + border-left: none; + border-bottom: 1px solid rgba(0, 0, 0, 0.3); + padding-left: 0; +} + +div.dt-button-collection { + position: absolute; + top: 0; + left: 0; + min-width: 200px; + margin-top: 3px !important; + margin-bottom: 3px !important; + z-index: 2002; + background: white; + border: 1px solid rgba(34, 36, 38, 0.15); + font-size: 1em; + padding: 0.5rem; +} +div.dt-button-collection.fixed { + position: fixed; + display: block; + top: 50%; + left: 50%; + margin-left: -75px; + border-radius: 5px; + background-color: white; +} +div.dt-button-collection.fixed.two-column { + margin-left: -200px; +} +div.dt-button-collection.fixed.three-column { + margin-left: -225px; +} +div.dt-button-collection.fixed.four-column { + margin-left: -300px; +} +div.dt-button-collection.fixed.columns { + margin-left: -409px; +} +@media screen and (max-width: 1024px) { + div.dt-button-collection.fixed.columns { + margin-left: -308px; + } +} +@media screen and (max-width: 640px) { + div.dt-button-collection.fixed.columns { + margin-left: -203px; + } +} +@media screen and (max-width: 460px) { + div.dt-button-collection.fixed.columns { + margin-left: -100px; + } +} +div.dt-button-collection.fixed > :last-child { + max-height: 100vh; + overflow: auto; +} +div.dt-button-collection.two-column > :last-child, div.dt-button-collection.three-column > :last-child, div.dt-button-collection.four-column > :last-child { + display: block !important; + -webkit-column-gap: 8px; + -moz-column-gap: 8px; + -ms-column-gap: 8px; + -o-column-gap: 8px; + column-gap: 8px; +} +div.dt-button-collection.two-column > :last-child > *, div.dt-button-collection.three-column > :last-child > *, div.dt-button-collection.four-column > :last-child > * { + -webkit-column-break-inside: avoid; + break-inside: avoid; +} +div.dt-button-collection.two-column { + width: 400px; +} +div.dt-button-collection.two-column > :last-child { + padding-bottom: 1px; + column-count: 2; +} +div.dt-button-collection.three-column { + width: 450px; +} +div.dt-button-collection.three-column > :last-child { + padding-bottom: 1px; + column-count: 3; +} +div.dt-button-collection.four-column { + width: 600px; +} +div.dt-button-collection.four-column > :last-child { + padding-bottom: 1px; + column-count: 4; +} +div.dt-button-collection .dt-button { + border-radius: 0; +} +div.dt-button-collection.columns { + width: auto; +} +div.dt-button-collection.columns > :last-child { + display: flex; + flex-wrap: wrap; + justify-content: flex-start; + align-items: center; + gap: 6px; + width: 818px; + padding-bottom: 1px; +} +div.dt-button-collection.columns > :last-child .dt-button { + min-width: 200px; + flex: 0 1; + margin: 0; +} +div.dt-button-collection.columns.dtb-b3 > :last-child, div.dt-button-collection.columns.dtb-b2 > :last-child, div.dt-button-collection.columns.dtb-b1 > :last-child { + justify-content: space-between; +} +div.dt-button-collection.columns.dtb-b3 .dt-button { + flex: 1 1 32%; +} +div.dt-button-collection.columns.dtb-b2 .dt-button { + flex: 1 1 48%; +} +div.dt-button-collection.columns.dtb-b1 .dt-button { + flex: 1 1 100%; +} +@media screen and (max-width: 1024px) { + div.dt-button-collection.columns > :last-child { + width: 612px; + } +} +@media screen and (max-width: 640px) { + div.dt-button-collection.columns > :last-child { + width: 406px; + } + div.dt-button-collection.columns.dtb-b3 .dt-button { + flex: 0 1 32%; + } +} +@media screen and (max-width: 460px) { + div.dt-button-collection.columns > :last-child { + width: 200px; + } +} +div.dt-button-collection div.dt-button-collection-title { + font-size: 1rem; +} +div.dt-button-collection.columns div.ui.basic.buttons span.dt-button-spacer { + background: transparent !important; + border: none; + flex: 1 1 100%; +} +div.dt-button-collection.columns div.ui.basic.buttons span.dt-button-spacer:hover { + background: transparent !important; +} +div.dt-button-collection:not(.columns) .ui.vertical.buttons { + width: 100%; + border: none; +} +div.dt-button-collection.columns .ui.vertical.buttons { + flex-direction: row; + border: none; +} +div.dt-button-collection button.dt-button { + border: 1px solid rgba(34, 36, 38, 0.15) !important; +} +div.dt-button-collection div.dt-btn-split-wrapper { + display: flex; +} +div.dt-button-collection div.dt-btn-split-wrapper button { + flex-grow: 1 !important; + flex-basis: auto !important; + width: auto !important; + border-top-right-radius: 0px !important; +} +div.dt-button-collection div.dt-btn-split-wrapper button.dt-btn-split-drop { + flex-grow: 0 !important; + flex-basis: auto !important; + border-bottom-left-radius: 0px !important; + border-bottom-right-radius: 0px !important; + border-top-right-radius: 4px !important; +} + +button.buttons-collection.ui.button span:after { + display: inline-block; + content: "▾"; + padding-left: 0.5em; +} + +div.dt-button-background { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 2001; +} + +@media screen and (max-width: 767px) { + div.dt-buttons { + float: none; + width: 100%; + text-align: center; + margin-bottom: 0.5em; + } + div.dt-buttons a.btn { + float: none; + } +} +div.dt-buttons button.button.processing, +div.dt-buttons div.button.processing, +div.dt-buttons a.button.processing { + position: relative; + color: rgba(0, 0, 0, 0.2); +} +div.dt-buttons button.button.processing:after, +div.dt-buttons div.button.processing:after, +div.dt-buttons a.button.processing:after { + position: absolute; + top: 50%; + left: 50%; + width: 16px; + height: 16px; + margin: -8px 0 0 -8px; + box-sizing: border-box; + display: block; + content: " "; + border: 2px solid rgb(40, 40, 40); + border-radius: 50%; + border-left-color: transparent; + border-right-color: transparent; + animation: dtb-spinner 1500ms infinite linear; + -o-animation: dtb-spinner 1500ms infinite linear; + -ms-animation: dtb-spinner 1500ms infinite linear; + -webkit-animation: dtb-spinner 1500ms infinite linear; + -moz-animation: dtb-spinner 1500ms infinite linear; +} +div.dt-buttons.ui.buttons { + flex-wrap: wrap; +} +div.dt-buttons.ui.basic.buttons .ui.button { + border-bottom: 1px solid rgba(34, 36, 38, 0.15); + margin-bottom: -1px; +} +div.dt-buttons.ui.basic.buttons .ui.button:hover { + background: transparent !important; +} + +span.dt-down-arrow { + display: none; +} + +span.dt-button-spacer { + cursor: inherit; +} +span.dt-button-spacer.bar { + padding-left: 1.5em; +} +span.dt-button-spacer.bar:empty { + height: inherit; +} + +div.dt-button-collection span.dt-button-spacer { + border-top: 1px solid rgba(34, 36, 38, 0.15); +} +div.dt-button-collection span.dt-button-spacer.bar { + border-bottom: none; + padding-left: 1.5em; +} + +div.dt-buttons.ui.basic.buttons .button.dt-button-spacer { + background: rgba(34, 36, 38, 0.05) !important; + box-shadow: none; + cursor: initial; +} +div.dt-buttons.ui.basic.buttons .button.dt-button-spacer:hover { + background-color: rgba(34, 36, 38, 0.05) !important; +} + +div.dt-btn-split-wrapper:active:not(.disabled) button.button, div.dt-btn-split-wrapper.active:not(.disabled) button.button { + background-color: #f8f8f8 !important; +} +div.dt-btn-split-wrapper:active:not(.disabled) button.dt-btn-split-drop, div.dt-btn-split-wrapper.active:not(.disabled) button.dt-btn-split-drop { + box-shadow: none; + background-color: transparent !important; +} +div.dt-btn-split-wrapper:active:not(.disabled) button.button:hover, div.dt-btn-split-wrapper.active:not(.disabled) button.button:hover { + background-color: transparent !important; +} diff --git a/public/assets/js/DataTables/Buttons-2.3.6/css/buttons.semanticui.min.css b/public/assets/js/DataTables/Buttons-2.3.6/css/buttons.semanticui.min.css new file mode 100644 index 0000000..093e7a9 --- /dev/null +++ b/public/assets/js/DataTables/Buttons-2.3.6/css/buttons.semanticui.min.css @@ -0,0 +1 @@ +@keyframes dtb-spinner{100%{transform:rotate(360deg)}}@-o-keyframes dtb-spinner{100%{-o-transform:rotate(360deg);transform:rotate(360deg)}}@-ms-keyframes dtb-spinner{100%{-ms-transform:rotate(360deg);transform:rotate(360deg)}}@-webkit-keyframes dtb-spinner{100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@-moz-keyframes dtb-spinner{100%{-moz-transform:rotate(360deg);transform:rotate(360deg)}}div.dataTables_wrapper{position:relative}div.dt-buttons{position:initial}div.dt-buttons .dt-button{overflow:hidden;text-overflow:ellipsis}div.dt-button-info{position:fixed;top:50%;left:50%;width:400px;margin-top:-100px;margin-left:-200px;background-color:white;border:2px solid #111;box-shadow:3px 4px 10px 1px rgba(0, 0, 0, 0.3);border-radius:3px;text-align:center;z-index:21}div.dt-button-info h2{padding:.5em;margin:0;font-weight:normal;border-bottom:1px solid #ddd;background-color:#f3f3f3}div.dt-button-info>div{padding:1em}div.dtb-popover-close{position:absolute;top:10px;right:10px;width:22px;height:22px;border:1px solid #eaeaea;background-color:#f9f9f9;text-align:center;border-radius:3px;cursor:pointer;z-index:2003}button.dtb-hide-drop{display:none !important}div.dt-button-collection-title{text-align:center;padding:.3em 0 .5em;margin-left:.5em;margin-right:.5em;font-size:.9em}div.dt-button-collection-title:empty{display:none}span.dt-button-spacer{display:inline-block;margin:.5em;white-space:nowrap}span.dt-button-spacer.bar{border-left:1px solid rgba(0, 0, 0, 0.3);vertical-align:middle;padding-left:.5em}span.dt-button-spacer.bar:empty{height:1em;width:1px;padding-left:0}div.dt-button-collection span.dt-button-spacer{width:100%;font-size:.9em;text-align:center;margin:.5em 0}div.dt-button-collection span.dt-button-spacer:empty{height:0;width:100%}div.dt-button-collection span.dt-button-spacer.bar{border-left:none;border-bottom:1px solid rgba(0, 0, 0, 0.3);padding-left:0}div.dt-button-collection{position:absolute;top:0;left:0;min-width:200px;margin-top:3px !important;margin-bottom:3px !important;z-index:2002;background:white;border:1px solid rgba(34, 36, 38, 0.15);font-size:1em;padding:.5rem}div.dt-button-collection.fixed{position:fixed;display:block;top:50%;left:50%;margin-left:-75px;border-radius:5px;background-color:white}div.dt-button-collection.fixed.two-column{margin-left:-200px}div.dt-button-collection.fixed.three-column{margin-left:-225px}div.dt-button-collection.fixed.four-column{margin-left:-300px}div.dt-button-collection.fixed.columns{margin-left:-409px}@media screen and (max-width: 1024px){div.dt-button-collection.fixed.columns{margin-left:-308px}}@media screen and (max-width: 640px){div.dt-button-collection.fixed.columns{margin-left:-203px}}@media screen and (max-width: 460px){div.dt-button-collection.fixed.columns{margin-left:-100px}}div.dt-button-collection.fixed>:last-child{max-height:100vh;overflow:auto}div.dt-button-collection.two-column>:last-child,div.dt-button-collection.three-column>:last-child,div.dt-button-collection.four-column>:last-child{display:block !important;-webkit-column-gap:8px;-moz-column-gap:8px;-ms-column-gap:8px;-o-column-gap:8px;column-gap:8px}div.dt-button-collection.two-column>:last-child>*,div.dt-button-collection.three-column>:last-child>*,div.dt-button-collection.four-column>:last-child>*{-webkit-column-break-inside:avoid;break-inside:avoid}div.dt-button-collection.two-column{width:400px}div.dt-button-collection.two-column>:last-child{padding-bottom:1px;column-count:2}div.dt-button-collection.three-column{width:450px}div.dt-button-collection.three-column>:last-child{padding-bottom:1px;column-count:3}div.dt-button-collection.four-column{width:600px}div.dt-button-collection.four-column>:last-child{padding-bottom:1px;column-count:4}div.dt-button-collection .dt-button{border-radius:0}div.dt-button-collection.columns{width:auto}div.dt-button-collection.columns>:last-child{display:flex;flex-wrap:wrap;justify-content:flex-start;align-items:center;gap:6px;width:818px;padding-bottom:1px}div.dt-button-collection.columns>:last-child .dt-button{min-width:200px;flex:0 1;margin:0}div.dt-button-collection.columns.dtb-b3>:last-child,div.dt-button-collection.columns.dtb-b2>:last-child,div.dt-button-collection.columns.dtb-b1>:last-child{justify-content:space-between}div.dt-button-collection.columns.dtb-b3 .dt-button{flex:1 1 32%}div.dt-button-collection.columns.dtb-b2 .dt-button{flex:1 1 48%}div.dt-button-collection.columns.dtb-b1 .dt-button{flex:1 1 100%}@media screen and (max-width: 1024px){div.dt-button-collection.columns>:last-child{width:612px}}@media screen and (max-width: 640px){div.dt-button-collection.columns>:last-child{width:406px}div.dt-button-collection.columns.dtb-b3 .dt-button{flex:0 1 32%}}@media screen and (max-width: 460px){div.dt-button-collection.columns>:last-child{width:200px}}div.dt-button-collection div.dt-button-collection-title{font-size:1rem}div.dt-button-collection.columns div.ui.basic.buttons span.dt-button-spacer{background:transparent !important;border:none;flex:1 1 100%}div.dt-button-collection.columns div.ui.basic.buttons span.dt-button-spacer:hover{background:transparent !important}div.dt-button-collection:not(.columns) .ui.vertical.buttons{width:100%;border:none}div.dt-button-collection.columns .ui.vertical.buttons{flex-direction:row;border:none}div.dt-button-collection button.dt-button{border:1px solid rgba(34, 36, 38, 0.15) !important}div.dt-button-collection div.dt-btn-split-wrapper{display:flex}div.dt-button-collection div.dt-btn-split-wrapper button{flex-grow:1 !important;flex-basis:auto !important;width:auto !important;border-top-right-radius:0px !important}div.dt-button-collection div.dt-btn-split-wrapper button.dt-btn-split-drop{flex-grow:0 !important;flex-basis:auto !important;border-bottom-left-radius:0px !important;border-bottom-right-radius:0px !important;border-top-right-radius:4px !important}button.buttons-collection.ui.button span:after{display:inline-block;content:"▾";padding-left:.5em}div.dt-button-background{position:fixed;top:0;left:0;width:100%;height:100%;z-index:2001}@media screen and (max-width: 767px){div.dt-buttons{float:none;width:100%;text-align:center;margin-bottom:.5em}div.dt-buttons a.btn{float:none}}div.dt-buttons button.button.processing,div.dt-buttons div.button.processing,div.dt-buttons a.button.processing{position:relative;color:rgba(0, 0, 0, 0.2)}div.dt-buttons button.button.processing:after,div.dt-buttons div.button.processing:after,div.dt-buttons a.button.processing:after{position:absolute;top:50%;left:50%;width:16px;height:16px;margin:-8px 0 0 -8px;box-sizing:border-box;display:block;content:" ";border:2px solid rgb(40, 40, 40);border-radius:50%;border-left-color:transparent;border-right-color:transparent;animation:dtb-spinner 1500ms infinite linear;-o-animation:dtb-spinner 1500ms infinite linear;-ms-animation:dtb-spinner 1500ms infinite linear;-webkit-animation:dtb-spinner 1500ms infinite linear;-moz-animation:dtb-spinner 1500ms infinite linear}div.dt-buttons.ui.buttons{flex-wrap:wrap}div.dt-buttons.ui.basic.buttons .ui.button{border-bottom:1px solid rgba(34, 36, 38, 0.15);margin-bottom:-1px}div.dt-buttons.ui.basic.buttons .ui.button:hover{background:transparent !important}span.dt-down-arrow{display:none}span.dt-button-spacer{cursor:inherit}span.dt-button-spacer.bar{padding-left:1.5em}span.dt-button-spacer.bar:empty{height:inherit}div.dt-button-collection span.dt-button-spacer{border-top:1px solid rgba(34, 36, 38, 0.15)}div.dt-button-collection span.dt-button-spacer.bar{border-bottom:none;padding-left:1.5em}div.dt-buttons.ui.basic.buttons .button.dt-button-spacer{background:rgba(34, 36, 38, 0.05) !important;box-shadow:none;cursor:initial}div.dt-buttons.ui.basic.buttons .button.dt-button-spacer:hover{background-color:rgba(34, 36, 38, 0.05) !important}div.dt-btn-split-wrapper:active:not(.disabled) button.button,div.dt-btn-split-wrapper.active:not(.disabled) button.button{background-color:#f8f8f8 !important}div.dt-btn-split-wrapper:active:not(.disabled) button.dt-btn-split-drop,div.dt-btn-split-wrapper.active:not(.disabled) button.dt-btn-split-drop{box-shadow:none;background-color:transparent !important}div.dt-btn-split-wrapper:active:not(.disabled) button.button:hover,div.dt-btn-split-wrapper.active:not(.disabled) button.button:hover{background-color:transparent !important} diff --git a/public/assets/js/DataTables/Buttons-2.3.6/css/common.scss b/public/assets/js/DataTables/Buttons-2.3.6/css/common.scss new file mode 100644 index 0000000..1c2cb17 --- /dev/null +++ b/public/assets/js/DataTables/Buttons-2.3.6/css/common.scss @@ -0,0 +1,106 @@ + +div.dataTables_wrapper { + position: relative; +} + +div.dt-buttons { + position: initial; + + .dt-button { + overflow: hidden; + text-overflow: ellipsis; + } +} + +div.dt-button-info { + position: fixed; + top: 50%; + left: 50%; + width: 400px; + margin-top: -100px; + margin-left: -200px; + background-color: white; + border: 2px solid #111; + box-shadow: 3px 4px 10px 1px rgba(0, 0, 0, 0.3); + border-radius: 3px; + text-align: center; + z-index: 21; + + h2 { + padding: 0.5em; + margin: 0; + font-weight: normal; + border-bottom: 1px solid #ddd; + background-color: #f3f3f3; + } + + > div { + padding: 1em; + } +} + +div.dtb-popover-close { + position: absolute; + top: 10px; + right: 10px; + width: 22px; + height: 22px; + border: 1px solid #eaeaea; + background-color: #f9f9f9; + text-align: center; + border-radius: 3px; + cursor: pointer; + z-index: 2003; +} + +button.dtb-hide-drop { + display: none !important; +} + +div.dt-button-collection-title { + text-align: center; + padding: 0.3em 0 0.5em; + margin-left: 0.5em; + margin-right: 0.5em; + font-size: 0.9em; +} + +div.dt-button-collection-title:empty { + display: none; +} + +span.dt-button-spacer { + display: inline-block; + margin: 0.5em; + white-space: nowrap; + + &.bar { + border-left: 1px solid rgba(0, 0, 0, 0.3); + vertical-align: middle; + padding-left: 0.5em; + + &:empty { + height: 1em; + width: 1px; + padding-left: 0; + } + } +} + +div.dt-button-collection span.dt-button-spacer { + width: 100%; + font-size: 0.9em; + text-align: center; + margin: 0.5em 0; + + &:empty { + height: 0; + width: 100%; + } + + &.bar { + border-left: none; + border-bottom: 1px solid rgba(0, 0, 0, 0.3); + padding-left: 0; + } +} diff --git a/public/assets/js/DataTables/Buttons-2.3.6/css/mixins.scss b/public/assets/js/DataTables/Buttons-2.3.6/css/mixins.scss new file mode 100644 index 0000000..3fee4ec --- /dev/null +++ b/public/assets/js/DataTables/Buttons-2.3.6/css/mixins.scss @@ -0,0 +1,228 @@ + +@function dtb-tint( $color, $percent ) { + @return mix(white, $color, $percent); +} + +@function dtb-shade( $color, $percent ) { + @return mix(black, $color, $percent); +} + +@mixin dtb-two-stop-gradient($fromColor, $toColor) { + background-color: $toColor; /* Fallback */ + background: linear-gradient(to bottom, $fromColor 0%, $toColor 100%); + filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr='#{nth( $fromColor, 1 )}', EndColorStr='#{nth( $toColor, 1 )}'); +} + +@mixin dtb-radial-gradient ($fromColor, $toColor ) { + background: $toColor; /* Fallback */ + background: radial-gradient(ellipse farthest-corner at center, $fromColor 0%, $toColor 100%); /* W3C Markup, IE10 Release Preview */ +} + + +@mixin dtb-fixed-collection { + // Fixed positioning feature + &.fixed { + position: fixed; + display: block; + top: 50%; + left: 50%; + margin-left: -75px; + border-radius: 5px; + background-color: white; + + &.two-column { + margin-left: -200px; + } + + &.three-column { + margin-left: -225px; + } + + &.four-column { + margin-left: -300px; + } + + &.columns { + // Four column + margin-left: -409px; + + @media screen and (max-width: 1024px) { + margin-left: -308px; + } + + @media screen and (max-width: 640px) { + margin-left: -203px; + } + + @media screen and (max-width: 460px) { + margin-left: -100px; + } + } + + > :last-child { + max-height: 100vh; + overflow: auto; + } + } + + &.two-column > :last-child, + &.three-column > :last-child, + &.four-column > :last-child { + > * { + -webkit-column-break-inside: avoid; + break-inside: avoid; + } + + // Multi-column layout feature + display: block !important; + -webkit-column-gap: 8px; + -moz-column-gap: 8px; + -ms-column-gap: 8px; + -o-column-gap: 8px; + column-gap: 8px; + } + + &.two-column { + width: 400px; + + > :last-child { + padding-bottom: 1px; + column-count: 2; + } + } + + &.three-column { + width: 450px; + + > :last-child { + padding-bottom: 1px; + column-count: 3; + } + } + + &.four-column { + width: 600px; + + > :last-child { + padding-bottom: 1px; + column-count: 4; + } + } + + // Chrome fix - 531528 + .dt-button { + border-radius: 0; + } + + &.columns { + // Four column layout + width: auto; + + > :last-child { + display: flex; + flex-wrap: wrap; + justify-content: flex-start; + align-items: center; + gap: 6px; + + width: 818px; + padding-bottom: 1px; + + .dt-button { + min-width: 200px; + flex: 0 1; + margin: 0; + } + } + + &.dtb-b3, + &.dtb-b2, + &.dtb-b1 { + > :last-child { + justify-content: space-between; + } + } + + &.dtb-b3 .dt-button { + flex: 1 1 32%; + } + &.dtb-b2 .dt-button { + flex: 1 1 48%; + } + &.dtb-b1 .dt-button { + flex: 1 1 100%; + } + + @media screen and (max-width: 1024px) { + // Three column layout + > :last-child { + width: 612px; + } + } + + @media screen and (max-width: 640px) { + // Two column layout + > :last-child { + width: 406px; + } + + &.dtb-b3 .dt-button { + flex: 0 1 32%; + } + } + + @media screen and (max-width: 460px) { + // Single column + > :last-child { + width: 200px; + } + } + } +} + + +@mixin dtb-processing { + color: rgba(0, 0, 0, 0.2); + + &:after { + position: absolute; + top: 50%; + left: 50%; + width: 16px; + height: 16px; + margin: -8px 0 0 -8px; + box-sizing: border-box; + + display: block; + content: ' '; + border: 2px solid rgb(40,40,40); + border-radius: 50%; + border-left-color: transparent; + border-right-color: transparent; + animation: dtb-spinner 1500ms infinite linear; + -o-animation: dtb-spinner 1500ms infinite linear; + -ms-animation: dtb-spinner 1500ms infinite linear; + -webkit-animation: dtb-spinner 1500ms infinite linear; + -moz-animation: dtb-spinner 1500ms infinite linear; + } +} + +@keyframes dtb-spinner { + 100%{ transform: rotate(360deg); } +} + +@-o-keyframes dtb-spinner { + 100%{ -o-transform: rotate(360deg); transform: rotate(360deg); } +} + +@-ms-keyframes dtb-spinner { + 100%{ -ms-transform: rotate(360deg); transform: rotate(360deg); } +} + +@-webkit-keyframes dtb-spinner { + 100%{ -webkit-transform: rotate(360deg); transform: rotate(360deg); } +} + +@-moz-keyframes dtb-spinner { + 100%{ -moz-transform: rotate(360deg); transform: rotate(360deg); } +} diff --git a/public/assets/js/DataTables/Buttons-2.3.6/js/buttons.bootstrap.js b/public/assets/js/DataTables/Buttons-2.3.6/js/buttons.bootstrap.js new file mode 100644 index 0000000..3edd363 --- /dev/null +++ b/public/assets/js/DataTables/Buttons-2.3.6/js/buttons.bootstrap.js @@ -0,0 +1,106 @@ +/*! Bootstrap integration for DataTables' Buttons + * ©2016 SpryMedia Ltd - datatables.net/license + */ + +(function( factory ){ + if ( typeof define === 'function' && define.amd ) { + // AMD + define( ['jquery', 'datatables.net-bs', 'datatables.net-buttons'], function ( $ ) { + return factory( $, window, document ); + } ); + } + else if ( typeof exports === 'object' ) { + // CommonJS + var jq = require('jquery'); + var cjsRequires = function (root, $) { + if ( ! $.fn.dataTable ) { + require('datatables.net-bs')(root, $); + } + + if ( ! $.fn.dataTable.Buttons ) { + require('datatables.net-buttons')(root, $); + } + }; + + if (typeof window !== 'undefined') { + module.exports = function (root, $) { + if ( ! root ) { + // CommonJS environments without a window global must pass a + // root. This will give an error otherwise + root = window; + } + + if ( ! $ ) { + $ = jq( root ); + } + + cjsRequires( root, $ ); + return factory( $, root, root.document ); + }; + } + else { + cjsRequires( window, jq ); + module.exports = factory( jq, window, window.document ); + } + } + else { + // Browser + factory( jQuery, window, document ); + } +}(function( $, window, document, undefined ) { +'use strict'; +var DataTable = $.fn.dataTable; + + + +$.extend( true, DataTable.Buttons.defaults, { + dom: { + container: { + className: 'dt-buttons btn-group' + }, + button: { + className: 'btn btn-default' + }, + collection: { + tag: 'ul', + className: 'dropdown-menu', + closeButton: false, + button: { + tag: 'li', + className: 'dt-button', + active: 'active', + disabled: 'disabled' + }, + buttonLiner: { + tag: 'a', + className: '' + } + }, + splitWrapper: { + tag: 'div', + className: 'dt-btn-split-wrapper btn-group', + closeButton: false, + }, + splitDropdown: { + tag: 'button', + text: '▼', + className: 'btn btn-default dt-btn-split-drop dropdown-toggle', + closeButton: false, + align: 'split-left', + splitAlignClass: 'dt-button-split-left' + }, + splitDropdownButton: { + tag: 'button', + className: 'dt-btn-split-drop-button btn btn-default', + closeButton: false + } + } +} ); + +DataTable.ext.buttons.collection.text = function ( dt ) { + return dt.i18n('buttons.collection', 'Collection '); +}; + + +return DataTable; +})); diff --git a/public/assets/js/DataTables/Buttons-2.3.6/js/buttons.bootstrap.min.js b/public/assets/js/DataTables/Buttons-2.3.6/js/buttons.bootstrap.min.js new file mode 100644 index 0000000..d581ce8 --- /dev/null +++ b/public/assets/js/DataTables/Buttons-2.3.6/js/buttons.bootstrap.min.js @@ -0,0 +1,4 @@ +/*! Bootstrap integration for DataTables' Buttons + * ©2016 SpryMedia Ltd - datatables.net/license + */ +!function(e){var o,a;"function"==typeof define&&define.amd?define(["jquery","datatables.net-bs","datatables.net-buttons"],function(t){return e(t,window,document)}):"object"==typeof exports?(o=require("jquery"),a=function(t,n){n.fn.dataTable||require("datatables.net-bs")(t,n),n.fn.dataTable.Buttons||require("datatables.net-buttons")(t,n)},"undefined"!=typeof window?module.exports=function(t,n){return t=t||window,n=n||o(t),a(t,n),e(n,0,t.document)}:(a(window,o),module.exports=e(o,window,window.document))):e(jQuery,window,document)}(function(t,n,e,o){"use strict";var a=t.fn.dataTable;return t.extend(!0,a.Buttons.defaults,{dom:{container:{className:"dt-buttons btn-group"},button:{className:"btn btn-default"},collection:{tag:"ul",className:"dropdown-menu",closeButton:!1,button:{tag:"li",className:"dt-button",active:"active",disabled:"disabled"},buttonLiner:{tag:"a",className:""}},splitWrapper:{tag:"div",className:"dt-btn-split-wrapper btn-group",closeButton:!1},splitDropdown:{tag:"button",text:"▼",className:"btn btn-default dt-btn-split-drop dropdown-toggle",closeButton:!1,align:"split-left",splitAlignClass:"dt-button-split-left"},splitDropdownButton:{tag:"button",className:"dt-btn-split-drop-button btn btn-default",closeButton:!1}}}),a.ext.buttons.collection.text=function(t){return t.i18n("buttons.collection",'Collection ')},a}); \ No newline at end of file diff --git a/public/assets/js/DataTables/Buttons-2.3.6/js/buttons.bootstrap4.js b/public/assets/js/DataTables/Buttons-2.3.6/js/buttons.bootstrap4.js new file mode 100644 index 0000000..8277853 --- /dev/null +++ b/public/assets/js/DataTables/Buttons-2.3.6/js/buttons.bootstrap4.js @@ -0,0 +1,106 @@ +/*! Bootstrap integration for DataTables' Buttons + * ©2016 SpryMedia Ltd - datatables.net/license + */ + +(function( factory ){ + if ( typeof define === 'function' && define.amd ) { + // AMD + define( ['jquery', 'datatables.net-bs4', 'datatables.net-buttons'], function ( $ ) { + return factory( $, window, document ); + } ); + } + else if ( typeof exports === 'object' ) { + // CommonJS + var jq = require('jquery'); + var cjsRequires = function (root, $) { + if ( ! $.fn.dataTable ) { + require('datatables.net-bs4')(root, $); + } + + if ( ! $.fn.dataTable.Buttons ) { + require('datatables.net-buttons')(root, $); + } + }; + + if (typeof window !== 'undefined') { + module.exports = function (root, $) { + if ( ! root ) { + // CommonJS environments without a window global must pass a + // root. This will give an error otherwise + root = window; + } + + if ( ! $ ) { + $ = jq( root ); + } + + cjsRequires( root, $ ); + return factory( $, root, root.document ); + }; + } + else { + cjsRequires( window, jq ); + module.exports = factory( jq, window, window.document ); + } + } + else { + // Browser + factory( jQuery, window, document ); + } +}(function( $, window, document, undefined ) { +'use strict'; +var DataTable = $.fn.dataTable; + + + +$.extend( true, DataTable.Buttons.defaults, { + dom: { + container: { + className: 'dt-buttons btn-group flex-wrap' + }, + button: { + className: 'btn btn-secondary' + }, + collection: { + tag: 'div', + className: 'dropdown-menu', + closeButton: false, + button: { + tag: 'a', + className: 'dt-button dropdown-item', + active: 'active', + disabled: 'disabled' + } + }, + splitWrapper: { + tag: 'div', + className: 'dt-btn-split-wrapper btn-group', + closeButton: false, + }, + splitDropdown: { + tag: 'button', + text: '', + className: 'btn btn-secondary dt-btn-split-drop dropdown-toggle dropdown-toggle-split', + closeButton: false, + align: 'split-left', + splitAlignClass: 'dt-button-split-left' + }, + splitDropdownButton: { + tag: 'button', + className: 'dt-btn-split-drop-button btn btn-secondary', + closeButton: false + } + }, + buttonCreated: function ( config, button ) { + return config.buttons ? + $('
    ').append(button) : + button; + } +} ); + +DataTable.ext.buttons.collection.className += ' dropdown-toggle'; +DataTable.ext.buttons.collection.rightAlignClassName = 'dropdown-menu-right'; + + +return DataTable; +})); diff --git a/public/assets/js/DataTables/Buttons-2.3.6/js/buttons.bootstrap4.min.js b/public/assets/js/DataTables/Buttons-2.3.6/js/buttons.bootstrap4.min.js new file mode 100644 index 0000000..4685697 --- /dev/null +++ b/public/assets/js/DataTables/Buttons-2.3.6/js/buttons.bootstrap4.min.js @@ -0,0 +1,4 @@ +/*! Bootstrap integration for DataTables' Buttons + * ©2016 SpryMedia Ltd - datatables.net/license + */ +!function(e){var o,a;"function"==typeof define&&define.amd?define(["jquery","datatables.net-bs4","datatables.net-buttons"],function(t){return e(t,window,document)}):"object"==typeof exports?(o=require("jquery"),a=function(t,n){n.fn.dataTable||require("datatables.net-bs4")(t,n),n.fn.dataTable.Buttons||require("datatables.net-buttons")(t,n)},"undefined"!=typeof window?module.exports=function(t,n){return t=t||window,n=n||o(t),a(t,n),e(n,0,t.document)}:(a(window,o),module.exports=e(o,window,window.document))):e(jQuery,window,document)}(function(e,t,n,o){"use strict";var a=e.fn.dataTable;return e.extend(!0,a.Buttons.defaults,{dom:{container:{className:"dt-buttons btn-group flex-wrap"},button:{className:"btn btn-secondary"},collection:{tag:"div",className:"dropdown-menu",closeButton:!1,button:{tag:"a",className:"dt-button dropdown-item",active:"active",disabled:"disabled"}},splitWrapper:{tag:"div",className:"dt-btn-split-wrapper btn-group",closeButton:!1},splitDropdown:{tag:"button",text:"",className:"btn btn-secondary dt-btn-split-drop dropdown-toggle dropdown-toggle-split",closeButton:!1,align:"split-left",splitAlignClass:"dt-button-split-left"},splitDropdownButton:{tag:"button",className:"dt-btn-split-drop-button btn btn-secondary",closeButton:!1}},buttonCreated:function(t,n){return t.buttons?e('
    ').append(n):n}}),a.ext.buttons.collection.className+=" dropdown-toggle",a.ext.buttons.collection.rightAlignClassName="dropdown-menu-right",a}); \ No newline at end of file diff --git a/public/assets/js/DataTables/Buttons-2.3.6/js/buttons.bootstrap5.js b/public/assets/js/DataTables/Buttons-2.3.6/js/buttons.bootstrap5.js new file mode 100644 index 0000000..6730cc4 --- /dev/null +++ b/public/assets/js/DataTables/Buttons-2.3.6/js/buttons.bootstrap5.js @@ -0,0 +1,106 @@ +/*! Bootstrap integration for DataTables' Buttons + * ©2016 SpryMedia Ltd - datatables.net/license + */ + +(function( factory ){ + if ( typeof define === 'function' && define.amd ) { + // AMD + define( ['jquery', 'datatables.net-bs5', 'datatables.net-buttons'], function ( $ ) { + return factory( $, window, document ); + } ); + } + else if ( typeof exports === 'object' ) { + // CommonJS + var jq = require('jquery'); + var cjsRequires = function (root, $) { + if ( ! $.fn.dataTable ) { + require('datatables.net-bs5')(root, $); + } + + if ( ! $.fn.dataTable.Buttons ) { + require('datatables.net-buttons')(root, $); + } + }; + + if (typeof window !== 'undefined') { + module.exports = function (root, $) { + if ( ! root ) { + // CommonJS environments without a window global must pass a + // root. This will give an error otherwise + root = window; + } + + if ( ! $ ) { + $ = jq( root ); + } + + cjsRequires( root, $ ); + return factory( $, root, root.document ); + }; + } + else { + cjsRequires( window, jq ); + module.exports = factory( jq, window, window.document ); + } + } + else { + // Browser + factory( jQuery, window, document ); + } +}(function( $, window, document, undefined ) { +'use strict'; +var DataTable = $.fn.dataTable; + + + +$.extend( true, DataTable.Buttons.defaults, { + dom: { + container: { + className: 'dt-buttons btn-group flex-wrap' + }, + button: { + className: 'btn btn-secondary' + }, + collection: { + tag: 'div', + className: 'dropdown-menu', + closeButton: false, + button: { + tag: 'a', + className: 'dt-button dropdown-item', + active: 'active', + disabled: 'disabled' + } + }, + splitWrapper: { + tag: 'div', + className: 'dt-btn-split-wrapper btn-group', + closeButton: false, + }, + splitDropdown: { + tag: 'button', + text: '', + className: 'btn btn-secondary dt-btn-split-drop dropdown-toggle dropdown-toggle-split', + closeButton: false, + align: 'split-left', + splitAlignClass: 'dt-button-split-left' + }, + splitDropdownButton: { + tag: 'button', + className: 'dt-btn-split-drop-button btn btn-secondary', + closeButton: false + } + }, + buttonCreated: function ( config, button ) { + return config.buttons ? + $('
    ').append(button) : + button; + } +} ); + +DataTable.ext.buttons.collection.className += ' dropdown-toggle'; +DataTable.ext.buttons.collection.rightAlignClassName = 'dropdown-menu-right'; + + +return DataTable; +})); diff --git a/public/assets/js/DataTables/Buttons-2.3.6/js/buttons.bootstrap5.min.js b/public/assets/js/DataTables/Buttons-2.3.6/js/buttons.bootstrap5.min.js new file mode 100644 index 0000000..a641a61 --- /dev/null +++ b/public/assets/js/DataTables/Buttons-2.3.6/js/buttons.bootstrap5.min.js @@ -0,0 +1,4 @@ +/*! Bootstrap integration for DataTables' Buttons + * ©2016 SpryMedia Ltd - datatables.net/license + */ +!function(e){var o,a;"function"==typeof define&&define.amd?define(["jquery","datatables.net-bs5","datatables.net-buttons"],function(t){return e(t,window,document)}):"object"==typeof exports?(o=require("jquery"),a=function(t,n){n.fn.dataTable||require("datatables.net-bs5")(t,n),n.fn.dataTable.Buttons||require("datatables.net-buttons")(t,n)},"undefined"!=typeof window?module.exports=function(t,n){return t=t||window,n=n||o(t),a(t,n),e(n,0,t.document)}:(a(window,o),module.exports=e(o,window,window.document))):e(jQuery,window,document)}(function(e,t,n,o){"use strict";var a=e.fn.dataTable;return e.extend(!0,a.Buttons.defaults,{dom:{container:{className:"dt-buttons btn-group flex-wrap"},button:{className:"btn btn-secondary"},collection:{tag:"div",className:"dropdown-menu",closeButton:!1,button:{tag:"a",className:"dt-button dropdown-item",active:"active",disabled:"disabled"}},splitWrapper:{tag:"div",className:"dt-btn-split-wrapper btn-group",closeButton:!1},splitDropdown:{tag:"button",text:"",className:"btn btn-secondary dt-btn-split-drop dropdown-toggle dropdown-toggle-split",closeButton:!1,align:"split-left",splitAlignClass:"dt-button-split-left"},splitDropdownButton:{tag:"button",className:"dt-btn-split-drop-button btn btn-secondary",closeButton:!1}},buttonCreated:function(t,n){return t.buttons?e('
    ').append(n):n}}),a.ext.buttons.collection.className+=" dropdown-toggle",a.ext.buttons.collection.rightAlignClassName="dropdown-menu-right",a}); \ No newline at end of file diff --git a/public/assets/js/DataTables/Buttons-2.3.6/js/buttons.bulma.js b/public/assets/js/DataTables/Buttons-2.3.6/js/buttons.bulma.js new file mode 100644 index 0000000..2197c58 --- /dev/null +++ b/public/assets/js/DataTables/Buttons-2.3.6/js/buttons.bulma.js @@ -0,0 +1,117 @@ +/*! Bulma integration for DataTables' Buttons + * ©2021 SpryMedia Ltd - datatables.net/license + */ + +(function( factory ){ + if ( typeof define === 'function' && define.amd ) { + // AMD + define( ['jquery', 'datatables.net-bm', 'datatables.net-buttons'], function ( $ ) { + return factory( $, window, document ); + } ); + } + else if ( typeof exports === 'object' ) { + // CommonJS + var jq = require('jquery'); + var cjsRequires = function (root, $) { + if ( ! $.fn.dataTable ) { + require('datatables.net-bm')(root, $); + } + + if ( ! $.fn.dataTable.Buttons ) { + require('datatables.net-buttons')(root, $); + } + }; + + if (typeof window !== 'undefined') { + module.exports = function (root, $) { + if ( ! root ) { + // CommonJS environments without a window global must pass a + // root. This will give an error otherwise + root = window; + } + + if ( ! $ ) { + $ = jq( root ); + } + + cjsRequires( root, $ ); + return factory( $, root, root.document ); + }; + } + else { + cjsRequires( window, jq ); + module.exports = factory( jq, window, window.document ); + } + } + else { + // Browser + factory( jQuery, window, document ); + } +}(function( $, window, document, undefined ) { +'use strict'; +var DataTable = $.fn.dataTable; + + + +$.extend( true, DataTable.Buttons.defaults, { + dom: { + container: { + className: 'dt-buttons field is-grouped' + }, + button: { + className: 'button is-light', + active: 'is-active', + disabled: 'is-disabled' + }, + collection: { + tag: 'div', + closeButton: false, + className: 'dropdown-content', + button: { + tag: 'a', + className: 'dt-button dropdown-item', + active: 'is-active', + disabled: 'is-disabled' + } + }, + splitWrapper: { + tag: 'div', + className: 'dt-btn-split-wrapper dropdown-trigger buttons has-addons', + closeButton: false + }, + splitDropdownButton: { + tag: 'button', + className: 'dt-btn-split-drop-button button is-light', + closeButton: false + }, + splitDropdown: { + tag: 'button', + text: '▼', + className: 'button is-light', + closeButton: false, + align: 'split-left', + splitAlignClass: 'dt-button-split-left' + } + }, + buttonCreated: function ( config, button ) { + // For collections + if (config.buttons) { + // Wrap the dropdown content in a menu element + config._collection = $('