├── po ├── potfiles.in ├── Makefile └── preprocess.php ├── www ├── images │ ├── admin-remove.png │ ├── admin-remove@2x.png │ ├── swat-tool-link-icons.png │ ├── admin-dialog-exception.png │ ├── admin-dialog-question.png │ ├── admin-menu-help-arrow.png │ ├── admin-dialog-exception@2x.png │ ├── admin-dialog-question@2x.png │ ├── admin-swat-actions-arrow.png │ ├── swat-tool-link-icons@2x.png │ ├── admin-swat-actions-arrow@2x.png │ ├── admin-checkbox-radio-buttons.png │ ├── admin-message-display-dismiss.png │ ├── admin-checkbox-radio-buttons@2x.png │ ├── admin-message-display-dismiss@2x.png │ ├── admin-swat-textarea-editor-icons.png │ ├── admin-swat-textarea-editor-icons@2x.png │ ├── admin-title-link-cell-renderer-icons.png │ ├── admin-title-link-cell-renderer-icons@2x.png │ ├── admin-swat-textarea-editor-icons-highlight.png │ └── admin-swat-textarea-editor-icons-highlight@2x.png ├── styles │ ├── admin-user-index-page.css │ ├── admin-change-password-page.css │ ├── admin-profile.css │ ├── admin-two-factor-authentication-page.css │ ├── admin-approval-page.css │ ├── admin-note.css │ ├── admin-login-page.css │ ├── admin-group-link-cell-renderer.css │ ├── admin-menu.css │ └── admin-layout.css └── javascript │ ├── admin-order.js │ └── admin-login.js ├── resources ├── mockups │ ├── palette.png │ ├── photo10572.jpg │ ├── photo22419.jpg │ ├── beach-palette.png │ ├── Swat-Admin-Colors.png │ └── Swat-Admin-Example.png ├── admin-checkbox-radio-buttons.xcf └── admin-title-link-cell-renderer-icons.xcf ├── locale ├── en_CA │ └── LC_MESSAGES │ │ └── admin.mo ├── en_GB │ └── LC_MESSAGES │ │ └── admin.mo └── en_US │ └── LC_MESSAGES │ └── admin.mo ├── sql ├── tables │ ├── AdminGroup.sql │ ├── AdminSection.sql │ ├── AdminUserInstanceBinding.sql │ ├── AdminUserHistory.sql │ ├── AdminSubComponent.sql │ ├── AdminUserAdminGroupBinding.sql │ ├── AdminComponentAdminGroupBinding.sql │ ├── AdminComponent.sql │ └── AdminUser.sql ├── views │ └── AdminUserLastLoginView.sql ├── triggers │ └── AdminUserHistoryInsertTrigger.sql ├── functions │ ├── getAdminMenu.mysql.sql │ └── getAdminMenu.pgsql.sql └── admin-changes.sql ├── phpstan.dist.neon ├── Admin ├── layouts │ ├── AdminLoginLayout.php │ ├── AdminLayout.php │ └── AdminMenuXMLRPCServerLayout.php ├── pages │ ├── AdminXMLRPCServer.php │ ├── order.xml │ ├── confirmation.xml │ ├── approval.xml │ ├── AdminDBOrder.php │ ├── AdminDBEdit.php │ ├── AdminDBDelete.php │ └── AdminConfirmation.php ├── AdminImportantNavBarEntry.php ├── templates │ ├── AdminCustomTemplate.php │ ├── AdminMSWordTemplate.php │ ├── AdminLoginTemplate.php │ └── AdminDefaultTemplate.php ├── exceptions │ ├── AdminUserException.php │ ├── AdminException.php │ ├── AdminNotFoundException.php │ └── AdminNoAccessException.php ├── components │ ├── AdminSite │ │ ├── front.xml │ │ ├── Front.php │ │ ├── Logout.php │ │ ├── forgot-password.xml │ │ ├── two_factor_authentication.xml │ │ ├── reset-password.xml │ │ ├── Exception.php │ │ ├── login.xml │ │ ├── change-password.xml │ │ ├── TwoFactorAuthentication.php │ │ ├── ChangePassword.php │ │ └── profile.xml │ ├── AdminUser │ │ ├── include │ │ │ ├── AdminUserTableView.php │ │ │ └── HistoryCellRenderer.php │ │ ├── details.xml │ │ ├── login-history.xml │ │ ├── Delete.php │ │ ├── LoginHistory.php │ │ ├── Details.php │ │ ├── Reactivate.php │ │ ├── edit.xml │ │ └── index.xml │ ├── AdminSection │ │ ├── Edit.php │ │ ├── edit.xml │ │ ├── Order.php │ │ ├── index.xml │ │ ├── Index.php │ │ └── Delete.php │ ├── AdminGroup │ │ ├── Index.php │ │ ├── edit.xml │ │ ├── index.xml │ │ ├── Delete.php │ │ └── Edit.php │ ├── AdminSubComponent │ │ ├── edit.xml │ │ ├── Order.php │ │ ├── Delete.php │ │ └── Edit.php │ └── AdminComponent │ │ ├── Order.php │ │ ├── edit.xml │ │ ├── Delete.php │ │ ├── Edit.php │ │ └── index.xml ├── AdminDependencyEntryWrapper.php ├── AdminDependencySummaryWrapper.php ├── AdminTableViewOrderableColumn.php ├── dataobjects │ ├── AdminGroupWrapper.php │ ├── AdminSectionWrapper.php │ ├── AdminComponentWrapper.php │ ├── AdminUserWrapper.php │ ├── AdminUserHistoryWrapper.php │ ├── AdminSubComponentWrapper.php │ ├── AdminUserHistory.php │ ├── AdminGroup.php │ └── AdminSection.php ├── AdminMenuSubcomponent.php ├── AdminMenuSection.php ├── AdminMenuComponent.php ├── AdminDependencyItem.php ├── AdminDependencySummary.php ├── AdminGroupLinkCellRenderer.php ├── AdminTreeTitleLinkCellRenderer.php ├── AdminUniqueEntry.php ├── AdminTwoFactorAuthentication.php ├── AdminNavBar.php ├── AdminDependencyEntry.php ├── AdminTreeControlCellRenderer.php ├── AdminPagination.php ├── AdminDateLinkCellRenderer.php ├── AdminSearchOperatorFlydown.php ├── AdminNote.php ├── AdminUI.php ├── Admin.php ├── AdminMenuStore.php └── AdminControlCellRenderer.php ├── phpcs.xml ├── .gitignore ├── .editorconfig ├── phpstan-baseline.neon ├── Jenkinsfile ├── .github ├── pull_request_template.md └── workflows │ └── pull-requests.yml ├── README.md ├── dependencies └── admin.yaml ├── .php-cs-fixer.php └── composer.json /po/potfiles.in: -------------------------------------------------------------------------------- 1 | ../Admin 2 | -------------------------------------------------------------------------------- /www/images/admin-remove.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cviebrock/admin/master/www/images/admin-remove.png -------------------------------------------------------------------------------- /resources/mockups/palette.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cviebrock/admin/master/resources/mockups/palette.png -------------------------------------------------------------------------------- /www/images/admin-remove@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cviebrock/admin/master/www/images/admin-remove@2x.png -------------------------------------------------------------------------------- /resources/mockups/photo10572.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cviebrock/admin/master/resources/mockups/photo10572.jpg -------------------------------------------------------------------------------- /resources/mockups/photo22419.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cviebrock/admin/master/resources/mockups/photo22419.jpg -------------------------------------------------------------------------------- /locale/en_CA/LC_MESSAGES/admin.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cviebrock/admin/master/locale/en_CA/LC_MESSAGES/admin.mo -------------------------------------------------------------------------------- /locale/en_GB/LC_MESSAGES/admin.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cviebrock/admin/master/locale/en_GB/LC_MESSAGES/admin.mo -------------------------------------------------------------------------------- /locale/en_US/LC_MESSAGES/admin.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cviebrock/admin/master/locale/en_US/LC_MESSAGES/admin.mo -------------------------------------------------------------------------------- /resources/mockups/beach-palette.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cviebrock/admin/master/resources/mockups/beach-palette.png -------------------------------------------------------------------------------- /www/images/swat-tool-link-icons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cviebrock/admin/master/www/images/swat-tool-link-icons.png -------------------------------------------------------------------------------- /www/images/admin-dialog-exception.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cviebrock/admin/master/www/images/admin-dialog-exception.png -------------------------------------------------------------------------------- /www/images/admin-dialog-question.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cviebrock/admin/master/www/images/admin-dialog-question.png -------------------------------------------------------------------------------- /www/images/admin-menu-help-arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cviebrock/admin/master/www/images/admin-menu-help-arrow.png -------------------------------------------------------------------------------- /resources/mockups/Swat-Admin-Colors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cviebrock/admin/master/resources/mockups/Swat-Admin-Colors.png -------------------------------------------------------------------------------- /resources/mockups/Swat-Admin-Example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cviebrock/admin/master/resources/mockups/Swat-Admin-Example.png -------------------------------------------------------------------------------- /www/images/admin-dialog-exception@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cviebrock/admin/master/www/images/admin-dialog-exception@2x.png -------------------------------------------------------------------------------- /www/images/admin-dialog-question@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cviebrock/admin/master/www/images/admin-dialog-question@2x.png -------------------------------------------------------------------------------- /www/images/admin-swat-actions-arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cviebrock/admin/master/www/images/admin-swat-actions-arrow.png -------------------------------------------------------------------------------- /www/images/swat-tool-link-icons@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cviebrock/admin/master/www/images/swat-tool-link-icons@2x.png -------------------------------------------------------------------------------- /resources/admin-checkbox-radio-buttons.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cviebrock/admin/master/resources/admin-checkbox-radio-buttons.xcf -------------------------------------------------------------------------------- /sql/tables/AdminGroup.sql: -------------------------------------------------------------------------------- 1 | create table AdminGroup ( 2 | id serial not null, 3 | title varchar(255), 4 | primary key(id) 5 | ); 6 | 7 | -------------------------------------------------------------------------------- /www/images/admin-swat-actions-arrow@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cviebrock/admin/master/www/images/admin-swat-actions-arrow@2x.png -------------------------------------------------------------------------------- /www/images/admin-checkbox-radio-buttons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cviebrock/admin/master/www/images/admin-checkbox-radio-buttons.png -------------------------------------------------------------------------------- /www/images/admin-message-display-dismiss.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cviebrock/admin/master/www/images/admin-message-display-dismiss.png -------------------------------------------------------------------------------- /www/images/admin-checkbox-radio-buttons@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cviebrock/admin/master/www/images/admin-checkbox-radio-buttons@2x.png -------------------------------------------------------------------------------- /www/images/admin-message-display-dismiss@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cviebrock/admin/master/www/images/admin-message-display-dismiss@2x.png -------------------------------------------------------------------------------- /www/images/admin-swat-textarea-editor-icons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cviebrock/admin/master/www/images/admin-swat-textarea-editor-icons.png -------------------------------------------------------------------------------- /resources/admin-title-link-cell-renderer-icons.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cviebrock/admin/master/resources/admin-title-link-cell-renderer-icons.xcf -------------------------------------------------------------------------------- /www/images/admin-swat-textarea-editor-icons@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cviebrock/admin/master/www/images/admin-swat-textarea-editor-icons@2x.png -------------------------------------------------------------------------------- /www/images/admin-title-link-cell-renderer-icons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cviebrock/admin/master/www/images/admin-title-link-cell-renderer-icons.png -------------------------------------------------------------------------------- /www/images/admin-title-link-cell-renderer-icons@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cviebrock/admin/master/www/images/admin-title-link-cell-renderer-icons@2x.png -------------------------------------------------------------------------------- /www/images/admin-swat-textarea-editor-icons-highlight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cviebrock/admin/master/www/images/admin-swat-textarea-editor-icons-highlight.png -------------------------------------------------------------------------------- /www/images/admin-swat-textarea-editor-icons-highlight@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cviebrock/admin/master/www/images/admin-swat-textarea-editor-icons-highlight@2x.png -------------------------------------------------------------------------------- /www/styles/admin-user-index-page.css: -------------------------------------------------------------------------------- 1 | tr.inactive td.email, 2 | tr.inactive td.name, 3 | tr.inactive td.enabled, 4 | tr.inactive td.last-login { 5 | opacity: 0.5; 6 | } 7 | -------------------------------------------------------------------------------- /www/styles/admin-change-password-page.css: -------------------------------------------------------------------------------- 1 | /* Change Password Page Styles */ 2 | 3 | #change_password_frame { 4 | width: 28em; 5 | margin: 4em auto 1em auto; 6 | text-align: left; 7 | } 8 | -------------------------------------------------------------------------------- /sql/views/AdminUserLastLoginView.sql: -------------------------------------------------------------------------------- 1 | create or replace view AdminUserLastLoginView as 2 | select usernum, max(login_date) as last_login, instance from AdminUserHistory 3 | group by usernum, instance; 4 | -------------------------------------------------------------------------------- /www/styles/admin-profile.css: -------------------------------------------------------------------------------- 1 | /* Change Password Page Styles */ 2 | 3 | .admin-two-factor-secret { 4 | font-size: 10px; 5 | color: #333; 6 | } 7 | 8 | #two_fa_token_field input { 9 | width: 8rem; 10 | } 11 | -------------------------------------------------------------------------------- /sql/tables/AdminSection.sql: -------------------------------------------------------------------------------- 1 | create table AdminSection ( 2 | id serial NOT null, 3 | title varchar(255), 4 | description varchar(255), 5 | displayorder integer default 0, 6 | visible boolean default true not null, 7 | primary key(id) 8 | ); 9 | -------------------------------------------------------------------------------- /phpstan.dist.neon: -------------------------------------------------------------------------------- 1 | includes: 2 | - phpstan-baseline.neon 3 | 4 | parameters: 5 | phpVersion: 80200 6 | level: 1 7 | paths: 8 | - Admin 9 | - po 10 | editorUrl: '%%file%%:%%line%%' 11 | editorUrlTitle: '%%file%%:%%line%%' 12 | -------------------------------------------------------------------------------- /sql/tables/AdminUserInstanceBinding.sql: -------------------------------------------------------------------------------- 1 | create table AdminUserInstanceBinding ( 2 | usernum int not null references AdminUser(id) on delete cascade, 3 | instance int not null references Instance(id) on delete cascade, 4 | primary key (usernum, instance) 5 | ); 6 | -------------------------------------------------------------------------------- /www/styles/admin-two-factor-authentication-page.css: -------------------------------------------------------------------------------- 1 | /* Change Password Page Styles */ 2 | 3 | #two_fa_frame { 4 | width: 28em; 5 | margin: 4em auto 1em auto; 6 | text-align: left; 7 | } 8 | 9 | #two_fa_token_field input { 10 | width: 8rem; 11 | } 12 | -------------------------------------------------------------------------------- /Admin/layouts/AdminLoginLayout.php: -------------------------------------------------------------------------------- 1 | content; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /sql/tables/AdminUserHistory.sql: -------------------------------------------------------------------------------- 1 | create table AdminUserHistory ( 2 | id serial, 3 | usernum integer not null 4 | constraint AdminUserHistory_usernum references AdminUser(id) 5 | on delete cascade, 6 | login_date timestamp, 7 | login_agent varchar(255), 8 | remote_ip varchar(15), 9 | instance integer references Instance(id) on delete cascade, 10 | primary key(id) 11 | ); 12 | 13 | -------------------------------------------------------------------------------- /sql/tables/AdminSubComponent.sql: -------------------------------------------------------------------------------- 1 | create table AdminSubComponent ( 2 | id serial not null, 3 | component integer not null 4 | constraint AdminSubcomponent_component references AdminComponent(id) 5 | on delete cascade, 6 | title varchar(255), 7 | shortname varchar(50), 8 | visible boolean default false not null, 9 | displayorder integer default 0, 10 | primary key(id) 11 | ); 12 | 13 | -------------------------------------------------------------------------------- /Admin/exceptions/AdminUserException.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | ./Admin 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /Admin/components/AdminSite/front.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | To get started, choose an item from the menu to the left. 7 | 8 | 9 | -------------------------------------------------------------------------------- /sql/tables/AdminUserAdminGroupBinding.sql: -------------------------------------------------------------------------------- 1 | create table AdminUserAdminGroupBinding ( 2 | usernum integer not null 3 | constraint AdminUserAdminGroupBinding_usernum references AdminUser(id) 4 | on delete cascade, 5 | groupnum integer not null 6 | constraint AdminUserAdminGroupBinding_groupnum references AdminGroup(id) 7 | on delete cascade, 8 | primary key(usernum, groupnum) 9 | ); 10 | 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | vendor/ 5 | node_modules/ 6 | 7 | # misc 8 | .DS_Store 9 | .env 10 | .idea/ 11 | .php-cs-fixer.cache 12 | npm-debug.log* 13 | yarn-debug.log* 14 | yarn-error.log* 15 | *.swp 16 | 17 | # dependency lock file 18 | composer.lock 19 | yarn.lock 20 | 21 | # overrides for local tooling 22 | /phpstan.neon 23 | -------------------------------------------------------------------------------- /Admin/AdminDependencyEntryWrapper.php: -------------------------------------------------------------------------------- 1 | row_wrapper_class = AdminDependencyEntry::class; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Admin/AdminDependencySummaryWrapper.php: -------------------------------------------------------------------------------- 1 | row_wrapper_class = AdminDependencySummary::class; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /sql/tables/AdminComponentAdminGroupBinding.sql: -------------------------------------------------------------------------------- 1 | create table AdminComponentAdminGroupBinding ( 2 | component integer not null 3 | constraint AdminComponentAdminGroupBinding_component references AdminComponent(id) 4 | on delete cascade, 5 | groupnum integer not null 6 | constraint AdminComponentAdminGroupBinding_groupnum references AdminGroup(id) 7 | on delete cascade, 8 | primary key(component, groupnum) 9 | ); 10 | 11 | -------------------------------------------------------------------------------- /sql/tables/AdminComponent.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE AdminComponent ( 2 | id serial NOT NULL, 3 | shortname character varying(255), 4 | title character varying(255), 5 | description text, 6 | displayorder integer DEFAULT 0, 7 | section integer NOT NULL 8 | constraint AdminComponent_section references AdminSection(id) 9 | on delete cascade, 10 | enabled boolean DEFAULT true NOT NULL, 11 | visible boolean DEFAULT true NOT NULL, 12 | primary key(id) 13 | ); 14 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | indent_style = space 13 | indent_size = 2 14 | 15 | [*.php] 16 | indent_size = 4 17 | 18 | [*.{diff,md}] 19 | trim_trailing_whitespace = false 20 | 21 | [Jenkinsfile] 22 | indent_size = 4 23 | -------------------------------------------------------------------------------- /Admin/AdminTableViewOrderableColumn.php: -------------------------------------------------------------------------------- 1 | link = $_GET['source'] ?? ''; 14 | $this->unset_get_vars = ['source']; 15 | parent::displayHeader(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /www/styles/admin-note.css: -------------------------------------------------------------------------------- 1 | .admin-note { 2 | margin: 16px 0; 3 | background: #fff9c4; 4 | color: #888; 5 | color: rgba(0,0,0,0.54); 6 | padding: 12px; 7 | border-radius: 2px; 8 | box-shadow: 0 0 2px rgba(0,0,0,0.12), 0 2px 2px rgba(0,0,0,0.24); 9 | } 10 | 11 | .swat-frame .admin-note { 12 | margin: 0 0 16px; 13 | } 14 | 15 | .admin-note-title { 16 | margin: 0; 17 | color: #888; 18 | color: rgba(0,0,0,0.54); 19 | } 20 | 21 | .admin-note dd { 22 | margin-bottom: 0.5em; 23 | margin-left: 1em; 24 | } 25 | -------------------------------------------------------------------------------- /Admin/dataobjects/AdminGroupWrapper.php: -------------------------------------------------------------------------------- 1 | row_wrapper_class = AdminGroup::class; 17 | $this->index_field = 'id'; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /sql/triggers/AdminUserHistoryInsertTrigger.sql: -------------------------------------------------------------------------------- 1 | CREATE OR REPLACE FUNCTION updateAdminUserHistory() RETURNS trigger AS $$ 2 | BEGIN 3 | delete from AdminUserHistory where usernum = NEW.usernum 4 | AND id not in (select id from AdminUserHistory 5 | where usernum = NEW.usernum order by login_date desc limit 9); 6 | 7 | RETURN NULL; 8 | END; 9 | $$ LANGUAGE 'plpgsql'; 10 | 11 | CREATE TRIGGER AdminUserHistoryInsertTrigger AFTER INSERT ON AdminUserHistory 12 | FOR EACH ROW EXECUTE PROCEDURE updateAdminUserHistory(); 13 | -------------------------------------------------------------------------------- /Admin/dataobjects/AdminSectionWrapper.php: -------------------------------------------------------------------------------- 1 | row_wrapper_class = AdminSection::class; 17 | $this->index_field = 'id'; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Admin/AdminMenuSubcomponent.php: -------------------------------------------------------------------------------- 1 | shortname = $shortname; 19 | $this->title = $title; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Admin/dataobjects/AdminComponentWrapper.php: -------------------------------------------------------------------------------- 1 | row_wrapper_class = AdminComponent::class; 17 | $this->index_field = 'id'; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Admin/dataobjects/AdminUserWrapper.php: -------------------------------------------------------------------------------- 1 | row_wrapper_class = SwatDBClassMap::get(AdminUser::class); 18 | $this->index_field = 'id'; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Admin/dataobjects/AdminUserHistoryWrapper.php: -------------------------------------------------------------------------------- 1 | row_wrapper_class = AdminUserHistory::class; 17 | $this->index_field = 'id'; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Admin/dataobjects/AdminSubComponentWrapper.php: -------------------------------------------------------------------------------- 1 | row_wrapper_class = AdminSubComponent::class; 17 | $this->index_field = 'id'; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Admin/components/AdminUser/include/AdminUserTableView.php: -------------------------------------------------------------------------------- 1 | is_active) { 14 | $classes[] = 'active'; 15 | } else { 16 | $classes[] = 'inactive'; 17 | } 18 | 19 | return $classes; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /sql/tables/AdminUser.sql: -------------------------------------------------------------------------------- 1 | create table AdminUser ( 2 | id serial not null, 3 | email varchar(50) not null, 4 | name varchar(100) not null, 5 | password varchar(255) not null, 6 | password_salt varchar(50), 7 | password_tag varchar(50), 8 | password_tag_date timestamp, 9 | force_change_password boolean not null default true, 10 | enabled boolean not null default true, 11 | all_instances boolean not null default false, 12 | activation_date timestamp, 13 | two_fa_secret varchar(255), 14 | two_fa_enabled boolean not null default false, 15 | two_fa_timeslice integer not null default 0, 16 | primary key(id) 17 | ); 18 | -------------------------------------------------------------------------------- /Admin/AdminMenuSection.php: -------------------------------------------------------------------------------- 1 | id = $id; 21 | $this->title = $title; 22 | $this->components = []; 23 | $this->show = true; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Admin/exceptions/AdminException.php: -------------------------------------------------------------------------------- 1 | getMessage(); 16 | $message .= "\n" . $error->getUserInfo(); 17 | $code = $error->getCode(); 18 | } 19 | 20 | parent::__construct($message, $code); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Admin/exceptions/AdminNotFoundException.php: -------------------------------------------------------------------------------- 1 | title = Admin::_('Not Found'); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Admin/components/AdminSite/Front.php: -------------------------------------------------------------------------------- 1 | ui->loadFromXML(__DIR__ . '/front.xml'); 15 | $this->navbar->popEntry(); 16 | } 17 | 18 | // build phase 19 | 20 | protected function buildInternal() 21 | { 22 | $note = $this->ui->getWidget('note'); 23 | $note->title = sprintf( 24 | Admin::_('Welcome to the %s Admin!'), 25 | $this->app->config->site->title 26 | ); 27 | $this->buildMessages(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Admin/AdminMenuComponent.php: -------------------------------------------------------------------------------- 1 | id = $id; 22 | $this->shortname = $shortname; 23 | $this->title = $title; 24 | $this->description = $description; 25 | $this->subcomponents = []; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Admin/pages/order.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Order Options 8 | 9 | 10 | 11 | Custom Order 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /phpstan-baseline.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | ignoreErrors: 3 | - 4 | message: "#^Access to an undefined property AdminSessionModule\\:\\:\\$history\\.$#" 5 | count: 1 6 | path: Admin/AdminSessionModule.php 7 | 8 | - 9 | message: "#^Access to an undefined property AdminSessionModule\\:\\:\\$user\\.$#" 10 | count: 10 11 | path: Admin/AdminSessionModule.php 12 | 13 | - 14 | message: "#^Variable \\$transaction might not be defined\\.$#" 15 | count: 1 16 | path: Admin/pages/AdminDBConfirmation.php 17 | 18 | - 19 | message: "#^Variable \\$transaction might not be defined\\.$#" 20 | count: 1 21 | path: Admin/pages/AdminDBEdit.php 22 | 23 | - 24 | message: "#^Variable \\$transaction might not be defined\\.$#" 25 | count: 1 26 | path: Admin/pages/AdminDBOrder.php 27 | -------------------------------------------------------------------------------- /www/javascript/admin-order.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Adds an order change event handler to a SwatChangeOrder that checks a radio 3 | * button when the order changes 4 | * 5 | * @param String radio_button_id the XHTML id of the radio button to check. 6 | * @param SwatChangeOrder change_order the change order widget to add event 7 | * handlers to. 8 | */ 9 | function AdminOrder(radio_button_id, change_order) 10 | { 11 | this.radio_button = document.getElementById(radio_button_id); 12 | if (change_order instanceof SwatChangeOrder) 13 | change_order.orderChangeEvent.subscribe( 14 | this.orderChangeHandler, this, true); 15 | } 16 | 17 | AdminOrder.prototype.orderChangeHandler = function() 18 | { 19 | if (this.radio_button) 20 | this.radio_button.checked = true; 21 | }; 22 | -------------------------------------------------------------------------------- /Admin/components/AdminUser/include/HistoryCellRenderer.php: -------------------------------------------------------------------------------- 1 | date !== null) { 18 | echo ' ('; 19 | $anchor = new SwatHtmlTag('a'); 20 | $anchor->setContent($this->title); 21 | $anchor->href = sprintf('AdminUser/Details&id=%s', $this->user); 22 | $anchor->display(); 23 | echo ')'; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Admin/AdminDependencyItem.php: -------------------------------------------------------------------------------- 1 | 0 && password.value.length > 0) {" + 22 | " submit.focus();" + 23 | " } else if (email.value.length > 0 && password.value.length == 0) {" + 24 | " password.focus();" + 25 | " } else {" + 26 | " email.focus();" + 27 | " }" + 28 | "}", 100); 29 | } 30 | -------------------------------------------------------------------------------- /Admin/components/AdminSection/Edit.php: -------------------------------------------------------------------------------- 1 | getObject()->title 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Admin/pages/confirmation.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | false 12 | 13 | 14 | 15 | apply 16 | 17 | 18 | cancel 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /Admin/templates/AdminMSWordTemplate.php: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | 17 | HTML; 18 | 19 | header('Content-Type: application/msword'); 20 | header( 21 | 'Content-Disposition: attachment; filename=' . $data->filename . '.doc' 22 | ); 23 | echo $data->content; 24 | echo <<<'HTML' 25 | 26 | 27 | 28 | 29 | HTML; 30 | // @codingStandardsIgnoreEnd 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | pipeline { 2 | agent any 3 | stages { 4 | stage('Install Composer Dependencies') { 5 | steps { 6 | sh 'rm -rf composer.lock vendor/' 7 | sh 'composer install' 8 | } 9 | } 10 | 11 | stage('Check Code Style for Modified Files') { 12 | when { 13 | not { 14 | branch 'master' 15 | } 16 | } 17 | steps { 18 | sh ''' 19 | files=$(git diff-tree --diff-filter=ACRM --no-commit-id --name-only -r HEAD) 20 | if [ -n "$files" ]; then 21 | composer run phpcs:ci $files 22 | fi 23 | ''' 24 | } 25 | } 26 | 27 | stage('Check Code Style for Entire Project') { 28 | when { 29 | branch 'master' 30 | } 31 | steps { 32 | sh 'composer run phpcs:ci' 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # Description 2 | 3 | Add a description of new changes, the reason for new changes, and how the new 4 | changes work here. 5 | 6 | # Testing Instructions (optional) 7 | 8 | Add step-by-step instructions for testing the PR, if necessary. 9 | 10 | 1. Check out this PR 11 | 2. … 12 | 13 | # Developer Checklist 14 | 15 | Before requesting review for this PR, make sure the following tasks are 16 | complete: 17 | 18 | - [ ] I added a link to the relevant Shortcut story, if applicable 19 | - [ ] I added testing instructions, if any 20 | - [ ] I made sure existing CI checks pass 21 | - [ ] I checked that all requirements of the ticket are fulfilled 22 | 23 | # Reviewer Checklist 24 | 25 | Before merging this PR, make sure the following tasks are complete: 26 | 27 | - [ ] I made sure there are no active labels that block merge 28 | - [ ] I followed the testing instructions 29 | - [ ] I made sure the CI checks pass 30 | - [ ] I reviewed the file changes on GitHub 31 | - [ ] I checked that all requirements of the ticket (if any) are fulfilled 32 | -------------------------------------------------------------------------------- /Admin/AdminDependencySummary.php: -------------------------------------------------------------------------------- 1 | count = $data->count; 31 | $this->parent = $data->parent; 32 | $this->status_level = $data->status_level; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Admin/components/AdminGroup/Index.php: -------------------------------------------------------------------------------- 1 | ui->loadFromXML(__DIR__ . '/index.xml'); 16 | } 17 | 18 | // process phase 19 | 20 | protected function processActions(SwatView $view, SwatActions $actions) 21 | { 22 | switch ($actions->selected->id) { 23 | case 'delete': 24 | $this->app->replacePage('AdminGroup/Delete'); 25 | $this->app->getPage()->setItems($view->getSelection()); 26 | break; 27 | } 28 | } 29 | 30 | // build phase 31 | 32 | protected function getTableModel(SwatView $view): AdminGroupWrapper 33 | { 34 | $sql = 'select id, title from AdminGroup order by title'; 35 | 36 | return SwatDB::query($this->app->db, $sql, AdminGroupWrapper::class); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Admin/layouts/AdminLayout.php: -------------------------------------------------------------------------------- 1 | addHtmlHeadEntrySet($yui->getHtmlHeadEntrySet()); 19 | 20 | $this->addHtmlHeadEntry('packages/admin/styles/admin-layout.css'); 21 | $this->addHtmlHeadEntry('packages/admin/styles/admin-swat-local.css'); 22 | } 23 | 24 | protected function getTagByFlagFile() 25 | { 26 | $tag = null; 27 | 28 | $www_root = dirname($_SERVER['SCRIPT_FILENAME']); 29 | $filename = $www_root . DIRECTORY_SEPARATOR . 30 | '..' . DIRECTORY_SEPARATOR . '.resource-tag'; 31 | 32 | if (file_exists($filename) && is_readable($filename)) { 33 | $tag = trim(file_get_contents($filename)); 34 | } 35 | 36 | return $tag; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /.github/workflows/pull-requests.yml: -------------------------------------------------------------------------------- 1 | name: Pull Requests 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | runner: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Check out repository code 14 | uses: actions/checkout@v4 15 | 16 | - name: Setup PHP with tools 17 | uses: silverorange/actions-setup-php@v2 18 | with: 19 | php-version: '8.2' 20 | extensions: gd # required by silverorange/Site 21 | 22 | - name: Get composer cache directory 23 | id: composer-cache 24 | run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT 25 | 26 | - name: Cache dependencies 27 | uses: actions/cache@v4 28 | with: 29 | path: ${{ steps.composer-cache.outputs.dir }} 30 | key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} 31 | restore-keys: ${{ runner.os }}-composer- 32 | 33 | - name: Install PHP dependencies 34 | run: 'composer install' 35 | 36 | - name: Run tests 37 | timeout-minutes: 5 38 | run: | 39 | composer run phpcs:ci 40 | composer run phpstan:ci 41 | -------------------------------------------------------------------------------- /Admin/AdminGroupLinkCellRenderer.php: -------------------------------------------------------------------------------- 1 | addStyleSheet( 16 | 'packages/admin/styles/admin-group-link-cell-renderer.css' 17 | ); 18 | } 19 | 20 | /** 21 | * Gets the array of CSS classes that are applied to this user-interface 22 | * object. 23 | * 24 | * User-interface objects aggregate the list of user-specified classes and 25 | * may add static CSS classes of their own in this method. 26 | * 27 | * @return array the array of CSS classes that are applied to this 28 | * user-interface object 29 | * 30 | * @see SwatUIObject::getCSSClassString() 31 | */ 32 | protected function getCSSClassNames() 33 | { 34 | $classes = ['admin-group-link-cell-renderer']; 35 | 36 | return array_merge($classes, $this->classes); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Admin/pages/approval.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Approve 9 | 10 | 11 | Delete 12 | Are you sure you want to delete? 13 | 14 | 15 | Skip 16 | 17 | 18 | text/xml 19 | 20 | 21 | 22 | 23 | text/xml 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /www/styles/admin-login-page.css: -------------------------------------------------------------------------------- 1 | /* Login Page Styles */ 2 | 3 | #login_frame, 4 | #forgot_password_frame { 5 | width: 360px; 6 | margin: 4em auto 1em auto; 7 | text-align: left; 8 | } 9 | 10 | #reset_password_frame { 11 | width: 40em; 12 | margin: 4em auto 1em auto; 13 | text-align: left; 14 | } 15 | 16 | #email.swat-entry, 17 | #password.swat-entry, 18 | #confirm_password.swat-entry { 19 | width: 314px; 20 | } 21 | 22 | #forgot_container.swat-displayable-container { 23 | text-align: center; 24 | } 25 | 26 | #forgot.swat-tool-link, 27 | #forgot.swat-tool-link:link, 28 | #forgot.swat-tool-link:visited, 29 | #forgot.swat-tool-link:active, 30 | #forgot.swat-tool-link:hover { 31 | color: #1976d2; 32 | font-size: 11px; 33 | border: 0; 34 | background: transparent; 35 | text-decoration: underline; 36 | padding: 4px 8px; 37 | margin: 1px 0 0 0; 38 | display: inline-block; 39 | cursor: pointer; 40 | text-shadow: none; 41 | border-radius: 0; 42 | box-shadow: none; 43 | } 44 | 45 | #forgot.swat-tool-link .swat-tool-link-title { 46 | text-decoration: underline; 47 | } 48 | 49 | #forgot.swat-tool-link:active, 50 | #forgot.swat-tool-link:hover { 51 | position: static; 52 | top: 0; 53 | left: 0; 54 | color: #666; 55 | } 56 | -------------------------------------------------------------------------------- /Admin/layouts/AdminMenuXMLRPCServerLayout.php: -------------------------------------------------------------------------------- 1 | initMenu(); 22 | } 23 | 24 | /** 25 | * Initializes layout menu view. 26 | */ 27 | protected function initMenu() 28 | { 29 | if ($this->menu === null) { 30 | $menu_store = SwatDB::executeStoredProc( 31 | $this->app->db, 32 | 'getAdminMenu', 33 | $this->app->db->quote( 34 | $this->app->session->getUserId(), 35 | 'integer' 36 | ), 37 | AdminMenuStore::class 38 | ); 39 | 40 | $class = $this->app->getMenuViewClass(); 41 | $this->menu = new $class($menu_store, $this->app); 42 | } 43 | 44 | $this->menu->init(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Admin 2 | ===== 3 | Admin is a framework for back-end administration systems. Admin is built using 4 | [Swat](https://github.com/silverorange/swat) and 5 | [Site](https://github.com/silverorange/site). 6 | 7 | Installation 8 | ----------- 9 | Make sure the silverorange composer repository is added to the `composer.json` 10 | for the project and then run: 11 | 12 | ```sh 13 | composer require silverorange/admin 14 | ``` 15 | 16 | Enabling 2FA (Two Factor Authentication) 17 | ----------- 18 | 1. Install the Admin package ≥ `6.1.0` 19 | 2. Add two composer packages: 20 | 21 | ```sh 22 | composer require robthree/twofactorauth 23 | composer require bacon/bacon-qr-code 24 | ``` 25 | 26 | 3. Run `composer install` 27 | 28 | 4. Add the new database fields: 29 | 30 | ```sql 31 | alter table adminuser add two_fa_secret varchar(255); 32 | alter table adminuser add two_fa_enabled boolean not null default false; 33 | alter table adminuser add two_fa_timeslice integer not null default 0; 34 | ``` 35 | 36 | 5. Edit your `.ini` files (both stage and production) and add: 37 | 38 | ``` 39 | [admin] 40 | two_fa_enabled = On 41 | ``` 42 | 43 | 6. Let your users know! They will now see 2FA setup in the “Login Settings” in the top-right corner. 44 | -------------------------------------------------------------------------------- /Admin/exceptions/AdminNoAccessException.php: -------------------------------------------------------------------------------- 1 | user = $user; 32 | $this->title = Admin::_('No Access'); 33 | } 34 | 35 | /** 36 | * Gets the user that was denied access. 37 | * 38 | * @return AdminUser the user that was denied access 39 | */ 40 | public function getUser() 41 | { 42 | return $this->user; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /www/styles/admin-group-link-cell-renderer.css: -------------------------------------------------------------------------------- 1 | .admin-group-link-cell-renderer:visited, 2 | .admin-group-link-cell-renderer:link, 3 | .admin-group-link-cell-renderer { 4 | cursor: default; 5 | padding: 3px 8px; 6 | text-decoration: none; 7 | margin-left: 8px; 8 | position: relative; 9 | top: -1px; 10 | color: #fff; 11 | background: #009688; 12 | font-weight: 800; 13 | font-family: "Open Sans", sans-serif; 14 | font-size: 11px; 15 | text-transform: uppercase; 16 | border-radius: 2px; 17 | border: 0; 18 | box-shadow: 0 0 2px rgba(0,0,0,0.12), 0 2px 2px rgba(0,0,0,0.24); 19 | transition: box-shadow 0.2s ease; 20 | } 21 | 22 | .admin-group-link-cell-renderer:hover { 23 | background: #00897b; 24 | } 25 | 26 | .admin-group-link-cell-renderer:focus { 27 | outline: none; 28 | border: 0; 29 | box-shadow: 0 0 4px rgba(0,0,0,0.12), 0 4px 4px rgba(0,0,0,0.24); 30 | } 31 | 32 | .admin-group-link-cell-renderer:active { 33 | background: #26a69a; 34 | box-shadow: 0 0 8px rgba(0,0,0,0.12), 0 8px 8px rgba(0,0,0,0.24); 35 | } 36 | 37 | span.admin-group-link-cell-renderer, 38 | span.admin-group-link-cell-renderer:hover, 39 | span.admin-group-link-cell-renderer:active { 40 | opacity: 0.4; 41 | background: #888; 42 | box-shadow: 0 0 2px rgba(0,0,0,0.12), 0 2px 2px rgba(0,0,0,0.24); 43 | } 44 | -------------------------------------------------------------------------------- /sql/functions/getAdminMenu.mysql.sql: -------------------------------------------------------------------------------- 1 | CREATE PROCEDURE getAdminMenu(param_userid integer) 2 | SELECT AdminComponent.shortname, AdminComponent.title, 3 | AdminComponent.description, 4 | AdminComponent.section, AdminSection.title AS section_title, 5 | AdminComponent.id AS component_id, 6 | AdminSubComponent.title AS subcomponent_title, 7 | AdminSubComponent.shortname AS subcomponent_shortname 8 | FROM AdminComponent 9 | 10 | LEFT OUTER JOIN AdminSubComponent on 11 | AdminSubComponent.component = AdminComponent.id and 12 | AdminSubComponent.visible = true 13 | 14 | INNER JOIN AdminSection ON 15 | AdminComponent.section = AdminSection.id 16 | 17 | WHERE AdminSection.visible = true AND 18 | AdminComponent.enabled = true AND 19 | AdminComponent.visible = true AND 20 | AdminComponent.id IN ( 21 | SELECT component 22 | FROM AdminComponentAdminGroupBinding 23 | INNER JOIN AdminUserAdminGroupBinding ON 24 | AdminComponentAdminGroupBinding.groupnum = 25 | AdminUserAdminGroupBinding.groupnum 26 | WHERE AdminUserAdminGroupBinding.usernum = param_userid) 27 | 28 | ORDER BY AdminSection.displayorder, AdminSection.title, 29 | AdminComponent.section, AdminComponent.displayorder, 30 | AdminComponent.title, AdminSubComponent.displayorder, 31 | AdminSubComponent.title; 32 | -------------------------------------------------------------------------------- /Admin/components/AdminSite/Logout.php: -------------------------------------------------------------------------------- 1 | layout->logout_form; 15 | $form->process(); 16 | 17 | if ($form->isProcessed() && $form->isAuthenticated()) { 18 | // log out 19 | $this->app->session->logout(); 20 | $this->app->relocate($this->app->getBaseHref()); 21 | } else { 22 | // add error message 23 | $message = new SwatMessage( 24 | Admin::_('Unable to log out.'), 25 | 'warning' 26 | ); 27 | 28 | $message->secondary_content = 29 | Admin::_('In order to ensure your security, we were ' . 30 | 'unable to process your logout request. Please try again.'); 31 | 32 | $this->app->messages->add($message); 33 | 34 | // go back where we came from 35 | $url = (isset($_SERVER['HTTP_REFERER'])) ? 36 | $_SERVER['HTTP_REFERER'] : 'Front'; 37 | 38 | $this->app->relocate($url); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Admin/AdminTreeTitleLinkCellRenderer.php: -------------------------------------------------------------------------------- 1 | child_count) > 0) { 20 | $this->setFromStock($this->base_stock_id . '-with-contents'); 21 | } else { 22 | $this->setFromStock($this->base_stock_id); 23 | } 24 | 25 | // setting stock_id overrides base_stock_id 26 | parent::setStockType(); 27 | } 28 | 29 | protected function getTitle() 30 | { 31 | if (intval($this->child_count) === 0) { 32 | return Admin::_('no sub-items'); 33 | } 34 | 35 | return sprintf( 36 | Admin::ngettext( 37 | '%d sub-item', 38 | '%d sub-items', 39 | $this->child_count 40 | ), 41 | $this->child_count 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Admin/components/AdminUser/details.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Login History 6 | 7 | 8 | 9 | Login Time 10 | 11 | login_date 12 | 13 | 14 | 15 | Agent 16 | 17 | login_agent 18 | 19 | 20 | 21 | Remote IP 22 | 23 | remote_ip 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /Admin/components/AdminSection/edit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Section 6 | 7 | 8 | Title 9 | 10 | true 11 | 255 12 | 13 | 14 | 15 | Show in Menu? 16 | 17 | true 18 | 19 | 20 | 21 | Description 22 | 23 | 255 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /Admin/AdminUniqueEntry.php: -------------------------------------------------------------------------------- 1 | size = 20; 26 | } 27 | 28 | /** 29 | * Processes this unique entry. 30 | * 31 | * Ensures the value entered by the user is unique and if it is not unique 32 | * attaches an error message to the control. 33 | */ 34 | public function process() 35 | { 36 | parent::process(); 37 | 38 | if ($this->alphanum && preg_match('/[^[:alnum:]\-_]/u', $this->value)) { 39 | $message = Admin::_('The %s field can only contain letters and ' . 40 | 'numbers. Spaces and other special characters are not ' . 41 | 'allowed.'); 42 | 43 | $this->addMessage(new SwatMessage($message, 'error')); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Admin/components/AdminSite/forgot-password.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Reset Forgotten Password 6 | 7 | 8 | 9 | 10 | Forgot your password? No problem. Simply enter your email address and a link to create a new password will be sent to you. 11 | 12 | 13 | Email 14 | 15 | true 16 | 25 17 | 18 | 19 | 20 | 21 | Send Email 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /Admin/components/AdminSite/two_factor_authentication.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Two Factor Authentication 7 | 8 | You’ll need the six-digit code from your authenticator to continue. 9 | 10 | 11 | 12 | Verification code 13 | 14 | false 15 | 6 16 | 7 17 | true 18 | 19 | 20 | 21 | 22 | Authenticate 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /Admin/components/AdminGroup/edit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Group 7 | 8 | 9 | Title 10 | 11 | true 12 | 255 13 | 14 | 15 | 16 | Component Access 17 | false 18 | 19 | 20 | 21 | Users in Group 22 | false 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /Admin/pages/AdminDBOrder.php: -------------------------------------------------------------------------------- 1 | app->db); 19 | $this->saveDBData(); 20 | $transaction->commit(); 21 | } catch (SwatDBException $e) { 22 | $transaction->rollback(); 23 | 24 | $message = new SwatMessage( 25 | Admin::_('A database error has occured. The item was not saved.'), 26 | 'system-error' 27 | ); 28 | 29 | $this->app->messages->add($message); 30 | $e->process(); 31 | } catch (SwatException $e) { 32 | $message = new SwatMessage( 33 | Admin::_('An error has occured. The item was not saved.'), 34 | 'system-error' 35 | ); 36 | 37 | $this->app->messages->add($message); 38 | $e->process(); 39 | } 40 | } 41 | 42 | protected function saveDBData() 43 | { 44 | $this->saveIndexes(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Admin/dataobjects/AdminUserHistory.php: -------------------------------------------------------------------------------- 1 | table = 'AdminUserHistory'; 48 | $this->id_field = 'integer:id'; 49 | $this->registerDateProperty('login_date'); 50 | $this->registerInternalProperty('instance', SiteInstance::class); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Admin/components/AdminSite/reset-password.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Update Password 6 | 7 | 8 | 9 | 10 | New Password 11 | Password is case-sensitive. 12 | 13 | 4 14 | true 15 | 16 | 17 | 18 | Confirm New Password 19 | 20 | 21 | 22 | 23 | Update Password and Log In 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /Admin/components/AdminSubComponent/edit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Sub-Component 7 | 8 | 9 | Title 10 | 11 | true 12 | 255 13 | 14 | 15 | 16 | Short Name 17 | 18 | true 19 | 50 20 | 21 | 22 | 23 | Show in menu? 24 | 25 | true 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /Admin/AdminTwoFactorAuthentication.php: -------------------------------------------------------------------------------- 1 | createSecret(); 19 | } 20 | 21 | public function getQrCodeDataUri($title, $secret, $size = 400) 22 | { 23 | $two_fa = new TwoFactorAuth(new BaconQrCodeProvider()); 24 | 25 | return $two_fa->getQRCodeImageAsDataUri($title, $secret, $size); 26 | } 27 | 28 | public function validateToken($secret, $token, &$timeslice) 29 | { 30 | // strip all non numeric characters like spaces and dashes that people 31 | // might enter (e.g. Authy adds spaces for readability) 32 | $token = preg_replace('/[^0-9]/', '', $token); 33 | 34 | // The timeslice is used to make sure tokens before this 35 | // can't be used to authenticate again. There's a "window" of token 36 | // use and without this, someone could capture the code, and re-use it. 37 | $two_fa = new TwoFactorAuth(new BaconQrCodeProvider()); 38 | $success = $two_fa->verifyCode($secret, $token, 1, null, $timeslice); 39 | 40 | return $success; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Admin/AdminNavBar.php: -------------------------------------------------------------------------------- 1 | link !== null && $link) { 31 | echo '

'; 32 | $a_tag = new SwatHtmlTag('a'); 33 | if ($first) { 34 | $a_tag->class = 'swat-navbar-first'; 35 | } 36 | 37 | $a_tag->href = $entry->link; 38 | $a_tag->setContent($entry->title); 39 | $a_tag->display(); 40 | echo '

'; 41 | } else { 42 | parent::displayEntry($entry, $link, $first); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Admin/AdminDependencyEntry.php: -------------------------------------------------------------------------------- 1 | id = $data->id; 50 | $this->title = $data->title; 51 | $this->parent = $data->parent; 52 | $this->status_level = $data->status_level; 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Admin/templates/AdminLoginTemplate.php: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | {$data->title} 21 | 22 | 23 | 24 | 25 | 26 | {$data->html_head_entries} 27 | 28 | body_classes}> 29 | 30 | {$data->content} 31 | 32 | 33 | 34 | 35 | HTML; 36 | // @codingStandardsIgnoreEnd 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Admin/AdminTreeControlCellRenderer.php: -------------------------------------------------------------------------------- 1 | visible) { 24 | return; 25 | } 26 | 27 | $this->width = 22; 28 | $this->height = 22; 29 | 30 | if ($this->childcount == 0) { 31 | $this->title = Admin::_('View Details'); 32 | $this->alt = Admin::_('Details'); 33 | $this->image = 'packages/admin/images/admin-generic-document.png'; 34 | } else { 35 | $this->title = sprintf( 36 | Admin::ngettext( 37 | 'View Details (%s sub-item)', 38 | 'View Details (%s sub-items)', 39 | $this->childcount 40 | ), 41 | SwatString::numberFormat($this->childcount) 42 | ); 43 | 44 | $this->alt = Admin::_('Details'); 45 | $this->image = 46 | 'packages/admin/images/admin-document-with-contents.png'; 47 | } 48 | 49 | parent::render(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Admin/components/AdminGroup/index.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Groups 7 | 8 | 9 | New Group 10 | AdminGroup/Edit 11 | create 12 | 13 | 14 | 15 | 16 | 17 | 18 | id 19 | 20 | 21 | 22 | Title 23 | 24 | title 25 | AdminGroup/Edit?id=%s 26 | id 27 | edit 28 | 29 | 30 | 31 | 32 | 33 | delete… 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /Admin/dataobjects/AdminGroup.php: -------------------------------------------------------------------------------- 1 | table = 'AdminGroup'; 36 | $this->id_field = 'integer:id'; 37 | } 38 | 39 | /** 40 | * Loads the components that this group has access to. 41 | * 42 | * @return AdminComponentWrapper the components this group has access to 43 | */ 44 | protected function loadComponents() 45 | { 46 | $sql = sprintf( 47 | 'select AdminComponent.* 48 | from AdminComponent 49 | inner join AdminComponentAdmingroupBinding on 50 | AdminComponentAdminGroupBinding.component = 51 | AdminComponent.id 52 | where groupnum = %s', 53 | $this->db->quote($this->id, 'integer') 54 | ); 55 | 56 | return SwatDB::query($this->db, $sql, AdminComponentWrapper::class); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /po/Makefile: -------------------------------------------------------------------------------- 1 | ## 2 | ## i18n Makefile 3 | ## 4 | 5 | DOMAIN = admin 6 | XGETTEXT = xgettext --from-code=UTF-8 7 | MSGFMT = msgfmt -c 8 | MSGMERGE = msgmerge --no-fuzzy-matching 9 | MSGUNIQ = msguniq 10 | INSTALL = install 11 | INSTALLDIR = mkdir -p 12 | DATA_MODE = 660 13 | 14 | TRANSLATIONS := en_CA en_US en_GB 15 | MO_FILES := $(patsubst %,%.mo, $(TRANSLATIONS)) 16 | 17 | POTFILE = $(DOMAIN).pot 18 | 19 | TARGETS = $(MO_FILES) 20 | 21 | LOCALEDIR = ../locale 22 | 23 | ###################################### 24 | 25 | all: 26 | make $(TARGETS) 27 | 28 | potfiles-php: 29 | rm -f $@-t1 $@-t2 $@ 30 | (sed -e '/^#/d' < potfiles.in) > $@-t1 31 | (for i in `cat $@-t1`; do find $$i -name \*.php; done) >> $@-t2 32 | (for i in `cat $@-t2`; do php preprocess.php $$i > $$i.gettext; echo $$i.gettext; done) >> $@ 33 | rm -f $@-t1 $@-t2 34 | 35 | potfiles-xml: 36 | rm -f $@-t $@ 37 | (sed -e '/^#/d' < potfiles.in) > $@-t 38 | (for i in `cat $@-t`; do find $$i -name \*.xml; done) >> $@ 39 | rm -f $@-t 40 | 41 | pot: $(POTFILE) 42 | 43 | $(POTFILE): potfiles-php potfiles-xml 44 | $(XGETTEXT) -o $(POTFILE) -L Php -f potfiles-php 45 | $(XGETTEXT) -j -o $(POTFILE) -L Glade -f potfiles-xml 46 | $(MSGUNIQ) -o $(POTFILE) $(POTFILE) 47 | for i in `cat potfiles-php`; do rm -f $$i; done 48 | rm -f potfiles-php 49 | 50 | %.mo: %.po 51 | $(MSGFMT) -o $@ $< 52 | 53 | install: 54 | for i in $(TRANSLATIONS); do \ 55 | $(INSTALLDIR) $(LOCALEDIR)/$$i/LC_MESSAGES ; \ 56 | $(INSTALL) -m $(DATA_MODE) $$i.mo $(LOCALEDIR)/$$i/LC_MESSAGES/$(DOMAIN).mo ; \ 57 | done 58 | 59 | update: pot 60 | for i in $(TRANSLATIONS); do \ 61 | $(MSGMERGE) -U $$i.po $(POTFILE); \ 62 | done 63 | 64 | clean: 65 | rm -f potfiles-* *.mo *.pot 66 | -------------------------------------------------------------------------------- /dependencies/admin.yaml: -------------------------------------------------------------------------------- 1 | ## 2 | ## Static Web-resource dependencies for the Admin package 3 | ## 4 | ## Copyright (c) 2010 silverorange 5 | ## 6 | ## This library is free software; you can redistribute it and/or modify 7 | ## it under the terms of the GNU Lesser General Public License as 8 | ## published by the Free Software Foundation; either version 2.1 of the 9 | ## License, or (at your option) any later version. 10 | ## 11 | ## This library is distributed in the hope that it will be useful, 12 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | ## Lesser General Public License for more details. 15 | ## 16 | ## You should have received a copy of the GNU Lesser General Public 17 | ## License along with this library; if not, write to the Free Software 18 | ## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 | ## 20 | Admin: 21 | Depends: 22 | # Package Dependencies 23 | - Swat 24 | - Site 25 | 26 | Provides: 27 | 28 | # JavaScript resources 29 | packages/admin/javascript/admin-login.js: 30 | packages/admin/javascript/admin-menu.js: 31 | packages/admin/javascript/admin-order.js: 32 | 33 | # Style-sheet resources 34 | packages/admin/styles/admin-approval-page.css: 35 | packages/admin/styles/admin-change-password-page.css: 36 | packages/admin/styles/admin-group-link-cell-renderer.css: 37 | packages/admin/styles/admin-layout.css: 38 | packages/admin/styles/admin-login-page.css: 39 | packages/admin/styles/admin-menu.css: 40 | packages/admin/styles/admin-note.css: 41 | packages/admin/styles/admin-title-link-cell-renderer.css: 42 | 43 | # vim: set expandtab tabstop=2 shiftwidth=2 softtabstop=2: 44 | -------------------------------------------------------------------------------- /Admin/components/AdminUser/login-history.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Admin User Login History 6 | 7 | 8 | 9 | Email 10 | 11 | email 12 | AdminUser/Details?id=%s 13 | usernum 14 | 15 | 16 | 17 | Login Time 18 | 19 | login_date 20 | 21 | 22 | 23 | Agent 24 | 25 | login_agent 26 | 27 | 28 | 29 | Remote IP 30 | 31 | remote_ip 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /Admin/components/AdminSection/Order.php: -------------------------------------------------------------------------------- 1 | parent = SiteApplication::initVar('parent'); 20 | $form = $this->ui->getWidget('order_form'); 21 | $form->addHiddenField('parent', $this->parent); 22 | } 23 | 24 | // process phase 25 | 26 | protected function saveIndex($id, $index) 27 | { 28 | SwatDB::updateColumn( 29 | $this->app->db, 30 | 'AdminSection', 31 | 'integer:displayorder', 32 | $index, 33 | 'integer:id', 34 | [$id] 35 | ); 36 | } 37 | 38 | // build phase 39 | 40 | protected function buildInternal() 41 | { 42 | $frame = $this->ui->getWidget('order_frame'); 43 | $frame->title = Admin::_('Order Sections'); 44 | parent::buildInternal(); 45 | } 46 | 47 | protected function loadData() 48 | { 49 | $order_widget = $this->ui->getWidget('order'); 50 | $order_widget->addOptionsByArray(SwatDB::getOptionArray( 51 | $this->app->db, 52 | 'AdminSection', 53 | 'title', 54 | 'id', 55 | 'displayorder, title' 56 | )); 57 | 58 | $sql = 'select sum(displayorder) from AdminSection'; 59 | $sum = SwatDB::queryOne($this->app->db, $sql, 'integer'); 60 | $options_list = $this->ui->getWidget('options'); 61 | $options_list->value = ($sum == 0) ? 'auto' : 'custom'; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Admin/components/AdminGroup/Delete.php: -------------------------------------------------------------------------------- 1 | getItemList('integer'); 19 | $sql = sprintf($sql, $item_list); 20 | $num = SwatDB::exec($this->app->db, $sql); 21 | 22 | $message = new SwatMessage(sprintf( 23 | Admin::ngettext( 24 | 'One admin group has been deleted.', 25 | '%s admin groups have been deleted.', 26 | $num 27 | ), 28 | SwatString::numberFormat($num) 29 | )); 30 | 31 | $this->app->messages->add($message); 32 | } 33 | 34 | // build phase 35 | 36 | protected function buildInternal() 37 | { 38 | parent::buildInternal(); 39 | 40 | $item_list = $this->getItemList('integer'); 41 | 42 | $dep = new AdminListDependency(); 43 | $dep->setTitle(Admin::_('group'), Admin::_('groups')); 44 | $dep->entries = AdminListDependency::queryEntries( 45 | $this->app->db, 46 | 'AdminGroup', 47 | 'integer:id', 48 | null, 49 | 'text:title', 50 | 'title', 51 | 'id in (' . $item_list . ')', 52 | AdminDependency::DELETE 53 | ); 54 | 55 | $message = $this->ui->getWidget('confirmation_message'); 56 | $message->content = $dep->getMessage(); 57 | $message->content_type = 'text/xml'; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /sql/functions/getAdminMenu.pgsql.sql: -------------------------------------------------------------------------------- 1 | CREATE TYPE type_admin_menu AS ( 2 | shortname varchar(255), 3 | title varchar(255), 4 | description text, 5 | section integer, 6 | section_title varchar(255), 7 | component_id integer, 8 | subcomponent_title varchar(255), 9 | subcomponent_shortname varchar(255) 10 | ); 11 | 12 | CREATE OR REPLACE FUNCTION getAdminMenu(integer) RETURNS SETOF type_admin_menu AS $$ 13 | DECLARE 14 | param_userid ALIAS FOR $1; 15 | returned_row type_admin_menu%ROWTYPE; 16 | BEGIN 17 | FOR returned_row IN 18 | SELECT AdminComponent.shortname, AdminComponent.title, 19 | AdminComponent.description, 20 | AdminComponent.section, AdminSection.title AS section_title, 21 | AdminComponent.id, 22 | AdminSubComponent.title as subcomponent_title, 23 | AdminSubCOmponent.shortname as subcomponent_shortname 24 | FROM AdminComponent 25 | 26 | LEFT OUTER JOIN AdminSubComponent on 27 | AdminSubComponent.component = AdminComponent.id and 28 | AdminSUbComponent.visible = true 29 | 30 | INNER JOIN AdminSection ON 31 | AdminComponent.section = AdminSection.id 32 | 33 | WHERE AdminSection.visible = true AND 34 | AdminComponent.enabled = true AND 35 | AdminComponent.visible = true AND 36 | AdminComponent.id IN ( 37 | SELECT component 38 | FROM AdminComponentAdminGroupBinding 39 | INNER JOIN AdminUserAdminGroupBinding ON 40 | AdminComponentAdminGroupBinding.groupnum = 41 | AdminUserAdminGroupBinding.groupnum 42 | WHERE AdminUserAdminGroupBinding.usernum = param_userid) 43 | 44 | ORDER BY AdminSection.displayorder, AdminSection.title, 45 | AdminComponent.section, AdminComponent.displayorder, 46 | AdminComponent.title, AdminSubComponent.displayorder, 47 | AdminSubComponent.title 48 | LOOP 49 | RETURN NEXT returned_row; 50 | END LOOP; 51 | 52 | RETURN; 53 | END; 54 | $$ LANGUAGE 'plpgsql'; 55 | -------------------------------------------------------------------------------- /Admin/components/AdminSite/Exception.php: -------------------------------------------------------------------------------- 1 | app, AdminDefaultTemplate::class); 18 | } 19 | 20 | // init phase 21 | 22 | public function init() 23 | { 24 | parent::init(); 25 | 26 | $this->container = new SwatFrame(); 27 | $this->container->classes[] = 'admin-exception-container'; 28 | } 29 | 30 | // build phase 31 | 32 | public function build() 33 | { 34 | parent::build(); 35 | if (isset($this->layout->navbar)) { 36 | $this->layout->navbar->popEntry(); 37 | $this->layout->navbar->popEntry(); 38 | $this->layout->navbar->createEntry('Error'); 39 | } 40 | } 41 | 42 | protected function display() 43 | { 44 | ob_start(); 45 | 46 | printf('

%s

', $this->getSummary()); 47 | 48 | echo '

This error has been reported.

'; 49 | 50 | if ($this->exception !== null) { 51 | $this->exception->process(false); 52 | } 53 | 54 | $content_block = new SwatContentBlock(); 55 | $content_block->content = ob_get_clean(); 56 | $content_block->content_type = 'text/xml'; 57 | 58 | $this->container->add($content_block); 59 | $this->container->display(); 60 | } 61 | 62 | // finalize phase 63 | 64 | public function finalize() 65 | { 66 | parent::finalize(); 67 | $this->layout->addHtmlHeadEntrySet( 68 | $this->container->getHtmlHeadEntrySet() 69 | ); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Admin/AdminPagination.php: -------------------------------------------------------------------------------- 1 | id, $_GET)) { 28 | $this->setCurrentPage($_GET[$this->id]); 29 | } 30 | } 31 | 32 | /** 33 | * Gets the base link for all page links. 34 | * 35 | * This removes all unwanted variables from the current HTTP GET variables 36 | * and adds all wanted variables ones back into the link string. 37 | * 38 | * @return string the base link for all pages with cleaned HTTP GET 39 | * variables 40 | */ 41 | protected function getLink() 42 | { 43 | $vars = $_GET; 44 | 45 | $this->unset_get_vars[] = $this->id; 46 | $this->unset_get_vars[] = 'source'; 47 | 48 | foreach ($vars as $name => $value) { 49 | if (in_array($name, $this->unset_get_vars)) { 50 | unset($vars[$name]); 51 | } 52 | } 53 | 54 | if ($this->link === null) { 55 | $link = '?'; 56 | } else { 57 | $link = $this->link . '?'; 58 | } 59 | 60 | foreach ($vars as $name => $value) { 61 | $link .= $name . '=' . urlencode($value) . '&'; 62 | } 63 | 64 | $link .= urlencode($this->id) . '=%s'; 65 | 66 | return $link; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Admin/components/AdminSite/login.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Login 6 | 7 | 8 | Email 9 | SHOW_OPTIONAL 10 | 11 | true 12 | 25 13 | 1 14 | 15 | 16 | 17 | Password 18 | SHOW_OPTIONAL 19 | 20 | true 21 | 18 22 | 2 23 | 24 | 25 | 26 | 27 | Forgot your password? 28 | AdminSite/ForgotPassword 29 | 30 | 31 | 32 | 33 | 34 | 35 | Login 36 | 3 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /Admin/AdminDateLinkCellRenderer.php: -------------------------------------------------------------------------------- 1 | date = $this->date; 55 | $date_renderer->format = $this->format; 56 | $date_renderer->time_zone_format = $this->time_zone_format; 57 | $date_renderer->display_time_zone = $this->display_time_zone; 58 | 59 | ob_start(); 60 | $date_renderer->render(); 61 | 62 | return ob_get_clean(); 63 | } 64 | 65 | protected function getTitle() 66 | { 67 | return parent::getText(); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Admin/components/AdminSite/change-password.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Change Password 7 | 8 | The password provided to you was automatically generated. For your security, please choose a new password for your account. 9 | 10 | 11 | 12 | Old Password 13 | 14 | false 15 | 4 16 | true 17 | 18 | 19 | 20 | New Password 21 | 22 | false 23 | 4 24 | true 25 | 26 | 27 | 28 | Confirm New Password 29 | 30 | true 31 | 32 | 33 | 34 | 35 | Update Password 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /Admin/dataobjects/AdminSection.php: -------------------------------------------------------------------------------- 1 | table = 'AdminSection'; 60 | $this->id_field = 'integer:id'; 61 | } 62 | 63 | /** 64 | * @return AdminComponentWrapper 65 | */ 66 | protected function loadComponents() 67 | { 68 | $sql = sprintf( 69 | 'select * from AdminComponent 70 | where section = %s 71 | order by displayorder, title', 72 | $this->db->quote($this->id, 'integer') 73 | ); 74 | 75 | return SwatDB::query($this->db, $sql, AdminComponentWrapper::class); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Admin/components/AdminComponent/Order.php: -------------------------------------------------------------------------------- 1 | parent = SiteApplication::initVar('parent'); 20 | $form = $this->ui->getWidget('order_form'); 21 | $form->addHiddenField('parent', $this->parent); 22 | } 23 | 24 | // process phase 25 | 26 | protected function saveIndex($id, $index) 27 | { 28 | SwatDB::updateColumn( 29 | $this->app->db, 30 | 'AdminComponent', 31 | 'integer:displayorder', 32 | $index, 33 | 'integer:id', 34 | [$id] 35 | ); 36 | } 37 | 38 | // build phase 39 | 40 | protected function buildInternal() 41 | { 42 | $frame = $this->ui->getWidget('order_frame'); 43 | $frame->title = Admin::_('Order Components'); 44 | parent::buildInternal(); 45 | } 46 | 47 | protected function loadData() 48 | { 49 | $where_clause = sprintf( 50 | 'section = %s', 51 | $this->app->db->quote($this->parent, 'integer') 52 | ); 53 | 54 | $order_widget = $this->ui->getWidget('order'); 55 | $order_widget->addOptionsByArray(SwatDB::getOptionArray( 56 | $this->app->db, 57 | 'AdminComponent', 58 | 'title', 59 | 'id', 60 | 'displayorder, title', 61 | $where_clause 62 | )); 63 | 64 | $sql = 'select sum(displayorder) from AdminComponent where ' . 65 | $where_clause; 66 | 67 | $sum = SwatDB::queryOne($this->app->db, $sql, 'integer'); 68 | $options_list = $this->ui->getWidget('options'); 69 | $options_list->value = ($sum == 0) ? 'auto' : 'custom'; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Admin/AdminSearchOperatorFlydown.php: -------------------------------------------------------------------------------- 1 | visible) { 29 | return; 30 | } 31 | 32 | $this->options = []; 33 | $this->show_blank = false; 34 | 35 | foreach ($this->operators as $op) { 36 | $this->addOption($op, self::getOperatorTitle($op)); 37 | } 38 | 39 | parent::display(); 40 | } 41 | 42 | private static function getOperatorTitle($id) 43 | { 44 | switch ($id) { 45 | case AdminSearchClause::OP_EQUALS: 46 | return Admin::_('is'); 47 | 48 | case AdminSearchClause::OP_GT: 49 | return '>'; 50 | 51 | case AdminSearchClause::OP_GTE: 52 | return '>='; 53 | 54 | case AdminSearchClause::OP_LT: 55 | return '<'; 56 | 57 | case AdminSearchClause::OP_LTE: 58 | return '<='; 59 | 60 | case AdminSearchClause::OP_CONTAINS: 61 | return Admin::_('contains'); 62 | 63 | case AdminSearchClause::OP_STARTS_WITH: 64 | return Admin::_('starts with'); 65 | 66 | case AdminSearchClause::OP_ENDS_WITH: 67 | return Admin::_('ends with'); 68 | 69 | default: 70 | throw new Exception('AdminSearchOperatorFlydown: unknown operator'); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /.php-cs-fixer.php: -------------------------------------------------------------------------------- 1 | in(__DIR__); 9 | 10 | return (new Config()) 11 | ->setParallelConfig(ParallelConfigFactory::detect(null, null, 2**18-1)) 12 | ->setRules([ 13 | '@PhpCsFixer' => true, 14 | '@PHP82Migration' => true, 15 | 'indentation_type' => true, 16 | 17 | // Overrides for (opinionated) @PhpCsFixer and @Symfony rules: 18 | 19 | // Align "=>" in multi-line array definitions, unless a blank line exists between elements 20 | 'binary_operator_spaces' => ['operators' => ['=>' => 'align_single_space_minimal']], 21 | 22 | // Subset of statements that should be proceeded with blank line 23 | 'blank_line_before_statement' => ['statements' => ['case', 'continue', 'declare', 'default', 'return', 'throw', 'try', 'yield', 'yield_from']], 24 | 25 | // Enforce space around concatenation operator 26 | 'concat_space' => ['spacing' => 'one'], 27 | 28 | // Use {} for empty loop bodies 29 | 'empty_loop_body' => ['style' => 'braces'], 30 | 31 | // Don't change any increment/decrement styles 32 | 'increment_style' => false, 33 | 34 | // Forbid multi-line whitespace before the closing semicolon 35 | 'multiline_whitespace_before_semicolons' => ['strategy' => 'no_multi_line'], 36 | 37 | // Clean up PHPDocs, but leave @inheritDoc entries alone 38 | 'no_superfluous_phpdoc_tags' => ['allow_mixed' => true, 'remove_inheritdoc' => false], 39 | 40 | // Ensure that traits are listed first in classes 41 | // (it would be nice to enforce more, but we'll start simple) 42 | 'ordered_class_elements' => ['order' => ['use_trait']], 43 | 44 | // Ensure that param and return types are sorted consistently, with null at end 45 | 'phpdoc_types_order' => ['sort_algorithm' => 'alpha', 'null_adjustment' => 'always_last'], 46 | 47 | // Yoda style is too weird 48 | 'yoda_style' => false, 49 | ]) 50 | ->setIndent(' ') 51 | ->setLineEnding("\n") 52 | ->setFinder($finder); 53 | -------------------------------------------------------------------------------- /Admin/AdminNote.php: -------------------------------------------------------------------------------- 1 | addStyleSheet('packages/admin/styles/admin-note.css'); 29 | } 30 | 31 | /** 32 | * Displays this content. 33 | * 34 | * Merely performs an echo of the content. 35 | */ 36 | public function display() 37 | { 38 | if (!$this->visible) { 39 | return; 40 | } 41 | 42 | SwatWidget::display(); 43 | 44 | $div = new SwatHtmlTag('div'); 45 | $div->id = $this->id; 46 | $div->class = $this->getCSSClassString(); 47 | $div->open(); 48 | 49 | if ($this->title != '') { 50 | $header_tag = new SwatHtmlTag('h3'); 51 | $header_tag->class = 'admin-note-title'; 52 | $header_tag->setContent($this->title); 53 | $header_tag->display(); 54 | } 55 | 56 | if ($this->content != '') { 57 | $content_div = new SwatHtmlTag('div'); 58 | $content_div->class = 'admin-note-content'; 59 | $content_div->setContent($this->content, $this->content_type); 60 | $content_div->display(); 61 | } 62 | 63 | $div->close(); 64 | } 65 | 66 | /** 67 | * Gets the array of CSS classes that are applied to this note. 68 | * 69 | * @return array the array of CSS classes that are applied to this note 70 | */ 71 | protected function getCSSClassNames() 72 | { 73 | $classes = ['admin-note']; 74 | 75 | return array_merge($classes, $this->classes); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Admin/templates/AdminDefaultTemplate.php: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | {$data->title} 21 | 22 | 23 | 24 | 25 | 26 | {$data->html_head_entries} 27 | 28 | body_classes}> 29 | 30 |
31 | 32 |
33 | 34 |
35 |
36 |
37 | {$data->header} 38 |
39 | {$data->navbar} 40 |
41 |
42 | {$data->content} 43 |
44 |
45 | 46 |
47 | {$data->menu} 48 |
49 | 50 |
51 | 52 |
53 | 54 | 55 | 56 | HTML; 57 | // @codingStandardsIgnoreEnd 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Admin/AdminUI.php: -------------------------------------------------------------------------------- 1 | getWidget($widget_id)->value; 35 | } 36 | 37 | return $values; 38 | } 39 | 40 | /** 41 | * Sets values of widgets. 42 | * 43 | * Convenience method to set values of multiple widgets at once. 44 | * This method is useful when using {@link SwatDB::rowQuery()} 45 | * but only works if the widget id and field name are the same, if this 46 | * is not the case you should manually set the values. 47 | * 48 | * If a widget id-value pair is passed for a widget that does not exist, 49 | * that value is ignored. 50 | * 51 | * @param array $values an array of widget values indexed by widget ids 52 | */ 53 | public function setValues(array $values) 54 | { 55 | foreach ($values as $id => $value) { 56 | try { 57 | $widget = $this->getWidget($id); 58 | $widget->value = $values[$id]; 59 | } catch (SwatWidgetNotFoundException $e) { 60 | // ignore 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Admin/components/AdminComponent/edit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Component 7 | 8 | 9 | Title 10 | 11 | true 12 | 255 13 | 14 | 15 | 16 | Short Name 17 | 18 | true 19 | 255 20 | 21 | 22 | 23 | Section 24 | 25 | true 26 | 27 | 28 | 29 | Show in Menu? 30 | 31 | true 32 | 33 | 34 | 35 | Enabled? 36 | 37 | true 38 | 39 | 40 | 41 | Description 42 | 43 | 44 | 45 | Groups 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /Admin/Admin.php: -------------------------------------------------------------------------------- 1 | '0', 63 | 'admin.two_fa_enabled' => false, 64 | ]; 65 | } 66 | 67 | public static function init() 68 | { 69 | if (self::$is_initialized) { 70 | return; 71 | } 72 | 73 | Swat::init(); 74 | Site::init(); 75 | 76 | self::setupGettext(); 77 | 78 | SwatUI::mapClassPrefixToPath('Admin', 'Admin'); 79 | 80 | self::$is_initialized = true; 81 | } 82 | 83 | /** 84 | * Prevent instantiation of this static class. 85 | */ 86 | private function __construct() {} 87 | } 88 | -------------------------------------------------------------------------------- /www/styles/admin-menu.css: -------------------------------------------------------------------------------- 1 | /* Admin menu styles */ 2 | 3 | .admin-menu { 4 | margin-top: 7px; 5 | margin-bottom: 1em; 6 | color: #666; 7 | } 8 | 9 | /* layouts with menu on left */ 10 | .yui-t1 .admin-menu, 11 | .yui-t2 .admin-menu, 12 | .yui-t3 .admin-menu { 13 | margin-left: 1em; 14 | } 15 | 16 | /* layouts with menu on right */ 17 | .yui-t4 .admin-menu, 18 | .yui-t5 .admin-menu, 19 | .yui-t6 .admin-menu { 20 | margin-right: 1em; 21 | } 22 | 23 | .admin-menu ul { 24 | list-style-type: none; 25 | margin: 0; 26 | padding: 0; 27 | color: #837352; 28 | } 29 | 30 | .admin-menu ul .menu-section-title { 31 | display: block; 32 | margin: 0; 33 | padding: 12px 12px 2px; 34 | color: #888; 35 | font-size: 85%; 36 | } 37 | 38 | .admin-menu ul .menu-section-title span { 39 | display: block; 40 | } 41 | 42 | .admin-menu li li a { 43 | display: block; 44 | padding: 3px 12px; 45 | } 46 | 47 | .admin-menu ul ul { 48 | display: block; 49 | list-style-type: none; 50 | padding: 0; 51 | } 52 | 53 | .admin-menu ul ul ul li { 54 | padding-left: 18px; 55 | font-size: 85%; 56 | } 57 | 58 | .admin-menu a:link, 59 | .admin-menu a:visited { 60 | text-decoration: none; 61 | } 62 | 63 | .admin-menu a:hover { 64 | background: #e2e0df; 65 | } 66 | 67 | .admin-menu ul ul li { 68 | position: relative; 69 | } 70 | 71 | .admin-menu ul .admin-menu-help { 72 | position: absolute; 73 | right: -186px; 74 | width: 200px; 75 | top: -13px; 76 | display: none; 77 | } 78 | 79 | .admin-menu ul .admin-menu-help-arrow { 80 | position: absolute; 81 | left: 1px; 82 | top: 14px; 83 | background: url(../images/admin-menu-help-arrow.png) no-repeat 0 0; 84 | width: 19px; 85 | height: 19px; 86 | display: block; 87 | } 88 | 89 | .admin-menu ul .admin-menu-help-content { 90 | margin-left: 19px; 91 | display: block; 92 | background: #111; 93 | background: rgba(0,0,0,0.9); 94 | color: #eee; 95 | padding: 12px 16px; 96 | border-radius: 4px; 97 | box-shadow: 0 2px 2px -1px rgba(0,0,0,0.5); 98 | } 99 | 100 | .admin-menu ul .admin-menu-help-content p:last-child { 101 | margin-bottom: 0; 102 | } 103 | 104 | 105 | /* Menu-hiding styles */ 106 | 107 | body .admin-menu-hide img { 108 | display: none; 109 | } 110 | 111 | body .admin-menu-show { 112 | display: none; 113 | } 114 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "silverorange/admin", 3 | "description": "Framework for backend admin website.", 4 | "type": "library", 5 | "keywords": [ 6 | "framework", 7 | "admin", 8 | "management" 9 | ], 10 | "homepage": "https://github.com/silverorange/admin", 11 | "license": "LGPL-2.1", 12 | "authors": [ 13 | { 14 | "name": "Charles Waddell", 15 | "email": "charles@silverorange.com" 16 | }, 17 | { 18 | "name": "Isaac Grant", 19 | "email": "isaac@silverorange.com" 20 | }, 21 | { 22 | "name": "Michael Gauthier", 23 | "email": "mike@silverorange.com" 24 | }, 25 | { 26 | "name": "Nathan Frederikson", 27 | "email": "nathan@silverorange.com" 28 | }, 29 | { 30 | "name": "Nick Burka", 31 | "email": "nick@silverorange.com" 32 | }, 33 | { 34 | "name": "Steven Garrity", 35 | "email": "steven@silverorange.com" 36 | } 37 | ], 38 | "repositories": [ 39 | { 40 | "type": "composer", 41 | "url": "https://composer.silverorange.com", 42 | "only": [ 43 | "silverorange/*" 44 | ] 45 | } 46 | ], 47 | "require": { 48 | "php": ">=8.2.0", 49 | "ext-mbstring": "*", 50 | "silverorange/site": "^15.3.2", 51 | "silverorange/swat": "^7.9.2" 52 | }, 53 | "require-dev": { 54 | "bacon/bacon-qr-code": "^3.0", 55 | "friendsofphp/php-cs-fixer": "3.64.0", 56 | "phpstan/phpstan": "^1.12", 57 | "robthree/twofactorauth": "^3.0" 58 | }, 59 | "suggest": { 60 | "robthree/twofactorauth": "required for the use of two factor authentication", 61 | "bacon/bacon-qr-code": "required to show QR codes for two factor auth" 62 | }, 63 | "autoload": { 64 | "classmap": [ 65 | "Admin/" 66 | ] 67 | }, 68 | "scripts": { 69 | "phpcs": "./vendor/bin/php-cs-fixer check -v", 70 | "phpcs:ci": "./vendor/bin/php-cs-fixer check --config=.php-cs-fixer.php --no-interaction --show-progress=none --diff --using-cache=no -vvv", 71 | "phpcs:write": "./vendor/bin/php-cs-fixer fix -v", 72 | "phpstan": "./vendor/bin/phpstan analyze", 73 | "phpstan:ci": "./vendor/bin/phpstan analyze -vvv --no-progress --memory-limit 2G", 74 | "phpstan:baseline": "./vendor/bin/phpstan analyze --generate-baseline" 75 | }, 76 | "config": { 77 | "sort-packages": true, 78 | "allow-plugins": { 79 | "php-http/discovery": true 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Admin/components/AdminSection/index.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Sections 7 | 8 | 9 | New Section 10 | AdminSection/Edit 11 | create 12 | 13 | 14 | Change Order 15 | AdminSection/Order 16 | change-order 17 | 18 | 19 | 20 | 21 | 22 | 23 | id 24 | 25 | 26 | 27 | Title 28 | 29 | title 30 | AdminSection/Edit?id=%s 31 | id 32 | edit 33 | 34 | 35 | 36 | Show in Menu 37 | 38 | visible 39 | 40 | 41 | 42 | 43 | 44 | delete… 45 | 46 | 47 | show 48 | 49 | 50 | hide 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /Admin/components/AdminSite/TwoFactorAuthentication.php: -------------------------------------------------------------------------------- 1 | app, AdminLoginTemplate::class); 15 | } 16 | 17 | protected function initInternal() 18 | { 19 | parent::initInternal(); 20 | 21 | $this->ui->loadFromXML(__DIR__ . '/two_factor_authentication.xml'); 22 | 23 | $form = $this->ui->getWidget('two_fa_form'); 24 | $form->action = 'AdminSite/TwoFactorAuthentication'; 25 | 26 | // remember where we came from 27 | $form->addHiddenField('relocate_uri', $this->app->getUri()); 28 | 29 | $user = $this->app->session->user; 30 | if ($user->is2FaAuthenticated() || !$user->two_fa_enabled) { 31 | $this->app->relocate('./'); 32 | } 33 | } 34 | 35 | // process phase 36 | 37 | protected function processInternal() 38 | { 39 | parent::processInternal(); 40 | 41 | $form = $this->ui->getWidget('two_fa_form'); 42 | if ($form->isProcessed()) { 43 | if (!$form->hasMessage()) { 44 | $this->validate2Fa(); 45 | } 46 | 47 | if (!$form->hasMessage()) { 48 | $this->app->session->user->set2FaAuthenticated(); 49 | 50 | // go back where we came from 51 | $uri = $form->getHiddenField('relocate_uri'); 52 | $this->app->relocate($uri); 53 | } 54 | } 55 | } 56 | 57 | protected function validate2Fa() 58 | { 59 | $success = $this->app->session->loginWithTwoFactorAuthentication( 60 | $this->ui->getWidget('two_fa_token')->value 61 | ); 62 | 63 | if (!$success) { 64 | $this->ui->getWidget('two_fa_token')->addMessage( 65 | new SwatMessage( 66 | Admin::_( 67 | 'Your two factor authentication token doesn’t ' . 68 | 'match. Try again, or contact support for help.' 69 | ), 70 | 'error' 71 | ) 72 | ); 73 | } 74 | } 75 | 76 | // finalize phase 77 | 78 | public function finalize() 79 | { 80 | parent::finalize(); 81 | 82 | $this->layout->addHtmlHeadEntry( 83 | 'packages/admin/styles/admin-two-factor-authentication-page.css' 84 | ); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Admin/components/AdminComponent/Delete.php: -------------------------------------------------------------------------------- 1 | getItemList('integer'); 19 | $sql = sprintf($sql, $item_list); 20 | $num = SwatDB::exec($this->app->db, $sql); 21 | 22 | $message = new SwatMessage(sprintf( 23 | Admin::ngettext( 24 | 'One component has been deleted.', 25 | '%s components have been deleted.', 26 | $num 27 | ), 28 | SwatString::numberFormat($num) 29 | )); 30 | 31 | $this->app->messages->add($message); 32 | } 33 | 34 | // build phase 35 | 36 | protected function buildInternal() 37 | { 38 | parent::buildInternal(); 39 | 40 | $item_list = $this->getItemList('integer'); 41 | 42 | $dep = new AdminListDependency(); 43 | $dep->setTitle(Admin::_('component'), Admin::_('components')); 44 | $dep->entries = AdminListDependency::queryEntries( 45 | $this->app->db, 46 | 'AdminComponent', 47 | 'integer:id', 48 | null, 49 | 'text:title', 50 | 'displayorder, title', 51 | 'id in (' . $item_list . ')', 52 | AdminDependency::DELETE 53 | ); 54 | 55 | $dep_subcomponents = new AdminSummaryDependency(); 56 | $dep_subcomponents->setTitle( 57 | Admin::_('sub-component'), 58 | Admin::_('sub-components') 59 | ); 60 | 61 | $dep_subcomponents->summaries = AdminSummaryDependency::querySummaries( 62 | $this->app->db, 63 | 'AdminSubComponent', 64 | 'integer:id', 65 | 'integer:component', 66 | 'component in (' . $item_list . ')', 67 | AdminDependency::DELETE 68 | ); 69 | 70 | $dep->addDependency($dep_subcomponents); 71 | 72 | $message = $this->ui->getWidget('confirmation_message'); 73 | $message->content = $dep->getMessage(); 74 | $message->content_type = 'text/xml'; 75 | 76 | if ($dep->getStatusLevelCount(AdminDependency::DELETE) == 0) { 77 | $this->switchToCancelButton(); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Admin/pages/AdminDBEdit.php: -------------------------------------------------------------------------------- 1 | app->db); 23 | $this->saveDBData(); 24 | $transaction->commit(); 25 | } catch (SwatDBException $e) { 26 | $transaction->rollback(); 27 | 28 | $message = new SwatMessage( 29 | Admin::_( 30 | 'A database error has occurred. The item was not saved.' 31 | ), 32 | 'system-error' 33 | ); 34 | 35 | $e->processAndContinue(); 36 | $relocate = false; 37 | } catch (SwatException $e) { 38 | $message = new SwatMessage( 39 | Admin::_( 40 | 'An error has occurred. The item was not saved.' 41 | ), 42 | 'system-error' 43 | ); 44 | 45 | $e->processAndContinue(); 46 | $relocate = false; 47 | } 48 | 49 | if ($message !== null) { 50 | $this->app->messages->add($message); 51 | } 52 | 53 | return $relocate; 54 | } 55 | 56 | /** 57 | * Save the data from the database. 58 | * 59 | * This method is called to save data from the widgets after processing. 60 | * Sub-classes should implement this method and perform whatever actions 61 | * are necessary to store the data. Widgets can be accessed through the 62 | * $ui class variable. 63 | */ 64 | abstract protected function saveDBData(): void; 65 | 66 | // build phase 67 | 68 | protected function loadData() 69 | { 70 | $this->loadDBData(); 71 | 72 | return true; 73 | } 74 | 75 | /** 76 | * Load the data from the database. 77 | * 78 | * This method is called to load data to be edited into the widgets. 79 | * Sub-classes should implement this method and perform whatever actions 80 | * are necessary to obtain the data. Widgets can be accessed through the 81 | * $ui class variable. 82 | */ 83 | abstract protected function loadDBData(); 84 | } 85 | -------------------------------------------------------------------------------- /sql/admin-changes.sql: -------------------------------------------------------------------------------- 1 | insert into AdminSection (id, title, description, displayorder, visible) values (1, 'Admin Settings', null, 100, true); 2 | SELECT setval('Adminsection_id_seq', max(id)) FROM AdminSection; 3 | 4 | INSERT INTO AdminComponent (id, shortname, title, description, displayorder, section, enabled, visible) 5 | VALUES ( 6 | 1, 7 | 'AdminUser', 8 | 'Admin Users', 9 | E'Manage who can log into the admin.\n\nAlso set group membership for admin users.', 10 | 4, 11 | 1, 12 | true, 13 | true 14 | ); 15 | 16 | INSERT INTO AdminComponent (id, shortname, title, description, displayorder, section, enabled, visible) 17 | VALUES ( 18 | 2, 19 | 'AdminGroup', 20 | 'Admin Groups', 21 | E'Manage admin group membership, and admin group component access.', 22 | 5, 23 | 1, 24 | true, 25 | true 26 | ); 27 | 28 | INSERT INTO AdminComponent (id, shortname, title, description, displayorder, section, enabled, visible) 29 | VALUES ( 30 | 3, 31 | 'AdminSection', 32 | 'Admin Sections', 33 | E'Manage the sections in the admin menu.', 34 | 3, 35 | 1, 36 | true, 37 | true 38 | ); 39 | 40 | INSERT INTO AdminComponent (id, shortname, title, description, displayorder, section, enabled, visible) 41 | VALUES ( 42 | 4, 43 | 'AdminComponent', 44 | 'Admin Components', 45 | E'Manage the available tools in the admin.\n\nOrganize tools in sections, and set admin group access for specific tools.', 46 | 1, 47 | 1, 48 | true, 49 | true 50 | ); 51 | 52 | INSERT INTO AdminComponent (id, shortname, title, description, displayorder, section, enabled, visible) 53 | VALUES ( 54 | 5, 55 | 'AdminSubComponent', 56 | 'Admin Sub-Components', 57 | NULL, 58 | 2, 59 | 1, 60 | true, 61 | false 62 | ); 63 | 64 | INSERT INTO AdminComponent (id, shortname, title, description, displayorder, section, enabled, visible) 65 | VALUES ( 66 | 5, 67 | 'Front', 68 | 'Front Page', 69 | NULL, 70 | 0, 71 | 1, 72 | true, 73 | false 74 | ); 75 | 76 | SELECT setval('admincomponent_id_seq', max(id)) FROM AdminComponent; 77 | 78 | -- default sub-components 79 | insert into AdminSubComponent (id, component, title, shortname, visible, displayorder) values (1, 1, 'Login History', 'LoginHistory', true, 0); 80 | 81 | SELECT setval('adminsubcomponent_id_seq', max(id)) FROM AdminSubComponent; 82 | 83 | -- default admin groups 84 | insert into AdminGroup (id, title) values (1, 'Default Group'); 85 | 86 | SELECT setval('admingroup_id_seq', max(id)) FROM AdminGroup; 87 | 88 | -- default AdminComponentAdminGroupBinding bindings 89 | insert into AdminComponentAdminGroupBinding (component, groupnum) 90 | select AdminComponent.id, AdminGroup.id from AdminComponent, AdminGroup; 91 | 92 | 93 | -------------------------------------------------------------------------------- /Admin/components/AdminUser/Delete.php: -------------------------------------------------------------------------------- 1 | getItemList('integer'); 18 | $sql = sprintf( 19 | 'delete from AdminUser where id in (%s) and id != %s', 20 | $item_list, 21 | $this->app->db->quote($this->app->session->getUserId(), 'integer') 22 | ); 23 | 24 | $num = SwatDB::exec($this->app->db, $sql); 25 | 26 | $message = new SwatMessage(sprintf( 27 | Admin::ngettext( 28 | 'One admin user has been deleted.', 29 | '%s admin users have been deleted.', 30 | $num 31 | ), 32 | SwatString::numberFormat($num) 33 | )); 34 | 35 | $this->app->messages->add($message); 36 | } 37 | 38 | // build phase 39 | 40 | public function buildInternal() 41 | { 42 | parent::buildInternal(); 43 | 44 | $item_list = $this->getItemList('integer'); 45 | 46 | $where_clause = sprintf( 47 | 'id in (%s) and id != %s', 48 | $item_list, 49 | $this->app->db->quote($this->app->session->getUserId(), 'integer') 50 | ); 51 | 52 | $dep = new AdminListDependency(); 53 | $dep->setTitle(Admin::_('admin user'), Admin::_('admin users')); 54 | $dep->entries = AdminListDependency::queryEntries( 55 | $this->app->db, 56 | 'AdminUser', 57 | 'integer:id', 58 | null, 59 | 'text:name', 60 | 'name', 61 | $where_clause, 62 | AdminDependency::DELETE 63 | ); 64 | 65 | $message = $this->ui->getWidget('confirmation_message'); 66 | $message->content = $dep->getMessage(); 67 | $message->content_type = 'text/xml'; 68 | 69 | if ($dep->getItemCount() == 0) { 70 | $this->switchToCancelButton(); 71 | } 72 | 73 | // display can't delete self message if current account is in selection 74 | if ($this->items->contains($this->app->session->getUserId())) { 75 | $header = new SwatHtmlTag('h3'); 76 | $header->setContent( 77 | Admin::_('You cannot delete your own account.') 78 | ); 79 | 80 | $message->content .= $header; 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Admin/components/AdminUser/LoginHistory.php: -------------------------------------------------------------------------------- 1 | ui->loadFromXML(__DIR__ . '/login-history.xml'); 16 | 17 | // set a default order on the table view 18 | $index_view = $this->ui->getWidget('index_view'); 19 | $index_view->setDefaultOrderbyColumn( 20 | $index_view->getColumn('login_date') 21 | ); 22 | 23 | $this->navbar->createEntry(Admin::_('Login History')); 24 | } 25 | 26 | // process phase 27 | 28 | protected function processInternal() 29 | { 30 | parent::processInternal(); 31 | 32 | $instance_id = $this->app->getInstanceId(); 33 | 34 | $pager = $this->ui->getWidget('pager'); 35 | $sql = 'select count(id) from AdminUserHistory where instance %s %s'; 36 | $pager->total_records = SwatDB::queryOne($this->app->db, sprintf( 37 | $sql, 38 | SwatDB::equalityOperator($instance_id), 39 | $this->app->db->quote($instance_id, 'integer') 40 | )); 41 | 42 | $pager->link = 'AdminUser/LoginHistory'; 43 | $pager->process(); 44 | } 45 | 46 | // build phase 47 | 48 | protected function buildInternal() 49 | { 50 | parent::buildInternal(); 51 | 52 | // set default time zone 53 | $date_column = 54 | $this->ui->getWidget('index_view')->getColumn('login_date'); 55 | 56 | $date_renderer = $date_column->getRendererByPosition(); 57 | $date_renderer->display_time_zone = $this->app->default_time_zone; 58 | } 59 | 60 | protected function getTableModel(SwatView $view): ?SwatTableModel 61 | { 62 | $pager = $this->ui->getWidget('pager'); 63 | $this->app->db->setLimit($pager->page_size, $pager->current_record); 64 | 65 | $instance_id = $this->app->getInstanceId(); 66 | 67 | $sql = 'select usernum, login_date, login_agent, remote_ip, email, 68 | AdminUser.name 69 | from AdminUserHistory 70 | inner join AdminUser on AdminUser.id = AdminUserHistory.usernum 71 | where instance %s %s 72 | order by %s'; 73 | 74 | $sql = sprintf( 75 | $sql, 76 | SwatDB::equalityOperator($instance_id), 77 | $this->app->db->quote($instance_id, 'integer'), 78 | $this->getOrderByClause($view, 'login_date desc') 79 | ); 80 | 81 | return SwatDB::query($this->app->db, $sql); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Admin/components/AdminUser/Details.php: -------------------------------------------------------------------------------- 1 | ui->loadFromXML(__DIR__ . '/details.xml'); 20 | 21 | $this->id = intval(SiteApplication::initVar('id')); 22 | 23 | $this->initUser(); 24 | 25 | // set a default order on the table view 26 | $index_view = $this->ui->getWidget('index_view'); 27 | $index_view->setDefaultOrderbyColumn( 28 | $index_view->getColumn('login_date') 29 | ); 30 | } 31 | 32 | protected function initUser() 33 | { 34 | $class_name = SwatDBClassMap::get(AdminUser::class); 35 | $this->user = new $class_name(); 36 | $this->user->setDatabase($this->app->db); 37 | 38 | if (!$this->user->load($this->id)) { 39 | throw new AdminNotFoundException( 40 | sprintf( 41 | Admin::_('User with id "%s" not found.'), 42 | $this->id 43 | ) 44 | ); 45 | } 46 | } 47 | 48 | // build phase 49 | 50 | protected function buildInternal() 51 | { 52 | parent::buildInternal(); 53 | 54 | $frame = $this->ui->getWidget('index_frame'); 55 | $frame->subtitle = sprintf($this->user->name); 56 | 57 | // rebuild the navbar 58 | $this->navbar->createEntry('Login History', 'AdminUser/LoginHistory'); 59 | $this->navbar->createEntry($this->user->name); 60 | 61 | // set default time zone 62 | $date_column = 63 | $this->ui->getWidget('index_view')->getColumn('login_date'); 64 | 65 | $date_renderer = $date_column->getRendererByPosition(); 66 | $date_renderer->display_time_zone = $this->app->default_time_zone; 67 | } 68 | 69 | protected function getTableModel(SwatView $view): AdminUserHistoryWrapper 70 | { 71 | $instance_id = $this->app->getInstanceId(); 72 | 73 | $sql = 'select * from AdminUserHistory 74 | where usernum = %s and instance %s %s 75 | order by %s'; 76 | 77 | $sql = sprintf( 78 | $sql, 79 | $this->app->db->quote($this->user->id, 'integer'), 80 | SwatDB::equalityOperator($instance_id), 81 | $this->app->db->quote($instance_id, 'integer'), 82 | $this->getOrderByClause($view, 'login_date desc') 83 | ); 84 | 85 | return SwatDB::query($this->app->db, $sql, AdminUserHistoryWrapper::class); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Admin/components/AdminSubComponent/Order.php: -------------------------------------------------------------------------------- 1 | parent = SiteApplication::initVar('parent'); 20 | $form = $this->ui->getWidget('order_form'); 21 | $form->addHiddenField('parent', $this->parent); 22 | } 23 | 24 | // process phase 25 | 26 | protected function saveIndex($id, $index) 27 | { 28 | SwatDB::updateColumn( 29 | $this->app->db, 30 | 'AdminSubComponent', 31 | 'integer:displayorder', 32 | $index, 33 | 'integer:id', 34 | [$id] 35 | ); 36 | } 37 | 38 | // build phase 39 | 40 | protected function buildInternal() 41 | { 42 | parent::buildInternal(); 43 | 44 | $frame = $this->ui->getWidget('order_frame'); 45 | $frame->title = Admin::_('Order Sub-Components'); 46 | 47 | // rebuild the navbar 48 | $parent_title = SwatDB::queryOneFromTable( 49 | $this->app->db, 50 | 'AdminComponent', 51 | 'text:title', 52 | 'id', 53 | $this->parent 54 | ); 55 | 56 | // pop two entries because the AdminDBOrder base class adds an entry 57 | $this->navbar->popEntries(2); 58 | $this->navbar->createEntry('Admin Components', 'AdminComponent'); 59 | $this->navbar->createEntry( 60 | $parent_title, 61 | 'AdminComponent/Details?id=' . $this->parent 62 | ); 63 | 64 | $this->navbar->createEntry('Order Sub-Components'); 65 | } 66 | 67 | protected function loadData() 68 | { 69 | $where_clause = sprintf( 70 | 'component = %s', 71 | $this->app->db->quote($this->parent, 'integer') 72 | ); 73 | 74 | $order_list = $this->ui->getWidget('order'); 75 | $order_list_options = SwatDB::getOptionArray( 76 | $this->app->db, 77 | 'AdminSubComponent', 78 | 'title', 79 | 'id', 80 | 'displayorder, title', 81 | $where_clause 82 | ); 83 | 84 | $order_list->addOptionsByArray($order_list_options); 85 | 86 | $sql = 'select sum(displayorder) from AdminSubComponent 87 | where ' . $where_clause; 88 | 89 | $sum = SwatDB::queryOne($this->app->db, $sql, 'integer'); 90 | $radio_list = $this->ui->getWidget('options'); 91 | $radio_list->value = ($sum == 0) ? 'auto' : 'custom'; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Admin/AdminMenuStore.php: -------------------------------------------------------------------------------- 1 | getMessage()); 30 | } 31 | 32 | $this->sections = []; 33 | $section = null; 34 | $component = null; 35 | 36 | do { 37 | while ($row = $rs->fetchRow(MDB2_FETCHMODE_OBJECT)) { 38 | if ($section === null || $row->section != $section->id) { 39 | $section = new AdminMenuSection( 40 | $row->section, 41 | $row->section_title 42 | ); 43 | 44 | $this->sections[$row->section] = $section; 45 | } 46 | 47 | if ($component === null 48 | || $row->component_id != $component->id) { 49 | $component = new AdminMenuComponent( 50 | $row->component_id, 51 | $row->shortname, 52 | $row->title, 53 | $row->description 54 | ); 55 | 56 | $section->components[$row->shortname] = $component; 57 | } 58 | 59 | if ($row->subcomponent_shortname != '') { 60 | $subcomponent = new AdminMenuSubcomponent( 61 | $row->subcomponent_shortname, 62 | $row->subcomponent_title 63 | ); 64 | 65 | $component->subcomponents[$row->subcomponent_shortname] = 66 | $subcomponent; 67 | } 68 | } 69 | } while ($rs->nextResult()); 70 | } 71 | 72 | public function getComponentByName($name) 73 | { 74 | foreach ($this->sections as $section) { 75 | foreach ($section->components as $component) { 76 | if ($component->shortname === $name) { 77 | return $component; 78 | } 79 | } 80 | } 81 | 82 | return null; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Admin/components/AdminSection/Index.php: -------------------------------------------------------------------------------- 1 | ui->loadFromXML(__DIR__ . '/index.xml'); 15 | } 16 | 17 | // process phase 18 | 19 | protected function processActions(SwatView $view, SwatActions $actions) 20 | { 21 | $num = count($view->checked_items); 22 | $message = null; 23 | 24 | switch ($actions->selected->id) { 25 | case 'delete': 26 | $this->app->replacePage('AdminSection/Delete'); 27 | $this->app->getPage()->setItems($view->getSelection()); 28 | break; 29 | 30 | case 'show': 31 | SwatDB::updateColumn( 32 | $this->app->db, 33 | 'AdminSection', 34 | 'boolean:visible', 35 | true, 36 | 'id', 37 | $view->getSelection() 38 | ); 39 | 40 | $message = new SwatMessage(sprintf( 41 | Admin::ngettext( 42 | 'One section has been shown.', 43 | '%s sections have been shown.', 44 | $num 45 | ), 46 | SwatString::numberFormat($num) 47 | )); 48 | 49 | break; 50 | 51 | case 'hide': 52 | SwatDB::updateColumn( 53 | $this->app->db, 54 | 'AdminSection', 55 | 'boolean:visible', 56 | false, 57 | 'id', 58 | $view->getSelection() 59 | ); 60 | 61 | $message = new SwatMessage(sprintf( 62 | Admin::ngettext( 63 | 'One section has been hidden.', 64 | '%s sections have been hidden.', 65 | $num 66 | ), 67 | SwatString::numberFormat($num) 68 | )); 69 | 70 | break; 71 | } 72 | 73 | if ($message !== null) { 74 | $this->app->messages->add($message); 75 | } 76 | } 77 | 78 | // build phase 79 | 80 | protected function getTableModel(SwatView $view): AdminSectionWrapper 81 | { 82 | $sql = 'select id, title, visible 83 | from AdminSection 84 | order by displayorder'; 85 | 86 | $sections = SwatDB::query($this->app->db, $sql, AdminSectionWrapper::class); 87 | 88 | if (count($sections) == 0) { 89 | $this->ui->getWidget('order_tool')->visible = false; 90 | } 91 | 92 | return $sections; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /Admin/components/AdminSubComponent/Delete.php: -------------------------------------------------------------------------------- 1 | parent = $parent; 16 | } 17 | 18 | // process phase 19 | 20 | protected function processDBData(): void 21 | { 22 | parent::processDBData(); 23 | 24 | $item_list = $this->getItemList('integer'); 25 | 26 | $sql = 'delete from AdminSubComponent where id in (%s)'; 27 | 28 | $sql = sprintf($sql, $item_list); 29 | $num = SwatDB::exec($this->app->db, $sql); 30 | 31 | $message = new SwatMessage(sprintf( 32 | Admin::ngettext( 33 | 'One sub-component has been deleted.', 34 | '%s sub-components have been deleted.', 35 | $num 36 | ), 37 | SwatString::numberFormat($num) 38 | )); 39 | 40 | $this->app->messages->add($message); 41 | } 42 | 43 | // build phase 44 | 45 | protected function buildInternal() 46 | { 47 | parent::buildInternal(); 48 | 49 | $item_list = $this->getItemList('integer'); 50 | 51 | $dep = new AdminListDependency(); 52 | $dep->setTitle(Admin::_('sub-component'), Admin::_('sub-components')); 53 | $dep->entries = AdminListDependency::queryEntries( 54 | $this->app->db, 55 | 'AdminSubComponent', 56 | 'integer:id', 57 | null, 58 | 'text:title', 59 | 'displayorder, title', 60 | 'id in (' . $item_list . ')', 61 | AdminDependency::DELETE 62 | ); 63 | 64 | $message = $this->ui->getWidget('confirmation_message'); 65 | $message->content = $dep->getMessage(); 66 | $message->content_type = 'text/xml'; 67 | 68 | if ($dep->getStatusLevelCount(AdminDependency::DELETE) == 0) { 69 | $this->switchToCancelButton(); 70 | } 71 | 72 | // rebuild the navbar 73 | $component_title = SwatDB::queryOneFromTable( 74 | $this->app->db, 75 | 'AdminComponent', 76 | 'text:title', 77 | 'id', 78 | $this->parent 79 | ); 80 | 81 | // pop two entries because the AdminDBOrder base class adds an entry 82 | $this->navbar->popEntries(2); 83 | $this->navbar->createEntry( 84 | Admin::_('Admin Components'), 85 | 'AdminComponent' 86 | ); 87 | 88 | $this->navbar->createEntry( 89 | $component_title, 90 | 'AdminComponent/Details?id=' . $this->parent 91 | ); 92 | 93 | $this->navbar->createEntry(Admin::_('Delete Sub-Component(s)')); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Admin/components/AdminUser/Reactivate.php: -------------------------------------------------------------------------------- 1 | navbar->popEntry(); 17 | $this->navbar->createEntry(Admin::_('Reactivate')); 18 | } 19 | 20 | // process phase 21 | 22 | protected function processDBData(): void 23 | { 24 | parent::processDBData(); 25 | 26 | $locale = SwatI18NLocale::get(); 27 | 28 | $now = new SwatDate(); 29 | $now->toUTC(); 30 | 31 | $sql = sprintf( 32 | 'update AdminUser set activation_date = %s 33 | where id in (%s)', 34 | $this->app->db->quote($now->getDate(), 'date'), 35 | $this->getItemList('integer') 36 | ); 37 | 38 | $num = SwatDB::exec($this->app->db, $sql); 39 | 40 | $this->app->messages->add( 41 | new SwatMessage( 42 | sprintf( 43 | Admin::ngettext( 44 | 'One admin user has been reactivated.', 45 | '%s admin users have been reactivated.', 46 | $num 47 | ), 48 | $locale->formatNumber($num) 49 | ) 50 | ) 51 | ); 52 | } 53 | 54 | // build phase 55 | 56 | public function buildInternal() 57 | { 58 | parent::buildInternal(); 59 | 60 | $users = SwatDB::query( 61 | $this->app->db, 62 | sprintf( 63 | 'select name from AdminUser 64 | where id in (%s) 65 | order by name asc', 66 | $this->getItemList('integer') 67 | ), 68 | AdminUserWrapper::class 69 | ); 70 | 71 | ob_start(); 72 | 73 | $h3_tag = new SwatHtmlTag('h3'); 74 | $h3_tag->setContent( 75 | Admin::ngettext( 76 | 'Reactivate the following admin user?', 77 | 'Reactivate the following admin users?', 78 | count($users) 79 | ) 80 | ); 81 | $h3_tag->display(); 82 | 83 | if (count($users) > 0) { 84 | echo ''; 91 | } 92 | 93 | $message = $this->ui->getWidget('confirmation_message'); 94 | $message->content = ob_get_clean(); 95 | $message->content_type = 'text/xml'; 96 | 97 | if (count($users) === 0) { 98 | $this->switchToCancelButton(); 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /Admin/components/AdminSection/Delete.php: -------------------------------------------------------------------------------- 1 | getItemList('integer'); 19 | $sql = sprintf($sql, $item_list); 20 | $num = SwatDB::exec($this->app->db, $sql); 21 | 22 | $message = new SwatMessage(sprintf( 23 | Admin::ngettext( 24 | 'One admin section has been deleted.', 25 | '%s admin sections have been deleted.', 26 | $num 27 | ), 28 | SwatString::numberFormat($num) 29 | )); 30 | 31 | $this->app->messages->add($message); 32 | } 33 | 34 | // build phase 35 | 36 | protected function buildInternal() 37 | { 38 | parent::buildInternal(); 39 | 40 | $item_list = $this->getItemList('integer'); 41 | 42 | $dep = new AdminListDependency(); 43 | $dep->setTitle(Admin::_('section'), Admin::_('sections')); 44 | $dep->entries = AdminListDependency::queryEntries( 45 | $this->app->db, 46 | 'AdminSection', 47 | 'integer:id', 48 | null, 49 | 'text:title', 50 | 'title', 51 | 'id in (' . $item_list . ')', 52 | AdminDependency::DELETE 53 | ); 54 | 55 | $dep_components = new AdminListDependency(); 56 | $dep_components->setTitle( 57 | Admin::_('component'), 58 | Admin::_('components') 59 | ); 60 | 61 | $dep_components->entries = AdminListDependency::queryEntries( 62 | $this->app->db, 63 | 'AdminComponent', 64 | 'integer:id', 65 | 'integer:section', 66 | 'text:title', 67 | 'title', 68 | 'section in (' . $item_list . ')', 69 | AdminDependency::DELETE 70 | ); 71 | 72 | $dep->addDependency($dep_components); 73 | 74 | $dep_subcomponents = new AdminSummaryDependency(); 75 | $dep_subcomponents->setTitle( 76 | Admin::_('sub-component'), 77 | Admin::_('sub-components') 78 | ); 79 | 80 | $dep_subcomponents->summaries = AdminSummaryDependency::querySummaries( 81 | $this->app->db, 82 | 'AdminSubComponent', 83 | 'integer:id', 84 | 'integer:component', 85 | 'component in (select id from AdminComponent where section in (' . 86 | $item_list . '))', 87 | AdminDependency::DELETE 88 | ); 89 | 90 | $dep_components->addDependency($dep_subcomponents); 91 | 92 | $message = $this->ui->getWidget('confirmation_message'); 93 | $message->content = $dep->getMessage(); 94 | $message->content_type = 'text/xml'; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Admin/pages/AdminDBDelete.php: -------------------------------------------------------------------------------- 1 | single_delete = (bool) SiteApplication::initVar( 24 | 'single_delete', 25 | false, 26 | SiteApplication::VAR_POST 27 | ); 28 | 29 | $id = SiteApplication::initVar('id', null, SiteApplication::VAR_GET); 30 | 31 | if ($id !== null) { 32 | $this->setItems([$id]); 33 | $this->single_delete = true; 34 | $form = $this->ui->getWidget('confirmation_form'); 35 | $form->addHiddenField('single_delete', $this->single_delete); 36 | } 37 | 38 | $yes_button = $this->ui->getWidget('yes_button'); 39 | $yes_button->setFromStock('delete'); 40 | 41 | $no_button = $this->ui->getWidget('no_button'); 42 | $no_button->setFromStock('cancel'); 43 | 44 | $this->navbar->popEntry(); 45 | $this->navbar->createEntry(Admin::_('Delete')); 46 | } 47 | 48 | // process phase 49 | 50 | protected function generateMessage(Throwable $e) 51 | { 52 | if ($e instanceof SwatDBException) { 53 | $message = new SwatMessage( 54 | Admin::_('A database error has occured. 55 | The item(s) were not deleted.'), 56 | 'system-error' 57 | ); 58 | } else { 59 | $message = new SwatMessage( 60 | Admin::_('An error has occured. 61 | The item(s) were not deleted.'), 62 | 'system-error' 63 | ); 64 | } 65 | 66 | $this->app->messages->add($message); 67 | } 68 | 69 | protected function relocate() 70 | { 71 | $form = $this->ui->getWidget('confirmation_form'); 72 | $url = $form->getHiddenField(self::RELOCATE_URL_FIELD); 73 | // always search for only the component name with slashes. This helps 74 | // for cases where the page name has a match to the component name we're 75 | // checking against, and if the trailing slash is missing, we're 76 | // redirecting to the index anyway. 77 | $component = '/' . $this->getComponentName() . '/'; 78 | 79 | // if the component name is in the relocate url, relocate to the 80 | // the component index page to prevent relocating to a details page that 81 | // will no longer exist. 82 | if (mb_strpos($url, $component) === false 83 | || $form->button->id == 'no_button') { 84 | parent::relocate(); 85 | } else { 86 | $this->app->relocate($this->getComponentName()); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Admin/components/AdminUser/edit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | User 7 | 8 | 9 | Name 10 | 11 | true 12 | 100 13 | 14 | 15 | 16 | Email 17 | 18 | true 19 | 50 20 | 21 | 22 | 23 | Enabled 24 | 25 | true 26 | 27 | 28 | 29 | 2FA Enabled 30 | 31 | false 32 | 33 | 34 | 35 | Belongs to Groups 36 | 37 | 38 | 39 | Belongs to Sites 40 | false 41 | 42 | 43 | 44 | Change Password 45 | false 46 | 47 | New Password 48 | 49 | false 50 | 4 51 | 52 | 53 | 54 | Confirm New Password 55 | Leave Password fields blank for them to remain the same. 56 | 57 | 58 | 59 | Force User to Change Password on Login 60 | 61 | true 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /Admin/AdminControlCellRenderer.php: -------------------------------------------------------------------------------- 1 | visible) { 31 | return; 32 | } 33 | 34 | if ($this->stock_id === null) { 35 | $this->setFromStock('details', false); 36 | } else { 37 | $this->setFromStock($this->stock_id, false); 38 | } 39 | 40 | parent::render(); 41 | } 42 | 43 | /** 44 | * Sets the values of this control cell renderer to a stock type. 45 | * 46 | * Valid stock type ids are: 47 | * 48 | * - details (default) 49 | * - edit 50 | * 51 | * @param string $stock_id the identifier of the stock type to use 52 | * @param bool $overwrite_properties whether to overwrite properties if 53 | * they are already set 54 | * 55 | * @throws SwatUndefinedStockTypeException 56 | */ 57 | public function setFromStock($stock_id, $overwrite_properties = true) 58 | { 59 | switch ($stock_id) { 60 | case 'details': 61 | $title = Admin::_('View Details'); 62 | $alt = Admin::_('Details'); 63 | $image = 'packages/admin/images/admin-generic-document.png'; 64 | $width = '22'; 65 | $height = '22'; 66 | break; 67 | 68 | case 'edit': 69 | $title = Admin::_('Edit'); 70 | $alt = Admin::_('Edit'); 71 | $image = 'packages/admin/images/admin-edit.png'; 72 | $width = '22'; 73 | $height = '22'; 74 | break; 75 | 76 | default: 77 | throw new SwatUndefinedStockTypeException( 78 | "Stock type with id of '{$stock_id}' not found.", 79 | 0, 80 | $stock_id 81 | ); 82 | } 83 | 84 | if ($overwrite_properties || ($this->title === null)) { 85 | $this->title = $title; 86 | } 87 | 88 | if ($overwrite_properties || ($this->alt === null)) { 89 | $this->alt = $alt; 90 | } 91 | 92 | if ($overwrite_properties || ($this->image === null)) { 93 | $this->image = $image; 94 | } 95 | 96 | if ($overwrite_properties || ($this->width === null)) { 97 | $this->width = $width; 98 | } 99 | 100 | if ($overwrite_properties || ($this->height === null)) { 101 | $this->height = $height; 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /Admin/components/AdminUser/index.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Admin Users 7 | 8 | 9 | New User 10 | AdminUser/Edit 11 | create 12 | 13 | 14 | 15 | false 16 | 17 | 18 | 19 | 20 | 21 | id 22 | 23 | 24 | 25 | is_active 26 | 27 | active_title 28 | 29 | 30 | 31 | Email 32 | 33 | email 34 | AdminUser/Edit?id=%s 35 | id 36 | person 37 | 38 | 39 | 40 | Name 41 | 42 | name 43 | 44 | 45 | 46 | Enabled 47 | 48 | enabled 49 | 50 | 51 | 52 | 2FA Enabled 53 | 54 | two_fa_enabled 55 | 56 | 57 | 58 | Last Login 59 | 60 | last_login 61 | 62 | 63 | history 64 | id 65 | last_login 66 | 67 | 68 | 69 | 70 | 71 | reactivate… 72 | 73 | 74 | delete… 75 | 76 | 77 | enable 78 | 79 | 80 | disable 81 | 82 | 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /Admin/components/AdminSite/ChangePassword.php: -------------------------------------------------------------------------------- 1 | app, AdminLoginTemplate::class); 15 | } 16 | 17 | protected function initInternal() 18 | { 19 | parent::initInternal(); 20 | 21 | $this->ui->loadFromXML(__DIR__ . '/change-password.xml'); 22 | 23 | $confirm = $this->ui->getWidget('confirm_password'); 24 | $confirm->password_widget = $this->ui->getWidget('password'); 25 | 26 | $form = $this->ui->getWidget('change_password_form'); 27 | $form->action = 'AdminSite/ChangePassword'; 28 | 29 | // remember where we came from 30 | $form->addHiddenField('relocate_uri', $this->app->getUri()); 31 | } 32 | 33 | // process phase 34 | 35 | protected function processInternal() 36 | { 37 | parent::processInternal(); 38 | 39 | $crypt = $this->app->getModule('SiteCryptModule'); 40 | 41 | $form = $this->ui->getWidget('change_password_form'); 42 | if ($form->isProcessed()) { 43 | $this->validatePasswords(); 44 | if (!$form->hasMessage()) { 45 | $password = $this->ui->getWidget('password')->value; 46 | 47 | $user = $this->app->session->user; 48 | $user->setPasswordHash($crypt->generateHash($password)); 49 | $user->force_change_password = false; 50 | $user->save(); 51 | 52 | $this->app->session->login($user->email, $password); 53 | 54 | $message = new SwatMessage( 55 | Admin::_('Your password has been updated.') 56 | ); 57 | 58 | $this->app->messages->add($message); 59 | 60 | // go back where we came from 61 | $uri = $form->getHiddenField('relocate_uri'); 62 | $this->app->relocate($uri); 63 | } 64 | } 65 | } 66 | 67 | protected function validatePasswords() 68 | { 69 | $user = $this->app->session->user; 70 | 71 | $old_password = $this->ui->getWidget('old_password')->value; 72 | $new_password = $this->ui->getWidget('password')->value; 73 | 74 | if ($old_password === null || $new_password === null) { 75 | return; 76 | } 77 | 78 | // make sure old password is not the same as new password 79 | if ($old_password == $new_password) { 80 | $message = new SwatMessage(Admin::_('Your new password can not be ' . 81 | 'the same as your old password'), 'error'); 82 | 83 | $this->ui->getWidget('password')->addMessage($message); 84 | } 85 | 86 | $crypt = $this->app->getModule('SiteCryptModule'); 87 | 88 | $password_hash = $user->password; 89 | $password_salt = $user->password_salt; 90 | 91 | // make sure old password is correct 92 | if (!$crypt->verifyHash( 93 | $old_password, 94 | $password_hash, 95 | $password_salt 96 | )) { 97 | $this->ui->getWidget('old_password')->addMessage( 98 | new SwatMessage( 99 | Admin::_('Your old password is not correct'), 100 | 'error' 101 | ) 102 | ); 103 | } 104 | } 105 | 106 | // finalize phase 107 | 108 | public function finalize() 109 | { 110 | parent::finalize(); 111 | 112 | $this->layout->addHtmlHeadEntry( 113 | 'packages/admin/styles/admin-change-password-page.css' 114 | ); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /Admin/components/AdminComponent/Edit.php: -------------------------------------------------------------------------------- 1 | initSections(); 40 | $this->initGroups(); 41 | } 42 | 43 | protected function initSections() 44 | { 45 | $section_flydown = $this->ui->getWidget('section'); 46 | $section_flydown_options = SwatDB::getOptionArray( 47 | $this->app->db, 48 | 'AdminSection', 49 | 'title', 50 | 'id', 51 | 'displayorder' 52 | ); 53 | 54 | $section_flydown->addOptionsByArray($section_flydown_options); 55 | } 56 | 57 | protected function initGroups() 58 | { 59 | $group_list = $this->ui->getWidget('groups'); 60 | $group_list_options = SwatDB::getOptionArray( 61 | $this->app->db, 62 | 'AdminGroup', 63 | 'title', 64 | 'id', 65 | 'title' 66 | ); 67 | 68 | $group_list->addOptionsByArray($group_list_options); 69 | } 70 | 71 | // process phase 72 | 73 | protected function validate(): void 74 | { 75 | $shortname_widget = $this->ui->getWidget('shortname'); 76 | $shortname = $shortname_widget->value; 77 | 78 | $should_validate_shortname = (!$this->isNew() || $shortname != ''); 79 | if ($should_validate_shortname 80 | && !$this->validateShortname($shortname)) { 81 | $message = new SwatMessage( 82 | Admin::_('Shortname already exists and must be unique.'), 83 | 'error' 84 | ); 85 | 86 | $shortname_widget->addMessage($message); 87 | } 88 | } 89 | 90 | protected function postSaveObject() 91 | { 92 | $this->updateGroupBindings(); 93 | } 94 | 95 | protected function updateGroupBindings() 96 | { 97 | $group_list = $this->ui->getWidget('groups'); 98 | 99 | SwatDB::updateBinding( 100 | $this->app->db, 101 | 'AdminComponentAdminGroupBinding', 102 | 'component', 103 | $this->getObject()->id, 104 | 'groupnum', 105 | $group_list->values, 106 | 'AdminGroup', 107 | 'id' 108 | ); 109 | } 110 | 111 | protected function getSavedMessagePrimaryContent() 112 | { 113 | return sprintf( 114 | Admin::_('Component “%s” has been saved.'), 115 | $this->getObject()->title 116 | ); 117 | } 118 | 119 | // build phase 120 | 121 | protected function loadObject() 122 | { 123 | parent::loadObject(); 124 | 125 | if (!$this->isNew()) { 126 | $this->loadGroupBindings(); 127 | } 128 | } 129 | 130 | protected function loadGroupBindings() 131 | { 132 | $group_list = $this->ui->getWidget('groups'); 133 | $group_list->values = SwatDB::queryColumn( 134 | $this->app->db, 135 | 'AdminComponentAdminGroupBinding', 136 | 'groupnum', 137 | 'component', 138 | $this->getObject()->id 139 | ); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /Admin/components/AdminComponent/index.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Components 7 | 8 | 9 | New Component 10 | AdminComponent/Edit 11 | create 12 | 13 | 14 | 15 | 16 | 17 | section 18 | section 19 | 20 | section_title 21 | 22 | 23 | Change Order 24 | AdminComponent/Order?parent=%s 25 | section 26 | section_order_sensitive 27 | 28 | 29 | 30 | 31 | id 32 | 33 | 34 | 35 | Title 36 | 37 | title 38 | AdminComponent/Details?id=%s 39 | id 40 | 41 | 42 | 43 | Short Name 44 | 45 | shortname 46 | 47 | 48 | 49 | Show in Menu 50 | 51 | visible 52 | 53 | 54 | 55 | Enabled 56 | 57 | enabled 58 | 59 | 60 | 61 | 62 | 63 | delete… 64 | 65 | 66 | show 67 | 68 | 69 | hide 70 | 71 | 72 | enable 73 | 74 | 75 | disable 76 | 77 | 78 | change section… 79 | 80 | false 81 | 82 | 83 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /Admin/pages/AdminConfirmation.php: -------------------------------------------------------------------------------- 1 | ui->loadFromXML($this->getUiXml()); 20 | $this->navbar->createEntry(Admin::_('Confirmation')); 21 | } 22 | 23 | protected function getUiXml() 24 | { 25 | return __DIR__ . '/confirmation.xml'; 26 | } 27 | 28 | // process phase 29 | 30 | protected function processInternal() 31 | { 32 | parent::processInternal(); 33 | 34 | $form = $this->ui->getWidget('confirmation_form'); 35 | $relocate = false; 36 | 37 | if ($form->isAuthenticated()) { 38 | if ($form->isProcessed()) { 39 | if ($this->ui->getWidget('no_button')->hasBeenClicked()) { 40 | // if the no (aka cancel) button has been hit, relocate even 41 | // if the form doesn't validate or process. 42 | $relocate = true; 43 | } elseif (!$form->hasMessage()) { 44 | // only process the response if the form validated and we're 45 | // not already relocating. 46 | $this->processResponse(); 47 | 48 | $relocate = true; 49 | } 50 | } 51 | 52 | if ($relocate) { 53 | $this->relocate(); 54 | } 55 | } else { 56 | $message = new SwatMessage(Admin::_('There is a problem with the ' . 57 | 'information submitted.'), 'warning'); 58 | 59 | $message->secondary_content = 60 | Admin::_('In order to ensure your security, we were unable to ' . 61 | 'process your request. Please try again.'); 62 | 63 | $this->app->messages->add($message); 64 | } 65 | } 66 | 67 | /** 68 | * Process the response. 69 | * 70 | * This method is called to perform whatever processing is required in 71 | * response to the button clicked. It is called by the 72 | * {@link AdminConfirmation::process} method. 73 | */ 74 | abstract protected function processResponse(): void; 75 | 76 | /** 77 | * Relocates to the previous page after processsing confirmation response. 78 | */ 79 | protected function relocate() 80 | { 81 | $form = $this->ui->getWidget('confirmation_form'); 82 | $url = $form->getHiddenField(self::RELOCATE_URL_FIELD); 83 | $this->app->relocate($url); 84 | } 85 | 86 | // build phase 87 | 88 | protected function buildInternal() 89 | { 90 | parent::buildInternal(); 91 | $this->buildForm(); 92 | $this->buildMessages(); 93 | } 94 | 95 | protected function buildForm() 96 | { 97 | $form = $this->ui->getWidget('confirmation_form'); 98 | $form->action = $this->source; 99 | 100 | if ($form->getHiddenField(self::RELOCATE_URL_FIELD) === null) { 101 | $url = $this->getRefererURL(); 102 | $form->addHiddenField(self::RELOCATE_URL_FIELD, $url); 103 | } 104 | } 105 | 106 | /** 107 | * Switches the default yes/no buttons to a cancel button. 108 | * 109 | * Call this method if a confirmation page is displayed and the desired 110 | * action of the user is impossible. 111 | */ 112 | protected function switchToCancelButton() 113 | { 114 | $this->ui->getWidget('yes_button')->visible = false; 115 | $this->ui->getWidget('no_button')->title = Admin::_('Cancel'); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /Admin/components/AdminSubComponent/Edit.php: -------------------------------------------------------------------------------- 1 | initAdminComponent(); 39 | } 40 | 41 | protected function initAdminComponent() 42 | { 43 | if ($this->isNew()) { 44 | $parent_id = SiteApplication::initVar('parent'); 45 | 46 | if ($parent_id === null) { 47 | throw new AdminNotFoundException( 48 | 'Must supply a Component ID for newly created ' . 49 | 'Sub-Compoenets.' 50 | ); 51 | } 52 | 53 | $class_name = SwatDBClassMap::get(AdminComponent::class); 54 | $this->admin_component = new $class_name(); 55 | $this->admin_component->setDatabase($this->app->db); 56 | 57 | if (!$this->admin_component->load($parent_id)) { 58 | throw new AdminNotFoundException( 59 | sprintf( 60 | 'Component with id "%s" not found.', 61 | $parent_id 62 | ) 63 | ); 64 | } 65 | } else { 66 | $this->admin_component = $this->getObject()->component; 67 | } 68 | } 69 | 70 | // process phase 71 | 72 | protected function validate(): void 73 | { 74 | $shortname_widget = $this->ui->getWidget('shortname'); 75 | $shortname = $shortname_widget->value; 76 | 77 | $should_validate_shortname = (!$this->isNew() || $shortname != ''); 78 | if ($should_validate_shortname 79 | && !$this->validateShortname($shortname)) { 80 | $message = new SwatMessage( 81 | Admin::_('Shortname already exists and must be unique.'), 82 | 'error' 83 | ); 84 | 85 | $shortname_widget->addMessage($message); 86 | } 87 | } 88 | 89 | protected function updateObject() 90 | { 91 | parent::updateObject(); 92 | 93 | if ($this->isNew()) { 94 | $this->getObject()->component = $this->admin_component; 95 | } 96 | } 97 | 98 | protected function getSavedMessagePrimaryContent() 99 | { 100 | return sprintf( 101 | Admin::_('Sub-Component “%s” has been saved.'), 102 | $this->getObject()->title 103 | ); 104 | } 105 | 106 | // build phase 107 | 108 | protected function buildForm() 109 | { 110 | parent::buildForm(); 111 | 112 | $form = $this->ui->getWidget('edit_form'); 113 | $form->addHiddenField('parent', $this->admin_component->id); 114 | } 115 | 116 | protected function buildNavBar() 117 | { 118 | $this->navbar->popEntry(); 119 | 120 | $this->navbar->createEntry( 121 | Admin::_('Admin Components'), 122 | 'AdminComponent' 123 | ); 124 | 125 | $this->navbar->createEntry( 126 | $this->admin_component->title, 127 | sprintf( 128 | 'AdminComponent/Details?id=%s', 129 | $this->admin_component->id 130 | ) 131 | ); 132 | 133 | $this->navbar->createEntry( 134 | ($this->isNew()) 135 | ? Admin::_('Add Sub-Component') 136 | : Admin::_('Edit Sub-Component') 137 | ); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /Admin/components/AdminSite/profile.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Login Settings 7 | 8 | 9 | Name 10 | 11 | true 12 | 13 | 14 | 15 | Email 16 | 17 | 50 18 | true 19 | 20 | 21 | 22 | false 23 | 2FA Enabled 24 | 25 | Yes, two factor authentication is enabled for this account. 26 | 27 | 28 | 29 | Change Password 30 | false 31 | 32 | Old Password 33 | 34 | false 35 | 4 36 | 37 | 38 | 39 | New Password 40 | 41 | false 42 | 4 43 | 44 | 45 | 46 | Confirm New Password 47 | 48 | 49 | 50 | 51 | Two Factor Authentication 52 | false 53 | false 54 | 55 | Step 1: Scan the QR code below using an authenticator app like 1Password, LastPass, Google Authenticator, or Authy. 56 | 57 | Scan QR Code 58 | 59 | text/xml 60 | 61 | 62 | 63 | 64 | Step 2: After scanning the QR code, your app will display a verification code which you will enter below 65 | 66 | Verification code 67 | 68 | false 69 | 6 70 | 7 71 | 72 | 73 | 74 | 75 | 76 | 77 | Update Profile 78 | 79 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /www/styles/admin-layout.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: #f7f5f3; 3 | color: #333; 4 | margin: 0; 5 | padding: 0; 6 | font-family: "Open Sans", sans-serif; 7 | } 8 | 9 | /* turn off 10px left/right margins from default YUI grid */ 10 | #doc, #doc2, #doc3 { 11 | margin: 0; 12 | } 13 | 14 | a:link, 15 | a:visited { 16 | color: #1976d2; 17 | } 18 | 19 | a:hover { 20 | color: #333; 21 | } 22 | 23 | a:active { 24 | color: #000; 25 | } 26 | 27 | img { 28 | border: 0; 29 | } 30 | 31 | /* YUI Reset basics */ 32 | strong { 33 | font-weight: bold; 34 | } 35 | 36 | em { 37 | font-style: italic; 38 | } 39 | 40 | p { 41 | margin: 0 0 1em 0; 42 | } 43 | 44 | #yui-main .yui-b { 45 | margin-top: 1em; 46 | } 47 | 48 | /* layouts with menu on left */ 49 | .yui-t1 #yui-main .yui-b, 50 | .yui-t2 #yui-main .yui-b, 51 | .yui-t3 #yui-main .yui-b { 52 | margin-right: 1em; 53 | } 54 | 55 | /* layouts with menu on right */ 56 | .yui-t4 #yui-main .yui-b, 57 | .yui-t5 #yui-main .yui-b, 58 | .yui-t6 #yui-main .yui-b { 59 | margin-left: 1em; 60 | } 61 | 62 | #doc3 { 63 | min-width: 74.923em; 64 | } 65 | 66 | 67 | /* Header Styles */ 68 | 69 | #hd { 70 | padding: 2px 0; 71 | color: #888; 72 | } 73 | 74 | #hd a { color: #555; } 75 | #hd a:hover { color: #777; } 76 | #hd a:active { color: #888; } 77 | 78 | #hd #admin-syslinks { 79 | float: right; 80 | margin: 0 0 0 2em; 81 | position: relative; 82 | top: -2px; 83 | z-index: 1; 84 | } 85 | 86 | #hd h1 { 87 | margin: 0; 88 | padding: 0; 89 | font-size: 100%; 90 | display: inline; 91 | font-weight: normal; 92 | } 93 | 94 | 95 | /* Basic styles to build up from YUI reset */ 96 | 97 | h1, h2, h3, h4, h5, h6{ font-weight: bold; } 98 | 99 | h1 { font-size: 152%; } 100 | h2 { font-size: 136%; } 101 | h3 { font-size: 122%; } 102 | h4 { font-size: 107%; } 103 | h5 { font-size: 92%; } 104 | h6 { font-size: 85%; } 105 | 106 | 107 | /* Content Styles */ 108 | 109 | .admin-light { 110 | color: #666; 111 | } 112 | 113 | 114 | /* Confirmation dialogs */ 115 | 116 | #confirmation_container { 117 | background: url(../images/admin-dialog-question.png) -4px -4px no-repeat; 118 | padding: 0 0 0 52px; 119 | min-height: 48px; 120 | margin: 8px 0 32px; 121 | } 122 | 123 | #confirmation_container > h3 { 124 | margin-top: 0; 125 | padding-top: 9px; 126 | } 127 | 128 | #confirmation_frame ul { 129 | list-style-type: disc; 130 | padding-left: 24px; 131 | } 132 | 133 | #confirmation_frame ul li { 134 | margin-left: 16px; 135 | margin-top: 0.1em; 136 | } 137 | 138 | 139 | /* Errors */ 140 | 141 | .swat-frame.admin-exception-container .swat-frame-contents { 142 | background-image: url(../images/admin-dialog-exception.png); 143 | background-position: 12px 20px; 144 | background-repeat: no-repeat; 145 | padding: 24px 16px 48px 68px; 146 | min-height: 48px; 147 | } 148 | 149 | .admin-exception-container .swat-exception { 150 | margin-left: 0; 151 | margin-right: 0; 152 | } 153 | 154 | 155 | /* Logout button */ 156 | 157 | address { font-style: normal; } 158 | 159 | #logout { 160 | display: inline; 161 | margin: 0; 162 | padding: 0; 163 | } 164 | 165 | #logout div.swat-form-field, 166 | #logout div.swat-form-field-contents { 167 | display: inline; 168 | margin: 0; 169 | } 170 | 171 | #logout input.swat-button { 172 | margin: 0; 173 | padding: 4px 8px; 174 | vertical-align: middle; 175 | font-size: 11px; 176 | } 177 | 178 | #admin-identifier { 179 | vertical-align: middle; 180 | } 181 | 182 | #admin-navbar { 183 | position: relative; 184 | top: 2px; 185 | vertical-align: middle; 186 | } 187 | 188 | .clearfix:after { 189 | content: "."; 190 | display: block; 191 | height: 0; 192 | clear: both; 193 | visibility: hidden; 194 | } 195 | 196 | .clearfix { 197 | display: inline-block; 198 | } 199 | 200 | @media (-webkit-min-device-pixel-ratio: 1.5), 201 | (min--moz-device-pixel-ratio: 1.5), 202 | (-o-min-device-pixel-ratio: 3/2), 203 | (min-resolution: 1.5dppx) { 204 | 205 | #confirmation_container { 206 | background-image: url(../images/admin-dialog-question@2x.png); 207 | background-size: 48px 48px 208 | } 209 | 210 | .swat-frame.admin-exception-container .swat-frame-contents { 211 | background-image: url(../images/admin-dialog-exception@2x.png); 212 | background-size: 48px 48px 213 | } 214 | 215 | } 216 | -------------------------------------------------------------------------------- /Admin/components/AdminGroup/Edit.php: -------------------------------------------------------------------------------- 1 | initUsers(); 35 | $this->initComponents(); 36 | } 37 | 38 | protected function initUsers() 39 | { 40 | $user_list = $this->ui->getWidget('users'); 41 | $user_list_options = SwatDB::getOptionArray( 42 | $this->app->db, 43 | 'AdminUser', 44 | 'name', 45 | 'id', 46 | 'name' 47 | ); 48 | 49 | $user_list->addOptionsByArray($user_list_options); 50 | } 51 | 52 | protected function initComponents() 53 | { 54 | $component_list = $this->ui->getWidget('components'); 55 | $component_list_options = SwatDB::getGroupedOptionArray( 56 | $this->app->db, 57 | 'AdminComponent', 58 | 'title', 59 | 'id', 60 | 'AdminSection', 61 | 'title', 62 | 'id', 63 | 'section', 64 | 'AdminSection.displayorder, AdminSection.title, ' . 65 | 'AdminComponent.displayorder, AdminComponent.title' 66 | ); 67 | 68 | $component_list->setTree($component_list_options); 69 | } 70 | 71 | // process phase 72 | 73 | protected function postSaveObject() 74 | { 75 | $this->updateUserBindings(); 76 | $this->updateComponentBindings(); 77 | } 78 | 79 | protected function updateUserBindings() 80 | { 81 | $user_list = $this->ui->getWidget('users'); 82 | 83 | SwatDB::updateBinding( 84 | $this->app->db, 85 | 'AdminUserAdminGroupBinding', 86 | 'groupnum', 87 | $this->getObject()->id, 88 | 'usernum', 89 | $user_list->values, 90 | 'AdminUser', 91 | 'id' 92 | ); 93 | } 94 | 95 | protected function updateComponentBindings() 96 | { 97 | $component_list = $this->ui->getWidget('components'); 98 | 99 | SwatDB::updateBinding( 100 | $this->app->db, 101 | 'AdminComponentAdminGroupBinding', 102 | 'groupnum', 103 | $this->getObject()->id, 104 | 'component', 105 | $component_list->values, 106 | 'AdminComponent', 107 | 'id' 108 | ); 109 | } 110 | 111 | protected function getSavedMessagePrimaryContent() 112 | { 113 | return sprintf( 114 | Admin::_('Group “%s” has been saved.'), 115 | $this->getObject()->title 116 | ); 117 | } 118 | 119 | // build phase 120 | 121 | protected function loadObject() 122 | { 123 | parent::loadObject(); 124 | 125 | if (!$this->isNew()) { 126 | $this->loadUserBindings(); 127 | $this->loadComponentBindings(); 128 | } 129 | } 130 | 131 | protected function loadUserBindings() 132 | { 133 | $user_list = $this->ui->getWidget('users'); 134 | $user_list->values = SwatDB::queryColumn( 135 | $this->app->db, 136 | 'AdminUserAdminGroupBinding', 137 | 'usernum', 138 | 'groupnum', 139 | $this->getObject()->id 140 | ); 141 | } 142 | 143 | protected function loadComponentBindings() 144 | { 145 | $component_list = $this->ui->getWidget('components'); 146 | $component_list->values = SwatDB::queryColumn( 147 | $this->app->db, 148 | 'AdminComponentAdminGroupBinding', 149 | 'component', 150 | 'groupnum', 151 | $this->getObject()->id 152 | ); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /po/preprocess.php: -------------------------------------------------------------------------------- 1 |