first commit

This commit is contained in:
2025-06-17 11:53:18 +02:00
commit 9f0f7ba12b
8804 changed files with 1369176 additions and 0 deletions

View File

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<access component="com_users">
<section name="component">
<action name="core.admin" title="JACTION_ADMIN" />
<action name="core.options" title="JACTION_OPTIONS" />
<action name="core.manage" title="JACTION_MANAGE" />
<action name="core.create" title="JACTION_CREATE" />
<action name="core.delete" title="JACTION_DELETE" />
<action name="core.edit" title="JACTION_EDIT" />
<action name="core.edit.state" title="JACTION_EDITSTATE" />
<action name="core.edit.value" title="JACTION_EDITVALUE" />
</section>
<section name="category">
<action name="core.create" title="JACTION_CREATE" />
<action name="core.delete" title="JACTION_DELETE" />
<action name="core.edit" title="JACTION_EDIT" />
<action name="core.edit.state" title="JACTION_EDITSTATE" />
<action name="core.edit.own" title="JACTION_EDITOWN" />
</section>
<section name="fieldgroup">
<action name="core.create" title="JACTION_CREATE" />
<action name="core.delete" title="JACTION_DELETE" />
<action name="core.edit" title="JACTION_EDIT" />
<action name="core.edit.state" title="JACTION_EDITSTATE" />
<action name="core.edit.own" title="JACTION_EDITOWN" />
<action name="core.edit.value" title="JACTION_EDITVALUE" />
</section>
<section name="field">
<action name="core.delete" title="JACTION_DELETE" />
<action name="core.edit" title="JACTION_EDIT" />
<action name="core.edit.state" title="JACTION_EDITSTATE" />
<action name="core.edit.value" title="JACTION_EDITVALUE" />
</section>
</access>

View File

@ -0,0 +1,441 @@
<?xml version="1.0" encoding="UTF-8"?>
<config>
<help key="Users:_Options"/>
<inlinehelp button="show"/>
<fieldset
name="user_options"
label="COM_USERS_CONFIG_USER_OPTIONS" >
<field
name="allowUserRegistration"
type="radio"
label="COM_USERS_CONFIG_FIELD_ALLOWREGISTRATION_LABEL"
layout="joomla.form.field.radio.switcher"
default="0"
>
<option value="0">JNO</option>
<option value="1">JYES</option>
</field>
<field
name="new_usertype"
type="usergrouplist"
label="COM_USERS_CONFIG_FIELD_NEW_USER_TYPE_LABEL"
default="2"
checksuperusergroup="1"
/>
<field
name="guest_usergroup"
type="usergrouplist"
label="COM_USERS_CONFIG_FIELD_GUEST_USER_GROUP_LABEL"
default="1"
checksuperusergroup="1"
/>
<field
name="sendpassword"
type="radio"
label="COM_USERS_CONFIG_FIELD_SENDPASSWORD_LABEL"
layout="joomla.form.field.radio.switcher"
default="1"
>
<option value="0">JNO</option>
<option value="1">JYES</option>
</field>
<field
name="useractivation"
type="list"
label="COM_USERS_CONFIG_FIELD_USERACTIVATION_LABEL"
default="2"
validate="options"
>
<option value="0">JNONE</option>
<option value="1">COM_USERS_CONFIG_FIELD_USERACTIVATION_OPTION_SELFACTIVATION</option>
<option value="2">COM_USERS_CONFIG_FIELD_USERACTIVATION_OPTION_ADMINACTIVATION</option>
</field>
<field
name="mail_to_admin"
type="radio"
label="COM_USERS_CONFIG_FIELD_MAILTOADMIN_LABEL"
layout="joomla.form.field.radio.switcher"
default="1"
>
<option value="0">JNO</option>
<option value="1">JYES</option>
</field>
<field
name="captcha"
type="plugins"
label="COM_USERS_CONFIG_FIELD_CAPTCHA_LABEL"
folder="captcha"
filter="cmd"
useglobal="true"
>
<option value="0">JOPTION_DO_NOT_USE</option>
</field>
<field
name="frontend_userparams"
type="radio"
label="COM_USERS_CONFIG_FIELD_FRONTEND_USERPARAMS_LABEL"
layout="joomla.form.field.radio.switcher"
default="1"
>
<option value="0">JHIDE</option>
<option value="1">JSHOW</option>
</field>
<field
name="site_language"
type="radio"
label="COM_USERS_CONFIG_FIELD_FRONTEND_LANG_LABEL"
layout="joomla.form.field.radio.switcher"
default="0"
showon="frontend_userparams:1"
>
<option value="0">JHIDE</option>
<option value="1">JSHOW</option>
</field>
<field
name="change_login_name"
type="radio"
label="COM_USERS_CONFIG_FIELD_CHANGEUSERNAME_LABEL"
layout="joomla.form.field.radio.switcher"
default="0"
>
<option value="0">JNO</option>
<option value="1">JYES</option>
</field>
</fieldset>
<fieldset
name="domain_options"
label="COM_USERS_CONFIG_DOMAIN_OPTIONS"
description="COM_USERS_CONFIG_FIELD_DOMAINS_DESC"
>
<field
name="domains"
type="subform"
label="COM_USERS_CONFIG_FIELD_DOMAINS_LABEL"
hiddenLabel="true"
multiple="true"
layout="joomla.form.field.subform.repeatable-table"
formsource="administrator/components/com_users/forms/config_domain.xml"
/>
</fieldset>
<fieldset
name="password_options"
label="COM_USERS_CONFIG_PASSWORD_OPTIONS" >
<field
name="reset_count"
type="number"
label="COM_USERS_CONFIG_FIELD_FRONTEND_RESET_COUNT_LABEL"
filter="integer"
min="0"
max="20"
step="1"
default="10"
validate="number"
/>
<field
name="reset_time"
type="number"
label="COM_USERS_CONFIG_FIELD_FRONTEND_RESET_TIME_LABEL"
filter="integer"
min="1"
max="24"
step="1"
default="1"
validate="number"
/>
<field
name="minimum_length"
type="number"
label="COM_USERS_CONFIG_FIELD_MINIMUM_PASSWORD_LENGTH"
filter="integer"
min="8"
step="1"
default="12"
validate="number"
/>
<field
name="minimum_integers"
type="number"
label="COM_USERS_CONFIG_FIELD_MINIMUM_INTEGERS"
filter="integer"
min="0"
step="1"
default="0"
validate="number"
/>
<field
name="minimum_symbols"
type="number"
label="COM_USERS_CONFIG_FIELD_MINIMUM_SYMBOLS"
filter="integer"
min="0"
step="1"
default="0"
validate="number"
/>
<field
name="minimum_uppercase"
type="number"
label="COM_USERS_CONFIG_FIELD_MINIMUM_UPPERCASE"
filter="integer"
min="0"
step="1"
default="0"
validate="number"
/>
<field
name="minimum_lowercase"
type="number"
label="COM_USERS_CONFIG_FIELD_MINIMUM_LOWERCASE"
filter="integer"
min="0"
step="1"
default="0"
validate="number"
/>
</fieldset>
<fieldset
name="multifactorauth"
label="COM_USERS_CONFIG_MULTIFACTORAUTH_SETTINGS_LABEL"
description="COM_USERS_CONFIG_MULTIFACTORAUTH_SETTINGS_DESC"
addfieldprefix="Joomla\Component\Users\Administrator\Field"
>
<field
name="allowed_positions_frontend"
type="ModulesPosition"
label="COM_USERS_CONFIG_ALLOWED_POSITIONS_FRONTEND_LABEL"
description="COM_USERS_CONFIG_ALLOWED_POSITIONS_FRONTEND_DESC"
default=""
layout="joomla.form.field.list-fancy-select"
client="site"
multiple="1"
/>
<field
name="frontend_show_title"
type="radio"
label="COM_USERS_CONFIG_FRONTEND_SHOW_TITLE_LABEL"
description="COM_USERS_CONFIG_FRONTEND_SHOW_TITLE_DESC"
layout="joomla.form.field.radio.switcher"
default="1"
>
<option value="0">JNO</option>
<option value="1">JYES</option>
</field>
<field
name="allowed_positions_backend"
type="ModulesPosition"
label="COM_USERS_CONFIG_ALLOWED_POSITIONS_BACKEND_LABEL"
description="COM_USERS_CONFIG_ALLOWED_POSITIONS_BACKEND_DESC"
default=""
layout="joomla.form.field.list-fancy-select"
client="administrator"
multiple="1"
/>
<field
name="neverMFAUserGroups"
type="UserGroupList"
label="COM_USERS_CONFIG_NEVERMFAUSERGROUPS_LABEL"
description="COM_USERS_CONFIG_NEVERMFAUSERGROUPS_DESC"
layout="joomla.form.field.list-fancy-select"
checksuperusergroup="1"
default=""
multiple="1"
>
<option value="0">COM_USERS_CONFIG_LBL_NOGROUP</option>
</field>
<field
name="forceMFAUserGroups"
type="UserGroupList"
label="COM_USERS_CONFIG_FORCEMFAUSERGROUPS_LABEL"
description="COM_USERS_CONFIG_FORCEMFAUSERGROUPS_DESC"
layout="joomla.form.field.list-fancy-select"
checksuperusergroup="1"
default=""
multiple="1"
>
<option value="0">COM_USERS_CONFIG_LBL_NOGROUP</option>
</field>
<field
name="captive_template"
type="templatestyle"
label="COM_USERS_CONFIG_FRONTEND_CAPTIVE_TEMPLATE_LABEL"
description="COM_USERS_CONFIG_FRONTEND_CAPTIVE_TEMPLATE_DESC"
client="site"
>
<option value="">JOPTION_USE_DEFAULT</option>
</field>
<field
name="mfaonsilent"
type="radio"
label="COM_USERS_CONFIG_MFAONSILENT_LABEL"
description="COM_USERS_CONFIG_MFAONSILENT_DESC"
layout="joomla.form.field.radio.switcher"
default="0"
>
<option value="0">JNO</option>
<option value="1">JYES</option>
</field>
<field
name="silentresponses"
type="text"
label="COM_USERS_CONFIG_SILENTRESPONSES_LABEL"
description="COM_USERS_CONFIG_SILENTRESPONSES_DESC"
default="cookie, passwordless"
showon="mfaonsilent:0"
/>
<field
name="mfaredirectonlogin"
type="radio"
label="COM_USERS_CONFIG_REDIRECTONLOGIN_LABEL"
description="COM_USERS_CONFIG_REDIRECTONLOGIN_DESC"
layout="joomla.form.field.radio.switcher"
default="0"
>
<option value="0">JNO</option>
<option value="1">JYES</option>
</field>
<field
name="mfaredirecturl"
type="text"
label="COM_USERS_CONFIG_REDIRECTURL_LABEL"
description="COM_USERS_CONFIG_REDIRECTURL_DESC"
default=""
showon="mfaredirectonlogin:1"
/>
<field
name="mfatrycount"
type="number"
label="COM_USERS_CONFIG_MFATRYCOUNT_LABEL"
filter="integer"
min="0"
max="20"
step="1"
default="10"
validate="number"
/>
<field
name="mfatrytime"
type="number"
label="COM_USERS_CONFIG_MFATRYTIME_LABEL"
filter="integer"
min="1"
max="24"
step="1"
default="1"
validate="number"
/>
</fieldset>
<fieldset
name="user_notes_history"
label="COM_USERS_CONFIG_FIELD_NOTES_HISTORY" >
<field
name="save_history"
type="radio"
label="JGLOBAL_SAVE_HISTORY_OPTIONS_LABEL"
layout="joomla.form.field.radio.switcher"
default="0"
>
<option value="0">JNO</option>
<option value="1">JYES</option>
</field>
<field
name="history_limit"
type="number"
label="JGLOBAL_HISTORY_LIMIT_OPTIONS_LABEL"
filter="integer"
default="5"
showon="save_history:1"
/>
</fieldset>
<fieldset
name="massmail"
label="COM_USERS_MASS_MAIL"
description="COM_USERS_MASS_MAIL_DESC">
<field
name="mailSubjectPrefix"
type="text"
label="COM_USERS_CONFIG_FIELD_SUBJECT_PREFIX_LABEL"
/>
<field
name="mailBodySuffix"
type="textarea"
label="COM_USERS_CONFIG_FIELD_MAILBODY_SUFFIX_LABEL"
rows="5"
cols="30"
/>
</fieldset>
<fieldset name="integration"
label="JGLOBAL_INTEGRATION_LABEL"
description="COM_USERS_CONFIG_INTEGRATION_SETTINGS_DESC"
>
<fieldset name="integration_customfields"
label="JGLOBAL_FIELDS_TITLE"
>
<field
name="custom_fields_enable"
type="radio"
label="JGLOBAL_CUSTOM_FIELDS_ENABLE_LABEL"
layout="joomla.form.field.radio.switcher"
default="1"
>
<option value="0">JNO</option>
<option value="1">JYES</option>
</field>
</fieldset>
</fieldset>
<fieldset
name="permissions"
label="JCONFIG_PERMISSIONS_LABEL"
>
<field
name="rules"
type="rules"
label="JCONFIG_PERMISSIONS_LABEL"
filter="rules"
validate="rules"
component="com_users"
section="component"
/>
</fieldset>
</config>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<form>
<help key="User_Notes:_New_or_Edit_Category" />
<listhelp key="User_Notes:_Categories" />
</form>

View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<form>
<fieldset>
<field
name="name"
type="text"
label="COM_USERS_CONFIG_FIELD_DOMAIN_NAME_LABEL"
description="COM_USERS_CONFIG_FIELD_DOMAIN_NAME_DESC"
required="true"
/>
<field
name="rule"
type="radio"
label="COM_USERS_CONFIG_FIELD_DOMAIN_RULE_LABEL"
description="COM_USERS_CONFIG_FIELD_DOMAIN_RULE_DESC"
layout="joomla.form.field.radio.switcher"
required="true"
default="0"
filter="integer"
validate="options"
>
<option value="0">COM_USERS_CONFIG_FIELD_DOMAIN_RULE_OPTION_DISALLOW</option>
<option value="1">COM_USERS_CONFIG_FIELD_DOMAIN_RULE_OPTION_ALLOW</option>
</field>
</fieldset>
</form>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<form>
<fields name="params" label="COM_FIELDS_FIELD_BASIC_LABEL">
<fieldset name="basic">
<field
name="display"
type="hidden"
default="2"
/>
</fieldset>
</fields>
</form>

View File

@ -0,0 +1,69 @@
<?xml version="1.0" encoding="UTF-8"?>
<form addfieldprefix="Joomla\Component\Users\Administrator\Field">
<fields name="filter">
<field
name="search"
type="text"
inputmode="search"
label="COM_USERS_SEARCH_ASSETS"
description="COM_USERS_SEARCH_IN_ASSETS"
hint="JSEARCH_FILTER"
/>
<field
name="component"
type="Components"
label="COM_USERS_OPTION_LABEL_COMPONENT"
class="js-select-submit-on-change"
>
<option value="">COM_USERS_OPTION_SELECT_COMPONENT</option>
</field>
<field
name="level_start"
type="Levels"
label="COM_USERS_OPTION_LABEL_LEVEL_START"
class="js-select-submit-on-change"
>
<option value="">COM_USERS_OPTION_SELECT_LEVEL_START</option>
</field>
<field
name="level_end"
type="Levels"
label="COM_USERS_OPTION_LABEL_LEVEL_END"
class="js-select-submit-on-change"
>
<option value="">COM_USERS_OPTION_SELECT_LEVEL_END</option>
</field>
</fields>
<fields name="list">
<field
name="fullordering"
type="list"
label="JGLOBAL_SORT_BY"
class="js-select-submit-on-change"
default="a.lft ASC"
validate="options"
>
<option value="">JGLOBAL_SORT_BY</option>
<option value="a.title ASC">COM_USERS_HEADING_ASSET_TITLE_ASC</option>
<option value="a.title DESC">COM_USERS_HEADING_ASSET_TITLE_DESC</option>
<option value="a.name ASC">COM_USERS_HEADING_ASSET_NAME_ASC</option>
<option value="a.name DESC">COM_USERS_HEADING_ASSET_NAME_DESC</option>
<option value="a.lft ASC">COM_USERS_HEADING_LFT_ASC</option>
<option value="a.lft DESC">COM_USERS_HEADING_LFT_DESC</option>
<option value="a.id ASC">JGRID_HEADING_ID_ASC</option>
<option value="a.id DESC">JGRID_HEADING_ID_DESC</option>
</field>
<field
name="limit"
type="limitbox"
label="JGLOBAL_LIST_LIMIT"
default="25"
class="js-select-submit-on-change"
/>
</fields>
</form>

View File

@ -0,0 +1,64 @@
<?xml version="1.0" encoding="UTF-8"?>
<form addfieldprefix="Joomla\Component\Users\Administrator\Field">
<fields name="filter">
<field
name="search"
type="text"
inputmode="search"
label="COM_USERS_SEARCH_ASSETS"
description="COM_USERS_SEARCH_IN_ASSETS"
hint="JSEARCH_FILTER"
/>
<field
name="component"
type="Components"
label="COM_USERS_OPTION_LABEL_COMPONENT"
class="js-select-submit-on-change"
>
<option value="">COM_USERS_OPTION_SELECT_COMPONENT</option>
</field>
<field
name="level_start"
type="Levels"
label="COM_USERS_OPTION_LABEL_LEVEL_START"
class="js-select-submit-on-change"
>
<option value="">COM_USERS_OPTION_SELECT_LEVEL_START</option>
</field>
<field
name="level_end"
type="Levels"
label="COM_USERS_OPTION_LABEL_LEVEL_END"
class="js-select-submit-on-change"
>
<option value="">COM_USERS_OPTION_SELECT_LEVEL_END</option>
</field>
</fields>
<fields name="list">
<field
name="fullordering"
type="list"
label="JGLOBAL_SORT_BY"
class="js-select-submit-on-change"
default="a.lft ASC"
validate="options"
>
<option value="">JGLOBAL_SORT_BY</option>
<option value="a.title ASC">COM_USERS_HEADING_ASSET_TITLE_ASC</option>
<option value="a.title DESC">COM_USERS_HEADING_ASSET_TITLE_DESC</option>
<option value="a.name ASC">COM_USERS_HEADING_ASSET_NAME_ASC</option>
<option value="a.name DESC">COM_USERS_HEADING_ASSET_NAME_DESC</option>
<option value="a.lft ASC">COM_USERS_HEADING_LFT_ASC</option>
<option value="a.lft DESC">COM_USERS_HEADING_LFT_DESC</option>
<option value="a.id ASC">JGRID_HEADING_ID_ASC</option>
<option value="a.id DESC">JGRID_HEADING_ID_DESC</option>
</field>
<field
name="limit"
type="limitbox"
label="JGLOBAL_LIST_LIMIT"
default="25"
class="js-select-submit-on-change"
/>
</fields>
</form>

View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<form>
<fields name="filter">
<field
name="search"
type="text"
inputmode="search"
label="COM_USERS_SEARCH_GROUPS_LABEL"
description="COM_USERS_SEARCH_IN_GROUPS"
hint="JSEARCH_FILTER"
/>
</fields>
<fields name="list">
<field
name="fullordering"
type="list"
label="JGLOBAL_SORT_BY"
class="js-select-submit-on-change"
default="a.lft ASC"
validate="options"
>
<option value="">JGLOBAL_SORT_BY</option>
<option value="a.lft ASC">JGRID_HEADING_ORDERING_ASC</option>
<option value="a.lft DESC">JGRID_HEADING_ORDERING_DESC</option>
<option value="a.title ASC">COM_USERS_HEADING_GROUP_TITLE_ASC</option>
<option value="a.title DESC">COM_USERS_HEADING_GROUP_TITLE_DESC</option>
<option value="a.id ASC">JGRID_HEADING_ID_ASC</option>
<option value="a.id DESC">JGRID_HEADING_ID_DESC</option>
</field>
<field
name="limit"
type="limitbox"
label="JGLOBAL_LIST_LIMIT"
default="25"
class="js-select-submit-on-change"
/>
</fields>
</form>

View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<form>
<fields name="filter">
<field
name="search"
type="text"
inputmode="search"
label="COM_USERS_SEARCH_ACCESS_LEVELS"
description="COM_USERS_SEARCH_IN_LEVEL_NAME"
hint="JSEARCH_FILTER"
/>
</fields>
<fields name="list">
<field
name="fullordering"
type="list"
label="JGLOBAL_SORT_BY"
class="js-select-submit-on-change"
default="a.ordering ASC"
validate="options"
>
<option value="">JGLOBAL_SORT_BY</option>
<option value="a.ordering ASC">JGRID_HEADING_ORDERING_ASC</option>
<option value="a.ordering DESC">JGRID_HEADING_ORDERING_DESC</option>
<option value="a.title ASC">COM_USERS_HEADING_LEVEL_NAME_ASC</option>
<option value="a.title DESC">COM_USERS_HEADING_LEVEL_NAME_DESC</option>
<option value="a.id ASC">JGRID_HEADING_ID_ASC</option>
<option value="a.id DESC">JGRID_HEADING_ID_DESC</option>
</field>
<field
name="limit"
type="limitbox"
label="JGLOBAL_LIST_LIMIT"
default="25"
class="js-select-submit-on-change"
/>
</fields>
</form>

View File

@ -0,0 +1,73 @@
<?xml version="1.0" encoding="UTF-8"?>
<form>
<fields name="filter">
<field
name="search"
type="text"
inputmode="search"
label="COM_USERS_SEARCH_USER_NOTES"
description="COM_USERS_SEARCH_IN_NOTE_TITLE"
hint="JSEARCH_FILTER"
/>
<field
name="published"
type="status"
label="JSTATUS"
class="js-select-submit-on-change"
>
<option value="">JOPTION_SELECT_PUBLISHED</option>
</field>
<field
name="category_id"
type="category"
label="JCATEGORY"
extension="com_users"
class="js-select-submit-on-change"
>
<option value="">JOPTION_SELECT_CATEGORY</option>
</field>
<field
name="level"
type="integer"
label="JGLOBAL_MAXLEVEL_LABEL"
first="1"
last="10"
step="1"
languages="*"
class="js-select-submit-on-change"
>
<option value="">JOPTION_SELECT_MAX_LEVELS</option>
</field>
</fields>
<fields name="list">
<field
name="fullordering"
type="list"
label="JGLOBAL_SORT_BY"
class="js-select-submit-on-change"
default="a.review_time DESC"
validate="options"
>
<option value="">JGLOBAL_SORT_BY</option>
<option value="a.state ASC">JSTATUS_ASC</option>
<option value="a.state DESC">JSTATUS_DESC</option>
<option value="a.subject ASC">COM_USERS_HEADING_SUBJECT_ASC</option>
<option value="a.subject DESC">COM_USERS_HEADING_SUBJECT_DESC</option>
<option value="c.title ASC">COM_USERS_HEADING_CATEGORY_ASC</option>
<option value="c.title DESC">COM_USERS_HEADING_CATEGORY_DESC</option>
<option value="u.name ASC">COM_USERS_HEADING_USER_ASC</option>
<option value="u.name DESC">COM_USERS_HEADING_USER_DESC</option>
<option value="a.review_time ASC">COM_USERS_HEADING_REVIEW_ASC</option>
<option value="a.review_time DESC">COM_USERS_HEADING_REVIEW_DESC</option>
<option value="a.id ASC">JGRID_HEADING_ID_ASC</option>
<option value="a.id DESC">JGRID_HEADING_ID_DESC</option>
</field>
<field
name="limit"
type="limitbox"
label="JGLOBAL_LIST_LIMIT"
default="25"
class="js-select-submit-on-change"
/>
</fields>
</form>

View File

@ -0,0 +1,98 @@
<?xml version="1.0" encoding="UTF-8"?>
<form>
<fields name="filter">
<field
name="search"
type="text"
inputmode="search"
label="COM_USERS_SEARCH_USERS"
description="COM_USERS_SEARCH_IN_NAME"
hint="JSEARCH_FILTER"
/>
<field
name="state"
type="userstate"
label="JSTATUS"
class="js-select-submit-on-change"
>
<option value="">COM_USERS_FILTER_STATE</option>
</field>
<field
name="mfa"
type="list"
label="COM_USERS_HEADING_MFA"
class="js-select-submit-on-change"
>
<option value="">COM_USERS_FILTER_MFA</option>
<option value="1">JENABLED</option>
<option value="0">JDISABLED</option>
</field>
<field
name="active"
type="useractive"
label="COM_USERS_HEADING_ACTIVATED"
class="js-select-submit-on-change"
>
<option value="">COM_USERS_FILTER_ACTIVE</option>
</field>
<field
name="group_id"
type="usergrouplist"
label="COM_USERS_HEADING_GROUPS"
class="js-select-submit-on-change"
>
<option value="">COM_USERS_FILTER_USERGROUP</option>
</field>
<field
name="lastvisitrange"
type="lastvisitdaterange"
label="COM_USERS_HEADING_LAST_VISIT_DATE"
class="js-select-submit-on-change"
>
<option value="">COM_USERS_OPTION_FILTER_LAST_VISIT_DATE</option>
</field>
<field
name="range"
type="registrationdaterange"
label="COM_USERS_HEADING_REGISTRATION_DATE"
class="js-select-submit-on-change"
>
<option value="">COM_USERS_OPTION_FILTER_DATE</option>
</field>
</fields>
<fields name="list">
<field
name="fullordering"
type="list"
label="JGLOBAL_SORT_BY"
class="js-select-submit-on-change"
default="a.name ASC"
validate="options"
>
<option value="">JGLOBAL_SORT_BY</option>
<option value="a.name ASC">JGLOBAL_NAME_ASC</option>
<option value="a.name DESC">JGLOBAL_NAME_DESC</option>
<option value="a.username ASC">COM_USERS_HEADING_USERNAME_ASC</option>
<option value="a.username DESC">COM_USERS_HEADING_USERNAME_DESC</option>
<option value="a.block ASC">COM_USERS_HEADING_ENABLED_ASC</option>
<option value="a.block DESC">COM_USERS_HEADING_ENABLED_DESC</option>
<option value="a.activation ASC">COM_USERS_HEADING_ACTIVATED_ASC</option>
<option value="a.activation DESC">COM_USERS_HEADING_ACTIVATED_DESC</option>
<option value="a.email ASC">COM_USERS_HEADING_EMAIL_ASC</option>
<option value="a.email DESC">COM_USERS_HEADING_EMAIL_DESC</option>
<option value="a.lastvisitDate ASC">COM_USERS_HEADING_LAST_VISIT_DATE_ASC</option>
<option value="a.lastvisitDate DESC">COM_USERS_HEADING_LAST_VISIT_DATE_DESC</option>
<option value="a.registerDate ASC">COM_USERS_HEADING_REGISTRATION_DATE_ASC</option>
<option value="a.registerDate DESC">COM_USERS_HEADING_REGISTRATION_DATE_DESC</option>
<option value="a.id ASC">JGRID_HEADING_ID_ASC</option>
<option value="a.id DESC">JGRID_HEADING_ID_DESC</option>
</field>
<field
name="limit"
type="limitbox"
label="JGLOBAL_LIST_LIMIT"
default="25"
class="js-select-submit-on-change"
/>
</fields>
</form>

View File

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<form addfieldprefix="Joomla\Component\Users\Administrator\Field">
<fieldset name="group_details">
<field
name="id"
type="hidden"
default="0"
readonly="true"
/>
<field
name="title"
type="text"
label="COM_USERS_GROUP_FIELD_TITLE_LABEL"
required="true"
/>
<field
name="parent_id"
type="groupparent"
label="COM_USERS_GROUP_FIELD_PARENT_LABEL"
validate="options"
>
<option value="0" disabled="disabled">COM_USERS_GROUP_FIELD_PARENT_SELECT</option>
</field>
<field
name="actions"
type="hidden"
multiple="true"
/>
<field
name="lft"
type="hidden"
filter="unset"
/>
<field
name="rgt"
type="hidden"
filter="unset"
/>
</fieldset>
</form>

View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<form>
<fieldset>
<field
name="id"
type="hidden"
default="0"
readonly="true"
required="true"
/>
<field
name="title"
type="text"
label="COM_USERS_LEVEL_FIELD_TITLE_LABEL"
required="true"
/>
<field
name="ordering"
type="text"
label="JFIELD_ORDERING_LABEL"
description="JFIELD_ORDERING_DESC"
default="0"
/>
<field
name="rules"
type="hidden"
filter="intarray"
/>
</fieldset>
</form>

View File

@ -0,0 +1,61 @@
<?xml version="1.0" encoding="UTF-8"?>
<form>
<fieldset>
<field
name="recurse"
type="checkbox"
label="COM_USERS_MAIL_FIELD_RECURSE_LABEL"
value="1"
/>
<field
name="mode"
type="checkbox"
label="COM_USERS_MAIL_FIELD_SEND_IN_HTML_MODE_LABEL"
value="1"
/>
<field
name="disabled"
type="checkbox"
label="COM_USERS_MAIL_FIELD_EMAIL_DISABLED_USERS_LABEL"
value="1"
/>
<field
name="group"
type="usergrouplist"
label="COM_USERS_MAIL_FIELD_GROUP_LABEL"
default="0"
>
<option value="0">COM_USERS_MAIL_FIELD_VALUE_ALL_USERS_GROUPS</option>
</field>
<field
name="bcc"
type="checkbox"
label="COM_USERS_MAIL_FIELD_SEND_AS_BLIND_CARBON_COPY_LABEL"
default="1"
value="1"
checked="1"
/>
<field
name="subject"
type="text"
label="COM_USERS_MAIL_FIELD_SUBJECT_LABEL"
maxlength="150"
required="true"
/>
<field
name="message"
type="textarea"
label="COM_USERS_MAIL_FIELD_MESSAGE_LABEL"
cols="70"
rows="20"
required="true"
/>
</fieldset>
</form>

View File

@ -0,0 +1,136 @@
<?xml version="1.0" encoding="UTF-8"?>
<form>
<fieldset addfieldprefix="Joomla\Component\Categories\Administrator\Field">
<field
name="id"
type="hidden"
label="COM_USERS_FIELD_ID_LABEL"
class="readonly"
default="0"
readonly="true"
/>
<field
name="user_id"
type="user"
label="COM_USERS_FIELD_USER_ID_LABEL"
required="true"
validate="UserId"
/>
<field
name="catid"
type="modal_category"
label="COM_USERS_FIELD_CATEGORY_ID_LABEL"
extension="com_users"
required="true"
select="true"
new="true"
edit="true"
clear="true"
/>
<field
name="subject"
type="text"
label="COM_USERS_FIELD_SUBJECT_LABEL"
/>
<field
name="body"
type="editor"
label="COM_USERS_FIELD_NOTEBODY_LABEL"
rows="10"
cols="80"
filter="safehtml"
/>
<field
name="state"
type="list"
label="JSTATUS"
class="form-select-color-state"
default="1"
validate="options"
>
<option value="1">JPUBLISHED</option>
<option value="0">JUNPUBLISHED</option>
<option value="2">JARCHIVED</option>
<option value="-2">JTRASHED</option>
</field>
<field
name="review_time"
type="calendar"
label="COM_USERS_FIELD_REVIEW_TIME_LABEL"
default="NOW"
translateformat="true"
filter="user_utc"
/>
<field
name="checked_out"
type="hidden"
filter="unset"
/>
<field
name="checked_out_time"
type="hidden"
filter="unset"
/>
<field
name="created_user_id"
type="hidden"
label="JGLOBAL_FIELD_CREATED_BY_LABEL"
filter="unset"
/>
<field
name="created_time"
type="hidden"
label="JGLOBAL_FIELD_CREATED_LABEL"
filter="unset"
/>
<field
name="modified_user_id"
type="hidden"
label="JGLOBAL_FIELD_MODIFIED_BY_LABEL"
filter="unset"
/>
<field
name="modified_time"
type="hidden"
label="JGLOBAL_FIELD_MODIFIED_LABEL"
filter="unset"
/>
<field
name="publish_up"
type="calendar"
label="JGLOBAL_FIELD_PUBLISH_UP_LABEL"
translateformat="true"
showtime="true"
filter="user_utc"
/>
<field
name="publish_down"
type="calendar"
label="JGLOBAL_FIELD_PUBLISH_DOWN_LABEL"
translateformat="true"
showtime="true"
filter="user_utc"
/>
<field
name="version_note"
type="text"
label="JGLOBAL_FIELD_VERSION_NOTE_LABEL"
maxlength="255"
/>
</fieldset>
</form>

View File

@ -0,0 +1,239 @@
<?xml version="1.0" encoding="UTF-8"?>
<form addfieldprefix="Joomla\Component\Users\Administrator\Field">
<fieldset name="user_details" label="COM_USERS_USER_ACCOUNT_DETAILS">
<field
name="name"
type="text"
label="COM_USERS_USER_FIELD_NAME_LABEL"
required="true"
/>
<field
name="username"
type="text"
label="COM_USERS_USER_FIELD_USERNAME_LABEL"
required="true"
/>
<field
name="password"
type="password"
label="JGLOBAL_PASSWORD"
rules="true"
autocomplete="new-password"
class="validate-password-strength"
filter="raw"
validate="password"
strengthmeter="true"
force="on"
/>
<field
name="password2"
type="password"
label="COM_USERS_USER_FIELD_PASSWORD2_LABEL"
autocomplete="new-password"
class="validate-passwordExtra"
filter="raw"
message="COM_USERS_USER_FIELD_PASSWORD1_MESSAGE"
validate="equals"
field="password"
/>
<field
name="email"
type="email"
label="JGLOBAL_EMAIL"
required="true"
validate="email"
validDomains="com_users.domains"
/>
<field
name="registerDate"
type="calendar"
label="COM_USERS_USER_FIELD_REGISTERDATE_LABEL"
class="readonly"
readonly="true"
translateformat="true"
showtime="true"
filter="user_utc"
/>
<field
name="lastvisitDate"
type="calendar"
label="COM_USERS_USER_FIELD_LASTVISIT_LABEL"
class="readonly"
readonly="true"
translateformat="true"
showtime="true"
filter="user_utc"
/>
<field
name="lastResetTime"
type="calendar"
label="COM_USERS_USER_FIELD_LASTRESET_LABEL"
class="readonly"
readonly="true"
translateformat="true"
showtime="true"
filter="user_utc"
/>
<field
name="resetCount"
type="number"
label="COM_USERS_USER_FIELD_RESETCOUNT_LABEL"
class="readonly"
default="0"
readonly="true"
/>
<field
name="sendEmail"
type="radio"
label="COM_USERS_USER_FIELD_SENDEMAIL_LABEL"
default="0"
layout="joomla.form.field.radio.switcher"
>
<option value="0">JNO</option>
<option value="1">JYES</option>
</field>
<field
name="block"
type="radio"
label="COM_USERS_USER_FIELD_BLOCK_LABEL"
layout="joomla.form.field.radio.switcher"
default="0"
>
<option value="1">COM_USERS_USER_FIELD_BLOCK</option>
<option value="0">COM_USERS_USER_FIELD_ENABLE</option>
</field>
<field
name="requireReset"
type="radio"
label="COM_USERS_USER_FIELD_REQUIRERESET_LABEL"
default="0"
layout="joomla.form.field.radio.switcher"
>
<option value="0">JNO</option>
<option value="1">JYES</option>
</field>
<field
name="id"
type="text"
label="JGLOBAL_FIELD_ID_LABEL"
class="readonly"
default="0"
readonly="true"
/>
</fieldset>
<field name="groups" type="hidden" />
<fields name="params">
<!-- Basic user account settings. -->
<fieldset name="settings" label="COM_USERS_SETTINGS_FIELDSET_LABEL">
<field
name="admin_style"
type="templatestyle"
label="COM_USERS_USER_FIELD_BACKEND_TEMPLATE_LABEL"
client="administrator"
filter="uint"
>
<option value="">JOPTION_USE_DEFAULT</option>
</field>
<field
name="admin_language"
type="language"
label="COM_USERS_USER_FIELD_BACKEND_LANGUAGE_LABEL"
client="administrator"
>
<option value="">JOPTION_USE_DEFAULT</option>
</field>
<field
name="language"
type="language"
label="COM_USERS_USER_FIELD_FRONTEND_LANGUAGE_LABEL"
client="site"
>
<option value="">JOPTION_USE_DEFAULT</option>
</field>
<field
name="editor"
type="plugins"
label="COM_USERS_USER_FIELD_EDITOR_LABEL"
folder="editors"
>
<option value="">JOPTION_USE_DEFAULT</option>
</field>
<field
name="timezone"
type="timezone"
label="COM_USERS_USER_FIELD_TIMEZONE_LABEL"
>
<option value="">JOPTION_USE_DEFAULT</option>
</field>
</fieldset>
<!-- User accessibility settings -->
<fieldset
name="accessibility"
label="COM_USERS_A11Y_SETTINGS_FIELDSET_LABEL"
description="COM_USERS_A11Y_SETTINGS_FIELDSET_DESC"
>
<field
name="a11y_mono"
type="radio"
label="COM_USERS_A11Y_SETTINGS_FIELD_MONOCHROME"
layout="joomla.form.field.radio.switcher"
default="0"
>
<option value="0">JNO</option>
<option value="monochrome">JYES</option>
</field>
<field
name="a11y_contrast"
type="radio"
label="COM_USERS_A11Y_SETTINGS_FIELD_CONTRAST"
layout="joomla.form.field.radio.switcher"
default="0"
>
<option value="0">JNO</option>
<option value="high_contrast">JYES</option>
</field>
<field
name="a11y_highlight"
type="radio"
label="COM_USERS_A11Y_SETTINGS_FIELD_HIGHLIGHT"
layout="joomla.form.field.radio.switcher"
default="0"
>
<option value="0">JNO</option>
<option value="highlight">JYES</option>
</field>
<field
name="a11y_font"
type="radio"
label="COM_USERS_A11Y_SETTINGS_FIELD_FONTSIZE"
layout="joomla.form.field.radio.switcher"
default="0"
>
<option value="0">JNO</option>
<option value="fontsize">JYES</option>
</field>
</fieldset>
</fields>
</form>

View File

@ -0,0 +1,29 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_users
*
* @copyright (C) 2010 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*
* @phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace
*/
use Joomla\Component\Users\Administrator\Helper\DebugHelper;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Users component debugging helper.
*
* @since 1.6
*
* @deprecated 4.3 will be removed in 6.0
* Use \Joomla\Component\Users\Administrator\Helper\DebugHelper instead
*/
class UsersHelperDebug extends DebugHelper
{
}

View File

@ -0,0 +1,27 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_users
*
* @copyright (C) 2009 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*
* @phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace
*/
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Users component helper.
*
* @since 1.6
*
* @deprecated 4.3 will be removed in 6.0
* Use \Joomla\Component\Users\Administrator\Helper\UsersHelper instead
*/
class UsersHelper extends \Joomla\Component\Users\Administrator\Helper\UsersHelper
{
}

View File

@ -0,0 +1,58 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_users
*
* @copyright (C) 2022 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
use Joomla\CMS\Factory;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\Database\DatabaseInterface;
use Joomla\Database\ParameterType;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Post-installation message about the new Multi-factor Authentication: condition check.
*
* Returns true if neither of the two new core MFA plugins are enabled.
*
* @return boolean
* @since 4.2.0
*/
function com_users_postinstall_mfa_condition(): bool
{
return \count(PluginHelper::getPlugin('multifactorauth')) < 1;
}
/**
* Post-installation message about the new Multi-factor Authentication: action.
*
* Enables the core MFA plugins.
*
* @return void
* @since 4.2.0
*/
function com_users_postinstall_mfa_action(): void
{
/** @var DatabaseInterface $db */
$db = Factory::getContainer()->get(DatabaseInterface::class);
$coreMfaPlugins = ['email', 'totp', 'webauthn', 'yubikey'];
$query = $db->getQuery(true)
->update($db->quoteName('#__extensions'))
->set($db->quoteName('enabled') . ' = 1')
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
->where($db->quoteName('folder') . ' = ' . $db->quote('multifactorauth'))
->whereIn($db->quoteName('element'), $coreMfaPlugins, ParameterType::STRING);
$db->setQuery($query);
$db->execute();
$url = 'index.php?option=com_plugins&filter[folder]=multifactorauth';
Factory::getApplication()->redirect($url);
}

View File

@ -0,0 +1,86 @@
<?xml version="1.0"?>
<menu
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="urn:joomla.org"
xsi:schemaLocation="urn:joomla.org menu.xsd"
>
<menuitem
title="COM_USERS_MENUS_USERS"
type="heading"
icon="users"
class="class:users"
>
<menuitem
title="COM_USERS_MENUS_USER_MANAGER"
type="component"
element="com_users"
link="index.php?option=com_users&amp;view=users"
quicktask="index.php?option=com_users&amp;task=user.add"
quicktask-title="COM_USERS_MENUS_ADD_USER"
/>
<menuitem
title="COM_USERS_MENUS_GROUPS"
type="component"
element="com_users"
link="index.php?option=com_users&amp;view=groups"
/>
<menuitem
title="COM_USERS_MENUS_LEVELS"
type="component"
element="com_users"
link="index.php?option=com_users&amp;view=levels"
/>
</menuitem>
<menuitem
title="COM_USERS_MENUS_FIELDS"
type="heading"
icon="user-tag"
class="class:users"
>
<menuitem
title="MOD_MENU_FIELDS"
type="component"
element="com_fields"
link="index.php?option=com_fields&amp;view=fields&amp;context=com_users.user"
/>
<menuitem
title="MOD_MENU_FIELDS_GROUP"
type="component"
element="com_fields"
link="index.php?option=com_fields&amp;view=groups&amp;context=com_users.user"
/>
</menuitem>
<menuitem
title="COM_USERS_MENUS_MISC"
type="heading"
icon="user-edit"
class="class:users"
>
<menuitem
title="COM_USERS_MENUS_NOTES"
type="component"
element="com_users"
link="index.php?option=com_users&amp;view=notes"
/>
<menuitem
title="COM_USERS_MENUS_NOTE_CATEGORIES"
type="component"
element="com_categories"
link="index.php?option=com_categories&amp;view=categories&amp;extension=com_users"
/>
<menuitem
title="MOD_MENU_MASS_MAIL_USERS"
type="component"
element="com_users"
link="index.php?option=com_users&amp;view=mail"
scope="massmail"
/>
</menuitem>
</menu>

View File

@ -0,0 +1,58 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_users
*
* @copyright (C) 2018 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
\defined('_JEXEC') or die;
use Joomla\CMS\Component\Router\RouterFactoryInterface;
use Joomla\CMS\Dispatcher\ComponentDispatcherFactoryInterface;
use Joomla\CMS\Extension\ComponentInterface;
use Joomla\CMS\Extension\Service\Provider\ComponentDispatcherFactory;
use Joomla\CMS\Extension\Service\Provider\MVCFactory;
use Joomla\CMS\Extension\Service\Provider\RouterFactory;
use Joomla\CMS\HTML\Registry;
use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
use Joomla\Component\Users\Administrator\Extension\UsersComponent;
use Joomla\DI\Container;
use Joomla\DI\ServiceProviderInterface;
/**
* The users service provider.
*
* @since 4.0.0
*/
return new class () implements ServiceProviderInterface {
/**
* Registers the service provider with a DI container.
*
* @param Container $container The DI container.
*
* @return void
*
* @since 4.0.0
*/
public function register(Container $container)
{
$container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Users'));
$container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Users'));
$container->registerServiceProvider(new RouterFactory('\\Joomla\\Component\\Users'));
$container->set(
ComponentInterface::class,
function (Container $container) {
$component = new UsersComponent($container->get(ComponentDispatcherFactoryInterface::class));
$component->setMVCFactory($container->get(MVCFactoryInterface::class));
$component->setRouterFactory($container->get(RouterFactoryInterface::class));
$component->setRegistry($container->get(Registry::class));
return $component;
}
);
}
};

View File

@ -0,0 +1,81 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_users
*
* @copyright (C) 2022 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Component\Users\Administrator\Controller;
use Joomla\CMS\Application\CMSApplication;
use Joomla\CMS\Event\MultiFactor\Callback;
use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\Controller\BaseController;
use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\Input\Input;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Multi-factor Authentication plugins' AJAX callback controller
*
* @since 4.2.0
*/
class CallbackController extends BaseController
{
/**
* Public constructor
*
* @param array $config Plugin configuration
* @param MVCFactoryInterface|null $factory MVC Factory for the com_users component
* @param CMSApplication|null $app CMS application object
* @param Input|null $input Joomla CMS input object
*
* @since 4.2.0
*/
public function __construct(array $config = [], MVCFactoryInterface $factory = null, ?CMSApplication $app = null, ?Input $input = null)
{
parent::__construct($config, $factory, $app, $input);
$this->registerDefaultTask('callback');
}
/**
* Implement a callback feature, typically used for OAuth2 authentication
*
* @param bool $cachable Can this view be cached
* @param array|bool $urlparams An array of safe url parameters and their variable types.
* @see \Joomla\CMS\Filter\InputFilter::clean() for valid values.
*
* @return void
* @since 4.2.0
*/
public function callback($cachable = false, $urlparams = false): void
{
$app = $this->app;
// Get the Method and make sure it's non-empty
$method = $this->input->getCmd('method', '');
if (empty($method)) {
throw new \RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403);
}
PluginHelper::importPlugin('multifactorauth');
$event = new Callback($method);
$this->app->getDispatcher()->dispatch($event->getName(), $event);
/**
* The first plugin to handle the request should either redirect or close the application. If we are still here
* no plugin handled the request successfully. Show an error.
*/
throw new \RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403);
}
}

View File

@ -0,0 +1,245 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_users
*
* @copyright (C) 2022 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Component\Users\Administrator\Controller;
use Joomla\CMS\Application\CMSApplication;
use Joomla\CMS\Date\Date;
use Joomla\CMS\Event\MultiFactor\NotifyActionLog;
use Joomla\CMS\Event\MultiFactor\Validate;
use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\Controller\BaseController;
use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Uri\Uri;
use Joomla\CMS\User\UserFactoryAwareInterface;
use Joomla\CMS\User\UserFactoryAwareTrait;
use Joomla\Component\Users\Administrator\Model\BackupcodesModel;
use Joomla\Component\Users\Administrator\Model\CaptiveModel;
use Joomla\Input\Input;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Captive Multi-factor Authentication page controller
*
* @since 4.2.0
*/
class CaptiveController extends BaseController implements UserFactoryAwareInterface
{
use UserFactoryAwareTrait;
/**
* Public constructor
*
* @param array $config Plugin configuration
* @param MVCFactoryInterface|null $factory MVC Factory for the com_users component
* @param CMSApplication|null $app CMS application object
* @param Input|null $input Joomla CMS input object
*
* @since 4.2.0
*/
public function __construct(array $config = [], MVCFactoryInterface $factory = null, ?CMSApplication $app = null, ?Input $input = null)
{
parent::__construct($config, $factory, $app, $input);
$this->registerTask('captive', 'display');
}
/**
* Displays the captive login page
*
* @param boolean $cachable Ignored. This page is never cached.
* @param boolean|array $urlparams Ignored. This page is never cached.
*
* @return void
* @throws \Exception
* @since 4.2.0
*/
public function display($cachable = false, $urlparams = false): void
{
$user = $this->app->getIdentity() ?: $this->getUserFactory()->loadUserById(0);
// Only allow logged in Users
if ($user->guest) {
throw new \RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403);
}
// Get the view object
$viewLayout = $this->input->get('layout', 'default', 'string');
$view = $this->getView(
'Captive',
'html',
'',
[
'base_path' => $this->basePath,
'layout' => $viewLayout,
]
);
$view->document = $this->app->getDocument();
// If we're already logged in go to the site's home page
if ((int) $this->app->getSession()->get('com_users.mfa_checked', 0) === 1) {
$url = Route::_('index.php?option=com_users&task=methods.display', false);
$this->setRedirect($url);
}
// Pass the model to the view
/** @var CaptiveModel $model */
$model = $this->getModel('Captive');
$view->setModel($model, true);
/** @var BackupcodesModel $codesModel */
$codesModel = $this->getModel('Backupcodes');
$view->setModel($codesModel, false);
try {
// Suppress all modules on the page except those explicitly allowed
$model->suppressAllModules();
} catch (\Exception $e) {
// If we can't kill the modules we can still survive.
}
// Pass the MFA record ID to the model
$recordId = $this->input->getInt('record_id', null);
$model->setState('record_id', $recordId);
// Do not go through $this->display() because it overrides the model.
$view->display();
}
/**
* Validate the MFA code entered by the user
*
* @param bool $cachable Ignored. This page is never cached.
* @param array $urlparameters Ignored. This page is never cached.
*
* @return void
* @throws \Exception
* @since 4.2.0
*/
public function validate($cachable = false, $urlparameters = [])
{
// CSRF Check
$this->checkToken($this->input->getMethod());
// Get the MFA parameters from the request
$recordId = $this->input->getInt('record_id', null);
$code = $this->input->get('code', null, 'raw');
/** @var CaptiveModel $model */
$model = $this->getModel('Captive');
// Validate the MFA record
$model->setState('record_id', $recordId);
$record = $model->getRecord();
if (empty($record)) {
$event = new NotifyActionLog('onComUsersCaptiveValidateInvalidMethod');
$this->app->getDispatcher()->dispatch($event->getName(), $event);
throw new \RuntimeException(Text::_('COM_USERS_MFA_INVALID_METHOD'), 500);
}
if (!$model->checkTryLimit($record)) {
// The try limit is reached, show error and return
$captiveURL = Route::_('index.php?option=com_users&view=captive&task=select', false);
$message = Text::_('COM_USERS_MFA_TRY_LIMIT_REACHED');
$this->setRedirect($captiveURL, $message, 'error');
$event = new NotifyActionLog('onComUsersCaptiveValidateTryLimitReached');
$this->app->getDispatcher()->dispatch($event->getName(), $event);
return;
}
// Validate the code
$user = $this->app->getIdentity() ?: $this->getUserFactory()->loadUserById(0);
$event = new Validate($record, $user, $code);
$results = $this->app
->getDispatcher()
->dispatch($event->getName(), $event)
->getArgument('result', []);
$isValidCode = false;
if ($record->method === 'backupcodes') {
/** @var BackupcodesModel $codesModel */
$codesModel = $this->getModel('Backupcodes');
$results = [$codesModel->isBackupCode($code, $user)];
/**
* This is required! Do not remove!
*
* There is a store() call below. It saves the in-memory MFA record to the database. That includes the
* options key which contains the configuration of the Method. For backup codes, these are the actual codes
* you can use. When we check for a backup code validity we also "burn" it, i.e. we remove it from the
* options table and save that to the database. However, this DOES NOT update the $record here. Therefore
* the call to saveRecord() would overwrite the database contents with a record that _includes_ the backup
* code we had just burned. As a result the single use backup codes end up being multiple use.
*
* By doing a getRecord() here, right after we have "burned" any correct backup codes, we resolve this
* issue. The loaded record will reflect the database contents where the options DO NOT include the code we
* just used. Therefore the call to store() will result in the correct database state, i.e. the used backup
* code being removed.
*/
$record = $model->getRecord();
}
$isValidCode = array_reduce(
$results,
function (bool $carry, $result) {
return $carry || \boolval($result);
},
false
);
if (!$isValidCode) {
// The code is wrong. Display an error and go back.
$captiveURL = Route::_('index.php?option=com_users&view=captive&record_id=' . $recordId, false);
$message = Text::_('COM_USERS_MFA_INVALID_CODE');
$this->setRedirect($captiveURL, $message, 'error');
$event = new NotifyActionLog('onComUsersCaptiveValidateFailed', [$record->title]);
$this->app->getDispatcher()->dispatch($event->getName(), $event);
return;
}
// Update the Last Used, UA and IP columns
$jNow = Date::getInstance();
$record->last_used = $jNow->toSql();
$record->tries = 0;
$record->last_try = null;
$record->store();
// Flag the user as fully logged in
$session = $this->app->getSession();
$session->set('com_users.mfa_checked', 1);
$session->set('com_users.mandatory_mfa_setup', 0);
// Get the return URL stored by the plugin in the session
$returnUrl = $session->get('com_users.return_url', '');
// If the return URL is not set or not internal to this site redirect to the site's front page
if (empty($returnUrl) || !Uri::isInternal($returnUrl)) {
$returnUrl = Uri::base();
}
$this->setRedirect($returnUrl);
$event = new NotifyActionLog('onComUsersCaptiveValidateSuccess', [$record->title]);
$this->app->getDispatcher()->dispatch($event->getName(), $event);
}
}

View File

@ -0,0 +1,140 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_users
*
* @copyright (C) 2005 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Component\Users\Administrator\Controller;
use Joomla\CMS\Access\Exception\NotAllowed;
use Joomla\CMS\Helper\ContentHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\Controller\BaseController;
use Joomla\CMS\Router\Route;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Users display controller.
*
* @since 1.6
*/
class DisplayController extends BaseController
{
/**
* The default view.
*
* @var string
* @since 1.6
*/
protected $default_view = 'users';
/**
* Checks whether a user can see this view.
*
* @param string $view The view name.
*
* @return boolean
*
* @since 1.6
*/
protected function canView($view)
{
$canDo = ContentHelper::getActions('com_users');
switch ($view) {
// Special permissions.
case 'groups':
case 'group':
case 'levels':
case 'level':
return $canDo->get('core.admin');
// Default permissions.
default:
return true;
}
}
/**
* Method to display a view.
*
* @param boolean $cachable If true, the view output will be cached
* @param array $urlparams An array of safe URL parameters and their variable types.
* @see \Joomla\CMS\Filter\InputFilter::clean() for valid values.
*
* @return BaseController|boolean This object to support chaining or false on failure.
*
* @since 1.5
*/
public function display($cachable = false, $urlparams = [])
{
$view = $this->input->get('view', 'users');
$layout = $this->input->get('layout', 'default');
$id = $this->input->getInt('id');
if (!$this->canView($view)) {
throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403);
}
// Check for edit form.
if ($view === 'user' && $layout === 'edit' && !$this->checkEditId('com_users.edit.user', $id)) {
// Somehow the person just went to the form - we don't allow that.
if (!\count($this->app->getMessageQueue())) {
$this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error');
}
$this->setRedirect(Route::_('index.php?option=com_users&view=users', false));
return false;
}
if ($view === 'group' && $layout === 'edit' && !$this->checkEditId('com_users.edit.group', $id)) {
// Somehow the person just went to the form - we don't allow that.
if (!\count($this->app->getMessageQueue())) {
$this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error');
}
$this->setRedirect(Route::_('index.php?option=com_users&view=groups', false));
return false;
}
if ($view === 'level' && $layout === 'edit' && !$this->checkEditId('com_users.edit.level', $id)) {
// Somehow the person just went to the form - we don't allow that.
if (!\count($this->app->getMessageQueue())) {
$this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error');
}
$this->setRedirect(Route::_('index.php?option=com_users&view=levels', false));
return false;
}
if ($view === 'note' && $layout === 'edit' && !$this->checkEditId('com_users.edit.note', $id)) {
// Somehow the person just went to the form - we don't allow that.
if (!\count($this->app->getMessageQueue())) {
$this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error');
}
$this->setRedirect(Route::_('index.php?option=com_users&view=notes', false));
return false;
}
if (\in_array($view, ['captive', 'callback', 'methods', 'method'])) {
$controller = $this->factory->createController($view, 'Administrator', [], $this->app, $this->input);
$task = $this->input->get('task', '');
return $controller->execute($task);
}
return parent::display($cachable, $urlparams);
}
}

View File

@ -0,0 +1,74 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_users
*
* @copyright (C) 2009 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Component\Users\Administrator\Controller;
use Joomla\CMS\Access\Access;
use Joomla\CMS\MVC\Controller\FormController;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* User view level controller class.
*
* @since 1.6
*/
class GroupController extends FormController
{
/**
* @var string The prefix to use with controller messages.
* @since 1.6
*/
protected $text_prefix = 'COM_USERS_GROUP';
/**
* Method to check if you can save a new or existing record.
*
* Overrides Joomla\CMS\MVC\Controller\FormController::allowSave to check the core.admin permission.
*
* @param array $data An array of input data.
* @param string $key The name of the key for the primary key.
*
* @return boolean
*
* @since 1.6
*/
protected function allowSave($data, $key = 'id')
{
return ($this->app->getIdentity()->authorise('core.admin', $this->option) && parent::allowSave($data, $key));
}
/**
* Overrides Joomla\CMS\MVC\Controller\FormController::allowEdit
*
* Checks that non-Super Admins are not editing Super Admins.
*
* @param array $data An array of input data.
* @param string $key The name of the key for the primary key.
*
* @return boolean
*
* @since 1.6
*/
protected function allowEdit($data = [], $key = 'id')
{
// Check if this group is a Super Admin
if (Access::checkGroup($data[$key], 'core.admin')) {
// If I'm not a Super Admin, then disallow the edit.
if (!$this->app->getIdentity()->authorise('core.admin')) {
return false;
}
}
return parent::allowEdit($data, $key);
}
}

View File

@ -0,0 +1,139 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_users
*
* @copyright (C) 2009 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Component\Users\Administrator\Controller;
use Joomla\CMS\Access\Exception\NotAllowed;
use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\Controller\AdminController;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* User groups list controller class.
*
* @since 1.6
*/
class GroupsController extends AdminController
{
/**
* @var string The prefix to use with controller messages.
* @since 1.6
*/
protected $text_prefix = 'COM_USERS_GROUPS';
/**
* Proxy for getModel.
*
* @param string $name The model name. Optional.
* @param string $prefix The class prefix. Optional.
* @param array $config Configuration array for model. Optional.
*
* @return object The model.
*
* @since 1.6
*/
public function getModel($name = 'Group', $prefix = 'Administrator', $config = ['ignore_request' => true])
{
return parent::getModel($name, $prefix, $config);
}
/**
* Removes an item.
*
* Overrides Joomla\CMS\MVC\Controller\AdminController::delete to check the core.admin permission.
*
* @return void
*
* @since 1.6
*/
public function delete()
{
if (!$this->app->getIdentity()->authorise('core.admin', $this->option)) {
throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403);
}
parent::delete();
}
/**
* Method to publish a list of records.
*
* Overrides Joomla\CMS\MVC\Controller\AdminController::publish to check the core.admin permission.
*
* @return void
*
* @since 1.6
*/
public function publish()
{
if (!$this->app->getIdentity()->authorise('core.admin', $this->option)) {
throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403);
}
parent::publish();
}
/**
* Changes the order of one or more records.
*
* Overrides Joomla\CMS\MVC\Controller\AdminController::reorder to check the core.admin permission.
*
* @return boolean True on success
*
* @since 1.6
*/
public function reorder()
{
if (!$this->app->getIdentity()->authorise('core.admin', $this->option)) {
throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403);
}
return parent::reorder();
}
/**
* Method to save the submitted ordering values for records.
*
* Overrides Joomla\CMS\MVC\Controller\AdminController::saveorder to check the core.admin permission.
*
* @return boolean True on success
*
* @since 1.6
*/
public function saveorder()
{
if (!$this->app->getIdentity()->authorise('core.admin', $this->option)) {
throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403);
}
return parent::saveorder();
}
/**
* Check in of one or more records.
*
* Overrides Joomla\CMS\MVC\Controller\AdminController::checkin to check the core.admin permission.
*
* @return boolean True on success
*
* @since 1.6
*/
public function checkin()
{
if (!$this->app->getIdentity()->authorise('core.admin', $this->option)) {
throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403);
}
return parent::checkin();
}
}

View File

@ -0,0 +1,125 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_users
*
* @copyright (C) 2009 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Component\Users\Administrator\Controller;
use Joomla\CMS\Access\Access;
use Joomla\CMS\Access\Exception\NotAllowed;
use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\Controller\FormController;
use Joomla\CMS\Router\Route;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* User view level controller class.
*
* @since 1.6
*/
class LevelController extends FormController
{
/**
* @var string The prefix to use with controller messages.
* @since 1.6
*/
protected $text_prefix = 'COM_USERS_LEVEL';
/**
* Method to check if you can save a new or existing record.
*
* Overrides Joomla\CMS\MVC\Controller\FormController::allowSave to check the core.admin permission.
*
* @param array $data An array of input data.
* @param string $key The name of the key for the primary key.
*
* @return boolean
*
* @since 1.6
*/
protected function allowSave($data, $key = 'id')
{
return ($this->app->getIdentity()->authorise('core.admin', $this->option) && parent::allowSave($data, $key));
}
/**
* Overrides JControllerForm::allowEdit
*
* Checks that non-Super Admins are not editing Super Admins.
*
* @param array $data An array of input data.
* @param string $key The name of the key for the primary key.
*
* @return boolean
*
* @since 3.8.8
*/
protected function allowEdit($data = [], $key = 'id')
{
// Check for if Super Admin can edit
$viewLevel = $this->getModel('Level', 'Administrator')->getItem((int) $data['id']);
// If this group is super admin and this user is not super admin, canEdit is false
if (!$this->app->getIdentity()->authorise('core.admin') && $viewLevel->rules && Access::checkGroup($viewLevel->rules[0], 'core.admin')) {
$this->setMessage(Text::_('JLIB_APPLICATION_ERROR_EDIT_NOT_PERMITTED'), 'error');
$this->setRedirect(
Route::_(
'index.php?option=' . $this->option . '&view=' . $this->view_list
. $this->getRedirectToListAppend(),
false
)
);
return false;
}
return parent::allowEdit($data, $key);
}
/**
* Removes an item.
*
* Overrides Joomla\CMS\MVC\Controller\FormController::delete to check the core.admin permission.
*
* @return void
*
* @since 1.6
*/
public function delete()
{
// Check for request forgeries.
$this->checkToken();
$ids = (array) $this->input->get('cid', [], 'int');
// Remove zero values resulting from input filter
$ids = array_filter($ids);
if (!$this->app->getIdentity()->authorise('core.admin', $this->option)) {
throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403);
}
if (empty($ids)) {
$this->setMessage(Text::_('COM_USERS_NO_LEVELS_SELECTED'), 'warning');
} else {
// Get the model.
$model = $this->getModel();
// Remove the items.
if ($model->delete($ids)) {
$this->setMessage(Text::plural('COM_USERS_N_LEVELS_DELETED', \count($ids)));
}
}
$this->setRedirect('index.php?option=com_users&view=levels');
}
}

View File

@ -0,0 +1,47 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_users
*
* @copyright (C) 2009 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Component\Users\Administrator\Controller;
use Joomla\CMS\MVC\Controller\AdminController;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* User view levels list controller class.
*
* @since 1.6
*/
class LevelsController extends AdminController
{
/**
* @var string The prefix to use with controller messages.
* @since 1.6
*/
protected $text_prefix = 'COM_USERS_LEVELS';
/**
* Proxy for getModel.
*
* @param string $name The model name. Optional.
* @param string $prefix The class prefix. Optional.
* @param array $config Configuration array for model. Optional.
*
* @return \Joomla\CMS\MVC\Model\BaseDatabaseModel The model.
*
* @since 1.6
*/
public function getModel($name = 'Level', $prefix = 'Administrator', $config = ['ignore_request' => true])
{
return parent::getModel($name, $prefix, $config);
}
}

View File

@ -0,0 +1,73 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_users
*
* @copyright (C) 2009 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Component\Users\Administrator\Controller;
use Joomla\CMS\MVC\Controller\BaseController;
use Joomla\CMS\Router\Route;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Users mail controller.
*
* @since 1.6
*/
class MailController extends BaseController
{
/**
* Send the mail
*
* @return void
*
* @since 1.6
*/
public function send()
{
// Redirect to admin index if mass mailer disabled in conf
if ($this->app->get('massmailoff', 0) == 1) {
$this->app->redirect(Route::_('index.php', false));
}
// Check for request forgeries.
$this->checkToken('request');
$model = $this->getModel('Mail');
if ($model->send()) {
$type = 'message';
} else {
$type = 'error';
}
$msg = $model->getError();
$this->setRedirect('index.php?option=com_users&view=mail', $msg, $type);
}
/**
* Cancel the mail
*
* @return void
*
* @since 1.6
*/
public function cancel()
{
// Check for request forgeries.
$this->checkToken('request');
// Clear data from session.
$this->app->setUserState('com_users.display.mail.data', null);
$this->setRedirect('index.php?option=com_users&view=users');
}
}

View File

@ -0,0 +1,486 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_users
*
* @copyright (C) 2022 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Component\Users\Administrator\Controller;
use Joomla\CMS\Application\CMSApplication;
use Joomla\CMS\Event\MultiFactor\NotifyActionLog;
use Joomla\CMS\Event\MultiFactor\SaveSetup;
use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\Controller\BaseController as BaseControllerAlias;
use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Uri\Uri;
use Joomla\CMS\User\User;
use Joomla\CMS\User\UserFactoryAwareInterface;
use Joomla\CMS\User\UserFactoryAwareTrait;
use Joomla\Component\Users\Administrator\Helper\Mfa as MfaHelper;
use Joomla\Component\Users\Administrator\Model\BackupcodesModel;
use Joomla\Component\Users\Administrator\Model\MethodModel;
use Joomla\Component\Users\Administrator\Table\MfaTable;
use Joomla\Input\Input;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Multi-factor Authentication method controller
*
* @since 4.2.0
*/
class MethodController extends BaseControllerAlias implements UserFactoryAwareInterface
{
use UserFactoryAwareTrait;
/**
* Public constructor
*
* @param array $config Plugin configuration
* @param MVCFactoryInterface|null $factory MVC Factory for the com_users component
* @param CMSApplication|null $app CMS application object
* @param Input|null $input Joomla CMS input object
*
* @since 4.2.0
*/
public function __construct(array $config = [], MVCFactoryInterface $factory = null, ?CMSApplication $app = null, ?Input $input = null)
{
// We have to tell Joomla what is the name of the view, otherwise it defaults to the name of the *component*.
$config['default_view'] = 'method';
$config['default_task'] = 'add';
parent::__construct($config, $factory, $app, $input);
}
/**
* Execute a task by triggering a Method in the derived class.
*
* @param string $task The task to perform. If no matching task is found, the '__default' task is executed, if
* defined.
*
* @return mixed The value returned by the called Method.
*
* @throws \Exception
* @since 4.2.0
*/
public function execute($task)
{
if (empty($task) || $task === 'display') {
$task = 'add';
}
return parent::execute($task);
}
/**
* Add a new MFA Method
*
* @param boolean $cachable Ignored. This page is never cached.
* @param boolean|array $urlparams Ignored. This page is never cached.
*
* @return void
* @throws \Exception
* @since 4.2.0
*/
public function add($cachable = false, $urlparams = []): void
{
$this->assertLoggedInUser();
// Make sure I am allowed to edit the specified user
$userId = $this->input->getInt('user_id', null);
$user = $this->getUserFactory()->loadUserById($userId);
$this->assertCanEdit($user);
// Also make sure the Method really does exist
$method = $this->input->getCmd('method');
$this->assertMethodExists($method);
/** @var MethodModel $model */
$model = $this->getModel('Method');
$model->setState('method', $method);
// Pass the return URL to the view
$returnURL = $this->input->getBase64('returnurl');
$viewLayout = $this->input->get('layout', 'default', 'string');
$view = $this->getView('Method', 'html');
$view->setLayout($viewLayout);
$view->returnURL = $returnURL;
$view->user = $user;
$view->document = $this->app->getDocument();
$view->setModel($model, true);
$event = new NotifyActionLog('onComUsersControllerMethodBeforeAdd', [$user, $method]);
$this->app->getDispatcher()->dispatch($event->getName(), $event);
$view->display();
}
/**
* Edit an existing MFA Method
*
* @param boolean $cachable Ignored. This page is never cached.
* @param boolean|array $urlparams Ignored. This page is never cached.
*
* @return void
* @throws \Exception
* @since 4.2.0
*/
public function edit($cachable = false, $urlparams = []): void
{
$this->assertLoggedInUser();
// Make sure I am allowed to edit the specified user
$userId = $this->input->getInt('user_id', null);
$user = $this->getUserFactory()->loadUserById($userId);
$this->assertCanEdit($user);
// Also make sure the Method really does exist
$id = $this->input->getInt('id');
$record = $this->assertValidRecordId($id, $user);
if ($id <= 0) {
throw new \RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403);
}
/** @var MethodModel $model */
$model = $this->getModel('Method');
$model->setState('id', $id);
// Pass the return URL to the view
$returnURL = $this->input->getBase64('returnurl');
$viewLayout = $this->input->get('layout', 'default', 'string');
$view = $this->getView('Method', 'html');
$view->setLayout($viewLayout);
$view->returnURL = $returnURL;
$view->user = $user;
$view->document = $this->app->getDocument();
$view->setModel($model, true);
$event = new NotifyActionLog('onComUsersControllerMethodBeforeEdit', [$id, $user]);
$this->app->getDispatcher()->dispatch($event->getName(), $event);
$view->display();
}
/**
* Regenerate backup codes
*
* @param boolean $cachable Ignored. This page is never cached.
* @param boolean|array $urlparams Ignored. This page is never cached.
*
* @return void
* @throws \Exception
* @since 4.2.0
*/
public function regenerateBackupCodes($cachable = false, $urlparams = []): void
{
$this->assertLoggedInUser();
$this->checkToken($this->input->getMethod());
// Make sure I am allowed to edit the specified user
$userId = $this->input->getInt('user_id', null);
$user = $this->getUserFactory()->loadUserById($userId);
$this->assertCanEdit($user);
/** @var BackupcodesModel $model */
$model = $this->getModel('Backupcodes');
$model->regenerateBackupCodes($user);
$backupCodesRecord = $model->getBackupCodesRecord($user);
// Redirect
$redirectUrl = 'index.php?option=com_users&task=method.edit&user_id=' . $userId . '&id=' . $backupCodesRecord->id;
$returnURL = $this->input->getBase64('returnurl');
if (!empty($returnURL) && Uri::isInternal(base64_decode($returnURL))) {
$redirectUrl .= '&returnurl=' . $returnURL;
}
$this->setRedirect(Route::_($redirectUrl, false));
$event = new NotifyActionLog('onComUsersControllerMethodAfterRegenerateBackupCodes');
$this->app->getDispatcher()->dispatch($event->getName(), $event);
}
/**
* Delete an existing MFA Method
*
* @param boolean $cachable Ignored. This page is never cached.
* @param boolean|array $urlparams Ignored. This page is never cached.
*
* @return void
* @since 4.2.0
*/
public function delete($cachable = false, $urlparams = []): void
{
$this->assertLoggedInUser();
$this->checkToken($this->input->getMethod());
// Make sure I am allowed to edit the specified user
$userId = $this->input->getInt('user_id', null);
$user = $this->getUserFactory()->loadUserById($userId);
$this->assertCanDelete($user);
// Also make sure the Method really does exist
$id = $this->input->getInt('id');
$record = $this->assertValidRecordId($id, $user);
if ($id <= 0) {
throw new \RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403);
}
$type = null;
$message = null;
$event = new NotifyActionLog('onComUsersControllerMethodBeforeDelete', [$id, $user]);
$this->app->getDispatcher()->dispatch($event->getName(), $event);
try {
$record->delete();
} catch (\Exception $e) {
$message = $e->getMessage();
$type = 'error';
}
// Redirect
$url = Route::_('index.php?option=com_users&task=methods.display&user_id=' . $userId, false);
$returnURL = $this->input->getBase64('returnurl');
if (!empty($returnURL) && Uri::isInternal(base64_decode($returnURL))) {
$url = base64_decode($returnURL);
}
$this->setRedirect($url, $message, $type);
}
/**
* Save the MFA Method
*
* @param boolean $cachable Ignored. This page is never cached.
* @param boolean|array $urlparams Ignored. This page is never cached.
*
* @return void
* @since 4.2.0
*/
public function save($cachable = false, $urlparams = []): void
{
$this->assertLoggedInUser();
$this->checkToken($this->input->getMethod());
// Make sure I am allowed to edit the specified user
$userId = $this->input->getInt('user_id', null);
$user = $this->getUserFactory()->loadUserById($userId);
$this->assertCanEdit($user);
// Redirect
$url = Route::_('index.php?option=com_users&task=methods.display&user_id=' . $userId, false);
$returnURL = $this->input->getBase64('returnurl');
if (!empty($returnURL) && Uri::isInternal(base64_decode($returnURL))) {
$url = base64_decode($returnURL);
}
// The record must either be new (ID zero) or exist
$id = $this->input->getInt('id', 0);
$record = $this->assertValidRecordId($id, $user);
// If it's a new record we need to read the Method from the request and update the (not yet created) record.
if ($record->id == 0) {
$methodName = $this->input->getCmd('method');
$this->assertMethodExists($methodName);
$record->method = $methodName;
}
/** @var MethodModel $model */
$model = $this->getModel('Method');
// Ask the plugin to validate the input by calling onUserMultifactorSaveSetup
$result = [];
$input = $this->app->getInput();
$event = new NotifyActionLog('onComUsersControllerMethodBeforeSave', [$id, $user]);
$this->app->getDispatcher()->dispatch($event->getName(), $event);
try {
$event = new SaveSetup($record, $input);
$pluginResults = $this->app
->getDispatcher()
->dispatch($event->getName(), $event)
->getArgument('result', []);
foreach ($pluginResults as $pluginResult) {
$result = array_merge($result, $pluginResult);
}
} catch (\RuntimeException $e) {
// Go back to the edit page
$nonSefUrl = 'index.php?option=com_users&task=method.';
if ($id) {
$nonSefUrl .= 'edit&id=' . (int) $id;
} else {
$nonSefUrl .= 'add&method=' . $record->method;
}
$nonSefUrl .= '&user_id=' . $userId;
if (!empty($returnURL)) {
$nonSefUrl .= '&returnurl=' . urlencode($returnURL);
}
$url = Route::_($nonSefUrl, false);
$this->setRedirect($url, $e->getMessage(), 'error');
return;
}
// Update the record's options with the plugin response
$title = $this->input->getString('title', null);
$title = trim($title);
if (empty($title)) {
$method = $model->getMethod($record->method);
$title = $method['display'];
}
// Update the record's "default" flag
$default = $this->input->getBool('default', false);
$record->title = $title;
$record->options = $result;
$record->default = $default ? 1 : 0;
// Ask the model to save the record
$saved = $record->store();
if (!$saved) {
// Go back to the edit page
$nonSefUrl = 'index.php?option=com_users&task=method.';
if ($id) {
$nonSefUrl .= 'edit&id=' . (int) $id;
} else {
$nonSefUrl .= 'add';
}
$nonSefUrl .= '&user_id=' . $userId;
if (!empty($returnURL)) {
$nonSefUrl .= '&returnurl=' . urlencode($returnURL);
}
$url = Route::_($nonSefUrl, false);
$this->setRedirect($url, $record->getError(), 'error');
return;
}
$this->setRedirect($url);
}
/**
* Assert that the provided ID is a valid record identified for the given user
*
* @param int $id Record ID to check
* @param User|null $user User record. Null to use current user.
*
* @return MfaTable The loaded record
* @since 4.2.0
*/
private function assertValidRecordId($id, ?User $user = null): MfaTable
{
if (\is_null($user)) {
$user = $this->app->getIdentity() ?: $this->getUserFactory()->loadUserById(0);
}
/** @var MethodModel $model */
$model = $this->getModel('Method');
$model->setState('id', $id);
$record = $model->getRecord($user);
if (\is_null($record) || ($record->id != $id) || ($record->user_id != $user->id)) {
throw new \RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403);
}
return $record;
}
/**
* Assert that the user can add / edit MFA methods.
*
* @param User|null $user User record. Null to use current user.
*
* @return void
* @throws \RuntimeException|\Exception
* @since 4.2.0
*/
private function assertCanEdit(?User $user = null): void
{
if (!MfaHelper::canAddEditMethod($user)) {
throw new \RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403);
}
}
/**
* Assert that the user can delete MFA records / disable MFA.
*
* @param User|null $user User record. Null to use current user.
*
* @return void
* @throws \RuntimeException|\Exception
* @since 4.2.0
*/
private function assertCanDelete(?User $user = null): void
{
if (!MfaHelper::canDeleteMethod($user)) {
throw new \RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403);
}
}
/**
* Assert that the specified MFA Method exists, is activated and enabled for the current user
*
* @param string|null $method The Method to check
*
* @return void
* @since 4.2.0
*/
private function assertMethodExists(?string $method): void
{
/** @var MethodModel $model */
$model = $this->getModel('Method');
if (empty($method) || !$model->methodExists($method)) {
throw new \RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403);
}
}
/**
* Assert that there is a logged in user.
*
* @return void
* @since 4.2.0
*/
private function assertLoggedInUser(): void
{
$user = $this->app->getIdentity() ?: $this->getUserFactory()->loadUserById(0);
if ($user->guest) {
throw new \RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403);
}
}
}

View File

@ -0,0 +1,212 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_users
*
* @copyright (C) 2022 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Component\Users\Administrator\Controller;
use Joomla\CMS\Application\CMSApplication;
use Joomla\CMS\Event\MultiFactor\NotifyActionLog;
use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\Controller\BaseController;
use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Uri\Uri;
use Joomla\CMS\User\UserFactoryAwareInterface;
use Joomla\CMS\User\UserFactoryAwareTrait;
use Joomla\Component\Users\Administrator\Helper\Mfa as MfaHelper;
use Joomla\Component\Users\Administrator\Model\MethodsModel;
use Joomla\Input\Input;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Multi-factor Authentication methods selection and management controller
*
* @since 4.2.0
*/
class MethodsController extends BaseController implements UserFactoryAwareInterface
{
use UserFactoryAwareTrait;
/**
* Public constructor
*
* @param array $config Plugin configuration
* @param MVCFactoryInterface|null $factory MVC Factory for the com_users component
* @param CMSApplication|null $app CMS application object
* @param Input|null $input Joomla CMS input object
*
* @since 4.2.0
*/
public function __construct($config = [], MVCFactoryInterface $factory = null, ?CMSApplication $app = null, ?Input $input = null)
{
// We have to tell Joomla what is the name of the view, otherwise it defaults to the name of the *component*.
$config['default_view'] = 'Methods';
parent::__construct($config, $factory, $app, $input);
}
/**
* Disable Multi-factor Authentication for the current user
*
* @param bool $cachable Can this view be cached
* @param array $urlparams An array of safe url parameters and their variable types.
* @see \Joomla\CMS\Filter\InputFilter::clean() for valid values.
*
* @return void
* @since 4.2.0
*/
public function disable($cachable = false, $urlparams = []): void
{
$this->assertLoggedInUser();
$this->checkToken($this->input->getMethod());
// Make sure I am allowed to edit the specified user
$userId = $this->input->getInt('user_id', null);
$user = ($userId === null)
? $this->app->getIdentity()
: $this->getUserFactory()->loadUserById($userId);
$user = $user ?? $this->getUserFactory()->loadUserById(0);
if (!MfaHelper::canDeleteMethod($user)) {
throw new \RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403);
}
// Delete all MFA Methods for the user
/** @var MethodsModel $model */
$model = $this->getModel('Methods');
$type = null;
$message = null;
$event = new NotifyActionLog('onComUsersControllerMethodsBeforeDisable', [$user]);
$this->app->getDispatcher()->dispatch($event->getName(), $event);
try {
$model->deleteAll($user);
} catch (\Exception $e) {
$message = $e->getMessage();
$type = 'error';
}
// Redirect
$url = Route::_('index.php?option=com_users&task=methods.display&user_id=' . $userId, false);
$returnURL = $this->input->getBase64('returnurl');
if (!empty($returnURL) && Uri::isInternal(base64_decode($returnURL))) {
$url = base64_decode($returnURL);
}
$this->setRedirect($url, $message, $type);
}
/**
* List all available Multi-factor Authentication Methods available and guide the user to setting them up
*
* @param bool $cachable Can this view be cached
* @param array $urlparams An array of safe url parameters and their variable types.
* @see \Joomla\CMS\Filter\InputFilter::clean() for valid values.
*
* @return void
* @since 4.2.0
*/
public function display($cachable = false, $urlparams = []): void
{
$this->assertLoggedInUser();
// Make sure I am allowed to edit the specified user
$userId = $this->input->getInt('user_id', null);
$user = ($userId === null)
? $this->app->getIdentity()
: $this->getUserFactory()->loadUserById($userId);
$user = $user ?? $this->getUserFactory()->loadUserById(0);
if (!MfaHelper::canShowConfigurationInterface($user)) {
throw new \RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403);
}
$returnURL = $this->input->getBase64('returnurl');
$viewLayout = $this->input->get('layout', 'default', 'string');
$view = $this->getView('Methods', 'html');
$view->setLayout($viewLayout);
$view->returnURL = $returnURL;
$view->user = $user;
$view->document = $this->app->getDocument();
$methodsModel = $this->getModel('Methods');
$view->setModel($methodsModel, true);
$backupCodesModel = $this->getModel('Backupcodes');
$view->setModel($backupCodesModel, false);
$view->display();
}
/**
* Disable Multi-factor Authentication for the current user
*
* @param bool $cachable Can this view be cached
* @param array $urlparams An array of safe url parameters and their variable types.
* @see \Joomla\CMS\Filter\InputFilter::clean() for valid values.
*
* @return void
* @since 4.2.0
*/
public function doNotShowThisAgain($cachable = false, $urlparams = []): void
{
$this->assertLoggedInUser();
$this->checkToken($this->input->getMethod());
// Make sure I am allowed to edit the specified user
$userId = $this->input->getInt('user_id', null);
$user = ($userId === null)
? $this->app->getIdentity()
: $this->getUserFactory()->loadUserById($userId);
$user = $user ?? $this->getUserFactory()->loadUserById(0);
if (!MfaHelper::canAddEditMethod($user)) {
throw new \RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403);
}
$event = new NotifyActionLog('onComUsersControllerMethodsBeforeDoNotShowThisAgain', [$user]);
$this->app->getDispatcher()->dispatch($event->getName(), $event);
/** @var MethodsModel $model */
$model = $this->getModel('Methods');
$model->setFlag($user, true);
// Redirect
$url = Uri::base();
$returnURL = $this->input->getBase64('returnurl');
if (!empty($returnURL) && Uri::isInternal(base64_decode($returnURL))) {
$url = base64_decode($returnURL);
}
$this->setRedirect($url);
}
/**
* Assert that there is a user currently logged in
*
* @return void
* @since 4.2.0
*/
private function assertLoggedInUser(): void
{
$user = $this->app->getIdentity() ?: $this->getUserFactory()->loadUserById(0);
if ($user->guest) {
throw new \RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403);
}
}
}

View File

@ -0,0 +1,59 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_users
*
* @copyright (C) 2011 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Component\Users\Administrator\Controller;
use Joomla\CMS\MVC\Controller\FormController;
use Joomla\CMS\Versioning\VersionableControllerTrait;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* User note controller class.
*
* @since 2.5
*/
class NoteController extends FormController
{
use VersionableControllerTrait;
/**
* The prefix to use with controller messages.
*
* @var string
* @since 2.5
*/
protected $text_prefix = 'COM_USERS_NOTE';
/**
* Gets the URL arguments to append to an item redirect.
*
* @param integer $recordId The primary key id for the item.
* @param string $key The name of the primary key variable.
*
* @return string The arguments to append to the redirect URL.
*
* @since 2.5
*/
protected function getRedirectToItemAppend($recordId = null, $key = 'id')
{
$append = parent::getRedirectToItemAppend($recordId, $key);
$userId = $this->input->get('u_id', 0, 'int');
if ($userId) {
$append .= '&u_id=' . $userId;
}
return $append;
}
}

View File

@ -0,0 +1,49 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_users
*
* @copyright (C) 2011 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Component\Users\Administrator\Controller;
use Joomla\CMS\MVC\Controller\AdminController;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* User notes controller class.
*
* @since 2.5
*/
class NotesController extends AdminController
{
/**
* The prefix to use with controller messages.
*
* @var string
* @since 2.5
*/
protected $text_prefix = 'COM_USERS_NOTES';
/**
* Method to get a model object, loading it if required.
*
* @param string $name The model name. Optional.
* @param string $prefix The class prefix. Optional.
* @param array $config Configuration array for model. Optional.
*
* @return \Joomla\CMS\MVC\Model\BaseDatabaseModel The model.
*
* @since 2.5
*/
public function getModel($name = 'Note', $prefix = 'Administrator', $config = ['ignore_request' => true])
{
return parent::getModel($name, $prefix, $config);
}
}

View File

@ -0,0 +1,163 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_users
*
* @copyright (C) 2007 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Component\Users\Administrator\Controller;
use Joomla\CMS\Access\Access;
use Joomla\CMS\MVC\Controller\FormController;
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Uri\Uri;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* User controller class.
*
* @since 1.6
*/
class UserController extends FormController
{
/**
* @var string The prefix to use with controller messages.
* @since 1.6
*/
protected $text_prefix = 'COM_USERS_USER';
/**
* Overrides Joomla\CMS\MVC\Controller\FormController::allowEdit
*
* Checks that non-Super Admins are not editing Super Admins.
*
* @param array $data An array of input data.
* @param string $key The name of the key for the primary key.
*
* @return boolean True if allowed, false otherwise.
*
* @since 1.6
*/
protected function allowEdit($data = [], $key = 'id')
{
// Check if this person is a Super Admin
if (Access::check($data[$key], 'core.admin')) {
// If I'm not a Super Admin, then disallow the edit.
if (!$this->app->getIdentity()->authorise('core.admin')) {
return false;
}
}
// Allow users to edit their own account
if (isset($data[$key]) && (int) $this->app->getIdentity()->id === (int) $data[$key]) {
return true;
}
return parent::allowEdit($data, $key);
}
/**
* Override parent cancel to redirect when using status edit account.
*
* @param string $key The name of the primary key of the URL variable.
*
* @return boolean True if access level checks pass, false otherwise.
*
* @since 4.0.0
*/
public function cancel($key = null)
{
$result = parent::cancel();
if ($return = $this->input->get('return', '', 'BASE64')) {
$return = base64_decode($return);
// Don't redirect to an external URL.
if (!Uri::isInternal($return)) {
$return = Uri::base();
}
$this->app->redirect($return);
}
return $result;
}
/**
* Override parent save to redirect when using status edit account.
*
* @param string $key The name of the primary key of the URL variable.
* @param string $urlVar The name of the URL variable if different from the primary key (sometimes required to avoid router collisions).
*
* @return boolean True if successful, false otherwise.
*
* @since 4.0.0
*/
public function save($key = null, $urlVar = null)
{
$result = parent::save($key, $urlVar);
$task = $this->getTask();
if ($task === 'save' && $return = $this->input->get('return', '', 'BASE64')) {
$return = base64_decode($return);
// Don't redirect to an external URL.
if (!Uri::isInternal($return)) {
$return = Uri::base();
}
$this->setRedirect($return);
}
// If a user has to renew a password but has no permission for users
if (!$this->app->getIdentity()->authorise('core.admin', 'com_users')) {
$this->setRedirect('index.php');
}
return $result;
}
/**
* Method to run batch operations.
*
* @param object $model The model.
*
* @return boolean True on success, false on failure
*
* @since 2.5
*/
public function batch($model = null)
{
$this->checkToken();
// Set the model
$model = $this->getModel('User', 'Administrator', []);
// Preset the redirect
$this->setRedirect(Route::_('index.php?option=com_users&view=users' . $this->getRedirectToListAppend(), false));
return parent::batch($model);
}
/**
* Function that allows child controller access to model data after the data has been saved.
*
* @param BaseDatabaseModel $model The data model object.
* @param array $validData The validated data.
*
* @return void
*
* @since 3.1
*/
protected function postSaveHook(BaseDatabaseModel $model, $validData = [])
{
}
}

View File

@ -0,0 +1,173 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_users
*
* @copyright (C) 2009 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Component\Users\Administrator\Controller;
use Joomla\CMS\Application\CMSApplication;
use Joomla\CMS\Input\Input;
use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\Controller\AdminController;
use Joomla\CMS\MVC\Controller\BaseController;
use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
use Joomla\CMS\Response\JsonResponse;
use Joomla\Utilities\ArrayHelper;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Users list controller class.
*
* @since 1.6
*/
class UsersController extends AdminController
{
/**
* @var string The prefix to use with controller messages.
* @since 1.6
*/
protected $text_prefix = 'COM_USERS_USERS';
/**
* Constructor.
*
* @param array $config An optional associative array of configuration settings.
* @param MVCFactoryInterface $factory The factory.
* @param CMSApplication $app The CMSApplication for the dispatcher
* @param Input $input Input
*
* @since 1.6
* @see BaseController
* @throws \Exception
*/
public function __construct($config = [], MVCFactoryInterface $factory = null, $app = null, $input = null)
{
parent::__construct($config, $factory, $app, $input);
$this->registerTask('block', 'changeBlock');
$this->registerTask('unblock', 'changeBlock');
}
/**
* Proxy for getModel.
*
* @param string $name The model name. Optional.
* @param string $prefix The class prefix. Optional.
* @param array $config Configuration array for model. Optional.
*
* @return object The model.
*
* @since 1.6
*/
public function getModel($name = 'User', $prefix = 'Administrator', $config = ['ignore_request' => true])
{
return parent::getModel($name, $prefix, $config);
}
/**
* Method to change the block status on a record.
*
* @return void
*
* @since 1.6
*/
public function changeBlock()
{
// Check for request forgeries.
$this->checkToken();
$ids = (array) $this->input->get('cid', [], 'int');
$values = ['block' => 1, 'unblock' => 0];
$task = $this->getTask();
$value = ArrayHelper::getValue($values, $task, 0, 'int');
// Remove zero values resulting from input filter
$ids = array_filter($ids);
if (empty($ids)) {
$this->setMessage(Text::_('COM_USERS_USERS_NO_ITEM_SELECTED'), 'warning');
} else {
// Get the model.
$model = $this->getModel();
// Change the state of the records.
if (!$model->block($ids, $value)) {
$this->setMessage($model->getError(), 'error');
} else {
if ($value == 1) {
$this->setMessage(Text::plural('COM_USERS_N_USERS_BLOCKED', \count($ids)));
} elseif ($value == 0) {
$this->setMessage(Text::plural('COM_USERS_N_USERS_UNBLOCKED', \count($ids)));
}
}
}
$this->setRedirect('index.php?option=com_users&view=users');
}
/**
* Method to activate a record.
*
* @return void
*
* @since 1.6
*/
public function activate()
{
// Check for request forgeries.
$this->checkToken();
$ids = (array) $this->input->get('cid', [], 'int');
// Remove zero values resulting from input filter
$ids = array_filter($ids);
if (empty($ids)) {
$this->setMessage(Text::_('COM_USERS_USERS_NO_ITEM_SELECTED'), 'error');
} else {
// Get the model.
$model = $this->getModel();
// Change the state of the records.
if (!$model->activate($ids)) {
$this->setMessage($model->getError(), 'error');
} else {
$this->setMessage(Text::plural('COM_USERS_N_USERS_ACTIVATED', \count($ids)));
}
}
$this->setRedirect('index.php?option=com_users&view=users');
}
/**
* Method to get the number of active users
*
* @return void
*
* @since 4.0.0
*/
public function getQuickiconContent()
{
$model = $this->getModel('Users');
$model->setState('filter.state', 0);
$amount = (int) $model->getTotal();
$result = [];
$result['amount'] = $amount;
$result['sronly'] = Text::plural('COM_USERS_N_QUICKICON_SRONLY', $amount);
$result['name'] = Text::plural('COM_USERS_N_QUICKICON', $amount);
echo new JsonResponse($result);
}
}

View File

@ -0,0 +1,198 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_users
*
* @copyright (C) 2022 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Component\Users\Administrator\DataShape;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* @property string $pre_message Custom HTML to display above the MFA form
* @property string $field_type How to render the MFA code field. "input" or "custom".
* @property string $input_type The type attribute for the HTML input box. Typically "text" or "password".
* @property string $placeholder Placeholder text for the HTML input box. Leave empty if you don't need it.
* @property string $label Label to show above the HTML input box. Leave empty if you don't need it.
* @property string $html Custom HTML. Only used when field_type = custom.
* @property string $post_message Custom HTML to display below the MFA form
* @property bool $hide_submit Should I hide the default Submit button?
* @property bool $allowEntryBatching Is this method validating against all configured authenticators of this type?
* @property string $help_url URL for help content
*
* @since 4.2.0
*/
class CaptiveRenderOptions extends DataShapeObject
{
/**
* Display a standard HTML5 input field. Use the input_type, placeholder and label properties to set it up.
*
* @since 4.2.0
*/
public const FIELD_INPUT = 'input';
/**
* Display a custom HTML document. Use the html property to set it up.
*
* @since 4.2.0
*/
public const FIELD_CUSTOM = 'custom';
/**
* Custom HTML to display above the MFA form
*
* @var string
* @since 4.2.0
*/
protected $pre_message = '';
/**
* How to render the MFA code field. "input" (HTML input element) or "custom" (custom HTML)
*
* @var string
* @since 4.2.0
*/
protected $field_type = 'input';
/**
* The type attribute for the HTML input box. Typically "text" or "password". Use any HTML5 input type.
*
* @var string
* @since 4.2.0
*/
protected $input_type = '';
/**
* Attributes other than type and id which will be added to the HTML input box.
*
* @var array
* @@since 4.2.0
*/
protected $input_attributes = [];
/**
* Placeholder text for the HTML input box. Leave empty if you don't need it.
*
* @var string
* @since 4.2.0
*/
protected $placeholder = '';
/**
* Label to show above the HTML input box. Leave empty if you don't need it.
*
* @var string
* @since 4.2.0
*/
protected $label = '';
/**
* Custom HTML. Only used when field_type = custom.
*
* @var string
* @since 4.2.0
*/
protected $html = '';
/**
* Custom HTML to display below the MFA form
*
* @var string
* @since 4.2.0
*/
protected $post_message = '';
/**
* Should I hide the default Submit button?
*
* @var boolean
* @since 4.2.0
*/
protected $hide_submit = false;
/**
* Additional CSS classes for the submit button (apply the MFA setup)
*
* @var string
* @since 4.2.0
*/
protected $submit_class = '';
/**
* Icon class to use for the submit button
*
* @var string
* @since 4.2.0
*/
protected $submit_icon = 'icon icon-rightarrow icon-arrow-right';
/**
* Language key to use for the text on the submit button
*
* @var string
* @since 4.2.0
*/
protected $submit_text = 'COM_USERS_MFA_VALIDATE';
/**
* Is this MFA method validating against all configured authenticators of the same type?
*
* @var boolean
* @since 4.2.0
*/
protected $allowEntryBatching = true;
/**
* URL for help content
*
* @var string
* @since 4.2.0
*/
protected $help_url = '';
/**
* Setter for the field_type property
*
* @param string $value One of self::FIELD_INPUT, self::FIELD_CUSTOM
*
* @since 4.2.0
* @throws \InvalidArgumentException
*/
// phpcs:ignore
protected function setField_type(string $value)
{
if (!\in_array($value, [self::FIELD_INPUT, self::FIELD_CUSTOM])) {
throw new \InvalidArgumentException('Invalid value for property field_type.');
}
$this->field_type = $value;
}
/**
* Setter for the input_attributes property.
*
* @param array $value The value to set
*
* @return void
* @@since 4.2.0
*/
// phpcs:ignore
protected function setInput_attributes(array $value)
{
$forbiddenAttributes = ['id', 'type', 'name', 'value'];
foreach ($forbiddenAttributes as $key) {
if (isset($value[$key])) {
unset($value[$key]);
}
}
$this->input_attributes = $value;
}
}

View File

@ -0,0 +1,191 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_users
*
* @copyright (C) 2022 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Component\Users\Administrator\DataShape;
/**
* Generic helper for handling data shapes in com_users
*
* @since 4.2.0
*/
abstract class DataShapeObject implements \ArrayAccess
{
/**
* Public constructor
*
* @param array $array The data to initialise this object with
*
* @since 4.2.0
*/
public function __construct(array $array = [])
{
if (!\is_array($array) && !($array instanceof self)) {
throw new \InvalidArgumentException(sprintf('%s needs an array or a %s object', __METHOD__, __CLASS__));
}
foreach (($array instanceof self) ? $array->asArray() : $array as $k => $v) {
$this[$k] = $v;
}
}
/**
* Get the data shape as a key-value array
*
* @return array
*
* @since 4.2.0
*/
public function asArray(): array
{
return get_object_vars($this);
}
/**
* Merge another data shape object or key-value array into this object.
*
* @param array|self $newValues The object or array to merge into self.
*
* @return $this
*
* @since 4.2.0
*/
public function merge($newValues): self
{
if (!\is_array($newValues) && !($newValues instanceof self)) {
throw new \InvalidArgumentException(sprintf('%s needs an array or a %s object', __METHOD__, __CLASS__));
}
foreach (($newValues instanceof self) ? $newValues->asArray() : $newValues as $k => $v) {
if (!isset($this->{$k})) {
continue;
}
$this[$k] = $v;
}
return $this;
}
/**
* Magic getter
*
* @param string $name The name of the property to retrieve
*
* @return mixed
*
* @since 4.2.0
*/
public function __get($name)
{
$methodName = 'get' . ucfirst($name);
if (method_exists($this, $methodName)) {
return $this->{$methodName};
}
if (property_exists($this, $name)) {
return $this->{$name};
}
throw new \InvalidArgumentException(sprintf('Property %s not found in %s', $name, __CLASS__));
}
/**
* Magic Setter
*
* @param string $name The property to set the value for
* @param mixed $value The property value to set it to
*
* @return mixed
* @since 4.2.0
*/
public function __set($name, $value)
{
$methodName = 'set' . ucfirst($name);
if (method_exists($this, $methodName)) {
return $this->{$methodName}($value);
}
if (property_exists($this, $name)) {
$this->{$name} = $value;
}
throw new \InvalidArgumentException(sprintf('Property %s not found in %s', $name, __CLASS__));
}
/**
* Is a property set?
*
* @param string $name Property name
*
* @return boolean Does it exist in the object?
* @since 4.2.0
*/
public function __isset($name): bool
{
$methodName = 'get' . ucfirst($name);
return method_exists($this, $methodName) || property_exists($this, $name);
}
/**
* Does the property exist (array access)?
*
* @param string $offset Property name
*
* @return boolean
* @since 4.2.0
*/
public function offsetExists($offset): bool
{
return isset($this->{$offset});
}
/**
* Get the value of a property (array access).
*
* @param string $offset Property name
*
* @return mixed
* @since 4.2.0
*/
public function offsetGet($offset): mixed
{
return $this->{$offset};
}
/**
* Set the value of a property (array access).
*
* @param string $offset Property name
* @param mixed $value Property value
*
* @return void
* @since 4.2.0
*/
public function offsetSet($offset, $value): void
{
$this->{$offset} = $value;
}
/**
* Unset a property (array access).
*
* @param string $offset Property name
*
* @return void
* @since 4.2.0
*/
public function offsetUnset($offset): void
{
throw new \LogicException(sprintf('You cannot unset members of %s', __CLASS__));
}
}

View File

@ -0,0 +1,120 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_users
*
* @copyright (C) 2022 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Component\Users\Administrator\DataShape;
use Joomla\Component\Users\Administrator\Table\MfaTable;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* @property string $name Internal code of this MFA Method
* @property string $display User-facing name for this MFA Method
* @property string $shortinfo Short description of this MFA Method displayed to the user
* @property string $image URL to the logo image for this Method
* @property bool $canDisable Are we allowed to disable it?
* @property bool $allowMultiple Are we allowed to have multiple instances of it per user?
* @property string $help_url URL for help content
* @property bool $allowEntryBatching Allow authentication against all entries of this MFA Method.
*
* @since 4.2.0
*/
class MethodDescriptor extends DataShapeObject
{
/**
* Internal code of this MFA Method
*
* @var string
* @since 4.2.0
*/
protected $name = '';
/**
* User-facing name for this MFA Method
*
* @var string
* @since 4.2.0
*/
protected $display = '';
/**
* Short description of this MFA Method displayed to the user
*
* @var string
* @since 4.2.0
*/
protected $shortinfo = '';
/**
* URL to the logo image for this Method
*
* @var string
* @since 4.2.0
*/
protected $image = '';
/**
* Are we allowed to disable it?
*
* @var boolean
* @since 4.2.0
*/
protected $canDisable = true;
/**
* Are we allowed to have multiple instances of it per user?
*
* @var boolean
* @since 4.2.0
*/
protected $allowMultiple = false;
/**
* URL for help content
*
* @var string
* @since 4.2.0
*/
protected $help_url = '';
/**
* Allow authentication against all entries of this MFA Method.
*
* Otherwise authentication takes place against a SPECIFIC entry at a time.
*
* @var boolean
* @since 4.2.0
*/
protected $allowEntryBatching = false;
/**
* Active authentication methods, used internally only
*
* @var MfaTable[]
* @since 4.2.0
* @internal
*/
protected $active = [];
/**
* Adds an active MFA method
*
* @param MfaTable $record The MFA method record to add
*
* @return void
* @since 4.2.0
*/
public function addActiveMethod(MfaTable $record)
{
$this->active[$record->id] = $record;
}
}

View File

@ -0,0 +1,241 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_users
*
* @copyright (C) 2022 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Component\Users\Administrator\DataShape;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Data shape for Method Setup Render Options
*
* @property string $default_title Default title if you are setting up this MFA Method for the first time
* @property string $pre_message Custom HTML to display above the MFA setup form
* @property string $table_heading Heading for displayed tabular data. Typically used to display a list of fixed MFA
* codes, TOTP setup parameters etc
* @property array $tabular_data Any tabular data to display (label => custom HTML). See above
* @property array $hidden_data Hidden fields to include in the form (name => value)
* @property string $field_type How to render the MFA setup code field. "input" (HTML input element) or "custom"
* (custom HTML)
* @property string $input_type The type attribute for the HTML input box. Typically "text" or "password". Use any
* HTML5 input type.
* @property string $input_value Pre-filled value for the HTML input box. Typically used for fixed codes, the fixed
* YubiKey ID etc.
* @property string $placeholder Placeholder text for the HTML input box. Leave empty if you don't need it.
* @property string $label Label to show above the HTML input box. Leave empty if you don't need it.
* @property string $html Custom HTML. Only used when field_type = custom.
* @property bool $show_submit Should I show the submit button (apply the MFA setup)?
* @property string $submit_class Additional CSS classes for the submit button (apply the MFA setup)
* @property string $post_message Custom HTML to display below the MFA setup form
* @property string $help_url A URL with help content for this Method to display to the user
*
* @since 4.2.0
*/
class SetupRenderOptions extends DataShapeObject
{
/**
* Display a standard HTML5 input field. Use the input_type, placeholder and label properties to set it up.
*
* @since 4.2.0
*/
public const FIELD_INPUT = 'input';
/**
* Display a custom HTML document. Use the html property to set it up.
*
* @since 4.2.0
*/
public const FIELD_CUSTOM = 'custom';
/**
* Default title if you are setting up this MFA Method for the first time
*
* @var string
* @since 4.2.0
*/
protected $default_title = '';
/**
* Custom HTML to display above the MFA setup form parameters etc
*
* @var string
* @since 4.2.0
*/
protected $pre_message = '';
/**
* Heading for displayed tabular data. Typically used to display a list of fixed MFA codes, TOTP setup
*
* @var string
* @since 4.2.0
*/
protected $table_heading = '';
/**
* Any tabular data to display (label => custom HTML). See above
*
* @var array
* @since 4.2.0
*/
protected $tabular_data = [];
/**
* Hidden fields to include in the form (name => value)
*
* @var array
* @since 4.2.0
*/
protected $hidden_data = [];
/**
* How to render the MFA setup code field. "input" (HTML input element) or "custom" (custom HTML)
*
* @var string
* @since 4.2.0
*/
protected $field_type = 'input';
/**
* The type attribute for the HTML input box. Typically "text" or "password". Use any HTML5 input type.
*
* @var string
* @since 4.2.0
*/
protected $input_type = 'text';
/**
* Attributes other than type and id which will be added to the HTML input box.
*
* @var array
* @@since 4.2.0
*/
protected $input_attributes = [];
/**
* Pre-filled value for the HTML input box. Typically used for fixed codes, the fixed YubiKey ID etc.
*
* @var string
* @since 4.2.0
*/
protected $input_value = '';
/**
* Placeholder text for the HTML input box. Leave empty if you don't need it.
*
* @var string
* @since 4.2.0
*/
protected $placeholder = '';
/**
* Label to show above the HTML input box. Leave empty if you don't need it.
*
* @var string
* @since 4.2.0
*/
protected $label = '';
/**
* Custom HTML. Only used when field_type = custom.
*
* @var string
* @since 4.2.0
*/
protected $html = '';
/**
* Should I show the submit button (apply the MFA setup)?
*
* @var boolean
* @since 4.2.0
*/
protected $show_submit = true;
/**
* Additional CSS classes for the submit button (apply the MFA setup)
*
* @var string
* @since 4.2.0
*/
protected $submit_class = '';
/**
* Icon class to use for the submit button
*
* @var string
* @since 4.2.0
*/
protected $submit_icon = 'icon icon-ok';
/**
* Language key to use for the text on the submit button
*
* @var string
* @since 4.2.0
*/
protected $submit_text = 'JSAVE';
/**
* Custom HTML to display below the MFA setup form
*
* @var string
* @since 4.2.0
*/
protected $post_message = '';
/**
* A URL with help content for this Method to display to the user
*
* @var string
* @since 4.2.0
*/
protected $help_url = '';
/**
* Setter for the field_type property
*
* @param string $value One of self::FIELD_INPUT, self::FIELD_CUSTOM
*
* @since 4.2.0
* @throws \InvalidArgumentException
*/
// phpcs:ignore
protected function setField_type($value)
{
if (!\in_array($value, [self::FIELD_INPUT, self::FIELD_CUSTOM])) {
throw new \InvalidArgumentException('Invalid value for property field_type.');
}
$this->field_type = $value;
}
/**
* Setter for the input_attributes property.
*
* @param array $value The value to set
*
* @return void
* @since 4.2.0
*/
// phpcs:ignore
protected function setInput_attributes(array $value)
{
$forbiddenAttributes = ['id', 'type', 'name', 'value'];
foreach ($forbiddenAttributes as $key) {
if (isset($value[$key])) {
unset($value[$key]);
}
}
$this->input_attributes = $value;
}
}

View File

@ -0,0 +1,73 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_users
*
* @copyright (C) 2021 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Component\Users\Administrator\Dispatcher;
use Joomla\CMS\Dispatcher\ComponentDispatcher;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* ComponentDispatcher class for com_users
*
* @since 4.0.0
*/
class Dispatcher extends ComponentDispatcher
{
/**
* Override checkAccess to allow users edit profile without having to have core.manager permission
*
* @return void
*
* @since 4.0.0
*/
protected function checkAccess()
{
$task = $this->input->getCmd('task');
$view = $this->input->getCmd('view');
$layout = $this->input->getCmd('layout');
$allowedTasks = ['user.edit', 'user.apply', 'user.save', 'user.cancel'];
// Allow users to edit their own account
if (\in_array($task, $allowedTasks, true) || ($view === 'user' && $layout === 'edit')) {
$user = $this->app->getIdentity();
$id = $this->input->getInt('id');
if ((int) $user->id === $id) {
return;
}
}
/**
* Special case: Multi-factor Authentication
*
* We allow access to all MFA views and tasks. Access control for MFA tasks is performed in
* the Controllers since what is allowed depends on who is logged in and whose account you
* are trying to modify. Implementing these checks in the Dispatcher would violate the
* separation of concerns.
*/
$allowedViews = ['callback', 'captive', 'method', 'methods'];
$isAllowedTask = array_reduce(
$allowedViews,
function ($carry, $taskPrefix) use ($task) {
return $carry || strpos($task ?? '', $taskPrefix . '.') === 0;
},
false
);
if (\in_array(strtolower($view ?? ''), $allowedViews) || $isAllowedTask) {
return;
}
parent::checkAccess();
}
}

View File

@ -0,0 +1,99 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_users
*
* @copyright (C) 2018 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Component\Users\Administrator\Extension;
use Joomla\CMS\Component\Router\RouterServiceInterface;
use Joomla\CMS\Component\Router\RouterServiceTrait;
use Joomla\CMS\Extension\BootableExtensionInterface;
use Joomla\CMS\Extension\MVCComponent;
use Joomla\CMS\Factory;
use Joomla\CMS\Fields\FieldsServiceInterface;
use Joomla\CMS\HTML\HTMLRegistryAwareTrait;
use Joomla\Component\Users\Administrator\Service\HTML\Users;
use Psr\Container\ContainerInterface;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Component class for com_users
*
* @since 4.0.0
*/
class UsersComponent extends MVCComponent implements BootableExtensionInterface, RouterServiceInterface, FieldsServiceInterface
{
use RouterServiceTrait;
use HTMLRegistryAwareTrait;
/**
* Booting the extension. This is the function to set up the environment of the extension like
* registering new class loaders, etc.
*
* If required, some initial set up can be done from services of the container, eg.
* registering HTML services.
*
* @param ContainerInterface $container The container
*
* @return void
*
* @since 4.0.0
*/
public function boot(ContainerInterface $container)
{
$this->getRegistry()->register('users', new Users());
}
/**
* Returns a valid section for the given section. If it is not valid then null is returned.
*
* @param string $section The section to get the mapping for
* @param object|null $item The content item or null
*
* @return string|null The new section or null
*
* @since 4.0.0
*/
public function validateSection($section, $item = null)
{
if (Factory::getApplication()->isClient('site')) {
switch ($section) {
case 'registration':
case 'profile':
return 'user';
}
}
if ($section === 'user') {
return $section;
}
// We don't know other sections.
return null;
}
/**
* Returns valid contexts.
*
* @return array Associative array with contexts as keys and translated strings as values
*
* @since 4.0.0
*/
public function getContexts(): array
{
$language = Factory::getApplication()->getLanguage();
$language->load('com_users', JPATH_ADMINISTRATOR);
return [
'com_users.user' => $language->_('COM_USERS'),
];
}
}

View File

@ -0,0 +1,100 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_users
*
* @copyright (C) 2010 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Component\Users\Administrator\Field;
use Joomla\CMS\Access\Access;
use Joomla\CMS\Factory;
use Joomla\CMS\Form\Field\ListField;
use Joomla\CMS\Helper\UserGroupsHelper;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* User Group Parent field..
*
* @since 1.6
*/
class GroupparentField extends ListField
{
/**
* The form field type.
*
* @var string
* @since 1.6
*/
protected $type = 'GroupParent';
/**
* Method to clean the Usergroup Options from all children starting by a given father
*
* @param array $userGroupsOptions The usergroup options to clean
* @param integer $fatherId The father ID to start with
*
* @return array The cleaned field options
*
* @since 3.9.4
*/
private function cleanOptionsChildrenByFather($userGroupsOptions, $fatherId)
{
foreach ($userGroupsOptions as $userGroupsOptionsId => $userGroupsOptionsData) {
if ((int) $userGroupsOptionsData->parent_id === (int) $fatherId) {
unset($userGroupsOptions[$userGroupsOptionsId]);
$userGroupsOptions = $this->cleanOptionsChildrenByFather($userGroupsOptions, $userGroupsOptionsId);
}
}
return $userGroupsOptions;
}
/**
* Method to get the field options.
*
* @return array The field option objects
*
* @since 1.6
*/
protected function getOptions()
{
$options = UserGroupsHelper::getInstance()->getAll();
$currentGroupId = (int) Factory::getApplication()->getInput()->get('id', 0, 'int');
// Prevent to set yourself as parent
if ($currentGroupId) {
unset($options[$currentGroupId]);
}
// We should not remove any groups when we are creating a new group
if ($currentGroupId !== 0) {
// Prevent parenting direct children and children of children of this item.
$options = $this->cleanOptionsChildrenByFather($options, $currentGroupId);
}
$options = array_values($options);
$isSuperAdmin = $this->getCurrentUser()->authorise('core.admin');
// Pad the option text with spaces using depth level as a multiplier.
for ($i = 0, $n = \count($options); $i < $n; $i++) {
// Show groups only if user is super admin or group is not super admin
if ($isSuperAdmin || !Access::checkGroup($options[$i]->id, 'core.admin')) {
$options[$i]->value = $options[$i]->id;
$options[$i]->text = str_repeat('- ', $options[$i]->level) . $options[$i]->title;
} else {
unset($options[$i]);
}
}
// Merge any additional options in the XML definition.
return array_merge(parent::getOptions(), $options);
}
}

View File

@ -0,0 +1,47 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_users
*
* @copyright (C) 2016 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Component\Users\Administrator\Field;
use Joomla\CMS\Form\Field\ListField;
use Joomla\Component\Users\Administrator\Helper\DebugHelper;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Access Levels field.
*
* @since 3.6.0
*/
class LevelsField extends ListField
{
/**
* The form field type.
*
* @var string
* @since 3.6.0
*/
protected $type = 'Levels';
/**
* Method to get the field options.
*
* @return array The field option objects
*
* @since 3.6.0
*/
protected function getOptions()
{
// Merge any additional options in the XML definition.
return array_merge(parent::getOptions(), DebugHelper::getLevelsOptions());
}
}

View File

@ -0,0 +1,26 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_users
*
* @copyright (C) 2022 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Component\Users\Administrator\Field;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Select modules positions.
*
* Reuses the same field from com_modules. Don't lose it; reuse it!
*
* @since 4.2.0
*/
class ModulesPositionField extends \Joomla\Component\Modules\Administrator\Field\ModulesPositionField
{
}

View File

@ -0,0 +1,163 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_users
*
* @copyright (C) 2017 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Component\Users\Administrator\Helper;
use Joomla\CMS\Access\Access;
use Joomla\CMS\Factory;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\Utilities\ArrayHelper;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Users component debugging helper.
*
* @since 1.6
*/
class DebugHelper
{
/**
* Get a list of the components.
*
* @return array
*
* @since 1.6
*/
public static function getComponents()
{
// Initialise variable.
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select('name AS text, element AS value')
->from('#__extensions')
->where('enabled >= 1')
->where('type =' . $db->quote('component'));
$items = $db->setQuery($query)->loadObjectList();
if (\count($items)) {
$lang = Factory::getLanguage();
foreach ($items as &$item) {
// Load language
$extension = $item->value;
$source = JPATH_ADMINISTRATOR . '/components/' . $extension;
$lang->load("$extension.sys", JPATH_ADMINISTRATOR)
|| $lang->load("$extension.sys", $source);
// Translate component name
$item->text = Text::_($item->text);
}
// Sort by component name
$items = ArrayHelper::sortObjects($items, 'text', 1, true, true);
}
return $items;
}
/**
* Get a list of the actions for the component or code actions.
*
* @param string $component The name of the component.
*
* @return array
*
* @since 1.6
*/
public static function getDebugActions($component = null)
{
$actions = [];
// Try to get actions for the component
if (!empty($component)) {
$component_actions = Access::getActionsFromFile(JPATH_ADMINISTRATOR . '/components/' . $component . '/access.xml');
if (!empty($component_actions)) {
foreach ($component_actions as &$action) {
$descr = (string) $action->title;
if (!empty($action->description)) {
$descr = (string) $action->description;
}
$actions[$action->title] = [$action->name, $descr];
}
}
}
// Use default actions from configuration if no component selected or component doesn't have actions
if (empty($actions)) {
$filename = JPATH_ADMINISTRATOR . '/components/com_config/forms/application.xml';
if (is_file($filename)) {
$xml = simplexml_load_file($filename);
foreach ($xml->children()->fieldset as $fieldset) {
if ('permissions' == (string) $fieldset['name']) {
foreach ($fieldset->children() as $field) {
if ('rules' == (string) $field['name']) {
foreach ($field->children() as $action) {
$descr = (string) $action['title'];
if (isset($action['description']) && !empty($action['description'])) {
$descr = (string) $action['description'];
}
$actions[(string) $action['title']] = [
(string) $action['name'],
$descr,
];
}
break;
}
}
}
}
// Load language
$lang = Factory::getLanguage();
$extension = 'com_config';
$source = JPATH_ADMINISTRATOR . '/components/' . $extension;
$lang->load($extension, JPATH_ADMINISTRATOR, null, false, false)
|| $lang->load($extension, $source, null, false, false)
|| $lang->load($extension, JPATH_ADMINISTRATOR, $lang->getDefault(), false, false)
|| $lang->load($extension, $source, $lang->getDefault(), false, false);
}
}
return $actions;
}
/**
* Get a list of filter options for the levels.
*
* @return array An array of \JHtmlOption elements.
*/
public static function getLevelsOptions()
{
// Build the filter options.
$options = [];
$options[] = HTMLHelper::_('select.option', '1', Text::sprintf('COM_USERS_OPTION_LEVEL_COMPONENT', 1));
$options[] = HTMLHelper::_('select.option', '2', Text::sprintf('COM_USERS_OPTION_LEVEL_CATEGORY', 2));
$options[] = HTMLHelper::_('select.option', '3', Text::sprintf('COM_USERS_OPTION_LEVEL_DEEPER', 3));
$options[] = HTMLHelper::_('select.option', '4', '4');
$options[] = HTMLHelper::_('select.option', '5', '5');
$options[] = HTMLHelper::_('select.option', '6', '6');
return $options;
}
}

View File

@ -0,0 +1,362 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_users
*
* @copyright (C) 2022 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Component\Users\Administrator\Helper;
use Exception;
use Joomla\CMS\Application\CMSApplication;
use Joomla\CMS\Component\ComponentHelper;
use Joomla\CMS\Document\HtmlDocument;
use Joomla\CMS\Event\MultiFactor\GetMethod;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\CMS\Uri\Uri;
use Joomla\CMS\User\User;
use Joomla\CMS\User\UserFactoryInterface;
use Joomla\Component\Users\Administrator\DataShape\MethodDescriptor;
use Joomla\Component\Users\Administrator\Model\BackupcodesModel;
use Joomla\Component\Users\Administrator\Model\MethodsModel;
use Joomla\Component\Users\Administrator\Table\MfaTable;
use Joomla\Component\Users\Administrator\View\Methods\HtmlView;
use Joomla\Database\DatabaseInterface;
use Joomla\Database\ParameterType;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Helper functions for captive MFA handling
*
* @since 4.2.0
*/
abstract class Mfa
{
/**
* Cache of all currently active MFAs
*
* @var array|null
* @since 4.2.0
*/
protected static $allMFAs = null;
/**
* Are we inside the administrator application
*
* @var boolean
* @since 4.2.0
*/
protected static $isAdmin = null;
/**
* Get the HTML for the Multi-factor Authentication configuration interface for a user.
*
* This helper method uses a sort of primitive HMVC to display the com_users' Methods page which
* renders the MFA configuration interface.
*
* @param User $user The user we are going to show the configuration UI for.
*
* @return string|null The HTML of the UI; null if we cannot / must not show it.
* @throws \Exception
* @since 4.2.0
*/
public static function getConfigurationInterface(User $user): ?string
{
// Check the conditions
if (!self::canShowConfigurationInterface($user)) {
return null;
}
/** @var CMSApplication $app */
$app = Factory::getApplication();
if (!$app->getInput()->getCmd('option', '') === 'com_users') {
$app->getLanguage()->load('com_users');
$app->getDocument()
->getWebAssetManager()
->getRegistry()
->addExtensionRegistryFile('com_users');
}
// Get a model
/** @var MVCFactoryInterface $factory */
$factory = Factory::getApplication()->bootComponent('com_users')->getMVCFactory();
/** @var MethodsModel $methodsModel */
$methodsModel = $factory->createModel('Methods', 'Administrator');
/** @var BackupcodesModel $methodsModel */
$backupCodesModel = $factory->createModel('Backupcodes', 'Administrator');
// Get a view object
$appRoot = $app->isClient('site') ? \JPATH_SITE : \JPATH_ADMINISTRATOR;
$prefix = $app->isClient('site') ? 'Site' : 'Administrator';
/** @var HtmlView $view */
$view = $factory->createView(
'Methods',
$prefix,
'Html',
[
'base_path' => $appRoot . '/components/com_users',
]
);
$view->setModel($methodsModel, true);
/** @noinspection PhpParamsInspection */
$view->setModel($backupCodesModel);
$view->document = $app->getDocument();
$view->returnURL = base64_encode(Uri::getInstance()->toString());
$view->user = $user;
$view->set('forHMVC', true);
$view->setLanguage($app->getLanguage());
@ob_start();
try {
$view->display();
} catch (\Throwable $e) {
@ob_end_clean();
/**
* This is intentional! When you are developing a Multi-factor Authentication plugin you
* will inevitably mess something up and end up with an error. This would cause the
* entire MFA configuration page to disappear. No problem! Set Debug System to Yes in
* Global Configuration and you can see the error exception which will help you solve
* your problem.
*/
if (\defined('JDEBUG') && JDEBUG) {
throw $e;
}
return null;
}
return @ob_get_clean();
}
/**
* Get a list of all of the MFA Methods
*
* @return MethodDescriptor[]
* @since 4.2.0
*/
public static function getMfaMethods(): array
{
PluginHelper::importPlugin('multifactorauth');
if (\is_null(self::$allMFAs)) {
// Get all the plugin results
$event = new GetMethod();
$temp = Factory::getApplication()
->getDispatcher()
->dispatch($event->getName(), $event)
->getArgument('result', []);
// Normalize the results
self::$allMFAs = [];
foreach ($temp as $method) {
if (!\is_array($method) && !($method instanceof MethodDescriptor)) {
continue;
}
$method = $method instanceof MethodDescriptor
? $method : new MethodDescriptor($method);
if (empty($method['name'])) {
continue;
}
self::$allMFAs[$method['name']] = $method;
}
}
return self::$allMFAs;
}
/**
* Is the current user allowed to add/edit MFA methods for $user?
*
* This is only allowed if I am adding / editing methods for myself.
*
* If the target user is a member of any group disallowed to use MFA this will return false.
*
* @param User|null $user The user you want to know if we're allowed to edit
*
* @return boolean
* @throws \Exception
* @since 4.2.0
*/
public static function canAddEditMethod(?User $user = null): bool
{
// Cannot do MFA operations on no user or a guest user.
if (\is_null($user) || $user->guest) {
return false;
}
// If the user is in a user group which disallows MFA we cannot allow adding / editing methods.
$neverMFAGroups = ComponentHelper::getParams('com_users')->get('neverMFAUserGroups', []);
$neverMFAGroups = \is_array($neverMFAGroups) ? $neverMFAGroups : [];
if (\count(array_intersect($user->getAuthorisedGroups(), $neverMFAGroups))) {
return false;
}
// Check if this is the same as the logged-in user.
$myUser = Factory::getApplication()->getIdentity()
?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0);
return $myUser->id === $user->id;
}
/**
* Is the current user allowed to delete MFA methods / disable MFA for $user?
*
* This is allowed if:
* - The user being queried is the same as the logged-in user
* - The logged-in user is a Super User AND the queried user is NOT a Super User.
*
* Note that Super Users can be edited by their own user only for security reasons. If a Super
* User gets locked out they must use the Backup Codes to regain access. If that's not possible,
* they will need to delete their records from the `#__user_mfa` table.
*
* @param User|null $user The user being queried.
*
* @return boolean
* @throws \Exception
* @since 4.2.0
*/
public static function canDeleteMethod(?User $user = null): bool
{
// Cannot do MFA operations on no user or a guest user.
if (\is_null($user) || $user->guest) {
return false;
}
$myUser = Factory::getApplication()->getIdentity()
?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0);
return $myUser->id === $user->id
|| ($myUser->authorise('core.admin') && !$user->authorise('core.admin'));
}
/**
* Return all MFA records for a specific user
*
* @param int|null $userId User ID. NULL for currently logged in user.
*
* @return MfaTable[]
* @throws \Exception
*
* @since 4.2.0
*/
public static function getUserMfaRecords(?int $userId): array
{
if (empty($userId)) {
$user = Factory::getApplication()->getIdentity() ?: Factory::getUser();
$userId = $user->id ?: 0;
}
/** @var DatabaseInterface $db */
$db = Factory::getContainer()->get(DatabaseInterface::class);
$query = $db->getQuery(true)
->select($db->quoteName('id'))
->from($db->quoteName('#__user_mfa'))
->where($db->quoteName('user_id') . ' = :user_id')
->bind(':user_id', $userId, ParameterType::INTEGER);
try {
$ids = $db->setQuery($query)->loadColumn() ?: [];
} catch (\Exception $e) {
$ids = [];
}
if (empty($ids)) {
return [];
}
/** @var MVCFactoryInterface $factory */
$factory = Factory::getApplication()->bootComponent('com_users')->getMVCFactory();
// Map all results to MFA table objects
$records = array_map(
function ($id) use ($factory) {
/** @var MfaTable $record */
$record = $factory->createTable('Mfa', 'Administrator');
$loaded = $record->load($id);
return $loaded ? $record : null;
},
$ids
);
// Let's remove Methods we couldn't decrypt when reading from the database.
$hasBackupCodes = false;
$records = array_filter(
$records,
function ($record) use (&$hasBackupCodes) {
$isValid = !\is_null($record) && (!empty($record->options));
if ($isValid && ($record->method === 'backupcodes')) {
$hasBackupCodes = true;
}
return $isValid;
}
);
// If the only Method is backup codes it's as good as having no records
if ((\count($records) === 1) && $hasBackupCodes) {
return [];
}
return $records;
}
/**
* Are the conditions for showing the MFA configuration interface met?
*
* @param User|null $user The user to be configured
*
* @return boolean
* @throws \Exception
* @since 4.2.0
*/
public static function canShowConfigurationInterface(?User $user = null): bool
{
// If I have no user to check against that's all the checking I can do.
if (empty($user)) {
return false;
}
// I need at least one MFA method plugin for the setup interface to make any sense.
$plugins = PluginHelper::getPlugin('multifactorauth');
if (\count($plugins) < 1) {
return false;
}
/** @var CMSApplication $app */
$app = Factory::getApplication();
// We can only show a configuration page in the front- or backend application.
if (!$app->isClient('site') && !$app->isClient('administrator')) {
return false;
}
// Only show the configuration page if we have an HTML document
if (!($app->getDocument() instanceof HtmlDocument)) {
return false;
}
// I must be able to add, edit or delete the user's MFA settings
return self::canAddEditMethod($user) || self::canDeleteMethod($user);
}
}

View File

@ -0,0 +1,192 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_users
*
* @copyright (C) 2017 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Component\Users\Administrator\Helper;
use Joomla\CMS\Factory;
use Joomla\CMS\Helper\ContentHelper;
use Joomla\CMS\Helper\UserGroupsHelper;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Object\CMSObject;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Users component helper.
*
* @since 1.6
*/
class UsersHelper extends ContentHelper
{
/**
* @var CMSObject A cache for the available actions.
* @since 1.6
*/
protected static $actions;
/**
* Get a list of filter options for the blocked state of a user.
*
* @return array An array of \JHtmlOption elements.
*
* @since 1.6
*/
public static function getStateOptions()
{
// Build the filter options.
$options = [];
$options[] = HTMLHelper::_('select.option', '0', Text::_('JENABLED'));
$options[] = HTMLHelper::_('select.option', '1', Text::_('JDISABLED'));
return $options;
}
/**
* Get a list of filter options for the activated state of a user.
*
* @return array An array of \JHtmlOption elements.
*
* @since 1.6
*/
public static function getActiveOptions()
{
// Build the filter options.
$options = [];
$options[] = HTMLHelper::_('select.option', '0', Text::_('COM_USERS_ACTIVATED'));
$options[] = HTMLHelper::_('select.option', '1', Text::_('COM_USERS_UNACTIVATED'));
return $options;
}
/**
* Get a list of the user groups for filtering.
*
* @return array An array of \JHtmlOption elements.
*
* @since 1.6
*/
public static function getGroups()
{
$options = UserGroupsHelper::getInstance()->getAll();
foreach ($options as &$option) {
$option->value = $option->id;
$option->text = str_repeat('- ', $option->level) . $option->title;
}
return $options;
}
/**
* Creates a list of range options used in filter select list
* used in com_users on users view
*
* @return array
*
* @since 2.5
*/
public static function getRangeOptions()
{
$options = [
HTMLHelper::_('select.option', 'today', Text::_('COM_USERS_OPTION_RANGE_TODAY')),
HTMLHelper::_('select.option', 'past_week', Text::_('COM_USERS_OPTION_RANGE_PAST_WEEK')),
HTMLHelper::_('select.option', 'past_1month', Text::_('COM_USERS_OPTION_RANGE_PAST_1MONTH')),
HTMLHelper::_('select.option', 'past_3month', Text::_('COM_USERS_OPTION_RANGE_PAST_3MONTH')),
HTMLHelper::_('select.option', 'past_6month', Text::_('COM_USERS_OPTION_RANGE_PAST_6MONTH')),
HTMLHelper::_('select.option', 'past_year', Text::_('COM_USERS_OPTION_RANGE_PAST_YEAR')),
HTMLHelper::_('select.option', 'post_year', Text::_('COM_USERS_OPTION_RANGE_POST_YEAR')),
];
return $options;
}
/**
* No longer used.
*
* @return array
*
* @since 3.2.0
* @throws \Exception
*
* @deprecated 4.2 will be removed in 6.0
* No longer used, will be removed without replacement
*/
public static function getTwoFactorMethods()
{
return [];
}
/**
* Get a list of the User Groups for Viewing Access Levels
*
* @param string $rules User Groups in JSON format
*
* @return string $groups Comma separated list of User Groups
*
* @since 3.6
*/
public static function getVisibleByGroups($rules)
{
$rules = json_decode($rules);
if (!$rules) {
return false;
}
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select($db->quoteName('title', 'text'))
->from($db->quoteName('#__usergroups'))
->whereIn($db->quoteName('id'), $rules);
$db->setQuery($query);
$groups = $db->loadColumn();
$groups = implode(', ', $groups);
return $groups;
}
/**
* Returns a valid section for users. If it is not valid then null
* is returned.
*
* @param string $section The section to get the mapping for
*
* @return string|null The new section
*
* @since 3.7.0
* @throws \Exception
*
* @deprecated 4.3 will be removed in 6.0
* Use \Joomla\Component\Users\Administrator\Extension\UsersComponent::validateSection() instead.
*/
public static function validateSection($section)
{
return Factory::getApplication()->bootComponent('com_users')->validateSection($section, null);
}
/**
* Returns valid contexts
*
* @return array
*
* @since 3.7.0
*
* @deprecated 4.3 will be removed in 6.0
* Use \Joomla\Component\Users\Administrator\Extension\UsersComponent::getContexts() instead.
*/
public static function getContexts()
{
return Factory::getApplication()->bootComponent('com_users')->getContexts();
}
}

View File

@ -0,0 +1,282 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_users
*
* @copyright (C) 2022 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Component\Users\Administrator\Model;
use Joomla\CMS\Crypt\Crypt;
use Joomla\CMS\Date\Date;
use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
use Joomla\CMS\User\User;
use Joomla\Component\Users\Administrator\Table\MfaTable;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Model for managing backup codes
*
* @since 4.2.0
*/
class BackupcodesModel extends BaseDatabaseModel
{
/**
* Caches the backup codes per user ID
*
* @var array
* @since 4.2.0
*/
protected $cache = [];
/**
* Get the backup codes record for the specified user
*
* @param User|null $user The user in question. Use null for the currently logged in user.
*
* @return MfaTable|null Record object or null if none is found
* @throws \Exception
* @since 4.2.0
*/
public function getBackupCodesRecord(User $user = null): ?MfaTable
{
// Make sure I have a user
if (empty($user)) {
$user = $this->getCurrentUser();
}
/** @var MfaTable $record */
$record = $this->getTable('Mfa', 'Administrator');
$loaded = $record->load(
[
'user_id' => $user->id,
'method' => 'backupcodes',
]
);
if (!$loaded) {
$record = null;
}
return $record;
}
/**
* Generate a new set of backup codes for the specified user. The generated codes are immediately saved to the
* database and the internal cache is updated.
*
* @param User|null $user Which user to generate codes for?
*
* @return void
* @throws \Exception
* @since 4.2.0
*/
public function regenerateBackupCodes(User $user = null): void
{
// Make sure I have a user
if (empty($user)) {
$user = $this->getCurrentUser();
}
// Generate backup codes
$backupCodes = [];
for ($i = 0; $i < 10; $i++) {
// Each backup code is 2 groups of 4 digits
$backupCodes[$i] = sprintf('%04u%04u', random_int(0, 9999), random_int(0, 9999));
}
// Save the backup codes to the database and update the cache
$this->saveBackupCodes($backupCodes, $user);
}
/**
* Saves the backup codes to the database
*
* @param array $codes An array of exactly 10 elements
* @param User|null $user The user for which to save the backup codes
*
* @return boolean
* @throws \Exception
* @since 4.2.0
*/
public function saveBackupCodes(array $codes, ?User $user = null): bool
{
// Make sure I have a user
if (empty($user)) {
$user = $this->getCurrentUser();
}
// Try to load existing backup codes
$existingCodes = $this->getBackupCodes($user);
$jNow = Date::getInstance();
/** @var MfaTable $record */
$record = $this->getTable('Mfa', 'Administrator');
if (\is_null($existingCodes)) {
$record->reset();
$newData = [
'user_id' => $user->id,
'title' => Text::_('COM_USERS_USER_BACKUPCODES'),
'method' => 'backupcodes',
'default' => 0,
'created_on' => $jNow->toSql(),
'options' => $codes,
];
} else {
$record->load(
[
'user_id' => $user->id,
'method' => 'backupcodes',
]
);
$newData = [
'options' => $codes,
];
}
$saved = $record->save($newData);
if (!$saved) {
return false;
}
// Finally, update the cache
$this->cache[$user->id] = $codes;
return true;
}
/**
* Returns the backup codes for the specified user. Cached values will be preferentially returned, therefore you
* MUST go through this model's Methods ONLY when dealing with backup codes.
*
* @param User|null $user The user for which you want the backup codes
*
* @return array|null The backup codes, or null if they do not exist
* @throws \Exception
* @since 4.2.0
*/
public function getBackupCodes(User $user = null): ?array
{
// Make sure I have a user
if (empty($user)) {
$user = $this->getCurrentUser();
}
if (isset($this->cache[$user->id])) {
return $this->cache[$user->id];
}
// If there is no cached record try to load it from the database
$this->cache[$user->id] = null;
// Try to load the record
/** @var MfaTable $record */
$record = $this->getTable('Mfa', 'Administrator');
$loaded = $record->load(
[
'user_id' => $user->id,
'method' => 'backupcodes',
]
);
if ($loaded) {
$this->cache[$user->id] = $record->options;
}
return $this->cache[$user->id];
}
/**
* Check if the provided string is a backup code. If it is, it will be removed from the list (replaced with an empty
* string) and the codes will be saved to the database. All comparisons are performed in a timing safe manner.
*
* @param string $code The code to check
* @param User|null $user The user to check against
*
* @return boolean
* @throws \Exception
* @since 4.2.0
*/
public function isBackupCode($code, ?User $user = null): bool
{
// Load the backup codes
$codes = $this->getBackupCodes($user) ?: array_fill(0, 10, '');
// Keep only the numbers in the provided $code
$code = filter_var($code, FILTER_SANITIZE_NUMBER_INT);
$code = trim($code);
// Check if the code is in the array. We always check against ten codes to prevent timing attacks which
// determine the amount of codes.
$result = false;
// The two arrays let us always add an element to an array, therefore having PHP expend the same amount of time
// for the correct code, the incorrect codes and the fake codes.
$newArray = [];
$dummyArray = [];
$realLength = \count($codes);
$restLength = 10 - $realLength;
for ($i = 0; $i < $realLength; $i++) {
if (hash_equals($codes[$i], $code)) {
// This may seem redundant but makes sure both branches of the if-block are isochronous
$result = $result || true;
$newArray[] = '';
$dummyArray[] = $codes[$i];
} else {
// This may seem redundant but makes sure both branches of the if-block are isochronous
$result = $result || false;
$dummyArray[] = '';
$newArray[] = $codes[$i];
}
}
/**
* This is an intentional waste of time, symmetrical to the code above, making sure
* evaluating each of the total of ten elements takes the same time. This code should never
* run UNLESS someone messed up with our backup codes array and it no longer contains 10
* elements.
*/
$otherResult = false;
$temp1 = '';
for ($i = 0; $i < 10; $i++) {
$temp1[$i] = random_int(0, 99999999);
}
for ($i = 0; $i < $restLength; $i++) {
if (Crypt::timingSafeCompare($temp1[$i], $code)) {
$otherResult = $otherResult || true;
$newArray[] = '';
$dummyArray[] = $temp1[$i];
} else {
$otherResult = $otherResult || false;
$newArray[] = '';
$dummyArray[] = $temp1[$i];
}
}
// This last check makes sure than an empty code does not validate
$result = $result && !hash_equals('', $code);
// Save the backup codes
$this->saveBackupCodes($newArray, $user);
// Finally return the result
return $result;
}
}

View File

@ -0,0 +1,450 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_users
*
* @copyright (C) 2022 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Component\Users\Administrator\Model;
use Joomla\CMS\Application\CMSApplication;
use Joomla\CMS\Component\ComponentHelper;
use Joomla\CMS\Date\Date;
use Joomla\CMS\Event\Module;
use Joomla\CMS\Event\MultiFactor\Captive;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
use Joomla\CMS\User\User;
use Joomla\Component\Users\Administrator\DataShape\CaptiveRenderOptions;
use Joomla\Component\Users\Administrator\Helper\Mfa as MfaHelper;
use Joomla\Component\Users\Administrator\Table\MfaTable;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Captive Multi-factor Authentication page's model
*
* @since 4.2.0
*/
class CaptiveModel extends BaseDatabaseModel
{
/**
* Cache of the names of the currently active MFA Methods
*
* @var array|null
* @since 4.2.0
*/
protected $activeMFAMethodNames = null;
/**
* Prevents Joomla from displaying any modules.
*
* This is implemented with a trick. If you use jdoc tags to load modules the JDocumentRendererHtmlModules
* uses JModuleHelper::getModules() to load the list of modules to render. This goes through JModuleHelper::load()
* which triggers the onAfterModuleList event after cleaning up the module list from duplicates. By resetting
* the list to an empty array we force Joomla to not display any modules.
*
* Similar code paths are followed by any canonical code which tries to load modules. So even if your template does
* not use jdoc tags this code will still work as expected.
*
* @param CMSApplication|null $app The CMS application to manipulate
*
* @return void
* @throws \Exception
*
* @since 4.2.0
*/
public function suppressAllModules(CMSApplication $app = null): void
{
if (\is_null($app)) {
$app = Factory::getApplication();
}
$app->registerEvent('onAfterModuleList', [$this, 'onAfterModuleList']);
}
/**
* Get the MFA records for the user which correspond to active plugins
*
* @param User|null $user The user for which to fetch records. Skip to use the current user.
* @param bool $includeBackupCodes Should I include the backup codes record?
*
* @return array
* @throws \Exception
*
* @since 4.2.0
*/
public function getRecords(User $user = null, bool $includeBackupCodes = false): array
{
if (\is_null($user)) {
$user = $this->getCurrentUser();
}
// Get the user's MFA records
$records = MfaHelper::getUserMfaRecords($user->id);
// No MFA Methods? Then we obviously don't need to display a Captive login page.
if (empty($records)) {
return [];
}
// Get the enabled MFA Methods' names
$methodNames = $this->getActiveMethodNames();
// Filter the records based on currently active MFA Methods
$ret = [];
$methodNames[] = 'backupcodes';
$methodNames = array_unique($methodNames);
if (!$includeBackupCodes) {
$methodNames = array_filter(
$methodNames,
function ($method) {
return $method != 'backupcodes';
}
);
}
foreach ($records as $record) {
// Backup codes must not be included in the list. We add them in the View, at the end of the list.
if (\in_array($record->method, $methodNames)) {
$ret[$record->id] = $record;
}
}
return $ret;
}
/**
* Return all the active MFA Methods' names
*
* @return array
* @since 4.2.0
*/
private function getActiveMethodNames(): ?array
{
if (!\is_null($this->activeMFAMethodNames)) {
return $this->activeMFAMethodNames;
}
// Let's get a list of all currently active MFA Methods
$mfaMethods = MfaHelper::getMfaMethods();
// If no MFA Method is active we can't really display a Captive login page.
if (empty($mfaMethods)) {
$this->activeMFAMethodNames = [];
return $this->activeMFAMethodNames;
}
// Get a list of just the Method names
$this->activeMFAMethodNames = [];
foreach ($mfaMethods as $mfaMethod) {
$this->activeMFAMethodNames[] = $mfaMethod['name'];
}
return $this->activeMFAMethodNames;
}
/**
* Get the currently selected MFA record for the current user. If the record ID is empty, it does not correspond to
* the currently logged in user or does not correspond to an active plugin null is returned instead.
*
* @param User|null $user The user for which to fetch records. Skip to use the current user.
*
* @return MfaTable|null
* @throws \Exception
*
* @since 4.2.0
*/
public function getRecord(?User $user = null): ?MfaTable
{
$id = (int) $this->getState('record_id', null);
if ($id <= 0) {
return null;
}
if (\is_null($user)) {
$user = $this->getCurrentUser();
}
/** @var MfaTable $record */
$record = $this->getTable('Mfa', 'Administrator');
$loaded = $record->load(
[
'user_id' => $user->id,
'id' => $id,
]
);
if (!$loaded) {
return null;
}
$methodNames = $this->getActiveMethodNames();
if (!\in_array($record->method, $methodNames) && ($record->method != 'backupcodes')) {
return null;
}
return $record;
}
/**
* Load the Captive login page render options for a specific MFA record
*
* @param MfaTable $record The MFA record to process
*
* @return CaptiveRenderOptions The rendering options
* @since 4.2.0
*/
public function loadCaptiveRenderOptions(?MfaTable $record): CaptiveRenderOptions
{
$renderOptions = new CaptiveRenderOptions();
if (empty($record)) {
return $renderOptions;
}
$event = new Captive($record);
$results = Factory::getApplication()
->getDispatcher()
->dispatch($event->getName(), $event)
->getArgument('result', []);
if (empty($results)) {
if ($record->method === 'backupcodes') {
return $renderOptions->merge(
[
'pre_message' => Text::_('COM_USERS_USER_BACKUPCODES_CAPTIVE_PROMPT'),
'input_type' => 'number',
'label' => Text::_('COM_USERS_USER_BACKUPCODE'),
]
);
}
return $renderOptions;
}
foreach ($results as $result) {
if (empty($result)) {
continue;
}
return $renderOptions->merge($result);
}
return $renderOptions;
}
/**
* Returns the title to display in the Captive login page, or an empty string if no title is to be displayed.
*
* @return string
* @since 4.2.0
*/
public function getPageTitle(): string
{
// In the frontend we can choose if we will display a title
$showTitle = (bool) ComponentHelper::getParams('com_users')
->get('frontend_show_title', 1);
if (!$showTitle) {
return '';
}
return Text::_('COM_USERS_USER_MULTIFACTOR_AUTH');
}
/**
* Translate a MFA Method's name into its human-readable, display name
*
* @param string $name The internal MFA Method name
*
* @return string
* @since 4.2.0
*/
public function translateMethodName(string $name): string
{
static $map = null;
if (!\is_array($map)) {
$map = [];
$mfaMethods = MfaHelper::getMfaMethods();
if (!empty($mfaMethods)) {
foreach ($mfaMethods as $mfaMethod) {
$map[$mfaMethod['name']] = $mfaMethod['display'];
}
}
}
if ($name == 'backupcodes') {
return Text::_('COM_USERS_USER_BACKUPCODES');
}
return $map[$name] ?? $name;
}
/**
* Translate a MFA Method's name into the relative URL if its logo image
*
* @param string $name The internal MFA Method name
*
* @return string
* @since 4.2.0
*/
public function getMethodImage(string $name): string
{
static $map = null;
if (!\is_array($map)) {
$map = [];
$mfaMethods = MfaHelper::getMfaMethods();
if (!empty($mfaMethods)) {
foreach ($mfaMethods as $mfaMethod) {
$map[$mfaMethod['name']] = $mfaMethod['image'];
}
}
}
if ($name == 'backupcodes') {
return 'media/com_users/images/emergency.svg';
}
return $map[$name] ?? $name;
}
/**
* Process the modules list on Joomla! 4.
*
* Joomla! 4.x is passing an Event object. The first argument of the event object is the array of modules. After
* filtering it we have to overwrite the event argument (NOT just return the new list of modules). If a future
* version of Joomla! uses immutable events we'll have to use Reflection to do that or Joomla! would have to fix
* the way this event is handled, taking its return into account. For now, we just abuse the mutable event
* properties - a feature of the event objects we discussed in the Joomla! 4 Working Group back in August 2015.
*
* @param Module\AfterModuleListEvent $event The Joomla! event object
*
* @return void
* @throws \Exception
*
* @since 4.2.0
*/
public function onAfterModuleList(Module\AfterModuleListEvent $event): void
{
$modules = $event->getModules();
if (empty($modules)) {
return;
}
$this->filterModules($modules);
$event->updateModules($modules);
}
/**
* This is the Method which actually filters the sites modules based on the allowed module positions specified by
* the user.
*
* @param array $modules The list of the site's modules. Passed by reference.
*
* @return void The by-reference value is modified instead.
* @since 4.2.0
* @throws \Exception
*/
private function filterModules(array &$modules): void
{
$allowedPositions = $this->getAllowedModulePositions();
if (empty($allowedPositions)) {
$modules = [];
return;
}
$filtered = [];
foreach ($modules as $module) {
if (\in_array($module->position, $allowedPositions)) {
$filtered[] = $module;
}
}
$modules = $filtered;
}
/**
* Get a list of module positions we are allowed to display
*
* @return array
* @throws \Exception
*
* @since 4.2.0
*/
private function getAllowedModulePositions(): array
{
$isAdmin = Factory::getApplication()->isClient('administrator');
// Load the list of allowed module positions from the component's settings. May be different for front- and back-end
$configKey = 'allowed_positions_' . ($isAdmin ? 'backend' : 'frontend');
$res = ComponentHelper::getParams('com_users')->get($configKey, []);
// In the backend we must always add the 'title' module position
if ($isAdmin) {
$res[] = 'title';
$res[] = 'toolbar';
}
return $res;
}
/**
* Method to check if the mfa method in question has reached it's usage limit
*
* @param MfaTable $method Mfa method record
*
* @return boolean true if user can use the method, false if not
*
* @since 4.3.2
* @throws \Exception
*/
public function checkTryLimit(MfaTable $method)
{
$params = ComponentHelper::getParams('com_users');
$jNow = Date::getInstance();
$maxTries = (int) $params->get('mfatrycount', 10);
$blockHours = (int) $params->get('mfatrytime', 1);
$lastTryTime = strtotime($method->last_try) ?: 0;
$hoursSinceLastTry = (strtotime(Factory::getDate()->toSql()) - $lastTryTime) / 3600;
if ($method->last_try !== null && $hoursSinceLastTry > $blockHours) {
// If it's been long enough, start a new reset count
$method->last_try = null;
$method->tries = 0;
} elseif ($method->tries < $maxTries) {
// If we are under the max count, just increment the counter
++$method->tries;
$method->last_try = $jNow->toSql();
} else {
// At this point, we know we have exceeded the maximum resets for the time period
return false;
}
// Store changes to try counter and/or the timestamp
$method->store();
return true;
}
}

View File

@ -0,0 +1,271 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_users
*
* @copyright (C) 2010 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Component\Users\Administrator\Model;
use Joomla\CMS\Access\Access;
use Joomla\CMS\Component\ComponentHelper;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
use Joomla\CMS\MVC\Model\ListModel;
use Joomla\CMS\Object\CMSObject;
use Joomla\Component\Users\Administrator\Helper\DebugHelper;
use Joomla\Database\DatabaseQuery;
use Joomla\Database\ParameterType;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Methods supporting a list of User ACL permissions
*
* @since 1.6
*/
class DebuggroupModel extends ListModel
{
/**
* Constructor.
*
* @param array $config An optional associative array of configuration settings.
* @param MVCFactoryInterface $factory The factory.
*
* @see \Joomla\CMS\MVC\Model\BaseDatabaseModel
* @since 3.2
*/
public function __construct($config = [], MVCFactoryInterface $factory = null)
{
if (empty($config['filter_fields'])) {
$config['filter_fields'] = [
'a.title',
'component', 'a.name',
'a.lft',
'a.id',
'level_start', 'level_end', 'a.level',
];
}
parent::__construct($config, $factory);
}
/**
* Get a list of the actions.
*
* @return array
*
* @since 1.6
*/
public function getDebugActions()
{
$component = $this->getState('filter.component');
return DebugHelper::getDebugActions($component);
}
/**
* Override getItems method.
*
* @return array
*
* @since 1.6
*/
public function getItems()
{
$groupId = $this->getState('group_id');
if (($assets = parent::getItems()) && $groupId) {
$actions = $this->getDebugActions();
foreach ($assets as &$asset) {
$asset->checks = [];
foreach ($actions as $action) {
$name = $action[0];
$asset->checks[$name] = Access::checkGroup($groupId, $name, $asset->name);
}
}
}
return $assets;
}
/**
* Method to auto-populate the model state.
*
* Note. Calling getState in this method will result in recursion.
*
* @param string $ordering An optional ordering field.
* @param string $direction An optional direction (asc|desc).
*
* @return void
*
* @since 1.6
*/
protected function populateState($ordering = 'a.lft', $direction = 'asc')
{
$app = Factory::getApplication();
// Adjust the context to support modal layouts.
$layout = $app->getInput()->get('layout', 'default');
if ($layout) {
$this->context .= '.' . $layout;
}
// Load the filter state.
$this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string'));
$this->setState('group_id', $this->getUserStateFromRequest($this->context . '.group_id', 'group_id', 0, 'int', false));
$levelStart = $this->getUserStateFromRequest($this->context . '.filter.level_start', 'filter_level_start', '', 'cmd');
$this->setState('filter.level_start', $levelStart);
$value = $this->getUserStateFromRequest($this->context . '.filter.level_end', 'filter_level_end', '', 'cmd');
if ($value > 0 && $value < $levelStart) {
$value = $levelStart;
}
$this->setState('filter.level_end', $value);
$this->setState('filter.component', $this->getUserStateFromRequest($this->context . '.filter.component', 'filter_component', '', 'string'));
// Load the parameters.
$params = ComponentHelper::getParams('com_users');
$this->setState('params', $params);
// List state information.
parent::populateState($ordering, $direction);
}
/**
* Method to get a store id based on model configuration state.
*
* This is necessary because the model is used by the component and
* different modules that might need different sets of data or different
* ordering requirements.
*
* @param string $id A prefix for the store id.
*
* @return string A store id.
*/
protected function getStoreId($id = '')
{
// Compile the store id.
$id .= ':' . $this->getState('group_id');
$id .= ':' . $this->getState('filter.search');
$id .= ':' . $this->getState('filter.level_start');
$id .= ':' . $this->getState('filter.level_end');
$id .= ':' . $this->getState('filter.component');
return parent::getStoreId($id);
}
/**
* Get the group being debugged.
*
* @return CMSObject
*
* @since 1.6
*/
public function getGroup()
{
$groupId = (int) $this->getState('group_id');
$db = $this->getDatabase();
$query = $db->getQuery(true)
->select($db->quoteName(['id', 'title']))
->from($db->quoteName('#__usergroups'))
->where($db->quoteName('id') . ' = :id')
->bind(':id', $groupId, ParameterType::INTEGER);
$db->setQuery($query);
try {
$group = $db->loadObject();
} catch (\RuntimeException $e) {
$this->setError($e->getMessage());
return false;
}
return $group;
}
/**
* Build an SQL query to load the list data.
*
* @return DatabaseQuery
*
* @since 1.6
*/
protected function getListQuery()
{
// Create a new query object.
$db = $this->getDatabase();
$query = $db->getQuery(true);
// Select the required fields from the table.
$query->select(
$this->getState(
'list.select',
'a.id, a.name, a.title, a.level, a.lft, a.rgt'
)
);
$query->from($db->quoteName('#__assets', 'a'));
// Filter the items over the search string if set.
if ($this->getState('filter.search')) {
$search = '%' . trim($this->getState('filter.search')) . '%';
// Add the clauses to the query.
$query->where(
'(' . $db->quoteName('a.name') . ' LIKE :name'
. ' OR ' . $db->quoteName('a.title') . ' LIKE :title)'
)
->bind(':name', $search)
->bind(':title', $search);
}
// Filter on the start and end levels.
$levelStart = (int) $this->getState('filter.level_start');
$levelEnd = (int) $this->getState('filter.level_end');
if ($levelEnd > 0 && $levelEnd < $levelStart) {
$levelEnd = $levelStart;
}
if ($levelStart > 0) {
$query->where($db->quoteName('a.level') . ' >= :levelStart')
->bind(':levelStart', $levelStart, ParameterType::INTEGER);
}
if ($levelEnd > 0) {
$query->where($db->quoteName('a.level') . ' <= :levelEnd')
->bind(':levelEnd', $levelEnd, ParameterType::INTEGER);
}
// Filter the items over the component if set.
if ($this->getState('filter.component')) {
$component = $this->getState('filter.component');
$lcomponent = $component . '.%';
$query->where(
'(' . $db->quoteName('a.name') . ' = :component'
. ' OR ' . $db->quoteName('a.name') . ' LIKE :lcomponent)'
)
->bind(':component', $component)
->bind(':lcomponent', $lcomponent);
}
// Add the list ordering clause.
$query->order($db->escape($this->getState('list.ordering', 'a.lft')) . ' ' . $db->escape($this->getState('list.direction', 'ASC')));
return $query;
}
}

View File

@ -0,0 +1,259 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_users
*
* @copyright (C) 2010 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Component\Users\Administrator\Model;
use Joomla\CMS\Component\ComponentHelper;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
use Joomla\CMS\MVC\Model\ListModel;
use Joomla\CMS\User\User;
use Joomla\CMS\User\UserFactoryAwareInterface;
use Joomla\CMS\User\UserFactoryAwareTrait;
use Joomla\Component\Users\Administrator\Helper\DebugHelper;
use Joomla\Database\DatabaseQuery;
use Joomla\Database\ParameterType;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Methods supporting a list of User ACL permissions
*
* @since 1.6
*/
class DebuguserModel extends ListModel implements UserFactoryAwareInterface
{
use UserFactoryAwareTrait;
/**
* Constructor.
*
* @param array $config An optional associative array of configuration settings.
* @param MVCFactoryInterface $factory The factory.
*
* @see \Joomla\CMS\MVC\Model\BaseDatabaseModel
* @since 3.2
*/
public function __construct($config = [], MVCFactoryInterface $factory = null)
{
if (empty($config['filter_fields'])) {
$config['filter_fields'] = [
'a.title',
'component', 'a.name',
'a.lft',
'a.id',
'level_start', 'level_end', 'a.level',
];
}
parent::__construct($config, $factory);
}
/**
* Get a list of the actions.
*
* @return array
*
* @since 1.6
*/
public function getDebugActions()
{
$component = $this->getState('filter.component');
return DebugHelper::getDebugActions($component);
}
/**
* Override getItems method.
*
* @return array
*
* @since 1.6
*/
public function getItems()
{
$userId = $this->getState('user_id');
$user = $this->getUserFactory()->loadUserById($userId);
if (($assets = parent::getItems()) && $userId) {
$actions = $this->getDebugActions();
foreach ($assets as &$asset) {
$asset->checks = [];
foreach ($actions as $action) {
$name = $action[0];
$asset->checks[$name] = $user->authorise($name, $asset->name);
}
}
}
return $assets;
}
/**
* Method to auto-populate the model state.
*
* Note. Calling getState in this method will result in recursion.
*
* @param string $ordering An optional ordering field.
* @param string $direction An optional direction (asc|desc).
*
* @return void
*
* @since 1.6
* @throws \Exception
*/
protected function populateState($ordering = 'a.lft', $direction = 'asc')
{
$app = Factory::getApplication();
// Adjust the context to support modal layouts.
$layout = $app->getInput()->get('layout', 'default');
if ($layout) {
$this->context .= '.' . $layout;
}
// Load the filter state.
$this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string'));
$this->setState('user_id', $this->getUserStateFromRequest($this->context . '.user_id', 'user_id', 0, 'int', false));
$levelStart = $this->getUserStateFromRequest($this->context . '.filter.level_start', 'filter_level_start', '', 'cmd');
$this->setState('filter.level_start', $levelStart);
$value = $this->getUserStateFromRequest($this->context . '.filter.level_end', 'filter_level_end', '', 'cmd');
if ($value > 0 && $value < $levelStart) {
$value = $levelStart;
}
$this->setState('filter.level_end', $value);
$this->setState('filter.component', $this->getUserStateFromRequest($this->context . '.filter.component', 'filter_component', '', 'string'));
// Load the parameters.
$params = ComponentHelper::getParams('com_users');
$this->setState('params', $params);
// List state information.
parent::populateState($ordering, $direction);
}
/**
* Method to get a store id based on model configuration state.
*
* This is necessary because the model is used by the component and
* different modules that might need different sets of data or different
* ordering requirements.
*
* @param string $id A prefix for the store id.
*
* @return string A store id.
*/
protected function getStoreId($id = '')
{
// Compile the store id.
$id .= ':' . $this->getState('user_id');
$id .= ':' . $this->getState('filter.search');
$id .= ':' . $this->getState('filter.level_start');
$id .= ':' . $this->getState('filter.level_end');
$id .= ':' . $this->getState('filter.component');
return parent::getStoreId($id);
}
/**
* Get the user being debugged.
*
* @return User
*
* @since 1.6
*/
public function getUser()
{
$userId = $this->getState('user_id');
return $this->getUserFactory()->loadUserById($userId);
}
/**
* Build an SQL query to load the list data.
*
* @return DatabaseQuery
*
* @since 1.6
*/
protected function getListQuery()
{
// Create a new query object.
$db = $this->getDatabase();
$query = $db->getQuery(true);
// Select the required fields from the table.
$query->select(
$this->getState(
'list.select',
'a.id, a.name, a.title, a.level, a.lft, a.rgt'
)
);
$query->from($db->quoteName('#__assets', 'a'));
// Filter the items over the search string if set.
if ($this->getState('filter.search')) {
$search = '%' . trim($this->getState('filter.search')) . '%';
// Add the clauses to the query.
$query->where(
'(' . $db->quoteName('a.name') . ' LIKE :name'
. ' OR ' . $db->quoteName('a.title') . ' LIKE :title)'
)
->bind(':name', $search)
->bind(':title', $search);
}
// Filter on the start and end levels.
$levelStart = (int) $this->getState('filter.level_start');
$levelEnd = (int) $this->getState('filter.level_end');
if ($levelEnd > 0 && $levelEnd < $levelStart) {
$levelEnd = $levelStart;
}
if ($levelStart > 0) {
$query->where($db->quoteName('a.level') . ' >= :levelStart')
->bind(':levelStart', $levelStart, ParameterType::INTEGER);
}
if ($levelEnd > 0) {
$query->where($db->quoteName('a.level') . ' <= :levelEnd')
->bind(':levelEnd', $levelEnd, ParameterType::INTEGER);
}
// Filter the items over the component if set.
if ($this->getState('filter.component')) {
$component = $this->getState('filter.component');
$lcomponent = $component . '.%';
$query->where(
'(' . $db->quoteName('a.name') . ' = :component'
. ' OR ' . $db->quoteName('a.name') . ' LIKE :lcomponent)'
)
->bind(':component', $component)
->bind(':lcomponent', $lcomponent);
}
// Add the list ordering clause.
$query->order($db->escape($this->getState('list.ordering', 'a.lft')) . ' ' . $db->escape($this->getState('list.direction', 'ASC')));
return $query;
}
}

View File

@ -0,0 +1,347 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_users
*
* @copyright (C) 2009 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Component\Users\Administrator\Model;
use Joomla\CMS\Access\Access;
use Joomla\CMS\Event\User\UserGroupAfterDeleteEvent;
use Joomla\CMS\Event\User\UserGroupBeforeDeleteEvent;
use Joomla\CMS\Factory;
use Joomla\CMS\Form\Form;
use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
use Joomla\CMS\MVC\Model\AdminModel;
use Joomla\CMS\Object\CMSObject;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\CMS\Table\Table;
use Joomla\String\StringHelper;
use Joomla\Utilities\ArrayHelper;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* User group model.
*
* @since 1.6
*/
class GroupModel extends AdminModel
{
/**
* Override parent constructor.
*
* @param array $config An optional associative array of configuration settings.
* @param MVCFactoryInterface $factory The factory.
*
* @see \Joomla\CMS\MVC\Model\BaseDatabaseModel
* @since 3.2
*/
public function __construct($config = [], MVCFactoryInterface $factory = null)
{
$config = array_merge(
[
'event_after_delete' => 'onUserAfterDeleteGroup',
'event_after_save' => 'onUserAfterSaveGroup',
'event_before_delete' => 'onUserBeforeDeleteGroup',
'event_before_save' => 'onUserBeforeSaveGroup',
'events_map' => ['delete' => 'user', 'save' => 'user'],
],
$config
);
parent::__construct($config, $factory);
}
/**
* Returns a reference to the a Table object, always creating it.
*
* @param string $type The table type to instantiate
* @param string $prefix A prefix for the table class name. Optional.
* @param array $config Configuration array for model. Optional.
*
* @return Table A database object
*
* @since 1.6
*/
public function getTable($type = 'Usergroup', $prefix = 'Joomla\\CMS\\Table\\', $config = [])
{
$return = Table::getInstance($type, $prefix, $config);
return $return;
}
/**
* Method to get the record form.
*
* @param array $data An optional array of data for the form to interrogate.
* @param boolean $loadData True if the form is to load its own data (default case), false if not.
*
* @return Form|bool A Form object on success, false on failure
*
* @since 1.6
*/
public function getForm($data = [], $loadData = true)
{
// Get the form.
$form = $this->loadForm('com_users.group', 'group', ['control' => 'jform', 'load_data' => $loadData]);
if (empty($form)) {
return false;
}
return $form;
}
/**
* Method to get the data that should be injected in the form.
*
* @return mixed The data for the form.
*
* @since 1.6
* @throws \Exception
*/
protected function loadFormData()
{
// Check the session for previously entered form data.
$data = Factory::getApplication()->getUserState('com_users.edit.group.data', []);
if (empty($data)) {
$data = $this->getItem();
}
$this->preprocessData('com_users.group', $data);
return $data;
}
/**
* Override preprocessForm to load the user plugin group instead of content.
*
* @param Form $form A form object.
* @param mixed $data The data expected for the form.
* @param string $group The name of the plugin group to import (defaults to "content").
*
* @return void
*
* @since 1.6
* @throws \Exception if there is an error loading the form.
*/
protected function preprocessForm(Form $form, $data, $group = '')
{
$obj = \is_array($data) ? ArrayHelper::toObject($data, CMSObject::class) : $data;
if (isset($obj->parent_id) && $obj->parent_id == 0 && $obj->id > 0) {
$form->setFieldAttribute('parent_id', 'type', 'hidden');
$form->setFieldAttribute('parent_id', 'hidden', 'true');
}
parent::preprocessForm($form, $data, 'user');
}
/**
* Method to save the form data.
*
* @param array $data The form data.
*
* @return boolean True on success.
*
* @since 1.6
*/
public function save($data)
{
// Include the user plugins for events.
PluginHelper::importPlugin($this->events_map['save']);
/**
* Check the super admin permissions for group
* We get the parent group permissions and then check the group permissions manually
* We have to calculate the group permissions manually because we haven't saved the group yet
*/
$parentSuperAdmin = Access::checkGroup($data['parent_id'], 'core.admin');
// Get core.admin rules from the root asset
$rules = Access::getAssetRules('root.1')->getData();
// Get the value for the current group (will be true (allowed), false (denied), or null (inherit)
$groupSuperAdmin = $rules['core.admin']->allow($data['id']);
// We only need to change the $groupSuperAdmin if the parent is true or false. Otherwise, the value set in the rule takes effect.
if ($parentSuperAdmin === false) {
// If parent is false (Denied), effective value will always be false
$groupSuperAdmin = false;
} elseif ($parentSuperAdmin === true) {
// If parent is true (allowed), group is true unless explicitly set to false
$groupSuperAdmin = ($groupSuperAdmin === false) ? false : true;
}
// Check for non-super admin trying to save with super admin group
$iAmSuperAdmin = $this->getCurrentUser()->authorise('core.admin');
if (!$iAmSuperAdmin && $groupSuperAdmin) {
$this->setError(Text::_('JLIB_USER_ERROR_NOT_SUPERADMIN'));
return false;
}
/**
* Check for super-admin changing self to be non-super-admin
* First, are we a super admin
*/
if ($iAmSuperAdmin) {
// Next, are we a member of the current group?
$myGroups = Access::getGroupsByUser($this->getCurrentUser()->get('id'), false);
if (\in_array($data['id'], $myGroups)) {
// Now, would we have super admin permissions without the current group?
$otherGroups = array_diff($myGroups, [$data['id']]);
$otherSuperAdmin = false;
foreach ($otherGroups as $otherGroup) {
$otherSuperAdmin = $otherSuperAdmin ?: Access::checkGroup($otherGroup, 'core.admin');
}
/**
* If we would not otherwise have super admin permissions
* and the current group does not have super admin permissions, throw an exception
*/
if ((!$otherSuperAdmin) && (!$groupSuperAdmin)) {
$this->setError(Text::_('JLIB_USER_ERROR_CANNOT_DEMOTE_SELF'));
return false;
}
}
}
if (Factory::getApplication()->getInput()->get('task') == 'save2copy') {
$data['title'] = $this->generateGroupTitle($data['parent_id'], $data['title']);
}
// Proceed with the save
return parent::save($data);
}
/**
* Method to delete rows.
*
* @param array &$pks An array of item ids.
*
* @return boolean Returns true on success, false on failure.
*
* @since 1.6
* @throws \Exception
*/
public function delete(&$pks)
{
// Typecast variable.
$pks = (array) $pks;
$user = $this->getCurrentUser();
$groups = Access::getGroupsByUser($user->get('id'));
$context = $this->option . '.' . $this->name;
$dispatcher = $this->getDispatcher();
// Get a row instance.
$table = $this->getTable();
// Load plugins.
PluginHelper::importPlugin($this->events_map['delete'], null, true, $dispatcher);
// Check if I am a Super Admin
$iAmSuperAdmin = $user->authorise('core.admin');
foreach ($pks as $pk) {
// Do not allow to delete groups to which the current user belongs
if (\in_array($pk, $groups)) {
Factory::getApplication()->enqueueMessage(Text::_('COM_USERS_DELETE_ERROR_INVALID_GROUP'), 'error');
return false;
}
if (!$table->load($pk)) {
// Item is not in the table.
$this->setError($table->getError());
return false;
}
}
// Iterate the items to delete each one.
foreach ($pks as $i => $pk) {
if ($table->load($pk)) {
// Access checks.
$allow = $user->authorise('core.edit.state', 'com_users');
// Don't allow non-super-admin to delete a super admin
$allow = (!$iAmSuperAdmin && Access::checkGroup($pk, 'core.admin')) ? false : $allow;
if ($allow) {
// Fire the before delete event.
$beforeDeleteEvent = new UserGroupBeforeDeleteEvent($this->event_before_delete, [
'data' => $table->getProperties(), // @TODO: Remove data argument in Joomla 6, see UserGroupBeforeDeleteEvent
'context' => $context,
'subject' => $table,
]);
$result = $dispatcher->dispatch($this->event_before_delete, $beforeDeleteEvent)->getArgument('result', []);
if (\in_array(false, $result, true)) {
$this->setError($table->getError());
return false;
}
if (!$table->delete($pk)) {
$this->setError($table->getError());
return false;
}
// Trigger the after delete event.
$dispatcher->dispatch($this->event_after_delete, new UserGroupAfterDeleteEvent($this->event_after_delete, [
'data' => $table->getProperties(), // @TODO: Remove data argument in Joomla 6, see UserGroupAfterDeleteEvent
'deletingResult' => true, // @TODO: Remove deletingResult argument in Joomla 6, see UserGroupAfterDeleteEvent
'errorMessage' => $this->getError(), // @TODO: Remove errorMessage argument in Joomla 6, see UserGroupAfterDeleteEvent
'context' => $context,
'subject' => $table,
]));
} else {
// Prune items that you can't change.
unset($pks[$i]);
Factory::getApplication()->enqueueMessage(Text::_('JERROR_CORE_DELETE_NOT_PERMITTED'), 'error');
}
}
}
return true;
}
/**
* Method to generate the title of group on Save as Copy action
*
* @param integer $parentId The id of the parent.
* @param string $title The title of group
*
* @return string Contains the modified title.
*
* @since 3.3.7
*/
protected function generateGroupTitle($parentId, $title)
{
// Alter the title & alias
$table = $this->getTable();
while ($table->load(['title' => $title, 'parent_id' => $parentId])) {
if ($title == $table->title) {
$title = StringHelper::increment($title);
}
}
return $title;
}
}

View File

@ -0,0 +1,246 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_users
*
* @copyright (C) 2009 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Component\Users\Administrator\Model;
use Joomla\CMS\Component\ComponentHelper;
use Joomla\CMS\Helper\UserGroupsHelper;
use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
use Joomla\CMS\MVC\Model\ListModel;
use Joomla\Database\DatabaseQuery;
use Joomla\Database\ParameterType;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Methods supporting a list of user group records.
*
* @since 1.6
*/
class GroupsModel extends ListModel
{
/**
* Override parent constructor.
*
* @param array $config An optional associative array of configuration settings.
* @param MVCFactoryInterface $factory The factory.
*
* @see \Joomla\CMS\MVC\Model\BaseDatabaseModel
* @since 3.2
*/
public function __construct($config = [], MVCFactoryInterface $factory = null)
{
if (empty($config['filter_fields'])) {
$config['filter_fields'] = [
'id', 'a.id',
'parent_id', 'a.parent_id',
'title', 'a.title',
'lft', 'a.lft',
'rgt', 'a.rgt',
];
}
parent::__construct($config, $factory);
}
/**
* Method to auto-populate the model state.
*
* Note. Calling getState in this method will result in recursion.
*
* @param string $ordering An optional ordering field.
* @param string $direction An optional direction (asc|desc).
*
* @return void
*
* @since 1.6
*/
protected function populateState($ordering = 'a.lft', $direction = 'asc')
{
// Load the parameters.
$params = ComponentHelper::getParams('com_users');
$this->setState('params', $params);
// List state information.
parent::populateState($ordering, $direction);
}
/**
* Method to get a store id based on model configuration state.
*
* This is necessary because the model is used by the component and
* different modules that might need different sets of data or different
* ordering requirements.
*
* @param string $id A prefix for the store id.
*
* @return string A store id.
*/
protected function getStoreId($id = '')
{
// Compile the store id.
$id .= ':' . $this->getState('filter.search');
return parent::getStoreId($id);
}
/**
* Gets the list of groups and adds expensive joins to the result set.
*
* @return mixed An array of data items on success, false on failure.
*
* @since 1.6
*/
public function getItems()
{
// Get a storage key.
$store = $this->getStoreId();
// Try to load the data from internal storage.
if (empty($this->cache[$store])) {
$items = parent::getItems();
// Bail out on an error or empty list.
if (empty($items)) {
$this->cache[$store] = $items;
return $items;
}
try {
$items = $this->populateExtraData($items);
} catch (\RuntimeException $e) {
$this->setError($e->getMessage());
return false;
}
// Add the items to the internal cache.
$this->cache[$store] = $items;
}
return $this->cache[$store];
}
/**
* Build an SQL query to load the list data.
*
* @return DatabaseQuery
*/
protected function getListQuery()
{
// Create a new query object.
$db = $this->getDatabase();
$query = $db->getQuery(true);
// Select the required fields from the table.
$query->select(
$this->getState(
'list.select',
'a.*'
)
);
$query->from($db->quoteName('#__usergroups') . ' AS a');
// Filter the comments over the search string if set.
$search = $this->getState('filter.search');
if (!empty($search)) {
if (stripos($search, 'id:') === 0) {
$ids = (int) substr($search, 3);
$query->where($db->quoteName('a.id') . ' = :id');
$query->bind(':id', $ids, ParameterType::INTEGER);
} else {
$search = '%' . trim($search) . '%';
$query->where($db->quoteName('a.title') . ' LIKE :title');
$query->bind(':title', $search);
}
}
// Add the list ordering clause.
$query->order($db->escape($this->getState('list.ordering', 'a.lft')) . ' ' . $db->escape($this->getState('list.direction', 'ASC')));
return $query;
}
/**
* Populate level & path for items.
*
* @param array $items Array of \stdClass objects
*
* @return array
*
* @since 3.6.3
*/
private function populateExtraData(array $items)
{
// First pass: get list of the group ids and reset the counts.
$groupsByKey = [];
foreach ($items as $item) {
$groupsByKey[(int) $item->id] = $item;
}
$groupIds = array_keys($groupsByKey);
$db = $this->getDatabase();
// Get total enabled users in group.
$query = $db->getQuery(true);
// Count the objects in the user group.
$query->select('map.group_id, COUNT(DISTINCT map.user_id) AS user_count')
->from($db->quoteName('#__user_usergroup_map', 'map'))
->join('LEFT', $db->quoteName('#__users', 'u'), $db->quoteName('u.id') . ' = ' . $db->quoteName('map.user_id'))
->whereIn($db->quoteName('map.group_id'), $groupIds)
->where($db->quoteName('u.block') . ' = 0')
->group($db->quoteName('map.group_id'));
$db->setQuery($query);
try {
$countEnabled = $db->loadAssocList('group_id', 'count_enabled');
} catch (\RuntimeException $e) {
$this->setError($e->getMessage());
return false;
}
// Get total disabled users in group.
$query->clear();
$query->select('map.group_id, COUNT(DISTINCT map.user_id) AS user_count')
->from($db->quoteName('#__user_usergroup_map', 'map'))
->join('LEFT', $db->quoteName('#__users', 'u'), $db->quoteName('u.id') . ' = ' . $db->quoteName('map.user_id'))
->whereIn($db->quoteName('map.group_id'), $groupIds)
->where($db->quoteName('u.block') . ' = 1')
->group($db->quoteName('map.group_id'));
$db->setQuery($query);
try {
$countDisabled = $db->loadAssocList('group_id', 'count_disabled');
} catch (\RuntimeException $e) {
$this->setError($e->getMessage());
return false;
}
// Inject the values back into the array.
foreach ($groupsByKey as &$item) {
$item->count_enabled = isset($countEnabled[$item->id]) ? (int) $countEnabled[$item->id]['user_count'] : 0;
$item->count_disabled = isset($countDisabled[$item->id]) ? (int) $countDisabled[$item->id]['user_count'] : 0;
$item->user_count = $item->count_enabled + $item->count_disabled;
}
$groups = new UserGroupsHelper($groupsByKey);
return array_values($groups->getAll());
}
}

View File

@ -0,0 +1,297 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_users
*
* @copyright (C) 2009 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Component\Users\Administrator\Model;
use Joomla\CMS\Access\Access;
use Joomla\CMS\Factory;
use Joomla\CMS\Filter\InputFilter;
use Joomla\CMS\Form\Form;
use Joomla\CMS\Helper\UserGroupsHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\Model\AdminModel;
use Joomla\CMS\Table\Table;
use Joomla\Utilities\ArrayHelper;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* User view level model.
*
* @since 1.6
*/
class LevelModel extends AdminModel
{
/**
* @var array A list of the access levels in use.
* @since 1.6
*/
protected $levelsInUse = null;
/**
* Method to test whether a record can be deleted.
*
* @param object $record A record object.
*
* @return boolean True if allowed to delete the record. Defaults to the permission set in the component.
*
* @since 1.6
*/
protected function canDelete($record)
{
$groups = json_decode($record->rules);
if ($groups === null) {
throw new \RuntimeException('Invalid rules schema');
}
$isAdmin = $this->getCurrentUser()->authorise('core.admin');
// Check permissions
foreach ($groups as $group) {
if (!$isAdmin && Access::checkGroup($group, 'core.admin')) {
$this->setError(Text::_('JERROR_ALERTNOAUTHOR'));
return false;
}
}
// Check if the access level is being used by any content.
if ($this->levelsInUse === null) {
// Populate the list once.
$this->levelsInUse = [];
$db = $this->getDatabase();
$query = $db->getQuery(true)
->select('DISTINCT access');
// Get all the tables and the prefix
$tables = $db->getTableList();
$prefix = $db->getPrefix();
foreach ($tables as $table) {
// Get all of the columns in the table
$fields = $db->getTableColumns($table);
/**
* We are looking for the access field. If custom tables are using something other
* than the 'access' field they are on their own unfortunately.
* Also make sure the table prefix matches the live db prefix (eg, it is not a "bak_" table)
*/
if (strpos($table, $prefix) === 0 && isset($fields['access'])) {
// Lookup the distinct values of the field.
$query->clear('from')
->from($db->quoteName($table));
$db->setQuery($query);
try {
$values = $db->loadColumn();
} catch (\RuntimeException $e) {
$this->setError($e->getMessage());
return false;
}
$this->levelsInUse = array_merge($this->levelsInUse, $values);
// @todo Could assemble an array of the tables used by each view level list those,
// giving the user a clue in the error where to look.
}
}
// Get uniques.
$this->levelsInUse = array_unique($this->levelsInUse);
// Ok, after all that we are ready to check the record :)
}
if (\in_array($record->id, $this->levelsInUse)) {
$this->setError(Text::sprintf('COM_USERS_ERROR_VIEW_LEVEL_IN_USE', $record->id, $record->title));
return false;
}
return parent::canDelete($record);
}
/**
* Returns a reference to the a Table object, always creating it.
*
* @param string $type The table type to instantiate
* @param string $prefix A prefix for the table class name. Optional.
* @param array $config Configuration array for model. Optional.
*
* @return Table A database object
*
* @since 1.6
*/
public function getTable($type = 'ViewLevel', $prefix = 'Joomla\\CMS\\Table\\', $config = [])
{
$return = Table::getInstance($type, $prefix, $config);
return $return;
}
/**
* Method to get a single record.
*
* @param integer $pk The id of the primary key.
*
* @return mixed Object on success, false on failure.
*
* @since 1.6
*/
public function getItem($pk = null)
{
$result = parent::getItem($pk);
// Convert the params field to an array.
$result->rules = $result->rules !== null ? json_decode($result->rules) : [];
return $result;
}
/**
* Method to get the record form.
*
* @param array $data An optional array of data for the form to interrogate.
* @param boolean $loadData True if the form is to load its own data (default case), false if not.
*
* @return Form|bool A Form object on success, false on failure
*
* @since 1.6
*/
public function getForm($data = [], $loadData = true)
{
// Get the form.
$form = $this->loadForm('com_users.level', 'level', ['control' => 'jform', 'load_data' => $loadData]);
if (empty($form)) {
return false;
}
return $form;
}
/**
* Method to get the data that should be injected in the form.
*
* @return mixed The data for the form.
*
* @since 1.6
* @throws \Exception
*/
protected function loadFormData()
{
// Check the session for previously entered form data.
$data = Factory::getApplication()->getUserState('com_users.edit.level.data', []);
if (empty($data)) {
$data = $this->getItem();
}
$this->preprocessData('com_users.level', $data);
return $data;
}
/**
* Method to preprocess the form
*
* @param Form $form A form object.
* @param mixed $data The data expected for the form.
* @param string $group The name of the plugin group to import (defaults to "content").
*
* @return void
*
* @since 1.6
* @throws \Exception if there is an error loading the form.
*/
protected function preprocessForm(Form $form, $data, $group = '')
{
// TO DO warning!
parent::preprocessForm($form, $data, 'user');
}
/**
* Method to save the form data.
*
* @param array $data The form data.
*
* @return boolean True on success.
*
* @since 1.6
*/
public function save($data)
{
if (!isset($data['rules'])) {
$data['rules'] = [];
}
$data['title'] = InputFilter::getInstance()->clean($data['title'], 'TRIM');
return parent::save($data);
}
/**
* Method to validate the form data.
*
* @param Form $form The form to validate against.
* @param array $data The data to validate.
* @param string $group The name of the field group to validate.
*
* @return array|boolean Array of filtered data if valid, false otherwise.
*
* @see \Joomla\CMS\Form\FormRule
* @see \Joomla\CMS\Filter\InputFilter
* @since 3.8.8
*/
public function validate($form, $data, $group = null)
{
$isSuperAdmin = $this->getCurrentUser()->authorise('core.admin');
// Non Super user should not be able to change the access levels of super user groups
if (!$isSuperAdmin) {
if (!isset($data['rules']) || !\is_array($data['rules'])) {
$data['rules'] = [];
}
$groups = array_values(UserGroupsHelper::getInstance()->getAll());
$rules = [];
if (!empty($data['id'])) {
$table = $this->getTable();
$table->load($data['id']);
$rules = json_decode($table->rules);
}
$rules = ArrayHelper::toInteger($rules);
for ($i = 0, $n = \count($groups); $i < $n; ++$i) {
if (Access::checkGroup((int) $groups[$i]->id, 'core.admin')) {
if (\in_array((int) $groups[$i]->id, $rules) && !\in_array((int) $groups[$i]->id, $data['rules'])) {
$data['rules'][] = (int) $groups[$i]->id;
} elseif (!\in_array((int) $groups[$i]->id, $rules) && \in_array((int) $groups[$i]->id, $data['rules'])) {
$this->setError(Text::_('JLIB_USER_ERROR_NOT_SUPERADMIN'));
return false;
}
}
}
}
return parent::validate($form, $data, $group);
}
}

View File

@ -0,0 +1,234 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_users
*
* @copyright (C) 2009 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Component\Users\Administrator\Model;
use Joomla\CMS\Component\ComponentHelper;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
use Joomla\CMS\MVC\Model\ListModel;
use Joomla\CMS\Table\Table;
use Joomla\Database\DatabaseQuery;
use Joomla\Database\ParameterType;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Methods supporting a list of user access level records.
*
* @since 1.6
*/
class LevelsModel extends ListModel
{
/**
* Override parent constructor.
*
* @param array $config An optional associative array of configuration settings.
* @param MVCFactoryInterface $factory The factory.
*
* @see \Joomla\CMS\MVC\Model\BaseDatabaseModel
* @since 3.2
*/
public function __construct($config = [], MVCFactoryInterface $factory = null)
{
if (empty($config['filter_fields'])) {
$config['filter_fields'] = [
'id', 'a.id',
'title', 'a.title',
'ordering', 'a.ordering',
];
}
parent::__construct($config, $factory);
}
/**
* Method to auto-populate the model state.
*
* Note. Calling getState in this method will result in recursion.
*
* @param string $ordering An optional ordering field.
* @param string $direction An optional direction (asc|desc).
*
* @return void
*
* @since 1.6
*/
protected function populateState($ordering = 'a.ordering', $direction = 'asc')
{
// Load the parameters.
$params = ComponentHelper::getParams('com_users');
$this->setState('params', $params);
// List state information.
parent::populateState($ordering, $direction);
}
/**
* Method to get a store id based on model configuration state.
*
* This is necessary because the model is used by the component and
* different modules that might need different sets of data or different
* ordering requirements.
*
* @param string $id A prefix for the store id.
*
* @return string A store id.
*/
protected function getStoreId($id = '')
{
// Compile the store id.
$id .= ':' . $this->getState('filter.search');
return parent::getStoreId($id);
}
/**
* Build an SQL query to load the list data.
*
* @return DatabaseQuery
*/
protected function getListQuery()
{
// Create a new query object.
$db = $this->getDatabase();
$query = $db->getQuery(true);
// Select the required fields from the table.
$query->select(
$this->getState(
'list.select',
'a.*'
)
);
$query->from($db->quoteName('#__viewlevels') . ' AS a');
// Add the level in the tree.
$query->group('a.id, a.title, a.ordering, a.rules');
// Filter the items over the search string if set.
$search = $this->getState('filter.search');
if (!empty($search)) {
if (stripos($search, 'id:') === 0) {
$ids = (int) substr($search, 3);
$query->where($db->quoteName('a.id') . ' = :id');
$query->bind(':id', $ids, ParameterType::INTEGER);
} else {
$search = '%' . trim($search) . '%';
$query->where('a.title LIKE :title')
->bind(':title', $search);
}
}
$query->group('a.id');
// Add the list ordering clause.
$query->order($db->escape($this->getState('list.ordering', 'a.ordering')) . ' ' . $db->escape($this->getState('list.direction', 'ASC')));
return $query;
}
/**
* Method to adjust the ordering of a row.
*
* @param integer $pk The ID of the primary key to move.
* @param integer $direction Increment, usually +1 or -1
*
* @return boolean False on failure or error, true otherwise.
*/
public function reorder($pk, $direction = 0)
{
// Sanitize the id and adjustment.
$pk = (!empty($pk)) ? $pk : (int) $this->getState('level.id');
$user = $this->getCurrentUser();
// Get an instance of the record's table.
$table = Table::getInstance('ViewLevel', 'Joomla\\CMS\Table\\');
// Load the row.
if (!$table->load($pk)) {
$this->setError($table->getError());
return false;
}
// Access checks.
$allow = $user->authorise('core.edit.state', 'com_users');
if (!$allow) {
$this->setError(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'));
return false;
}
// Move the row.
// @todo: Where clause to restrict category.
$table->move($pk);
return true;
}
/**
* Saves the manually set order of records.
*
* @param array $pks An array of primary key ids.
* @param integer $order Order position
*
* @return boolean Boolean true on success, boolean false
*
* @throws \Exception
*/
public function saveorder($pks, $order)
{
$table = Table::getInstance('viewlevel', 'Joomla\\CMS\Table\\');
$user = $this->getCurrentUser();
$conditions = [];
if (empty($pks)) {
Factory::getApplication()->enqueueMessage(Text::_('COM_USERS_ERROR_LEVELS_NOLEVELS_SELECTED'), 'error');
return false;
}
// Update ordering values
foreach ($pks as $i => $pk) {
$table->load((int) $pk);
// Access checks.
$allow = $user->authorise('core.edit.state', 'com_users');
if (!$allow) {
// Prune items that you can't change.
unset($pks[$i]);
Factory::getApplication()->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), 'error');
} elseif ($table->ordering != $order[$i]) {
$table->ordering = $order[$i];
if (!$table->store()) {
$this->setError($table->getError());
return false;
}
}
}
// Execute reorder for each category.
foreach ($conditions as $cond) {
$table->load($cond[0]);
$table->reorder($cond[1]);
}
return true;
}
}

View File

@ -0,0 +1,246 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_users
*
* @copyright (C) 2009 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Component\Users\Administrator\Model;
use Joomla\CMS\Access\Access;
use Joomla\CMS\Component\ComponentHelper;
use Joomla\CMS\Factory;
use Joomla\CMS\Filter\InputFilter;
use Joomla\CMS\Form\Form;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Log\Log;
use Joomla\CMS\Mail\Exception\MailDisabledException;
use Joomla\CMS\Mail\MailTemplate;
use Joomla\CMS\MVC\Model\AdminModel;
use Joomla\Database\ParameterType;
use PHPMailer\PHPMailer\Exception as phpMailerException;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Users mail model.
*
* @since 1.6
*/
class MailModel extends AdminModel
{
/**
* Method to get the row form.
*
* @param array $data An optional array of data for the form to interrogate.
* @param boolean $loadData True if the form is to load its own data (default case), false if not.
*
* @return Form A Form object on success, false on failure
*
* @since 1.6
*/
public function getForm($data = [], $loadData = true)
{
// Get the form.
$form = $this->loadForm('com_users.mail', 'mail', ['control' => 'jform', 'load_data' => $loadData]);
if (empty($form)) {
return false;
}
return $form;
}
/**
* Method to get the data that should be injected in the form.
*
* @return mixed The data for the form.
*
* @since 1.6
* @throws \Exception
*/
protected function loadFormData()
{
// Check the session for previously entered form data.
$data = Factory::getApplication()->getUserState('com_users.display.mail.data', []);
$this->preprocessData('com_users.mail', $data);
return $data;
}
/**
* Method to preprocess the form
*
* @param Form $form A form object.
* @param mixed $data The data expected for the form.
* @param string $group The name of the plugin group to import (defaults to "content").
*
* @return void
*
* @since 1.6
* @throws \Exception if there is an error loading the form.
*/
protected function preprocessForm(Form $form, $data, $group = 'user')
{
parent::preprocessForm($form, $data, $group);
}
/**
* Send the email
*
* @return boolean
*
* @throws \Exception
*/
public function send()
{
$app = Factory::getApplication();
$data = $app->getInput()->post->get('jform', [], 'array');
$user = $this->getCurrentUser();
$access = new Access();
$db = $this->getDatabase();
$language = Factory::getLanguage();
$mode = \array_key_exists('mode', $data) ? (int) $data['mode'] : 0;
$subject = \array_key_exists('subject', $data) ? $data['subject'] : '';
$grp = \array_key_exists('group', $data) ? (int) $data['group'] : 0;
$recurse = \array_key_exists('recurse', $data) ? (int) $data['recurse'] : 0;
$bcc = \array_key_exists('bcc', $data) ? (int) $data['bcc'] : 0;
$disabled = \array_key_exists('disabled', $data) ? (int) $data['disabled'] : 0;
$message_body = \array_key_exists('message', $data) ? $data['message'] : '';
// Automatically removes html formatting
if (!$mode) {
$message_body = InputFilter::getInstance()->clean($message_body, 'string');
}
// Check for a message body and subject
if (!$message_body || !$subject) {
$app->setUserState('com_users.display.mail.data', $data);
$this->setError(Text::_('COM_USERS_MAIL_PLEASE_FILL_IN_THE_FORM_CORRECTLY'));
return false;
}
// Get users in the group out of the ACL, if group is provided.
$to = $grp !== 0 ? $access->getUsersByGroup($grp, $recurse) : [];
// When group is provided but no users are found in the group.
if ($grp !== 0 && !$to) {
$rows = [];
} else {
// Get all users email and group except for senders
$uid = (int) $user->id;
$query = $db->getQuery(true)
->select(
[
$db->quoteName('email'),
$db->quoteName('name'),
]
)
->from($db->quoteName('#__users'))
->where($db->quoteName('id') . ' != :id')
->bind(':id', $uid, ParameterType::INTEGER);
if ($grp !== 0) {
$query->whereIn($db->quoteName('id'), $to);
}
if ($disabled === 0) {
$query->where($db->quoteName('block') . ' = 0');
}
$db->setQuery($query);
$rows = $db->loadObjectList();
}
// Check to see if there are any users in this group before we continue
if (!$rows) {
$app->setUserState('com_users.display.mail.data', $data);
if (\in_array($user->id, $to)) {
$this->setError(Text::_('COM_USERS_MAIL_ONLY_YOU_COULD_BE_FOUND_IN_THIS_GROUP'));
} else {
$this->setError(Text::_('COM_USERS_MAIL_NO_USERS_COULD_BE_FOUND_IN_THIS_GROUP'));
}
return false;
}
// Get the Mailer
$mailer = new MailTemplate('com_users.massmail.mail', $language->getTag());
$params = ComponentHelper::getParams('com_users');
try {
// Build email message format.
$data = [
'subject' => stripslashes($subject),
'body' => $message_body,
'subjectprefix' => $params->get('mailSubjectPrefix', ''),
'bodysuffix' => $params->get('mailBodySuffix', ''),
];
$mailer->addTemplateData($data);
$recipientType = $bcc ? 'bcc' : 'to';
// Add recipients
foreach ($rows as $row) {
$mailer->addRecipient($row->email, $row->name, $recipientType);
}
if ($bcc) {
$mailer->addRecipient($app->get('mailfrom'), $app->get('fromname'));
}
// Send the Mail
$rs = $mailer->send();
} catch (MailDisabledException | phpMailerException $exception) {
try {
Log::add(Text::_($exception->getMessage()), Log::WARNING, 'jerror');
$rs = false;
} catch (\RuntimeException $exception) {
Factory::getApplication()->enqueueMessage(Text::_($exception->errorMessage()), 'warning');
$rs = false;
}
}
// Check for an error
if ($rs !== true) {
$app->setUserState('com_users.display.mail.data', $data);
$this->setError($mailer->ErrorInfo);
return false;
}
if (empty($rs)) {
$app->setUserState('com_users.display.mail.data', $data);
$this->setError(Text::_('COM_USERS_MAIL_THE_MAIL_COULD_NOT_BE_SENT'));
return false;
}
/**
* Fill the data (specially for the 'mode', 'group' and 'bcc': they could not exist in the array
* when the box is not checked and in this case, the default value would be used instead of the '0'
* one)
*/
$data['mode'] = $mode;
$data['subject'] = $subject;
$data['group'] = $grp;
$data['recurse'] = $recurse;
$data['bcc'] = $bcc;
$data['message'] = $message_body;
$app->setUserState('com_users.display.mail.data', []);
$app->enqueueMessage(Text::plural('COM_USERS_MAIL_EMAIL_SENT_TO_N_USERS', \count($rows)), 'message');
return true;
}
}

View File

@ -0,0 +1,257 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_users
*
* @copyright (C) 2022 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Component\Users\Administrator\Model;
use Joomla\CMS\Event\MultiFactor\GetSetup;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
use Joomla\CMS\User\User;
use Joomla\Component\Users\Administrator\DataShape\SetupRenderOptions;
use Joomla\Component\Users\Administrator\Helper\Mfa as MfaHelper;
use Joomla\Component\Users\Administrator\Table\MfaTable;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Multi-factor Authentication management model
*
* @since 4.2.0
*/
class MethodModel extends BaseDatabaseModel
{
/**
* List of MFA Methods
*
* @var array
* @since 4.2.0
*/
protected $mfaMethods = null;
/**
* Get the specified MFA Method's record
*
* @param string $method The Method to retrieve.
*
* @return array
* @since 4.2.0
*/
public function getMethod(string $method): array
{
if (!$this->methodExists($method)) {
return [
'name' => $method,
'display' => '',
'shortinfo' => '',
'image' => '',
'canDisable' => true,
'allowMultiple' => true,
];
}
return $this->mfaMethods[$method];
}
/**
* Is the specified MFA Method available?
*
* @param string $method The Method to check.
*
* @return boolean
* @since 4.2.0
*/
public function methodExists(string $method): bool
{
if (!\is_array($this->mfaMethods)) {
$this->populateMfaMethods();
}
return isset($this->mfaMethods[$method]);
}
/**
* @param User|null $user The user record. Null to use the currently logged in user.
*
* @return array
* @throws \Exception
*
* @since 4.2.0
*/
public function getRenderOptions(?User $user = null): SetupRenderOptions
{
if (\is_null($user)) {
$user = $this->getCurrentUser();
}
$renderOptions = new SetupRenderOptions();
$event = new GetSetup($this->getRecord($user));
$results = Factory::getApplication()
->getDispatcher()
->dispatch($event->getName(), $event)
->getArgument('result', []);
if (empty($results)) {
return $renderOptions;
}
foreach ($results as $result) {
if (empty($result)) {
continue;
}
return $renderOptions->merge($result);
}
return $renderOptions;
}
/**
* Get the specified MFA record. It will return a fake default record when no record ID is specified.
*
* @param User|null $user The user record. Null to use the currently logged in user.
*
* @return MfaTable
* @throws \Exception
*
* @since 4.2.0
*/
public function getRecord(User $user = null): MfaTable
{
if (\is_null($user)) {
$user = $this->getCurrentUser();
}
$defaultRecord = $this->getDefaultRecord($user);
$id = (int) $this->getState('id', 0);
if ($id <= 0) {
return $defaultRecord;
}
/** @var MfaTable $record */
$record = $this->getTable('Mfa', 'Administrator');
$loaded = $record->load(
[
'user_id' => $user->id,
'id' => $id,
]
);
if (!$loaded) {
return $defaultRecord;
}
if (!$this->methodExists($record->method)) {
return $defaultRecord;
}
return $record;
}
/**
* Return the title to use for the page
*
* @return string
*
* @since 4.2.0
*/
public function getPageTitle(): string
{
$task = $this->getState('task', 'edit');
switch ($task) {
case 'mfa':
$key = 'COM_USERS_USER_MULTIFACTOR_AUTH';
break;
default:
$key = sprintf('COM_USERS_MFA_%s_PAGE_HEAD', $task);
break;
}
return Text::_($key);
}
/**
* @param User|null $user The user record. Null to use the current user.
*
* @return MfaTable
* @throws \Exception
*
* @since 4.2.0
*/
protected function getDefaultRecord(?User $user = null): MfaTable
{
if (\is_null($user)) {
$user = $this->getCurrentUser();
}
$method = $this->getState('method');
$title = '';
if (\is_null($this->mfaMethods)) {
$this->populateMfaMethods();
}
if ($method && isset($this->mfaMethods[$method])) {
$title = $this->mfaMethods[$method]['display'];
}
/** @var MfaTable $record */
$record = $this->getTable('Mfa', 'Administrator');
$record->bind(
[
'id' => null,
'user_id' => $user->id,
'title' => $title,
'method' => $method,
'default' => 0,
'options' => [],
]
);
return $record;
}
/**
* Populate the list of MFA Methods
*
* @return void
* @since 4.2.0
*/
private function populateMfaMethods(): void
{
$this->mfaMethods = [];
$mfaMethods = MfaHelper::getMfaMethods();
if (empty($mfaMethods)) {
return;
}
foreach ($mfaMethods as $method) {
$this->mfaMethods[$method['name']] = $method;
}
// We also need to add the backup codes Method
$this->mfaMethods['backupcodes'] = [
'name' => 'backupcodes',
'display' => Text::_('COM_USERS_USER_BACKUPCODES'),
'shortinfo' => Text::_('COM_USERS_USER_BACKUPCODES_DESC'),
'image' => 'media/com_users/images/emergency.svg',
'canDisable' => false,
'allowMultiple' => false,
];
}
}

View File

@ -0,0 +1,218 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_users
*
* @copyright (C) 2022 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Component\Users\Administrator\Model;
use Joomla\CMS\Date\Date;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
use Joomla\CMS\User\User;
use Joomla\Component\Users\Administrator\Helper\Mfa as MfaHelper;
use Joomla\Database\ParameterType;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Multi-factor Authentication Methods list page's model
*
* @since 4.2.0
*/
class MethodsModel extends BaseDatabaseModel
{
/**
* Returns a list of all available MFA methods and their currently active records for a given user.
*
* @param User|null $user The user object. Skip to use the current user.
*
* @return array
* @throws \Exception
*
* @since 4.2.0
*/
public function getMethods(?User $user = null): array
{
if (\is_null($user)) {
$user = $this->getCurrentUser();
}
if ($user->guest) {
return [];
}
// Get an associative array of MFA Methods
$rawMethods = MfaHelper::getMfaMethods();
$methods = [];
foreach ($rawMethods as $method) {
$method['active'] = [];
$methods[$method['name']] = $method;
}
// Put the user MFA records into the Methods array
$userMfaRecords = MfaHelper::getUserMfaRecords($user->id);
if (!empty($userMfaRecords)) {
foreach ($userMfaRecords as $record) {
if (!isset($methods[$record->method])) {
continue;
}
$methods[$record->method]->addActiveMethod($record);
}
}
return $methods;
}
/**
* Delete all Multi-factor Authentication Methods for the given user.
*
* @param User|null $user The user object to reset MFA for. Null to use the current user.
*
* @return void
* @throws \Exception
*
* @since 4.2.0
*/
public function deleteAll(?User $user = null): void
{
// Make sure we have a user object
if (\is_null($user)) {
$user = $this->getCurrentUser() ?: Factory::getApplication()->getIdentity();
}
// If the user object is a guest (who can't have MFA) we stop with an error
if ($user->guest) {
throw new \RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403);
}
$db = $this->getDatabase();
$query = $db->getQuery(true)
->delete($db->quoteName('#__user_mfa'))
->where($db->quoteName('user_id') . ' = :user_id')
->bind(':user_id', $user->id, ParameterType::INTEGER);
$db->setQuery($query)->execute();
}
/**
* Format a relative timestamp. It deals with timestamps today and yesterday in a special manner. Example returns:
* Yesterday, 13:12
* Today, 08:33
* January 1, 2015
*
* @param string $dateTimeText The database time string to use, e.g. "2017-01-13 13:25:36"
*
* @return string The formatted, human-readable date
* @throws \Exception
*
* @since 4.2.0
*/
public function formatRelative(?string $dateTimeText): string
{
if (empty($dateTimeText)) {
return Text::_('JNEVER');
}
// The timestamp is given in UTC. Make sure Joomla! parses it as such.
$utcTimeZone = new \DateTimeZone('UTC');
$jDate = new Date($dateTimeText, $utcTimeZone);
$unixStamp = $jDate->toUnix();
// I'm pretty sure we didn't have MFA in Joomla back in 1970 ;)
if ($unixStamp < 0) {
return Text::_('JNEVER');
}
// I need to display the date in the user's local timezone. That's how you do it.
$user = $this->getCurrentUser();
$userTZ = $user->getParam('timezone', 'UTC');
$tz = new \DateTimeZone($userTZ);
$jDate->setTimezone($tz);
// Default format string: way in the past, the time of the day is not important
$formatString = Text::_('COM_USERS_MFA_LBL_DATE_FORMAT_PAST');
$containerString = Text::_('COM_USERS_MFA_LBL_PAST');
// If the timestamp is within the last 72 hours we may need a special format
if ($unixStamp > (time() - (72 * 3600))) {
// Is this timestamp today?
$jNow = new Date();
$jNow->setTimezone($tz);
$checkNow = $jNow->format('Ymd', true);
$checkDate = $jDate->format('Ymd', true);
if ($checkDate == $checkNow) {
$formatString = Text::_('COM_USERS_MFA_LBL_DATE_FORMAT_TODAY');
$containerString = Text::_('COM_USERS_MFA_LBL_TODAY');
} else {
// Is this timestamp yesterday?
$jYesterday = clone $jNow;
$jYesterday->setTime(0, 0, 0);
$oneSecond = new \DateInterval('PT1S');
$jYesterday->sub($oneSecond);
$checkYesterday = $jYesterday->format('Ymd', true);
if ($checkDate == $checkYesterday) {
$formatString = Text::_('COM_USERS_MFA_LBL_DATE_FORMAT_YESTERDAY');
$containerString = Text::_('COM_USERS_MFA_LBL_YESTERDAY');
}
}
}
return sprintf($containerString, $jDate->format($formatString, true));
}
/**
* Set the user's "don't show this again" flag.
*
* @param User $user The user to check
* @param bool $flag True to set the flag, false to unset it (it will be set to 0, actually)
*
* @return void
*
* @since 4.2.0
*/
public function setFlag(User $user, bool $flag = true): void
{
$db = $this->getDatabase();
$profileKey = 'mfa.dontshow';
$query = $db->getQuery(true)
->select($db->quoteName('profile_value'))
->from($db->quoteName('#__user_profiles'))
->where($db->quoteName('user_id') . ' = :user_id')
->where($db->quoteName('profile_key') . ' = :profileKey')
->bind(':user_id', $user->id, ParameterType::INTEGER)
->bind(':profileKey', $profileKey, ParameterType::STRING);
try {
$result = $db->setQuery($query)->loadResult();
} catch (\Exception $e) {
return;
}
$exists = !\is_null($result);
$object = (object) [
'user_id' => $user->id,
'profile_key' => 'mfa.dontshow',
'profile_value' => ($flag ? 1 : 0),
'ordering' => 1,
];
if (!$exists) {
$db->insertObject('#__user_profiles', $object);
} else {
$db->updateObject('#__user_profiles', $object, ['user_id', 'profile_key']);
}
}
}

View File

@ -0,0 +1,140 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_users
*
* @copyright (C) 2011 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Component\Users\Administrator\Model;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\Model\AdminModel;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\CMS\Versioning\VersionableModelTrait;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* User note model.
*
* @since 2.5
*/
class NoteModel extends AdminModel
{
use VersionableModelTrait;
/**
* The type alias for this content type.
*
* @var string
* @since 3.2
*/
public $typeAlias = 'com_users.note';
/**
* Method to get the record form.
*
* @param array $data Data for the form.
* @param boolean $loadData True if the form is to load its own data (default case), false if not.
*
* @return \Joomla\CMS\Form\Form|bool A Form object on success, false on failure
*
* @since 2.5
*/
public function getForm($data = [], $loadData = true)
{
// Get the form.
$form = $this->loadForm('com_users.note', 'note', ['control' => 'jform', 'load_data' => $loadData]);
if (empty($form)) {
return false;
}
return $form;
}
/**
* Method to get a single record.
*
* @param integer $pk The id of the primary key.
*
* @return mixed Object on success, false on failure.
*
* @since 2.5
* @throws \Exception
*/
public function getItem($pk = null)
{
$result = parent::getItem($pk);
// Get the dispatcher and load the content plugins.
PluginHelper::importPlugin('content');
// Load the user plugins for backward compatibility (v3.3.3 and earlier).
PluginHelper::importPlugin('user');
// Trigger the data preparation event.
Factory::getApplication()->triggerEvent('onContentPrepareData', ['com_users.note', $result]);
return $result;
}
/**
* Method to get the data that should be injected in the form.
*
* @return mixed The data for the form.
*
* @since 1.6
* @throws \Exception
*/
protected function loadFormData()
{
// Get the application
$app = Factory::getApplication();
// Check the session for previously entered form data.
$data = $app->getUserState('com_users.edit.note.data', []);
if (empty($data)) {
$data = $this->getItem();
// Prime some default values.
if ($this->getState('note.id') == 0) {
$data->set('catid', $app->getInput()->get('catid', $app->getUserState('com_users.notes.filter.category_id'), 'int'));
}
$userId = $app->getInput()->get('u_id', 0, 'int');
if ($userId != 0) {
$data->user_id = $userId;
}
}
$this->preprocessData('com_users.note', $data);
return $data;
}
/**
* Method to auto-populate the model state.
*
* Note. Calling getState in this method will result in recursion.
*
* @return void
*
* @since 2.5
* @throws \Exception
*/
protected function populateState()
{
parent::populateState();
$userId = Factory::getApplication()->getInput()->get('u_id', 0, 'int');
$this->setState('note.user_id', $userId);
}
}

View File

@ -0,0 +1,231 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_users
*
* @copyright (C) 2011 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Component\Users\Administrator\Model;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
use Joomla\CMS\MVC\Model\ListModel;
use Joomla\CMS\User\User;
use Joomla\Database\DatabaseQuery;
use Joomla\Database\ParameterType;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* User notes model class.
*
* @since 2.5
*/
class NotesModel extends ListModel
{
/**
* Constructor.
*
* @param array $config An optional associative array of configuration settings.
* @param MVCFactoryInterface $factory The factory.
*
* @see \Joomla\CMS\MVC\Model\BaseDatabaseModel
* @since 3.2
*/
public function __construct($config = [], MVCFactoryInterface $factory = null)
{
// Set the list ordering fields.
if (empty($config['filter_fields'])) {
$config['filter_fields'] = [
'id', 'a.id',
'user_id', 'a.user_id',
'u.name',
'subject', 'a.subject',
'catid', 'a.catid', 'category_id',
'state', 'a.state', 'published',
'c.title',
'review_time', 'a.review_time',
'publish_up', 'a.publish_up',
'publish_down', 'a.publish_down',
'level', 'c.level',
];
}
parent::__construct($config, $factory);
}
/**
* Build an SQL query to load the list data.
*
* @return DatabaseQuery A DatabaseQuery object to retrieve the data set.
*
* @since 2.5
*/
protected function getListQuery()
{
$db = $this->getDatabase();
$query = $db->getQuery(true);
// Select the required fields from the table.
$query->select(
$this->getState(
'list.select',
'a.id, a.subject, a.checked_out, a.checked_out_time,' .
'a.catid, a.created_time, a.review_time,' .
'a.state, a.publish_up, a.publish_down'
)
);
$query->from('#__user_notes AS a');
// Join over the category
$query->select('c.title AS category_title, c.params AS category_params')
->join('LEFT', '#__categories AS c ON c.id = a.catid');
// Join over the users for the note user.
$query->select('u.name AS user_name')
->join('LEFT', '#__users AS u ON u.id = a.user_id');
// Join over the users for the checked out user.
$query->select('uc.name AS editor')
->join('LEFT', '#__users AS uc ON uc.id = a.checked_out');
// Filter by search in title
$search = $this->getState('filter.search');
if (!empty($search)) {
if (stripos($search, 'id:') === 0) {
$search3 = (int) substr($search, 3);
$query->where($db->quoteName('a.id') . ' = :id');
$query->bind(':id', $search3, ParameterType::INTEGER);
} elseif (stripos($search, 'uid:') === 0) {
$search4 = (int) substr($search, 4);
$query->where($db->quoteName('a.user_id') . ' = :id');
$query->bind(':id', $search4, ParameterType::INTEGER);
} else {
$search = '%' . trim($search) . '%';
$query->where(
'(' . $db->quoteName('a.subject') . ' LIKE :subject'
. ' OR ' . $db->quoteName('u.name') . ' LIKE :name'
. ' OR ' . $db->quoteName('u.username') . ' LIKE :username)'
);
$query->bind(':subject', $search);
$query->bind(':name', $search);
$query->bind(':username', $search);
}
}
// Filter by published state
$published = $this->getState('filter.published');
if (is_numeric($published)) {
$query->where($db->quoteName('a.state') . ' = :state')
->bind(':state', $published, ParameterType::INTEGER);
} elseif ($published !== '*') {
$query->whereIn($db->quoteName('a.state'), [0, 1]);
}
// Filter by a single category.
$categoryId = (int) $this->getState('filter.category_id');
if ($categoryId) {
$query->where($db->quoteName('a.catid') . ' = :catid')
->bind(':catid', $categoryId, ParameterType::INTEGER);
}
// Filter by a single user.
$userId = (int) $this->getState('filter.user_id');
if ($userId) {
// Add the body and where filter.
$query->select('a.body')
->where($db->quoteName('a.user_id') . ' = :user_id')
->bind(':user_id', $userId, ParameterType::INTEGER);
}
// Filter on the level.
if ($level = $this->getState('filter.level')) {
$level = (int) $level;
$query->where($db->quoteName('c.level') . ' <= :level')
->bind(':level', $level, ParameterType::INTEGER);
}
// Add the list ordering clause.
$query->order($db->escape($this->getState('list.ordering', 'a.review_time')) . ' ' . $db->escape($this->getState('list.direction', 'DESC')));
return $query;
}
/**
* Method to get a store id based on model configuration state.
*
* This is necessary because the model is used by the component and
* different modules that might need different sets of data or different
* ordering requirements.
*
* @param string $id A prefix for the store id.
*
* @return string A store id.
*
* @since 2.5
*/
protected function getStoreId($id = '')
{
// Compile the store id.
$id .= ':' . $this->getState('filter.search');
$id .= ':' . $this->getState('filter.published');
$id .= ':' . $this->getState('filter.category_id');
$id .= ':' . $this->getState('filter.user_id');
$id .= ':' . $this->getState('filter.level');
return parent::getStoreId($id);
}
/**
* Gets a user object if the user filter is set.
*
* @return User The User object
*
* @since 2.5
*/
public function getUser()
{
$user = new User();
// Filter by search in title
$search = (int) $this->getState('filter.user_id');
if ($search != 0) {
$user->load((int) $search);
}
return $user;
}
/**
* Method to auto-populate the model state.
*
* Note. Calling getState in this method will result in recursion.
*
* @param string $ordering An optional ordering field.
* @param string $direction An optional direction (asc|desc).
*
* @return void
*
* @since 1.6
* @throws \Exception
*/
protected function populateState($ordering = 'a.review_time', $direction = 'desc')
{
// Adjust the context to support modal layouts.
if ($layout = Factory::getApplication()->getInput()->get('layout')) {
$this->context .= '.' . $layout;
}
parent::populateState($ordering, $direction);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,607 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_users
*
* @copyright (C) 2008 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Component\Users\Administrator\Model;
use Joomla\CMS\Component\ComponentHelper;
use Joomla\CMS\Date\Date;
use Joomla\CMS\Factory;
use Joomla\CMS\Form\Form;
use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
use Joomla\CMS\MVC\Model\ListModel;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\Database\DatabaseQuery;
use Joomla\Database\ParameterType;
use Joomla\Utilities\ArrayHelper;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Methods supporting a list of user records.
*
* @since 1.6
*/
class UsersModel extends ListModel
{
/**
* A list of filter variables to not merge into the model's state
*
* @var array
* @since 4.0.0
*/
protected $filterForbiddenList = ['groups', 'excluded'];
/**
* Override parent constructor.
*
* @param array $config An optional associative array of configuration settings.
* @param MVCFactoryInterface $factory The factory.
*
* @see \Joomla\CMS\MVC\Model\BaseDatabaseModel
* @since 3.2
*/
public function __construct($config = [], MVCFactoryInterface $factory = null)
{
if (empty($config['filter_fields'])) {
$config['filter_fields'] = [
'id', 'a.id',
'name', 'a.name',
'username', 'a.username',
'email', 'a.email',
'block', 'a.block',
'sendEmail', 'a.sendEmail',
'registerDate', 'a.registerDate',
'lastvisitDate', 'a.lastvisitDate',
'activation', 'a.activation',
'active',
'group_id',
'range',
'lastvisitrange',
'state',
'mfa',
];
}
parent::__construct($config, $factory);
}
/**
* Method to auto-populate the model state.
*
* Note. Calling getState in this method will result in recursion.
*
* @param string $ordering An optional ordering field.
* @param string $direction An optional direction (asc|desc).
*
* @return void
*
* @since 1.6
* @throws \Exception
*/
protected function populateState($ordering = 'a.name', $direction = 'asc')
{
$app = Factory::getApplication();
$input = $app->getInput();
// Adjust the context to support modal layouts.
if ($layout = $input->get('layout', 'default', 'cmd')) {
$this->context .= '.' . $layout;
}
$groups = json_decode(base64_decode($input->get('groups', '', 'BASE64')));
if (isset($groups)) {
$groups = ArrayHelper::toInteger($groups);
}
$this->setState('filter.groups', $groups);
$excluded = json_decode(base64_decode($input->get('excluded', '', 'BASE64')));
if (isset($excluded)) {
$excluded = ArrayHelper::toInteger($excluded);
}
$this->setState('filter.excluded', $excluded);
// Load the parameters.
$params = ComponentHelper::getParams('com_users');
$this->setState('params', $params);
// List state information.
parent::populateState($ordering, $direction);
}
/**
* Method to get a store id based on model configuration state.
*
* This is necessary because the model is used by the component and
* different modules that might need different sets of data or different
* ordering requirements.
*
* @param string $id A prefix for the store id.
*
* @return string A store id.
*
* @since 1.6
*/
protected function getStoreId($id = '')
{
// Compile the store id.
$id .= ':' . $this->getState('filter.search');
$id .= ':' . $this->getState('filter.active');
$id .= ':' . $this->getState('filter.state');
$id .= ':' . $this->getState('filter.group_id');
$id .= ':' . $this->getState('filter.range');
if (PluginHelper::isEnabled('multifactorauth')) {
$id .= ':' . $this->getState('filter.mfa');
}
return parent::getStoreId($id);
}
/**
* Gets the list of users and adds expensive joins to the result set.
*
* @return mixed An array of data items on success, false on failure.
*
* @since 1.6
*/
public function getItems()
{
// Get a storage key.
$store = $this->getStoreId();
// Try to load the data from internal storage.
if (empty($this->cache[$store])) {
$groups = $this->getState('filter.groups');
$groupId = $this->getState('filter.group_id');
if (isset($groups) && (empty($groups) || $groupId && !\in_array($groupId, $groups))) {
$items = [];
} else {
$items = parent::getItems();
}
// Bail out on an error or empty list.
if (empty($items)) {
$this->cache[$store] = $items;
return $items;
}
// Joining the groups with the main query is a performance hog.
// Find the information only on the result set.
// First pass: get list of the user ids and reset the counts.
$userIds = [];
foreach ($items as $item) {
$userIds[] = (int) $item->id;
$item->group_count = 0;
$item->group_names = '';
$item->note_count = 0;
}
// Get the counts from the database only for the users in the list.
$db = $this->getDatabase();
$query = $db->getQuery(true);
// Join over the group mapping table.
$query->select('map.user_id, COUNT(map.group_id) AS group_count')
->from('#__user_usergroup_map AS map')
->whereIn($db->quoteName('map.user_id'), $userIds)
->group('map.user_id')
// Join over the user groups table.
->join('LEFT', '#__usergroups AS g2 ON g2.id = map.group_id');
$db->setQuery($query);
// Load the counts into an array indexed on the user id field.
try {
$userGroups = $db->loadObjectList('user_id');
} catch (\RuntimeException $e) {
$this->setError($e->getMessage());
return false;
}
$query->clear()
->select('n.user_id, COUNT(n.id) As note_count')
->from('#__user_notes AS n')
->whereIn($db->quoteName('n.user_id'), $userIds)
->where('n.state >= 0')
->group('n.user_id');
$db->setQuery($query);
// Load the counts into an array indexed on the aro.value field (the user id).
try {
$userNotes = $db->loadObjectList('user_id');
} catch (\RuntimeException $e) {
$this->setError($e->getMessage());
return false;
}
// Second pass: collect the group counts into the main items array.
foreach ($items as &$item) {
if (isset($userGroups[$item->id])) {
$item->group_count = $userGroups[$item->id]->group_count;
// Group_concat in other databases is not supported
$item->group_names = $this->getUserDisplayedGroups($item->id);
}
if (isset($userNotes[$item->id])) {
$item->note_count = $userNotes[$item->id]->note_count;
}
}
// Add the items to the internal cache.
$this->cache[$store] = $items;
}
return $this->cache[$store];
}
/**
* Get the filter form
*
* @param array $data data
* @param boolean $loadData load current data
*
* @return Form|null The \JForm object or null if the form can't be found
*
* @since 4.2.0
*/
public function getFilterForm($data = [], $loadData = true)
{
$form = parent::getFilterForm($data, $loadData);
if ($form && !PluginHelper::isEnabled('multifactorauth')) {
$form->removeField('mfa', 'filter');
}
return $form;
}
/**
* Build an SQL query to load the list data.
*
* @return DatabaseQuery
*
* @since 1.6
*/
protected function getListQuery()
{
// Create a new query object.
$db = $this->getDatabase();
$query = $db->getQuery(true);
// Select the required fields from the table.
$query->select(
$this->getState(
'list.select',
'a.*'
)
);
$query->from($db->quoteName('#__users') . ' AS a');
// Include MFA information
if (PluginHelper::isEnabled('multifactorauth')) {
$subQuery = $db->getQuery(true)
->select(
[
'MIN(' . $db->quoteName('user_id') . ') AS ' . $db->quoteName('uid'),
'COUNT(*) AS ' . $db->quoteName('mfaRecords'),
]
)
->from($db->quoteName('#__user_mfa'))
->group($db->quoteName('user_id'));
$query->select($db->quoteName('mfa.mfaRecords'))
->join(
'left',
'(' . $subQuery . ') AS ' . $db->quoteName('mfa'),
$db->quoteName('mfa.uid') . ' = ' . $db->quoteName('a.id')
);
$mfaState = $this->getState('filter.mfa');
if (is_numeric($mfaState)) {
$mfaState = (int) $mfaState;
if ($mfaState === 1) {
$query->where(
'((' . $db->quoteName('mfa.mfaRecords') . ' > 0) OR (' .
$db->quoteName('a.otpKey') . ' IS NOT NULL AND ' .
$db->quoteName('a.otpKey') . ' != ' . $db->quote('') . '))'
);
} else {
$query->where(
'((' . $db->quoteName('mfa.mfaRecords') . ' = 0 OR ' .
$db->quoteName('mfa.mfaRecords') . ' IS NULL) AND (' .
$db->quoteName('a.otpKey') . ' IS NULL OR ' .
$db->quoteName('a.otpKey') . ' = ' . $db->quote('') . '))'
);
}
}
}
// If the model is set to check item state, add to the query.
$state = $this->getState('filter.state');
if (is_numeric($state)) {
$query->where($db->quoteName('a.block') . ' = :state')
->bind(':state', $state, ParameterType::INTEGER);
}
// If the model is set to check the activated state, add to the query.
$active = $this->getState('filter.active');
if (is_numeric($active)) {
if ($active == '0') {
$query->whereIn($db->quoteName('a.activation'), ['', '0']);
} elseif ($active == '1') {
$query->where($query->length($db->quoteName('a.activation')) . ' > 1');
}
}
// Filter the items over the group id if set.
$groupId = $this->getState('filter.group_id');
$groups = $this->getState('filter.groups');
if ($groupId || isset($groups)) {
$group_by = [
'a.id',
'a.name',
'a.username',
'a.password',
'a.block',
'a.sendEmail',
'a.registerDate',
'a.lastvisitDate',
'a.activation',
'a.params',
'a.email',
'a.lastResetTime',
'a.resetCount',
'a.otpKey',
'a.otep',
'a.requireReset',
];
if (PluginHelper::isEnabled('multifactorauth')) {
$group_by[] = 'mfa.mfaRecords';
}
$query->join('LEFT', '#__user_usergroup_map AS map2 ON map2.user_id = a.id')
->group($db->quoteName($group_by));
if ($groupId) {
$groupId = (int) $groupId;
$query->where($db->quoteName('map2.group_id') . ' = :group_id')
->bind(':group_id', $groupId, ParameterType::INTEGER);
}
if (isset($groups)) {
$query->whereIn($db->quoteName('map2.group_id'), $groups);
}
}
// Filter the items over the search string if set.
$search = $this->getState('filter.search');
if (!empty($search)) {
if (stripos($search, 'id:') === 0) {
$ids = (int) substr($search, 3);
$query->where($db->quoteName('a.id') . ' = :id');
$query->bind(':id', $ids, ParameterType::INTEGER);
} elseif (stripos($search, 'username:') === 0) {
$search = '%' . substr($search, 9) . '%';
$query->where($db->quoteName('a.username') . ' LIKE :username');
$query->bind(':username', $search);
} else {
$search = '%' . trim($search) . '%';
// Add the clauses to the query.
$query->where(
'(' . $db->quoteName('a.name') . ' LIKE :name'
. ' OR ' . $db->quoteName('a.username') . ' LIKE :username'
. ' OR ' . $db->quoteName('a.email') . ' LIKE :email)'
)
->bind(':name', $search)
->bind(':username', $search)
->bind(':email', $search);
}
}
// Add filter for registration time ranges select list. UI Visitors get a range of predefined
// values. API users can do a full range based on ISO8601
$range = $this->getState('filter.range');
$registrationStart = $this->getState('filter.registrationDateStart');
$registrationEnd = $this->getState('filter.registrationDateEnd');
// Apply the range filter.
if ($range || ($registrationStart && $registrationEnd)) {
if ($range) {
$dates = $this->buildDateRange($range);
} else {
$dates = [
'dNow' => $registrationEnd,
'dStart' => $registrationStart,
];
}
if ($dates['dStart'] !== false) {
$dStart = $dates['dStart']->format('Y-m-d H:i:s');
if ($dates['dNow'] === false) {
$query->where($db->quoteName('a.registerDate') . ' < :registerDate');
$query->bind(':registerDate', $dStart);
} else {
$dNow = $dates['dNow']->format('Y-m-d H:i:s');
$query->where($db->quoteName('a.registerDate') . ' BETWEEN :registerDate1 AND :registerDate2');
$query->bind(':registerDate1', $dStart);
$query->bind(':registerDate2', $dNow);
}
}
}
// Add filter for last visit time ranges select list. UI Visitors get a range of predefined
// values. API users can do a full range based on ISO8601
$lastvisitrange = $this->getState('filter.lastvisitrange');
$lastVisitStart = $this->getState('filter.lastVisitStart');
$lastVisitEnd = $this->getState('filter.lastVisitEnd');
// Apply the range filter.
if ($lastvisitrange || ($lastVisitStart && $lastVisitEnd)) {
if ($lastvisitrange) {
$dates = $this->buildDateRange($lastvisitrange);
} else {
$dates = [
'dNow' => $lastVisitEnd,
'dStart' => $lastVisitStart,
];
}
if ($dates['dStart'] === false) {
$query->where($db->quoteName('a.lastvisitDate') . ' IS NULL');
} else {
$query->where($db->quoteName('a.lastvisitDate') . ' IS NOT NULL');
$dStart = $dates['dStart']->format('Y-m-d H:i:s');
if ($dates['dNow'] === false) {
$query->where($db->quoteName('a.lastvisitDate') . ' < :lastvisitDate');
$query->bind(':lastvisitDate', $dStart);
} else {
$dNow = $dates['dNow']->format('Y-m-d H:i:s');
$query->where($db->quoteName('a.lastvisitDate') . ' BETWEEN :lastvisitDate1 AND :lastvisitDate2');
$query->bind(':lastvisitDate1', $dStart);
$query->bind(':lastvisitDate2', $dNow);
}
}
}
// Filter by excluded users
$excluded = $this->getState('filter.excluded');
if (!empty($excluded)) {
$query->whereNotIn($db->quoteName('id'), $excluded);
}
// Add the list ordering clause.
$query->order(
$db->quoteName($db->escape($this->getState('list.ordering', 'a.name'))) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))
);
return $query;
}
/**
* Construct the date range to filter on.
*
* @param string $range The textual range to construct the filter for.
*
* @return array The date range to filter on.
*
* @since 3.6.0
* @throws \Exception
*/
private function buildDateRange($range)
{
// Get UTC for now.
$dNow = new Date();
$dStart = clone $dNow;
switch ($range) {
case 'past_week':
$dStart->modify('-7 day');
break;
case 'past_1month':
$dStart->modify('-1 month');
break;
case 'past_3month':
$dStart->modify('-3 month');
break;
case 'past_6month':
$dStart->modify('-6 month');
$arr = [];
break;
case 'post_year':
$dNow = false;
// No break
case 'past_year':
$dStart->modify('-1 year');
break;
case 'today':
// Ranges that need to align with local 'days' need special treatment.
$app = Factory::getApplication();
$offset = $app->get('offset');
// Reset the start time to be the beginning of today, local time.
$dStart = new Date('now', $offset);
$dStart->setTime(0, 0, 0);
// Now change the timezone back to UTC.
$tz = new \DateTimeZone('GMT');
$dStart->setTimezone($tz);
break;
case 'never':
$dNow = false;
$dStart = false;
break;
}
return ['dNow' => $dNow, 'dStart' => $dStart];
}
/**
* SQL server change
*
* @param integer $userId User identifier
*
* @return string Groups titles imploded :$
*/
protected function getUserDisplayedGroups($userId)
{
$db = $this->getDatabase();
$query = $db->getQuery(true)
->select($db->quoteName('title'))
->from($db->quoteName('#__usergroups', 'ug'))
->join('LEFT', $db->quoteName('#__user_usergroup_map', 'map') . ' ON (ug.id = map.group_id)')
->where($db->quoteName('map.user_id') . ' = :user_id')
->bind(':user_id', $userId, ParameterType::INTEGER);
try {
$result = $db->setQuery($query)->loadColumn();
} catch (\RuntimeException $e) {
$result = [];
}
return implode("\n", $result);
}
}

View File

@ -0,0 +1,132 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_users
*
* @copyright (C) 2022 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Component\Users\Administrator\Service;
use Joomla\CMS\Encrypt\Aes;
use Joomla\CMS\Factory;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Data encryption service.
*
* @since 4.2.0
*/
class Encrypt
{
/**
* The encryption engine used by this service
*
* @var Aes
* @since 4.2.0
*/
private $aes;
/**
* EncryptService constructor.
*
* @since 4.2.0
*/
public function __construct()
{
$this->initialize();
}
/**
* Encrypt the plaintext $data and return the ciphertext prefixed by ###AES128###
*
* @param string $data The plaintext data
*
* @return string The ciphertext, prefixed by ###AES128###
*
* @since 4.2.0
*/
public function encrypt(string $data): string
{
if (!\is_object($this->aes)) {
return $data;
}
$this->aes->setPassword($this->getPassword(), false);
$encrypted = $this->aes->encryptString($data, true);
return '###AES128###' . $encrypted;
}
/**
* Decrypt the ciphertext, prefixed by ###AES128###, and return the plaintext.
*
* @param string $data The ciphertext, prefixed by ###AES128###
* @param bool $legacy Use legacy key expansion. We recommend against using it.
*
* @return string The plaintext data
*
* @since 4.2.0
*/
public function decrypt(string $data, bool $legacy = false): string
{
if (substr($data, 0, 12) != '###AES128###') {
return $data;
}
$data = substr($data, 12);
if (!\is_object($this->aes)) {
return $data;
}
$this->aes->setPassword($this->getPassword(), $legacy);
$decrypted = $this->aes->decryptString($data, true);
// Decrypted data is null byte padded. We have to remove the padding before proceeding.
return rtrim($decrypted, "\0");
}
/**
* Initialize the AES cryptography object
*
* @return void
* @since 4.2.0
*/
private function initialize(): void
{
if (\is_object($this->aes)) {
return;
}
$password = $this->getPassword();
if (empty($password)) {
return;
}
$this->aes = new Aes('cbc');
$this->aes->setPassword($password);
}
/**
* Returns the password used to encrypt information in the component
*
* @return string
*
* @since 4.2.0
*/
private function getPassword(): string
{
try {
return Factory::getApplication()->get('secret', '');
} catch (\Exception $e) {
return '';
}
}
}

View File

@ -0,0 +1,425 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_users
*
* @copyright (C) 2009 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Component\Users\Administrator\Service\HTML;
use Joomla\CMS\Factory;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\LanguageHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Uri\Uri;
use Joomla\Database\ParameterType;
use Joomla\Filesystem\Path;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Extended Utility class for the Users component.
*
* @since 2.5
*/
class Users
{
/**
* Display an image.
*
* @param string $src The source of the image
*
* @return string A <img> element if the specified file exists, otherwise, a null string
*
* @since 2.5
* @throws \Exception
*/
public function image($src)
{
$src = preg_replace('#[^A-Z0-9\-_\./]#i', '', $src);
$file = JPATH_SITE . '/' . $src;
Path::check($file);
if (!file_exists($file)) {
return '';
}
return '<img src="' . Uri::root() . $src . '" alt="">';
}
/**
* Displays an icon to add a note for this user.
*
* @param integer $userId The user ID
*
* @return string A link to add a note
*
* @since 2.5
*/
public function addNote($userId)
{
$title = Text::_('COM_USERS_ADD_NOTE');
return '<a href="' . Route::_('index.php?option=com_users&task=note.add&u_id=' . (int) $userId)
. '" class="btn btn-secondary btn-sm"><span class="icon-plus pe-1" aria-hidden="true">'
. '</span>' . $title . '</a>';
}
/**
* Displays an icon to filter the notes list on this user.
*
* @param integer $count The number of notes for the user
* @param integer $userId The user ID
*
* @return string A link to apply a filter
*
* @since 2.5
*/
public function filterNotes($count, $userId)
{
if (empty($count)) {
return '';
}
$title = Text::_('COM_USERS_FILTER_NOTES');
return '<a href="' . Route::_('index.php?option=com_users&view=notes&filter[search]=uid:' . (int) $userId)
. '" class="dropdown-item"><span class="icon-list pe-1" aria-hidden="true"></span>' . $title . '</a>';
}
/**
* Displays a note icon.
*
* @param integer $count The number of notes for the user
* @param integer $userId The user ID
*
* @return string A link to a modal window with the user notes
*
* @since 2.5
*/
public function notes($count, $userId)
{
if (empty($count)) {
return '';
}
$title = Text::plural('COM_USERS_N_USER_NOTES', $count);
return '<button type="button" data-bs-target="#userModal_' . (int) $userId . '" id="modal-' . (int) $userId
. '" data-bs-toggle="modal" class="dropdown-item"><span class="icon-eye pe-1" aria-hidden="true"></span>' . $title . '</button>';
}
/**
* Renders the modal html.
*
* @param integer $count The number of notes for the user
* @param integer $userId The user ID
*
* @return string The html for the rendered modal
*
* @since 3.4.1
*/
public function notesModal($count, $userId)
{
if (empty($count)) {
return '';
}
$title = Text::plural('COM_USERS_N_USER_NOTES', $count);
$footer = '<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">'
. Text::_('JTOOLBAR_CLOSE') . '</button>';
return HTMLHelper::_(
'bootstrap.renderModal',
'userModal_' . (int) $userId,
[
'title' => $title,
'backdrop' => 'static',
'keyboard' => true,
'closeButton' => true,
'footer' => $footer,
'url' => Route::_('index.php?option=com_users&view=notes&tmpl=component&layout=modal&filter[user_id]=' . (int) $userId),
'height' => '300px',
'width' => '800px',
]
);
}
/**
* Build an array of block/unblock user states to be used by jgrid.state,
* State options will be different for any user
* and for currently logged in user
*
* @param boolean $self True if state array is for currently logged in user
*
* @return array a list of possible states to display
*
* @since 3.0
*/
public function blockStates($self = false)
{
if ($self) {
$states = [
1 => [
'task' => 'unblock',
'text' => '',
'active_title' => 'COM_USERS_TOOLBAR_BLOCK',
'inactive_title' => '',
'tip' => true,
'active_class' => 'unpublish',
'inactive_class' => 'unpublish',
],
0 => [
'task' => 'block',
'text' => '',
'active_title' => '',
'inactive_title' => 'COM_USERS_USERS_ERROR_CANNOT_BLOCK_SELF',
'tip' => true,
'active_class' => 'publish',
'inactive_class' => 'publish',
],
];
} else {
$states = [
1 => [
'task' => 'unblock',
'text' => '',
'active_title' => 'COM_USERS_TOOLBAR_UNBLOCK',
'inactive_title' => '',
'tip' => true,
'active_class' => 'unpublish',
'inactive_class' => 'unpublish',
],
0 => [
'task' => 'block',
'text' => '',
'active_title' => 'COM_USERS_TOOLBAR_BLOCK',
'inactive_title' => '',
'tip' => true,
'active_class' => 'publish',
'inactive_class' => 'publish',
],
];
}
return $states;
}
/**
* Build an array of activate states to be used by jgrid.state,
*
* @return array a list of possible states to display
*
* @since 3.0
*/
public function activateStates()
{
$states = [
1 => [
'task' => 'activate',
'text' => '',
'active_title' => 'COM_USERS_TOOLBAR_ACTIVATE',
'inactive_title' => '',
'tip' => true,
'active_class' => 'unpublish',
'inactive_class' => 'unpublish',
],
0 => [
'task' => '',
'text' => '',
'active_title' => '',
'inactive_title' => 'COM_USERS_ACTIVATED',
'tip' => true,
'active_class' => 'publish',
'inactive_class' => 'publish',
],
];
return $states;
}
/**
* Get the sanitized value
*
* @param mixed $value Value of the field
*
* @return mixed String/void
*
* @since 1.6
*/
public function value($value)
{
if (\is_string($value)) {
$value = trim($value);
}
if (empty($value)) {
return Text::_('COM_USERS_PROFILE_VALUE_NOT_FOUND');
}
if (!\is_array($value)) {
return htmlspecialchars($value, ENT_COMPAT, 'UTF-8');
}
}
/**
* Get the space symbol
*
* @param mixed $value Value of the field
*
* @return string
*
* @since 1.6
*/
public function spacer($value)
{
return '';
}
/**
* Get the sanitized template style
*
* @param mixed $value Value of the field
*
* @return mixed String/void
*
* @since 1.6
*/
public function templatestyle($value)
{
if (empty($value)) {
return static::value($value);
}
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select($db->quoteName('title'))
->from($db->quoteName('#__template_styles'))
->where($db->quoteName('id') . ' = :id')
->bind(':id', $value, ParameterType::INTEGER);
$db->setQuery($query);
$title = $db->loadResult();
if ($title) {
return htmlspecialchars($title, ENT_COMPAT, 'UTF-8');
}
return static::value('');
}
/**
* Get the sanitized language
*
* @param mixed $value Value of the field
*
* @return mixed String/void
*
* @since 1.6
*/
public function admin_language($value)
{
if (!$value) {
return static::value($value);
}
$path = LanguageHelper::getLanguagePath(JPATH_ADMINISTRATOR, $value);
$file = $path . '/langmetadata.xml';
if (!is_file($file)) {
// For language packs from before 4.0.
$file = $path . '/' . $value . '.xml';
if (!is_file($file)) {
return static::value($value);
}
}
$result = LanguageHelper::parseXMLLanguageFile($file);
if ($result) {
return htmlspecialchars($result['name'], ENT_COMPAT, 'UTF-8');
}
return static::value($value);
}
/**
* Get the sanitized language
*
* @param mixed $value Value of the field
*
* @return mixed String/void
*
* @since 1.6
*/
public function language($value)
{
if (!$value) {
return static::value($value);
}
$path = LanguageHelper::getLanguagePath(JPATH_SITE, $value);
$file = $path . '/langmetadata.xml';
if (!is_file($file)) {
// For language packs from before 4.0.
$file = $path . '/' . $value . '.xml';
if (!is_file($file)) {
return static::value($value);
}
}
$result = LanguageHelper::parseXMLLanguageFile($file);
if ($result) {
return htmlspecialchars($result['name'], ENT_COMPAT, 'UTF-8');
}
return static::value($value);
}
/**
* Get the sanitized editor name
*
* @param mixed $value Value of the field
*
* @return mixed String/void
*
* @since 1.6
*/
public function editor($value)
{
if (empty($value)) {
return static::value($value);
}
$db = Factory::getDbo();
$lang = Factory::getLanguage();
$query = $db->getQuery(true)
->select($db->quoteName('name'))
->from($db->quoteName('#__extensions'))
->where($db->quoteName('element') . ' = :element')
->where($db->quoteName('folder') . ' = ' . $db->quote('editors'))
->bind(':element', $value);
$db->setQuery($query);
$title = $db->loadResult();
if ($title) {
$lang->load("plg_editors_$value.sys", JPATH_ADMINISTRATOR)
|| $lang->load("plg_editors_$value.sys", JPATH_PLUGINS . '/editors/' . $value);
$lang->load($title . '.sys');
return Text::_($title);
}
return static::value('');
}
}

View File

@ -0,0 +1,427 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_users
*
* @copyright (C) 2022 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Component\Users\Administrator\Table;
use Joomla\CMS\Date\Date;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
use Joomla\CMS\Table\Table;
use Joomla\CMS\User\CurrentUserInterface;
use Joomla\CMS\User\CurrentUserTrait;
use Joomla\CMS\User\UserFactoryAwareInterface;
use Joomla\CMS\User\UserFactoryAwareTrait;
use Joomla\Component\Users\Administrator\Helper\Mfa as MfaHelper;
use Joomla\Component\Users\Administrator\Model\BackupcodesModel;
use Joomla\Component\Users\Administrator\Service\Encrypt;
use Joomla\Database\DatabaseDriver;
use Joomla\Database\ParameterType;
use Joomla\Event\DispatcherInterface;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Table for the Multi-Factor Authentication records
*
* @property int $id Record ID.
* @property int $user_id User ID
* @property string $title Record title.
* @property string $method MFA Method (corresponds to one of the plugins).
* @property int $default Is this the default Method?
* @property array $options Configuration options for the MFA Method.
* @property string $created_on Date and time the record was created.
* @property string $last_used Date and time the record was last used successfully.
* @property int $tries Counter for unsuccessful tries
* @property string $last_try Date and time of the last unsuccessful try
*
* @since 4.2.0
*/
class MfaTable extends Table implements CurrentUserInterface, UserFactoryAwareInterface
{
use CurrentUserTrait;
use UserFactoryAwareTrait;
/**
* Delete flags per ID, set up onBeforeDelete and used onAfterDelete
*
* @var array
* @since 4.2.0
*/
private $deleteFlags = [];
/**
* Encryption service
*
* @var Encrypt
* @since 4.2.0
*/
private $encryptService;
/**
* Indicates that columns fully support the NULL value in the database
*
* @var boolean
* @since 4.2.0
*/
// phpcs:ignore
protected $_supportNullValue = true;
/**
* Table constructor
*
* @param DatabaseDriver $db Database driver object
* @param ?DispatcherInterface $dispatcher Events dispatcher object
*
* @since 4.2.0
*/
public function __construct(DatabaseDriver $db, DispatcherInterface $dispatcher = null)
{
parent::__construct('#__user_mfa', 'id', $db, $dispatcher);
$this->encryptService = new Encrypt();
}
/**
* Method to store a row in the database from the Table instance properties.
*
* If a primary key value is set the row with that primary key value will be updated with the instance property values.
* If no primary key value is set a new row will be inserted into the database with the properties from the Table instance.
*
* @param boolean $updateNulls True to update fields even if they are null.
*
* @return boolean True on success.
*
* @since 4.2.0
*/
public function store($updateNulls = true)
{
// Encrypt the options before saving them
$this->options = $this->encryptService->encrypt(json_encode($this->options ?: []));
// Set last_used date to null if empty or zero date
if (!((int) $this->last_used)) {
$this->last_used = null;
}
$records = MfaHelper::getUserMfaRecords($this->user_id);
if ($this->id) {
// Existing record. Remove it from the list of records.
$records = array_filter(
$records,
function ($rec) {
return $rec->id != $this->id;
}
);
}
// Update the dates on a new record
if (empty($this->id)) {
$this->created_on = Date::getInstance()->toSql();
$this->last_used = null;
}
// Do I need to mark this record as the default?
if ($this->default == 0) {
$hasDefaultRecord = array_reduce(
$records,
function ($carry, $record) {
return $carry || ($record->default == 1);
},
false
);
$this->default = $hasDefaultRecord ? 0 : 1;
}
// Let's find out if we are saving a new MFA method record without having backup codes yet.
$mustCreateBackupCodes = false;
if (empty($this->id) && $this->method !== 'backupcodes') {
// Do I have any backup records?
$hasBackupCodes = array_reduce(
$records,
function (bool $carry, $record) {
return $carry || $record->method === 'backupcodes';
},
false
);
$mustCreateBackupCodes = !$hasBackupCodes;
// If the only other entry is the backup records one I need to make this the default method
if ($hasBackupCodes && \count($records) === 1) {
$this->default = 1;
}
}
// Store the record
try {
$result = parent::store($updateNulls);
} catch (\Throwable $e) {
$this->setError($e->getMessage());
$result = false;
}
// Decrypt the options (they must be decrypted in memory)
$this->decryptOptions();
if ($result) {
// If this record is the default unset the default flag from all other records
$this->switchDefaultRecord();
// Do I need to generate backup codes?
if ($mustCreateBackupCodes) {
$this->generateBackupCodes();
}
}
return $result;
}
/**
* Method to load a row from the database by primary key and bind the fields to the Table instance properties.
*
* @param mixed $keys An optional primary key value to load the row by, or an array of fields to match.
* If not set the instance property value is used.
* @param boolean $reset True to reset the default values before loading the new row.
*
* @return boolean True if successful. False if row not found.
*
* @since 4.2.0
* @throws \InvalidArgumentException
* @throws \RuntimeException
* @throws \UnexpectedValueException
*/
public function load($keys = null, $reset = true)
{
$result = parent::load($keys, $reset);
if ($result) {
$this->decryptOptions();
}
return $result;
}
/**
* Method to delete a row from the database table by primary key value.
*
* @param mixed $pk An optional primary key value to delete. If not set the instance property value is used.
*
* @return boolean True on success.
*
* @since 4.2.0
* @throws \UnexpectedValueException
*/
public function delete($pk = null)
{
$record = $this;
if ($pk != $this->id) {
$record = clone $this;
$record->reset();
$result = $record->load($pk);
if (!$result) {
// If the record does not exist I will stomp my feet and deny your request
throw new \RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403);
}
}
$user = $this->getCurrentUser();
// The user must be a registered user, not a guest
if ($user->guest) {
throw new \RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403);
}
// Save flags used onAfterDelete
$this->deleteFlags[$record->id] = [
'default' => $record->default,
'numRecords' => $this->getNumRecords($record->user_id),
'user_id' => $record->user_id,
'method' => $record->method,
];
if (\is_null($pk)) {
$pk = [$this->_tbl_key => $this->id];
} elseif (!\is_array($pk)) {
$pk = [$this->_tbl_key => $pk];
}
$isDeleted = parent::delete($pk);
if ($isDeleted) {
$this->afterDelete($pk);
}
return $isDeleted;
}
/**
* Decrypt the possibly encrypted options
*
* @return void
* @since 4.2.0
*/
private function decryptOptions(): void
{
// Try with modern decryption
$decrypted = @json_decode($this->encryptService->decrypt($this->options ?? ''), true);
if (\is_string($decrypted)) {
$decrypted = @json_decode($decrypted, true);
}
// Fall back to legacy decryption
if (!\is_array($decrypted)) {
$decrypted = @json_decode($this->encryptService->decrypt($this->options ?? '', true), true);
if (\is_string($decrypted)) {
$decrypted = @json_decode($decrypted, true);
}
}
$this->options = $decrypted ?: [];
}
/**
* If this record is set to be the default, unset the default flag from the other records for the same user.
*
* @return void
* @since 4.2.0
*/
private function switchDefaultRecord(): void
{
if (!$this->default) {
return;
}
/**
* This record is marked as default, therefore we need to unset the default flag from all other records for this
* user.
*/
$db = $this->getDbo();
$query = $db->getQuery(true)
->update($db->quoteName('#__user_mfa'))
->set($db->quoteName('default') . ' = 0')
->where($db->quoteName('user_id') . ' = :user_id')
->where($db->quoteName('id') . ' != :id')
->bind(':user_id', $this->user_id, ParameterType::INTEGER)
->bind(':id', $this->id, ParameterType::INTEGER);
$db->setQuery($query)->execute();
}
/**
* Regenerate backup code is the flag is set.
*
* @return void
* @throws \Exception
* @since 4.2.0
*/
private function generateBackupCodes(): void
{
/** @var MVCFactoryInterface $factory */
$factory = Factory::getApplication()->bootComponent('com_users')->getMVCFactory();
/** @var BackupcodesModel $backupCodes */
$backupCodes = $factory->createModel('Backupcodes', 'Administrator');
$user = $this->getUserFactory()->loadUserById($this->user_id);
$backupCodes->regenerateBackupCodes($user);
}
/**
* Runs after successfully deleting a record
*
* @param int|array $pk The promary key of the deleted record
*
* @return void
* @since 4.2.0
*/
private function afterDelete($pk): void
{
if (\is_array($pk)) {
$pk = $pk[$this->_tbl_key] ?? array_shift($pk);
}
if (!isset($this->deleteFlags[$pk])) {
return;
}
if (($this->deleteFlags[$pk]['numRecords'] <= 2) && ($this->deleteFlags[$pk]['method'] != 'backupcodes')) {
/**
* This was the second to last MFA record in the database (the last one is the `backupcodes`). Therefore, we
* need to delete the remaining entry and go away. We don't trigger this if the Method we are deleting was
* the `backupcodes` because we might just be regenerating the backup codes.
*/
$db = $this->getDbo();
$query = $db->getQuery(true)
->delete($db->quoteName('#__user_mfa'))
->where($db->quoteName('user_id') . ' = :user_id')
->bind(':user_id', $this->deleteFlags[$pk]['user_id'], ParameterType::INTEGER);
$db->setQuery($query)->execute();
unset($this->deleteFlags[$pk]);
return;
}
// This was the default record. Promote the next available record to default.
if ($this->deleteFlags[$pk]['default']) {
$db = $this->getDbo();
$query = $db->getQuery(true)
->select($db->quoteName('id'))
->from($db->quoteName('#__user_mfa'))
->where($db->quoteName('user_id') . ' = :user_id')
->where($db->quoteName('method') . ' != ' . $db->quote('backupcodes'))
->bind(':user_id', $this->deleteFlags[$pk]['user_id'], ParameterType::INTEGER);
$ids = $db->setQuery($query)->loadColumn();
if (empty($ids)) {
return;
}
$id = array_shift($ids);
$query = $db->getQuery(true)
->update($db->quoteName('#__user_mfa'))
->set($db->quoteName('default') . ' = 1')
->where($db->quoteName('id') . ' = :id')
->bind(':id', $id, ParameterType::INTEGER);
$db->setQuery($query)->execute();
}
}
/**
* Get the number of MFA records for a give user ID
*
* @param int $userId The user ID to check
*
* @return integer
*
* @since 4.2.0
*/
private function getNumRecords(int $userId): int
{
$db = $this->getDbo();
$query = $db->getQuery(true)
->select('COUNT(*)')
->from($db->quoteName('#__user_mfa'))
->where($db->quoteName('user_id') . ' = :user_id')
->bind(':user_id', $userId, ParameterType::INTEGER);
$numOldRecords = $db->setQuery($query)->loadResult();
return (int) $numOldRecords;
}
}

View File

@ -0,0 +1,131 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_users
*
* @copyright (C) 2011 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Component\Users\Administrator\Table;
use Joomla\CMS\Factory;
use Joomla\CMS\Table\Table;
use Joomla\CMS\User\CurrentUserInterface;
use Joomla\CMS\User\CurrentUserTrait;
use Joomla\CMS\Versioning\VersionableTableInterface;
use Joomla\Database\DatabaseDriver;
use Joomla\Event\DispatcherInterface;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* User notes table class
*
* @since 2.5
*/
class NoteTable extends Table implements VersionableTableInterface, CurrentUserInterface
{
use CurrentUserTrait;
/**
* Indicates that columns fully support the NULL value in the database
*
* @var boolean
* @since 4.0.0
*/
protected $_supportNullValue = true;
/**
* Constructor
*
* @param DatabaseDriver $db Database connector object
* @param ?DispatcherInterface $dispatcher Event dispatcher for this table
*
* @since 2.5
*/
public function __construct(DatabaseDriver $db, DispatcherInterface $dispatcher = null)
{
$this->typeAlias = 'com_users.note';
parent::__construct('#__user_notes', 'id', $db, $dispatcher);
$this->setColumnAlias('published', 'state');
}
/**
* Overloaded store method for the notes table.
*
* @param boolean $updateNulls Toggle whether null values should be updated.
*
* @return boolean True on success, false on failure.
*
* @since 2.5
*/
public function store($updateNulls = true)
{
$date = Factory::getDate()->toSql();
$userId = $this->getCurrentUser()->get('id');
if (!((int) $this->review_time)) {
$this->review_time = null;
}
if ($this->id) {
// Existing item
$this->modified_time = $date;
$this->modified_user_id = $userId;
} else {
// New record.
$this->created_time = $date;
$this->created_user_id = $userId;
$this->modified_time = $date;
$this->modified_user_id = $userId;
}
// Attempt to store the data.
return parent::store($updateNulls);
}
/**
* Method to perform sanity checks on the Table instance properties to ensure they are safe to store in the database.
*
* @return boolean True if the instance is sane and able to be stored in the database.
*
* @since 4.0.0
*/
public function check()
{
try {
parent::check();
} catch (\Exception $e) {
$this->setError($e->getMessage());
return false;
}
if (empty($this->modified_time)) {
$this->modified_time = $this->created_time;
}
if (empty($this->modified_user_id)) {
$this->modified_user_id = $this->created_user_id;
}
return true;
}
/**
* Get the type alias for the history table
*
* @return string The alias as described above
*
* @since 4.0.0
*/
public function getTypeAlias()
{
return $this->typeAlias;
}
}

View File

@ -0,0 +1,219 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_users
*
* @copyright (C) 2022 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Component\Users\Administrator\View\Captive;
use Joomla\CMS\Event\MultiFactor\BeforeDisplayMethods;
use Joomla\CMS\Event\MultiFactor\NotifyActionLog;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\CMS\Toolbar\Button\BasicButton;
use Joomla\CMS\Toolbar\Toolbar;
use Joomla\CMS\Toolbar\ToolbarHelper;
use Joomla\Component\Users\Administrator\Helper\Mfa as MfaHelper;
use Joomla\Component\Users\Administrator\Model\BackupcodesModel;
use Joomla\Component\Users\Administrator\Model\CaptiveModel;
use Joomla\Component\Users\Administrator\View\SiteTemplateTrait;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* View for Multi-factor Authentication captive page
*
* @since 4.2.0
*/
class HtmlView extends BaseHtmlView
{
use SiteTemplateTrait;
/**
* The MFA Method records for the current user which correspond to enabled plugins
*
* @var array
* @since 4.2.0
*/
public $records = [];
/**
* The currently selected MFA Method record against which we'll be authenticating
*
* @var null|\stdClass
* @since 4.2.0
*/
public $record = null;
/**
* The Captive MFA page's rendering options
*
* @var array|null
* @since 4.2.0
*/
public $renderOptions = null;
/**
* The title to display at the top of the page
*
* @var string
* @since 4.2.0
*/
public $title = '';
/**
* Is this an administrator page?
*
* @var boolean
* @since 4.2.0
*/
public $isAdmin = false;
/**
* Does the currently selected Method allow authenticating against all of its records?
*
* @var boolean
* @since 4.2.0
*/
public $allowEntryBatching = false;
/**
* All enabled MFA Methods (plugins)
*
* @var array
* @since 4.2.0
*/
public $mfaMethods;
/**
* Execute and display a template script.
*
* @param string $tpl The name of the template file to parse; automatically searches through the template paths.
*
* @return void A string if successful, otherwise an Error object.
*
* @throws \Exception
* @since 4.2.0
*/
public function display($tpl = null)
{
$this->setSiteTemplateStyle();
$app = Factory::getApplication();
$user = $this->getCurrentUser();
PluginHelper::importPlugin('multifactorauth');
$event = new BeforeDisplayMethods($user);
$app->getDispatcher()->dispatch($event->getName(), $event);
/** @var CaptiveModel $model */
$model = $this->getModel();
// Load data from the model
$this->isAdmin = $app->isClient('administrator');
$this->records = $this->get('records');
$this->record = $this->get('record');
$this->mfaMethods = MfaHelper::getMfaMethods();
if (!empty($this->records)) {
/** @var BackupcodesModel $codesModel */
$codesModel = $this->getModel('Backupcodes');
$backupCodesRecord = $codesModel->getBackupCodesRecord();
if (!\is_null($backupCodesRecord)) {
$backupCodesRecord->title = Text::_('COM_USERS_USER_BACKUPCODES');
$this->records[] = $backupCodesRecord;
}
}
// If we only have one record there's no point asking the user to select a MFA Method
if (empty($this->record) && !empty($this->records)) {
// Default to the first record
$this->record = reset($this->records);
// If we have multiple records try to make this record the default
if (\count($this->records) > 1) {
foreach ($this->records as $record) {
if ($record->default) {
$this->record = $record;
break;
}
}
}
}
// Set the correct layout based on the availability of a MFA record
$this->setLayout('default');
// If we have no record selected or explicitly asked to run the 'select' task use the correct layout
if (\is_null($this->record) || ($model->getState('task') == 'select')) {
$this->setLayout('select');
}
switch ($this->getLayout()) {
case 'select':
$this->allowEntryBatching = 1;
$event = new NotifyActionLog('onComUsersCaptiveShowSelect', []);
Factory::getApplication()->getDispatcher()->dispatch($event->getName(), $event);
break;
case 'default':
default:
$this->renderOptions = $model->loadCaptiveRenderOptions($this->record);
$this->allowEntryBatching = $this->renderOptions['allowEntryBatching'] ?? 0;
$event = new NotifyActionLog(
'onComUsersCaptiveShowCaptive',
[
$this->escape($this->record->title),
]
);
Factory::getApplication()->getDispatcher()->dispatch($event->getName(), $event);
break;
}
// Which title should I use for the page?
$this->title = $this->get('PageTitle');
// Back-end: always show a title in the 'title' module position, not in the page body
if ($this->isAdmin) {
ToolbarHelper::title(Text::_('COM_USERS_USER_MULTIFACTOR_AUTH'), 'users user-lock');
$this->title = '';
}
if ($this->isAdmin && $this->getLayout() === 'default') {
$bar = Toolbar::getInstance();
$button = (new BasicButton('user-mfa-submit'))
->text($this->renderOptions['submit_text'])
->icon($this->renderOptions['submit_icon']);
$bar->appendButton($button);
$button = (new BasicButton('user-mfa-logout'))
->text('COM_USERS_MFA_LOGOUT')
->buttonClass('btn btn-danger')
->icon('icon icon-lock');
$bar->appendButton($button);
if (\count($this->records) > 1) {
$arrow = Factory::getApplication()->getLanguage()->isRtl() ? 'arrow-right' : 'arrow-left';
$button = (new BasicButton('user-mfa-choose-another'))
->text('COM_USERS_MFA_USE_DIFFERENT_METHOD')
->icon('icon-' . $arrow);
$bar->appendButton($button);
}
}
// Display the view
parent::display($tpl);
}
}

View File

@ -0,0 +1,139 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_users
*
* @copyright (C) 2010 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Component\Users\Administrator\View\Debuggroup;
use Joomla\CMS\Access\Exception\NotAllowed;
use Joomla\CMS\Helper\ContentHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\View\GenericDataException;
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
use Joomla\CMS\Toolbar\Toolbar;
use Joomla\CMS\Toolbar\ToolbarHelper;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* View class for a list of User Group ACL permissions.
*
* @since 1.6
*/
class HtmlView extends BaseHtmlView
{
/**
* List of component actions
*
* @var array
*/
protected $actions;
/**
* The item data.
*
* @var object
* @since 1.6
*/
protected $items;
/**
* The pagination object.
*
* @var \Joomla\CMS\Pagination\Pagination
* @since 1.6
*/
protected $pagination;
/**
* The model state.
*
* @var \Joomla\Registry\Registry
* @since 1.6
*/
protected $state;
/**
* The id and title for the user group.
*
* @var \stdClass
* @since 4.0.0
*/
protected $group;
/**
* Form object for search filters
*
* @var \Joomla\CMS\Form\Form
*/
public $filterForm;
/**
* The active search filters
*
* @var array
*/
public $activeFilters;
/**
* Display the view
*
* @param string $tpl The name of the template file to parse; automatically searches through the template paths.
*
* @return void
*/
public function display($tpl = null)
{
// Access check.
if (!$this->getCurrentUser()->authorise('core.manage', 'com_users')) {
throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403);
}
$this->actions = $this->get('DebugActions');
$this->items = $this->get('Items');
$this->pagination = $this->get('Pagination');
$this->state = $this->get('State');
$this->group = $this->get('Group');
$this->filterForm = $this->get('FilterForm');
$this->activeFilters = $this->get('ActiveFilters');
// Check for errors.
if (\count($errors = $this->get('Errors'))) {
throw new GenericDataException(implode("\n", $errors), 500);
}
$this->addToolbar();
parent::display($tpl);
}
/**
* Add the page title and toolbar.
*
* @return void
*
* @since 1.6
*/
protected function addToolbar()
{
$canDo = ContentHelper::getActions('com_users');
$toolbar = Toolbar::getInstance();
ToolbarHelper::title(Text::sprintf('COM_USERS_VIEW_DEBUG_GROUP_TITLE', $this->group->id, $this->escape($this->group->title)), 'users groups');
$toolbar->cancel('group.cancel');
if ($canDo->get('core.admin') || $canDo->get('core.options')) {
$toolbar->preferences('com_users');
$toolbar->divider();
}
$toolbar->help('Permissions_for_Group');
}
}

View File

@ -0,0 +1,139 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_users
*
* @copyright (C) 2010 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Component\Users\Administrator\View\Debuguser;
use Joomla\CMS\Access\Exception\NotAllowed;
use Joomla\CMS\Helper\ContentHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\View\GenericDataException;
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
use Joomla\CMS\Toolbar\Toolbar;
use Joomla\CMS\Toolbar\ToolbarHelper;
use Joomla\CMS\User\User;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* View class for a list of User ACL permissions.
*
* @since 1.6
*/
class HtmlView extends BaseHtmlView
{
/**
* List of component actions
*
* @var array
*/
protected $actions;
/**
* The item data.
*
* @var object
* @since 1.6
*/
protected $items;
/**
* The pagination object.
*
* @var \Joomla\CMS\Pagination\Pagination
* @since 1.6
*/
protected $pagination;
/**
* The model state.
*
* @var \Joomla\Registry\Registry
* @since 1.6
*/
protected $state;
/**
* The user object of the user being debugged.
*
* @var User
*/
protected $user;
/**
* Form object for search filters
*
* @var \Joomla\CMS\Form\Form
*/
public $filterForm;
/**
* The active search filters
*
* @var array
*/
public $activeFilters;
/**
* Display the view
*
* @param string $tpl The name of the template file to parse; automatically searches through the template paths.
*
* @return void
*/
public function display($tpl = null)
{
// Access check.
if (!$this->getCurrentUser()->authorise('core.manage', 'com_users')) {
throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403);
}
$this->actions = $this->get('DebugActions');
$this->items = $this->get('Items');
$this->pagination = $this->get('Pagination');
$this->state = $this->get('State');
$this->user = $this->get('User');
$this->filterForm = $this->get('FilterForm');
$this->activeFilters = $this->get('ActiveFilters');
// Check for errors.
if (\count($errors = $this->get('Errors'))) {
throw new GenericDataException(implode("\n", $errors), 500);
}
$this->addToolbar();
parent::display($tpl);
}
/**
* Add the page title and toolbar.
*
* @return void
*
* @since 1.6
*/
protected function addToolbar()
{
$canDo = ContentHelper::getActions('com_users');
$toolbar = Toolbar::getInstance();
ToolbarHelper::title(Text::sprintf('COM_USERS_VIEW_DEBUG_USER_TITLE', $this->user->id, $this->escape($this->user->name)), 'users user');
$toolbar->cancel('user.cancel');
if ($canDo->get('core.admin') || $canDo->get('core.options')) {
$toolbar->preferences('com_users');
$toolbar->divider();
}
$toolbar->help('Permissions_for_User');
}
}

View File

@ -0,0 +1,126 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_users
*
* @copyright (C) 2009 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Component\Users\Administrator\View\Group;
use Joomla\CMS\Factory;
use Joomla\CMS\Helper\ContentHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\View\GenericDataException;
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
use Joomla\CMS\Toolbar\Toolbar;
use Joomla\CMS\Toolbar\ToolbarHelper;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* View to edit a user group.
*
* @since 1.6
*/
class HtmlView extends BaseHtmlView
{
/**
* The Form object
*
* @var \Joomla\CMS\Form\Form
*/
protected $form;
/**
* The item data.
*
* @var object
* @since 1.6
*/
protected $item;
/**
* The model state.
*
* @var \Joomla\Registry\Registry
* @since 1.6
*/
protected $state;
/**
* Display the view
*
* @param string $tpl The name of the template file to parse; automatically searches through the template paths.
*
* @return void
*/
public function display($tpl = null)
{
$this->state = $this->get('State');
$this->item = $this->get('Item');
$this->form = $this->get('Form');
// Check for errors.
if (\count($errors = $this->get('Errors'))) {
throw new GenericDataException(implode("\n", $errors), 500);
}
$this->addToolbar();
parent::display($tpl);
}
/**
* Add the page title and toolbar.
*
* @return void
*
* @since 1.6
* @throws \Exception
*/
protected function addToolbar()
{
Factory::getApplication()->getInput()->set('hidemainmenu', true);
$isNew = ($this->item->id == 0);
$canDo = ContentHelper::getActions('com_users');
$toolbar = Toolbar::getInstance();
ToolbarHelper::title(Text::_($isNew ? 'COM_USERS_VIEW_NEW_GROUP_TITLE' : 'COM_USERS_VIEW_EDIT_GROUP_TITLE'), 'users-cog groups-add');
if ($canDo->get('core.edit') || $canDo->get('core.create')) {
$toolbar->apply('group.apply');
}
$saveGroup = $toolbar->dropdownButton('save-group');
$saveGroup->configure(
function (Toolbar $childBar) use ($canDo, $isNew) {
if ($canDo->get('core.edit') || $canDo->get('core.create')) {
$childBar->save('group.save');
}
if ($canDo->get('core.create')) {
$childBar->save2new('group.save2new');
}
// If an existing item, can save to a copy.
if (!$isNew && $canDo->get('core.create')) {
$childBar->save2copy('group.save2copy');
}
}
);
if (empty($this->item->id)) {
$toolbar->cancel('group.cancel', 'JTOOLBAR_CANCEL');
} else {
$toolbar->cancel('group.cancel');
}
$toolbar->divider();
$toolbar->help('Users:_New_or_Edit_Group');
}
}

View File

@ -0,0 +1,127 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_users
*
* @copyright (C) 2009 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Component\Users\Administrator\View\Groups;
use Joomla\CMS\Helper\ContentHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\View\GenericDataException;
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
use Joomla\CMS\Toolbar\Toolbar;
use Joomla\CMS\Toolbar\ToolbarHelper;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* View class for a list of user groups.
*
* @since 1.6
*/
class HtmlView extends BaseHtmlView
{
/**
* The item data.
*
* @var object
* @since 1.6
*/
protected $items;
/**
* The pagination object.
*
* @var \Joomla\CMS\Pagination\Pagination
* @since 1.6
*/
protected $pagination;
/**
* The model state.
*
* @var \Joomla\Registry\Registry
* @since 1.6
*/
protected $state;
/**
* Form object for search filters
*
* @var \Joomla\CMS\Form\Form
*
* @since 4.0.0
*/
public $filterForm;
/**
* The active search filters
*
* @var array
* @since 4.0.0
*/
public $activeFilters;
/**
* Display the view
*
* @param string $tpl The name of the template file to parse; automatically searches through the template paths.
*
* @return void
*/
public function display($tpl = null)
{
$this->items = $this->get('Items');
$this->pagination = $this->get('Pagination');
$this->state = $this->get('State');
$this->filterForm = $this->get('FilterForm');
$this->activeFilters = $this->get('ActiveFilters');
// Check for errors.
if (\count($errors = $this->get('Errors'))) {
throw new GenericDataException(implode("\n", $errors), 500);
}
$this->addToolbar();
parent::display($tpl);
}
/**
* Add the page title and toolbar.
*
* @return void
*
* @since 1.6
*/
protected function addToolbar()
{
$canDo = ContentHelper::getActions('com_users');
$toolbar = Toolbar::getInstance();
ToolbarHelper::title(Text::_('COM_USERS_VIEW_GROUPS_TITLE'), 'users-cog groups');
if ($canDo->get('core.create')) {
$toolbar->addNew('group.add');
}
if ($canDo->get('core.delete')) {
$toolbar->delete('groups.delete')
->message('JGLOBAL_CONFIRM_DELETE');
$toolbar->divider();
}
if ($canDo->get('core.admin') || $canDo->get('core.options')) {
$toolbar->preferences('com_users');
$toolbar->divider();
}
$toolbar->help('Users:_Groups');
}
}

View File

@ -0,0 +1,126 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_users
*
* @copyright (C) 2009 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Component\Users\Administrator\View\Level;
use Joomla\CMS\Factory;
use Joomla\CMS\Helper\ContentHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\View\GenericDataException;
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
use Joomla\CMS\Toolbar\Toolbar;
use Joomla\CMS\Toolbar\ToolbarHelper;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* View to edit a user view level.
*
* @since 1.6
*/
class HtmlView extends BaseHtmlView
{
/**
* The Form object
*
* @var \Joomla\CMS\Form\Form
*/
protected $form;
/**
* The item data.
*
* @var object
* @since 1.6
*/
protected $item;
/**
* The model state.
*
* @var \Joomla\Registry\Registry
* @since 1.6
*/
protected $state;
/**
* Display the view
*
* @param string $tpl The name of the template file to parse; automatically searches through the template paths.
*
* @return void
*/
public function display($tpl = null)
{
$this->form = $this->get('Form');
$this->item = $this->get('Item');
$this->state = $this->get('State');
// Check for errors.
if (\count($errors = $this->get('Errors'))) {
throw new GenericDataException(implode("\n", $errors), 500);
}
$this->addToolbar();
parent::display($tpl);
}
/**
* Add the page title and toolbar.
*
* @return void
*
* @since 1.6
* @throws \Exception
*/
protected function addToolbar()
{
Factory::getApplication()->getInput()->set('hidemainmenu', true);
$isNew = ($this->item->id == 0);
$canDo = ContentHelper::getActions('com_users');
$toolbar = Toolbar::getInstance();
ToolbarHelper::title(Text::_($isNew ? 'COM_USERS_VIEW_NEW_LEVEL_TITLE' : 'COM_USERS_VIEW_EDIT_LEVEL_TITLE'), 'user-lock levels-add');
if ($canDo->get('core.edit') || $canDo->get('core.create')) {
$toolbar->apply('level.apply');
}
$saveGroup = $toolbar->dropdownButton('save-group');
$saveGroup->configure(
function (Toolbar $childBar) use ($canDo, $isNew) {
if ($canDo->get('core.edit') || $canDo->get('core.create')) {
$childBar->save('level.save');
}
if ($canDo->get('core.create')) {
$childBar->save2new('level.save2new');
}
// If an existing item, can save to a copy.
if (!$isNew && $canDo->get('core.create')) {
$childBar->save2copy('level.save2copy');
}
}
);
if (empty($this->item->id)) {
$toolbar->cancel('level.cancel', 'JTOOLBAR_CANCEL');
} else {
$toolbar->cancel('level.cancel');
}
$toolbar->divider();
$toolbar->help('Users:_Edit_Viewing_Access_Level');
}
}

View File

@ -0,0 +1,127 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_users
*
* @copyright (C) 2009 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Component\Users\Administrator\View\Levels;
use Joomla\CMS\Helper\ContentHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\View\GenericDataException;
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
use Joomla\CMS\Toolbar\Toolbar;
use Joomla\CMS\Toolbar\ToolbarHelper;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* View class for a list of view levels.
*
* @since 1.6
*/
class HtmlView extends BaseHtmlView
{
/**
* The item data.
*
* @var object
* @since 1.6
*/
protected $items;
/**
* The pagination object.
*
* @var \Joomla\CMS\Pagination\Pagination
* @since 1.6
*/
protected $pagination;
/**
* The model state.
*
* @var \Joomla\Registry\Registry
* @since 1.6
*/
protected $state;
/**
* Form object for search filters
*
* @var \Joomla\CMS\Form\Form
*
* @since 4.0.0
*/
public $filterForm;
/**
* The active search filters
*
* @var array
* @since 4.0.0
*/
public $activeFilters;
/**
* Display the view
*
* @param string $tpl The name of the template file to parse; automatically searches through the template paths.
*
* @return void
*/
public function display($tpl = null)
{
$this->items = $this->get('Items');
$this->pagination = $this->get('Pagination');
$this->state = $this->get('State');
$this->filterForm = $this->get('FilterForm');
$this->activeFilters = $this->get('ActiveFilters');
// Check for errors.
if (\count($errors = $this->get('Errors'))) {
throw new GenericDataException(implode("\n", $errors), 500);
}
$this->addToolbar();
parent::display($tpl);
}
/**
* Add the page title and toolbar.
*
* @return void
*
* @since 1.6
*/
protected function addToolbar()
{
$canDo = ContentHelper::getActions('com_users');
$toolbar = Toolbar::getInstance();
ToolbarHelper::title(Text::_('COM_USERS_VIEW_LEVELS_TITLE'), 'user-lock levels');
if ($canDo->get('core.create')) {
$toolbar->addNew('level.add');
}
if ($canDo->get('core.delete')) {
$toolbar->delete('level.delete')
->message('JGLOBAL_CONFIRM_DELETE');
$toolbar->divider();
}
if ($canDo->get('core.admin') || $canDo->get('core.options')) {
$toolbar->preferences('com_users');
$toolbar->divider();
}
$toolbar->help('Users:_Viewing_Access_Levels');
}
}

View File

@ -0,0 +1,85 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_users
*
* @copyright (C) 2009 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Component\Users\Administrator\View\Mail;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Toolbar\Toolbar;
use Joomla\CMS\Toolbar\ToolbarHelper;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Users mail view.
*
* @since 1.6
*/
class HtmlView extends BaseHtmlView
{
/**
* The Form object
*
* @var \Joomla\CMS\Form\Form
*/
protected $form;
/**
* Display the view
*
* @param string $tpl The name of the template file to parse; automatically searches through the template paths.
*
* @return void
*
* @throws \Exception
*/
public function display($tpl = null)
{
// Redirect to admin index if mass mailer disabled in conf
if (Factory::getApplication()->get('massmailoff', 0) == 1) {
Factory::getApplication()->redirect(Route::_('index.php', false));
}
// Get data from the model
$this->form = $this->get('Form');
$this->addToolbar();
parent::display($tpl);
}
/**
* Add the page title and toolbar.
*
* @return void
*
* @since 1.6
* @throws \Exception
*/
protected function addToolbar()
{
Factory::getApplication()->getInput()->set('hidemainmenu', true);
ToolbarHelper::title(Text::_('COM_USERS_MASS_MAIL'), 'users massmail');
$toolbar = Toolbar::getInstance();
$toolbar->standardButton('COM_USERS_TOOLBAR_MAIL_SEND_MAIL', 'COM_USERS_TOOLBAR_MAIL_SEND_MAIL', 'mail.send')
->icon('icon-envelope')
->formValidation(true);
$toolbar->cancel('mail.cancel', 'JTOOLBAR_CANCEL');
$toolbar->divider();
$toolbar->preferences('com_users');
$toolbar->divider();
$toolbar->help('Mass_Mail_Users');
}
}

View File

@ -0,0 +1,220 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_users
*
* @copyright (C) 2022 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Component\Users\Administrator\View\Method;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Toolbar\Button\BasicButton;
use Joomla\CMS\Toolbar\Button\LinkButton;
use Joomla\CMS\Toolbar\Toolbar;
use Joomla\CMS\Toolbar\ToolbarHelper;
use Joomla\CMS\Uri\Uri;
use Joomla\CMS\User\User;
use Joomla\Component\Users\Administrator\Model\MethodModel;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* View for Multi-factor Authentication method add/edit page
*
* @since 4.2.0
*/
class HtmlView extends BaseHtmlView
{
/**
* Is this an administrator page?
*
* @var boolean
* @since 4.2.0
*/
public $isAdmin = false;
/**
* The editor page render options
*
* @var array
* @since 4.2.0
*/
public $renderOptions = [];
/**
* The MFA Method record being edited
*
* @var object
* @since 4.2.0
*/
public $record = null;
/**
* The title text for this page
*
* @var string
* @since 4.2.0
*/
public $title = '';
/**
* The return URL to use for all links and forms
*
* @var string
* @since 4.2.0
*/
public $returnURL = null;
/**
* The user object used to display this page
*
* @var User
* @since 4.2.0
*/
public $user = null;
/**
* The backup codes for the current user. Only applies when the backup codes record is being "edited"
*
* @var array
* @since 4.2.0
*/
public $backupCodes = [];
/**
* Am I editing an existing Method? If it's false then I'm adding a new Method.
*
* @var boolean
* @since 4.2.0
*/
public $isEditExisting = false;
/**
* Execute and display a template script.
*
* @param string $tpl The name of the template file to parse; automatically searches through the template paths.
*
* @return void
*
* @throws \Exception
* @see \JViewLegacy::loadTemplate()
* @since 4.2.0
*/
public function display($tpl = null): void
{
$app = Factory::getApplication();
if (empty($this->user)) {
$this->user = $this->getCurrentUser();
}
/** @var MethodModel $model */
$model = $this->getModel();
$this->setLayout('edit');
$this->renderOptions = $model->getRenderOptions($this->user);
$this->record = $model->getRecord($this->user);
$this->title = $model->getPageTitle();
$this->isAdmin = $app->isClient('administrator');
$toolbar = Toolbar::getInstance();
// Backup codes are a special case, rendered with a special layout
if ($this->record->method == 'backupcodes') {
$this->setLayout('backupcodes');
$backupCodes = $this->record->options;
if (!\is_array($backupCodes)) {
$backupCodes = [];
}
$backupCodes = array_filter(
$backupCodes,
function ($x) {
return !empty($x);
}
);
if (\count($backupCodes) % 2 != 0) {
$backupCodes[] = '';
}
/**
* The call to array_merge resets the array indices. This is necessary since array_filter kept the indices,
* meaning our elements are completely out of order.
*/
$this->backupCodes = array_merge($backupCodes);
}
// Set up the isEditExisting property.
$this->isEditExisting = !empty($this->record->id);
// Back-end: always show a title in the 'title' module position, not in the page body
if ($this->isAdmin) {
ToolbarHelper::title($this->title, 'users user-lock');
$helpUrl = $this->renderOptions['help_url'];
if (!empty($helpUrl)) {
$toolbar->help('', false, $helpUrl);
}
$this->title = '';
}
$returnUrl = empty($this->returnURL) ? '' : base64_decode($this->returnURL);
$returnUrl = ($returnUrl && Uri::isInternal($returnUrl))
? $returnUrl
: Route::_('index.php?option=com_users&task=methods.display&user_id=' . $this->user->id);
if ($this->isAdmin && $this->getLayout() === 'edit') {
$button = (new BasicButton('user-mfa-edit-save'))
->text($this->renderOptions['submit_text'])
->icon($this->renderOptions['submit_icon'])
->onclick('document.getElementById(\'user-mfa-edit-save\').click()');
if ($this->renderOptions['show_submit'] || $this->isEditExisting) {
$toolbar->appendButton($button);
}
$button = (new LinkButton('user-mfa-edit-cancel'))
->url($returnUrl)
->text('JCANCEL')
->buttonClass('btn btn-danger')
->icon('icon-cancel-2');
$toolbar->appendButton($button);
} elseif ($this->isAdmin && $this->getLayout() === 'backupcodes') {
$arrow = Factory::getApplication()->getLanguage()->isRtl() ? 'arrow-right' : 'arrow-left';
$button = (new LinkButton('user-mfa-edit-cancel'))
->url($returnUrl)
->text('JTOOLBAR_BACK')
->icon('icon-' . $arrow);
$toolbar->appendButton($button);
$button = (new LinkButton('user-mfa-edit-cancel'))
->url(
Route::_(
sprintf(
"index.php?option=com_users&task=method.regenerateBackupCodes&user_id=%s&%s=1&returnurl=%s",
$this->user->id,
Factory::getApplication()->getFormToken(),
base64_encode($returnUrl)
)
)
)
->text('COM_USERS_MFA_BACKUPCODES_RESET')
->buttonClass('btn btn-danger')
->icon('icon-refresh');
$toolbar->appendButton($button);
}
// Display the view
parent::display($tpl);
}
}

View File

@ -0,0 +1,196 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_users
*
* @copyright (C) 2022 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Component\Users\Administrator\View\Methods;
use Joomla\CMS\Event\MultiFactor\NotifyActionLog;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
use Joomla\CMS\Toolbar\Toolbar;
use Joomla\CMS\Toolbar\ToolbarHelper;
use Joomla\CMS\User\User;
use Joomla\Component\Users\Administrator\DataShape\MethodDescriptor;
use Joomla\Component\Users\Administrator\Model\BackupcodesModel;
use Joomla\Component\Users\Administrator\Model\MethodsModel;
use Joomla\Component\Users\Administrator\View\SiteTemplateTrait;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* View for Multi-factor Authentication methods list page
*
* @since 4.2.0
*/
class HtmlView extends BaseHtmlView
{
use SiteTemplateTrait;
/**
* Is this an administrator page?
*
* @var boolean
* @since 4.2.0
*/
public $isAdmin = false;
/**
* The MFA Methods available for this user
*
* @var array
* @since 4.2.0
*/
public $methods = [];
/**
* The return URL to use for all links and forms
*
* @var string
* @since 4.2.0
*/
public $returnURL = null;
/**
* Are there any active MFA Methods at all?
*
* @var boolean
* @since 4.2.0
*/
public $mfaActive = false;
/**
* Which Method has the default record?
*
* @var string
* @since 4.2.0
*/
public $defaultMethod = '';
/**
* The user object used to display this page
*
* @var User
* @since 4.2.0
*/
public $user = null;
/**
* Is this page part of the mandatory Multi-factor Authentication setup?
*
* @var boolean
* @since 4.2.0
*/
public $isMandatoryMFASetup = false;
/**
* Execute and display a template script.
*
* @param string $tpl The name of the template file to parse; automatically searches through the template paths.
*
* @return void
*
* @throws \Exception
* @see \JViewLegacy::loadTemplate()
* @since 4.2.0
*/
public function display($tpl = null): void
{
$this->setSiteTemplateStyle();
$app = Factory::getApplication();
if (empty($this->user)) {
$this->user = $this->getCurrentUser();
}
/** @var MethodsModel $model */
$model = $this->getModel();
if ($this->getLayout() !== 'firsttime') {
$this->setLayout('default');
}
$this->methods = $model->getMethods($this->user);
$this->isAdmin = $app->isClient('administrator');
$activeRecords = 0;
foreach ($this->methods as $methodName => $method) {
$methodActiveRecords = \count($method['active']);
if (!$methodActiveRecords) {
continue;
}
$activeRecords += $methodActiveRecords;
$this->mfaActive = true;
foreach ($method['active'] as $record) {
if ($record->default) {
$this->defaultMethod = $methodName;
break;
}
}
}
// If there are no backup codes yet we should create new ones
/** @var BackupcodesModel $model */
$model = $this->getModel('backupcodes');
$backupCodes = $model->getBackupCodes($this->user);
if ($activeRecords && empty($backupCodes)) {
$model->regenerateBackupCodes($this->user);
}
$backupCodesRecord = $model->getBackupCodesRecord($this->user);
if (!\is_null($backupCodesRecord)) {
$this->methods = array_merge(
[
'backupcodes' => new MethodDescriptor(
[
'name' => 'backupcodes',
'display' => Text::_('COM_USERS_USER_BACKUPCODES'),
'shortinfo' => Text::_('COM_USERS_USER_BACKUPCODES_DESC'),
'image' => 'media/com_users/images/emergency.svg',
'canDisable' => false,
'active' => [$backupCodesRecord],
]
),
],
$this->methods
);
}
$this->isMandatoryMFASetup = $activeRecords === 0 && $app->getSession()->get('com_users.mandatory_mfa_setup', 0) === 1;
// Back-end: always show a title in the 'title' module position, not in the page body
if ($this->isAdmin) {
ToolbarHelper::title(Text::_('COM_USERS_MFA_LIST_PAGE_HEAD'), 'users user-lock');
if ($this->getCurrentUser()->authorise('core.manage', 'com_users')) {
$toolbar = Toolbar::getInstance();
$arrow = Factory::getApplication()->getLanguage()->isRtl() ? 'arrow-right' : 'arrow-left';
$toolbar->link('JTOOLBAR_BACK', 'index.php?option=com_users')
->icon('icon-' . $arrow);
}
}
// Display the view
parent::display($tpl);
$event = new NotifyActionLog('onComUsersViewMethodsAfterDisplay', [$this]);
Factory::getApplication()->getDispatcher()->dispatch($event->getName(), $event);
Text::script('JGLOBAL_CONFIRM_DELETE');
}
}

View File

@ -0,0 +1,145 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_users
*
* @copyright (C) 2011 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Component\Users\Administrator\View\Note;
use Joomla\CMS\Component\ComponentHelper;
use Joomla\CMS\Factory;
use Joomla\CMS\Helper\ContentHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\View\GenericDataException;
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
use Joomla\CMS\Toolbar\Toolbar;
use Joomla\CMS\Toolbar\ToolbarHelper;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* User note edit view
*
* @since 2.5
*/
class HtmlView extends BaseHtmlView
{
/**
* The edit form.
*
* @var \Joomla\CMS\Form\Form
*
* @since 2.5
*/
protected $form;
/**
* The item data.
*
* @var object
* @since 2.5
*/
protected $item;
/**
* The model state.
*
* @var \Joomla\Registry\Registry
* @since 2.5
*/
protected $state;
/**
* Override the display method for the view.
*
* @param string $tpl The name of the template file to parse; automatically searches through the template paths.
*
* @return void
*
* @since 2.5
* @throws \Exception
*/
public function display($tpl = null)
{
// Initialise view variables.
$this->state = $this->get('State');
$this->item = $this->get('Item');
$this->form = $this->get('Form');
// Check for errors.
if (\count($errors = $this->get('Errors'))) {
throw new GenericDataException(implode("\n", $errors), 500);
}
parent::display($tpl);
$this->addToolbar();
}
/**
* Display the toolbar.
*
* @return void
*
* @since 2.5
* @throws \Exception
*/
protected function addToolbar()
{
$input = Factory::getApplication()->getInput();
$input->set('hidemainmenu', 1);
$user = $this->getCurrentUser();
$isNew = ($this->item->id == 0);
$checkedOut = !(\is_null($this->item->checked_out) || $this->item->checked_out == $user->get('id'));
$toolbar = Toolbar::getInstance();
// Since we don't track these assets at the item level, use the category id.
$canDo = ContentHelper::getActions('com_users', 'category', $this->item->catid);
ToolbarHelper::title(Text::_('COM_USERS_NOTES'), 'users user');
// If not checked out, can save the item.
if (!$checkedOut && ($canDo->get('core.edit') || \count($user->getAuthorisedCategories('com_users', 'core.create')))) {
$toolbar->apply('note.apply');
}
$saveGroup = $toolbar->dropdownButton('save-group');
$saveGroup->configure(
function (Toolbar $childBar) use ($checkedOut, $canDo, $user, $isNew) {
// If not checked out, can save the item.
if (!$checkedOut && ($canDo->get('core.edit') || \count($user->getAuthorisedCategories('com_users', 'core.create')))) {
$childBar->save('note.save');
}
if (!$checkedOut && \count($user->getAuthorisedCategories('com_users', 'core.create'))) {
$childBar->save2new('note.save2new');
}
// If an existing item, can save to a copy.
if (!$isNew && (\count($user->getAuthorisedCategories('com_users', 'core.create')) > 0)) {
$childBar->save2copy('note.save2copy');
}
}
);
if (empty($this->item->id)) {
$toolbar->cancel('note.cancel', 'JTOOLBAR_CANCEL');
} else {
$toolbar->cancel('note.cancel');
if (ComponentHelper::isEnabled('com_contenthistory') && $this->state->params->get('save_history', 0) && $canDo->get('core.edit')) {
$toolbar->versions('com_users.note', $this->item->id);
}
}
$toolbar->divider();
$toolbar->help('User_Notes:_New_or_Edit');
}
}

View File

@ -0,0 +1,180 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_users
*
* @copyright (C) 2011 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Component\Users\Administrator\View\Notes;
use Joomla\CMS\Helper\ContentHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\View\GenericDataException;
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
use Joomla\CMS\Toolbar\Button\DropdownButton;
use Joomla\CMS\Toolbar\Toolbar;
use Joomla\CMS\Toolbar\ToolbarHelper;
use Joomla\CMS\User\User;
use Joomla\Registry\Registry;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* User notes list view
*
* @since 2.5
*/
class HtmlView extends BaseHtmlView
{
/**
* A list of user note objects.
*
* @var array
* @since 2.5
*/
protected $items;
/**
* The pagination object.
*
* @var \Joomla\CMS\Pagination\Pagination
* @since 2.5
*/
protected $pagination;
/**
* The model state.
*
* @var \Joomla\Registry\Registry
* @since 2.5
*/
protected $state;
/**
* The model state.
*
* @var User
* @since 2.5
*/
protected $user;
/**
* Form object for search filters
*
* @var \Joomla\CMS\Form\Form
*
* @since 4.0.0
*/
public $filterForm;
/**
* The active search filters
*
* @var array
* @since 4.0.0
*/
public $activeFilters;
/**
* Is this view an Empty State
*
* @var boolean
* @since 4.0.0
*/
private $isEmptyState = false;
/**
* Override the display method for the view.
*
* @param string $tpl The name of the template file to parse; automatically searches through the template paths.
*
* @return void
*
* @since 2.5
*/
public function display($tpl = null)
{
// Initialise view variables.
$this->items = $this->get('Items');
$this->pagination = $this->get('Pagination');
$this->state = $this->get('State');
$this->user = $this->get('User');
$this->filterForm = $this->get('FilterForm');
$this->activeFilters = $this->get('ActiveFilters');
if (!\count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) {
$this->setLayout('emptystate');
}
// Check for errors.
if (\count($errors = $this->get('Errors'))) {
throw new GenericDataException(implode("\n", $errors), 500);
}
// Turn parameters into registry objects
foreach ($this->items as $item) {
$item->cparams = new Registry($item->category_params);
}
$this->addToolbar();
parent::display($tpl);
}
/**
* Display the toolbar.
*
* @return void
*
* @since 2.5
*/
protected function addToolbar()
{
$canDo = ContentHelper::getActions('com_users', 'category', $this->state->get('filter.category_id'));
$toolbar = Toolbar::getInstance();
ToolbarHelper::title(Text::_('COM_USERS_VIEW_NOTES_TITLE'), 'users user');
if ($canDo->get('core.create')) {
$toolbar->addNew('note.add');
}
if (!$this->isEmptyState && ($canDo->get('core.edit.state') || $canDo->get('core.admin'))) {
/** @var DropdownButton $dropdown */
$dropdown = $toolbar->dropdownButton('status-group', 'JTOOLBAR_CHANGE_STATUS')
->toggleSplit(false)
->icon('icon-ellipsis-h')
->buttonClass('btn btn-action')
->listCheck(true);
$childBar = $dropdown->getChildToolbar();
if ($canDo->get('core.edit.state')) {
$childBar->publish('notes.publish')->listCheck(true);
$childBar->unpublish('notes.unpublish')->listCheck(true);
$childBar->archive('notes.archive')->listCheck(true);
$childBar->checkin('notes.checkin')->listCheck(true);
}
if ($this->state->get('filter.published') != -2 && $canDo->get('core.edit.state')) {
$childBar->trash('notes.trash');
}
}
if (!$this->isEmptyState && $this->state->get('filter.published') == -2 && $canDo->get('core.delete')) {
$toolbar->delete('notes.delete', 'JTOOLBAR_EMPTY_TRASH')
->message('JGLOBAL_CONFIRM_DELETE')
->listCheck(true);
}
if ($canDo->get('core.admin') || $canDo->get('core.options')) {
$toolbar->preferences('com_users');
}
$toolbar->help('User_Notes');
}
}

View File

@ -0,0 +1,65 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_users
*
* @copyright (C) 2022 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Component\Users\Administrator\View;
use Joomla\CMS\Component\ComponentHelper;
use Joomla\CMS\Factory;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Dynamically modify the frontend template when showing a MFA captive page.
*
* @since 4.2.0
*/
trait SiteTemplateTrait
{
/**
* Set a specific site template style in the frontend application
*
* @return void
* @throws \Exception
* @since 4.2.0
*/
private function setSiteTemplateStyle(): void
{
$app = Factory::getApplication();
$templateStyle = (int) ComponentHelper::getParams('com_users')->get('captive_template', '');
if (empty($templateStyle) || !$app->isClient('site')) {
return;
}
$itemId = $app->getInput()->get('Itemid');
if (!empty($itemId)) {
return;
}
$app->getInput()->set('templateStyle', $templateStyle);
try {
$refApp = new \ReflectionObject($app);
$refTemplate = $refApp->getProperty('template');
$refTemplate->setAccessible(true);
$refTemplate->setValue($app, null);
} catch (\ReflectionException $e) {
return;
}
$template = $app->getTemplate(true);
$app->set('theme', $template->template);
$app->set('themeParams', $template->params);
}
}

View File

@ -0,0 +1,188 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_users
*
* @copyright (C) 2007 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Component\Users\Administrator\View\User;
use Joomla\CMS\Factory;
use Joomla\CMS\Helper\ContentHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\View\GenericDataException;
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
use Joomla\CMS\Toolbar\Toolbar;
use Joomla\CMS\Toolbar\ToolbarHelper;
use Joomla\CMS\User\UserFactoryAwareInterface;
use Joomla\CMS\User\UserFactoryAwareTrait;
use Joomla\Component\Users\Administrator\Helper\Mfa;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* User view class.
*
* @since 1.5
*/
class HtmlView extends BaseHtmlView implements UserFactoryAwareInterface
{
use UserFactoryAwareTrait;
/**
* The Form object
*
* @var \Joomla\CMS\Form\Form
*/
protected $form;
/**
* The active item
*
* @var object
*/
protected $item;
/**
* Gets the available groups
*
* @var array
*/
protected $grouplist;
/**
* The groups this user is assigned to
*
* @var array
* @since 1.6
*/
protected $groups;
/**
* The model state
*
* @var \Joomla\Registry\Registry
*/
protected $state;
/**
* The Multi-factor Authentication configuration interface for the user.
*
* @var string|null
* @since 4.2.0
*/
protected $mfaConfigurationUI;
/**
* Display the view
*
* @param string $tpl The name of the template file to parse; automatically searches through the template paths.
*
* @return void
*
* @since 1.5
*/
public function display($tpl = null)
{
// If no item found, dont show the edit screen, redirect with message
if (false === $this->item = $this->get('Item')) {
$app = Factory::getApplication();
$app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_NOT_EXIST'), 'error');
$app->redirect('index.php?option=com_users&view=users');
}
$this->form = $this->get('Form');
$this->state = $this->get('State');
// Check for errors.
if (\count($errors = $this->get('Errors'))) {
throw new GenericDataException(implode("\n", $errors), 500);
}
// Prevent user from modifying own group(s)
$user = $this->getCurrentUser();
if ((int) $user->id != (int) $this->item->id || $user->authorise('core.admin')) {
$this->grouplist = $this->get('Groups');
$this->groups = $this->get('AssignedGroups');
}
$this->form->setValue('password', null);
$this->form->setValue('password2', null);
$userBeingEdited = $this->getUserFactory()->loadUserById($this->item->id);
if ($this->item->id > 0 && (int) $userBeingEdited->id == (int) $this->item->id) {
try {
$this->mfaConfigurationUI = Mfa::canShowConfigurationInterface($userBeingEdited)
? Mfa::getConfigurationInterface($userBeingEdited)
: '';
} catch (\Exception $e) {
// In case something goes really wrong with the plugins; prevents hard breaks.
$this->mfaConfigurationUI = null;
}
}
parent::display($tpl);
$this->addToolbar();
}
/**
* Add the page title and toolbar.
*
* @return void
*
* @since 1.6
* @throws \Exception
*/
protected function addToolbar()
{
Factory::getApplication()->getInput()->set('hidemainmenu', true);
$user = $this->getCurrentUser();
$canDo = ContentHelper::getActions('com_users');
$isNew = ($this->item->id == 0);
$isProfile = $this->item->id == $user->id;
$toolbar = Toolbar::getInstance();
ToolbarHelper::title(
Text::_(
$isNew ? 'COM_USERS_VIEW_NEW_USER_TITLE' : ($isProfile ? 'COM_USERS_VIEW_EDIT_PROFILE_TITLE' : 'COM_USERS_VIEW_EDIT_USER_TITLE')
),
'user ' . ($isNew ? 'user-add' : ($isProfile ? 'user-profile' : 'user-edit'))
);
if ($canDo->get('core.edit') || $canDo->get('core.create') || $isProfile) {
$toolbar->apply('user.apply');
}
$saveGroup = $toolbar->dropdownButton('save-group');
$saveGroup->configure(
function (Toolbar $childBar) use ($canDo, $isProfile) {
if ($canDo->get('core.edit') || $canDo->get('core.create') || $isProfile) {
$childBar->save('user.save');
}
if ($canDo->get('core.create') && $canDo->get('core.manage')) {
$childBar->save2new('user.save2new');
}
}
);
if (empty($this->item->id)) {
$toolbar->cancel('user.cancel', 'JTOOLBAR_CANCEL');
} else {
$toolbar->cancel('user.cancel');
}
$toolbar->divider();
$toolbar->help('Users:_Edit_Profile');
}
}

View File

@ -0,0 +1,181 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_users
*
* @copyright (C) 2007 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Component\Users\Administrator\View\Users;
use Joomla\CMS\Factory;
use Joomla\CMS\Helper\ContentHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\View\GenericDataException;
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
use Joomla\CMS\Toolbar\Button\DropdownButton;
use Joomla\CMS\Toolbar\Toolbar;
use Joomla\CMS\Toolbar\ToolbarHelper;
use Joomla\Database\DatabaseDriver;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* View class for a list of users.
*
* @since 1.6
*/
class HtmlView extends BaseHtmlView
{
/**
* The item data.
*
* @var object
* @since 1.6
*/
protected $items;
/**
* The pagination object.
*
* @var \Joomla\CMS\Pagination\Pagination
* @since 1.6
*/
protected $pagination;
/**
* The model state.
*
* @var \Joomla\Registry\Registry
* @since 1.6
*/
protected $state;
/**
* A Form instance with filter fields.
*
* @var \Joomla\CMS\Form\Form
*
* @since 3.6.3
*/
public $filterForm;
/**
* An array with active filters.
*
* @var array
* @since 3.6.3
*/
public $activeFilters;
/**
* An ACL object to verify user rights.
*
* @var \Joomla\Registry\Registry
* @since 3.6.3
*/
protected $canDo;
/**
* An instance of DatabaseDriver.
*
* @var DatabaseDriver
* @since 3.6.3
*
* @deprecated 4.3 will be removed in 6.0
* Will be removed without replacement use database from the container instead
* Example: Factory::getContainer()->get(DatabaseInterface::class);
*/
protected $db;
/**
* Display the view
*
* @param string $tpl The name of the template file to parse; automatically searches through the template paths.
*
* @return void
*/
public function display($tpl = null)
{
$this->items = $this->get('Items');
$this->pagination = $this->get('Pagination');
$this->state = $this->get('State');
$this->filterForm = $this->get('FilterForm');
$this->activeFilters = $this->get('ActiveFilters');
$this->canDo = ContentHelper::getActions('com_users');
$this->db = Factory::getDbo();
// Check for errors.
if (\count($errors = $this->get('Errors'))) {
throw new GenericDataException(implode("\n", $errors), 500);
}
$this->addToolbar();
parent::display($tpl);
}
/**
* Add the page title and toolbar.
*
* @return void
*
* @since 1.6
*/
protected function addToolbar()
{
$canDo = $this->canDo;
$user = $this->getCurrentUser();
// Get the toolbar object instance
$toolbar = Toolbar::getInstance('toolbar');
ToolbarHelper::title(Text::_('COM_USERS_VIEW_USERS_TITLE'), 'users user');
if ($canDo->get('core.create')) {
$toolbar->addNew('user.add');
}
if ($canDo->get('core.edit.state') || $canDo->get('core.admin')) {
/** @var DropdownButton $dropdown */
$dropdown = $toolbar->dropdownButton('status-group', 'JTOOLBAR_CHANGE_STATUS')
->toggleSplit(false)
->icon('icon-ellipsis-h')
->buttonClass('btn btn-action')
->listCheck(true);
$childBar = $dropdown->getChildToolbar();
$childBar->publish('users.activate', 'COM_USERS_TOOLBAR_ACTIVATE');
$childBar->unpublish('users.block', 'COM_USERS_TOOLBAR_BLOCK');
$childBar->standardButton('unblock', 'COM_USERS_TOOLBAR_UNBLOCK', 'users.unblock')
->listCheck(true);
// Add a batch button
if (
$user->authorise('core.create', 'com_users')
&& $user->authorise('core.edit', 'com_users')
&& $user->authorise('core.edit.state', 'com_users')
) {
$childBar->popupButton('batch', 'JTOOLBAR_BATCH')
->selector('collapseModal')
->listCheck(true);
}
if ($canDo->get('core.delete')) {
$childBar->delete('users.delete', 'JTOOLBAR_DELETE')
->message('JGLOBAL_CONFIRM_DELETE')
->listCheck(true);
}
}
if ($canDo->get('core.admin') || $canDo->get('core.options')) {
$toolbar->preferences('com_users');
}
$toolbar->help('Users');
}
}

View File

@ -0,0 +1,134 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_users
*
* @copyright (C) 2022 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
use Joomla\CMS\Factory;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route;
use Joomla\Component\Users\Administrator\Model\CaptiveModel;
use Joomla\Component\Users\Administrator\View\Captive\HtmlView;
use Joomla\Utilities\ArrayHelper;
/**
* @var HtmlView $this View object
* @var CaptiveModel $model The model
*/
$model = $this->getModel();
$this->document->getWebAssetManager()
->useScript('com_users.two-factor-focus');
?>
<div class="users-mfa-captive card card-body">
<h2 id="users-mfa-title">
<?php if (!empty($this->title)) : ?>
<?php echo $this->title ?> <small> &ndash;
<?php endif; ?>
<?php if (!$this->allowEntryBatching) : ?>
<?php echo $this->escape($this->record->title) ?>
<?php else : ?>
<?php echo $this->escape($this->getModel()->translateMethodName($this->record->method)) ?>
<?php endif; ?>
<?php if (!empty($this->title)) : ?>
</small>
<?php endif; ?>
<?php if (!empty($this->renderOptions['help_url'])) : ?>
<span class="float-end">
<a href="<?php echo $this->renderOptions['help_url'] ?>"
class="btn btn-sm btn-secondary"
target="_blank"
>
<span class="icon icon-question-sign" aria-hidden="true"></span>
<span class="visually-hidden"><?php echo Text::_('JHELP') ?></span>
</a>
</span>
<?php endif;?>
</h2>
<?php if ($this->renderOptions['pre_message']) : ?>
<div class="users-mfa-captive-pre-message text-muted">
<?php echo $this->renderOptions['pre_message'] ?>
</div>
<?php endif; ?>
<form action="<?php echo Route::_('index.php?option=com_users&task=captive.validate&record_id=' . ((int) $this->record->id)) ?>"
id="users-mfa-captive-form"
method="post"
class="form-horizontal"
>
<?php echo HTMLHelper::_('form.token') ?>
<div id="users-mfa-captive-form-method-fields" class="container">
<?php if ($this->renderOptions['field_type'] == 'custom') : ?>
<?php echo $this->renderOptions['html']; ?>
<?php endif; ?>
<div class="row mb-3 <?php echo $this->renderOptions['input_type'] === 'hidden' ? 'd-none' : '' ?>">
<?php if ($this->renderOptions['label']) : ?>
<label for="users-mfa-code" class="col-sm-3 col-form-label">
<?php echo $this->renderOptions['label'] ?>
</label>
<?php endif; ?>
<?php
$attributes = array_merge(
[
'type' => $this->renderOptions['input_type'],
'name' => 'code',
'value' => '',
'placeholder' => $this->renderOptions['placeholder'] ?? null,
'id' => 'users-mfa-code',
'class' => 'form-control',
'autocomplete' => $this->renderOptions['autocomplete'] ?? 'one-time-code'
],
$this->renderOptions['input_attributes']
);
if (strpos($attributes['class'], 'form-control') === false) {
$attributes['class'] .= ' form-control';
}
?>
<input <?php echo ArrayHelper::toString($attributes) ?>>
</div>
</div>
<div id="users-mfa-captive-form-standard-buttons" class="row my-3 d-sm-none">
<div class="col-sm-9 offset-sm-3">
<button class="btn btn-primary me-3 <?php echo $this->renderOptions['submit_class'] ?>"
id="users-mfa-captive-button-submit"
style="<?php echo $this->renderOptions['hide_submit'] ? 'display: none' : '' ?>"
type="submit">
<span class="<?php echo $this->renderOptions['submit_icon'] ?>" aria-hidden="true"></span>
<?php echo Text::_($this->renderOptions['submit_text']); ?>
</button>
<a href="<?php echo Route::_('index.php?option=com_login&task=logout&' . Factory::getApplication()->getFormToken() . '=1') ?>"
class="btn btn-danger btn-sm"
id="users-mfa-captive-button-logout">
<span class="icon icon-lock" aria-hidden="true"></span>
<?php echo Text::_('COM_USERS_MFA_LOGOUT'); ?>
</a>
<?php if (count($this->records) > 1) : ?>
<a id="users-mfa-captive-form-choose-another"
class="btn btn-link"
href="<?php echo Route::_('index.php?option=com_users&view=captive&task=select') ?>">
<?php echo Text::_('COM_USERS_MFA_USE_DIFFERENT_METHOD'); ?>
</a>
<?php endif; ?>
</div>
</div>
</form>
<?php if ($this->renderOptions['post_message']) : ?>
<div class="users-mfa-captive-post-message">
<?php echo $this->renderOptions['post_message'] ?>
</div>
<?php endif; ?>
</div>

View File

@ -0,0 +1,78 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_users
*
* @copyright (C) 2022 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
// Prevent direct access
defined('_JEXEC') or die;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Uri\Uri;
use Joomla\Component\Users\Administrator\View\Captive\HtmlView;
/** @var HtmlView $this */
$shownMethods = [];
?>
<div id="com-users-select">
<h2 id="com-users-select-heading">
<?php echo Text::_('COM_USERS_MFA_SELECT_PAGE_HEAD'); ?>
</h2>
<div id="com-users-select-information">
<p>
<?php echo Text::_('COM_USERS_LBL_SELECT_INSTRUCTIONS'); ?>
</p>
</div>
<div class="com-users-select-methods p-2">
<?php foreach ($this->records as $record) :
if (!array_key_exists($record->method, $this->mfaMethods) && ($record->method != 'backupcodes')) {
continue;
}
$allowEntryBatching = isset($this->mfaMethods[$record->method]) ? $this->mfaMethods[$record->method]['allowEntryBatching'] : false;
if ($this->allowEntryBatching) {
if ($allowEntryBatching && in_array($record->method, $shownMethods)) {
continue;
}
$shownMethods[] = $record->method;
}
$methodName = $this->getModel()->translateMethodName($record->method);
?>
<a class="com-users-method p-2 border-top border-dark bg-light d-flex flex-row flex-wrap justify-content-start align-items-center text-decoration-none gap-2 text-body"
href="<?php echo Route::_('index.php?option=com_users&view=captive&record_id=' . $record->id)?>">
<img src="<?php echo Uri::root() . $this->getModel()->getMethodImage($record->method) ?>"
alt="<?php echo $this->escape(strip_tags($record->title)) ?>"
class="com-users-method-image img-fluid" />
<?php if (!$this->allowEntryBatching || !$allowEntryBatching) : ?>
<span class="com-users-method-title flex-grow-1 fs-5 fw-bold">
<?php if ($record->method === 'backupcodes') : ?>
<?php echo $record->title ?>
<?php else : ?>
<?php echo $this->escape($record->title) ?>
<?php endif; ?>
</span>
<small class="com-users-method-name text-muted">
<?php echo $methodName ?>
</small>
<?php else : ?>
<span class="com-users-method-title flex-grow-1 fs-5 fw-bold">
<?php echo $methodName ?>
</span>
<small class="com-users-method-name text-muted">
<?php echo $methodName ?>
</small>
<?php endif; ?>
</a>
<?php endforeach; ?>
</div>
</div>

View File

@ -0,0 +1,158 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_users
*
* @copyright (C) 2010 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
defined('_JEXEC') or die;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Layout\LayoutHelper;
use Joomla\CMS\Router\Route;
$listOrder = $this->escape($this->state->get('list.ordering'));
$listDirn = $this->escape($this->state->get('list.direction'));
/** @var Joomla\CMS\WebAsset\WebAssetManager $wa */
$wa = $this->document->getWebAssetManager();
$wa->useScript('table.columns');
?>
<form action="<?php echo Route::_('index.php?option=com_users&view=debuggroup&group_id=' . (int) $this->state->get('group_id')); ?>" method="post" name="adminForm" id="adminForm">
<div id="j-main-container" class="j-main-container">
<?php echo LayoutHelper::render('joomla.searchtools.default', ['view' => $this]); ?>
<?php if (empty($this->items)) : ?>
<div class="alert alert-info">
<span class="icon-info-circle" aria-hidden="true"></span><span class="visually-hidden"><?php echo Text::_('INFO'); ?></span>
<?php echo Text::_('JGLOBAL_NO_MATCHING_RESULTS'); ?>
</div>
<?php else : ?>
<?php
// Split the actions table
foreach ($this->actions as $action) :
$name = $action[0];
if (in_array($name, ['core.login.site', 'core.login.admin', 'core.login.offline', 'core.login.api', 'core.admin'])) :
$loginActions[] = $action;
else :
$actions[] = $action;
endif;
endforeach;
?>
<div class="d-flex flex-wrap">
<?php foreach ($loginActions as $action) :
$name = $action[0];
$check = $this->items[0]->checks[$name];
if ($check === true) :
$class = 'text-success icon-check';
$button = 'btn-success';
$text = Text::_('COM_USERS_DEBUG_EXPLICIT_ALLOW');
elseif ($check === false) :
$class = 'text-danger icon-times';
$button = 'btn-danger';
$text = Text::_('COM_USERS_DEBUG_EXPLICIT_DENY');
elseif ($check === null) :
$class = 'text-danger icon-minus-circle';
$button = 'btn-warning';
$text = Text::_('COM_USERS_DEBUG_IMPLICIT_DENY');
endif; ?>
<div class="d-inline p-2">
<?php echo Text::_($action[1]); ?>
<span class="<?php echo $class; ?>" aria-hidden="true"></span>
<span class="visually-hidden"><?php echo $text; ?></span>
</div>
<?php endforeach; ?>
</div>
<table class="table">
<caption class="visually-hidden">
<?php echo Text::_('COM_USERS_DEBUG_GROUP_TABLE_CAPTION'); ?>,
<span id="orderedBy"><?php echo Text::_('JGLOBAL_SORTED_BY'); ?> </span>,
<span id="filteredBy"><?php echo Text::_('JGLOBAL_FILTERED_BY'); ?></span>
</caption>
<thead>
<tr>
<th scope="col">
<?php echo HTMLHelper::_('searchtools.sort', 'COM_USERS_HEADING_ASSET_TITLE', 'a.title', $listDirn, $listOrder); ?>
</th>
<th scope="col">
<?php echo HTMLHelper::_('searchtools.sort', 'COM_USERS_HEADING_ASSET_NAME', 'a.name', $listDirn, $listOrder); ?>
</th>
<?php foreach ($actions as $key => $action) : ?>
<th scope="col" class="w-6 text-center">
<?php echo Text::_($action[1]); ?>
</th>
<?php endforeach; ?>
<th scope="col" class="w-6">
<?php echo HTMLHelper::_('searchtools.sort', 'COM_USERS_HEADING_LFT', 'a.lft', $listDirn, $listOrder); ?>
</th>
<th scope="col" class="w-3">
<?php echo HTMLHelper::_('searchtools.sort', 'JGRID_HEADING_ID', 'a.id', $listDirn, $listOrder); ?>
</th>
</tr>
</thead>
<tbody>
<?php foreach ($this->items as $i => $item) : ?>
<tr class="row0">
<th scope="row">
<?php echo $this->escape(Text::_($item->title)); ?>
</th>
<td>
<?php echo LayoutHelper::render('joomla.html.treeprefix', ['level' => $item->level + 1]) . $this->escape($item->name); ?>
</td>
<?php foreach ($actions as $action) : ?>
<?php
$name = $action[0];
$check = $item->checks[$name];
if ($check === true) :
$class = 'text-success icon-check';
$button = 'btn-success';
$text = Text::_('COM_USERS_DEBUG_EXPLICIT_ALLOW');
elseif ($check === false) :
$class = 'text-danger icon-times';
$button = 'btn-danger';
$text = Text::_('COM_USERS_DEBUG_EXPLICIT_DENY');
elseif ($check === null) :
$class = 'text-danger icon-minus-circle';
$button = 'btn-warning';
$text = Text::_('COM_USERS_DEBUG_IMPLICIT_DENY');
else :
$class = '';
$button = '';
$text = '';
endif;
?>
<td class="text-center">
<span class="<?php echo $class; ?>" aria-hidden="true"></span>
<span class="visually-hidden"> <?php echo $text; ?></span>
</td>
<?php endforeach; ?>
<td>
<?php echo (int) $item->lft; ?>
- <?php echo (int) $item->rgt; ?>
</td>
<td>
<?php echo (int) $item->id; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<div class="legend">
<span class="text-danger icon-minus-circle" aria-hidden="true"></span>&nbsp;<?php echo Text::_('COM_USERS_DEBUG_IMPLICIT_DENY'); ?>&nbsp;
<span class="text-success icon-check" aria-hidden="true"></span>&nbsp;<?php echo Text::_('COM_USERS_DEBUG_EXPLICIT_ALLOW'); ?>&nbsp;
<span class="text-danger icon-times" aria-hidden="true"></span>&nbsp;<?php echo Text::_('COM_USERS_DEBUG_EXPLICIT_DENY'); ?>
</div>
<?php // load the pagination. ?>
<?php echo $this->pagination->getListFooter(); ?>
<input type="hidden" name="task" value="">
<input type="hidden" name="boxchecked" value="0">
<?php echo HTMLHelper::_('form.token'); ?>
<?php endif; ?>
</div>
</form>

View File

@ -0,0 +1,164 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_users
*
* @copyright (C) 2010 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
defined('_JEXEC') or die;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Layout\LayoutHelper;
use Joomla\CMS\Router\Route;
$listOrder = $this->escape($this->state->get('list.ordering'));
$listDirn = $this->escape($this->state->get('list.direction'));
$loginActions = [];
$actions = [];
/** @var Joomla\CMS\WebAsset\WebAssetManager $wa */
$wa = $this->document->getWebAssetManager();
$wa->useScript('table.columns');
?>
<form action="<?php echo Route::_('index.php?option=com_users&view=debuguser&user_id=' . (int) $this->state->get('user_id')); ?>" method="post" name="adminForm" id="adminForm">
<div id="j-main-container" class="j-main-container">
<?php echo LayoutHelper::render('joomla.searchtools.default', ['view' => $this]); ?>
<?php if (empty($this->items)) : ?>
<div class="alert alert-info">
<span class="icon-info-circle" aria-hidden="true"></span><span class="visually-hidden"><?php echo Text::_('INFO'); ?></span>
<?php echo Text::_('JGLOBAL_NO_MATCHING_RESULTS'); ?>
</div>
<?php else : ?>
<?php
// Split the actions table
foreach ($this->actions as $action) :
$name = $action[0];
if (in_array($name, ['core.login.site', 'core.login.admin', 'core.login.api', 'core.login.offline'])) :
$loginActions[] = $action;
else :
$actions[] = $action;
endif;
endforeach;
?>
<div class="d-flex flex-wrap">
<?php foreach ($loginActions as $action) :
$name = $action[0];
$check = $this->items[0]->checks[$name];
if ($check === true) :
$class = 'text-success icon-check';
$button = 'btn-success';
$text = Text::_('COM_USERS_DEBUG_EXPLICIT_ALLOW');
elseif ($check === false) :
$class = 'text-danger icon-times';
$button = 'btn-danger';
$text = Text::_('COM_USERS_DEBUG_EXPLICIT_DENY');
elseif ($check === null) :
$class = 'text-danger icon-minus-circle';
$button = 'btn-warning';
$text = Text::_('COM_USERS_DEBUG_IMPLICIT_DENY');
endif;
?>
<div class="d-inline p-2">
<?php echo Text::_($action[1]); ?>
<span class="<?php echo $class; ?>" aria-hidden="true"></span>
<span class="visually-hidden"><?php echo Text::_($text); ?></span>
</div>
<?php endforeach; ?>
</div>
<table class="table">
<caption class="visually-hidden">
<?php echo Text::_('COM_USERS_DEBUG_USER_TABLE_CAPTION'); ?>,
<span id="orderedBy"><?php echo Text::_('JGLOBAL_SORTED_BY'); ?> </span>,
<span id="filteredBy"><?php echo Text::_('JGLOBAL_FILTERED_BY'); ?></span>
</caption>
<thead>
<tr>
<th scope="col">
<?php echo HTMLHelper::_('searchtools.sort', 'COM_USERS_HEADING_ASSET_TITLE', 'a.title', $listDirn, $listOrder); ?>
</th>
<th scope="col">
<?php echo HTMLHelper::_('searchtools.sort', 'COM_USERS_HEADING_ASSET_NAME', 'a.name', $listDirn, $listOrder); ?>
</th>
<?php foreach ($actions as $key => $action) : ?>
<th scope="col" class="w-6 text-center">
<?php echo Text::_($action[1]); ?>
</th>
<?php endforeach; ?>
<th scope="col" class="w-6">
<?php echo HTMLHelper::_('searchtools.sort', 'COM_USERS_HEADING_LFT', 'a.lft', $listDirn, $listOrder); ?>
</th>
<th scope="col" class="w-3">
<?php echo HTMLHelper::_('searchtools.sort', 'JGRID_HEADING_ID', 'a.id', $listDirn, $listOrder); ?>
</th>
</tr>
</thead>
<tbody>
<?php foreach ($this->items as $i => $item) :?>
<tr class="row0" scope="row">
<td>
<?php echo $this->escape(Text::_($item->title)); ?>
</td>
<td>
<?php echo LayoutHelper::render('joomla.html.treeprefix', ['level' => $item->level + 1]) . $this->escape($item->name); ?>
</td>
<?php foreach ($actions as $action) : ?>
<?php
$name = $action[0];
$check = $item->checks[$name];
if ($check === true) :
$class = 'text-success icon-check';
$button = 'btn-success';
$text = Text::_('COM_USERS_DEBUG_EXPLICIT_ALLOW');
elseif ($check === false) :
$class = 'text-danger icon-times';
$button = 'btn-danger';
$text = Text::_('COM_USERS_DEBUG_EXPLICIT_DENY');
elseif ($check === null) :
$class = 'text-danger icon-minus-circle';
$button = 'btn-warning';
$text = Text::_('COM_USERS_DEBUG_IMPLICIT_DENY');
else :
$class = '';
$button = '';
$text = '';
endif;
?>
<td class="text-center">
<span class="<?php echo $class; ?>" aria-hidden="true"></span>
<span class="visually-hidden"> <?php echo $text; ?></span>
</td>
<?php endforeach; ?>
<td>
<?php echo (int) $item->lft; ?>
- <?php echo (int) $item->rgt; ?>
</td>
<td>
<?php echo (int) $item->id; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<div class="legend">
<span class="text-danger icon-minus-circle" aria-hidden="true"></span>&nbsp;<?php echo Text::_('COM_USERS_DEBUG_IMPLICIT_DENY'); ?>&nbsp;
<span class="text-success icon-check" aria-hidden="true"></span>&nbsp;<?php echo Text::_('COM_USERS_DEBUG_EXPLICIT_ALLOW'); ?>&nbsp;
<span class="text-danger icon-times" aria-hidden="true">&nbsp;</span><?php echo Text::_('COM_USERS_DEBUG_EXPLICIT_DENY'); ?>
</div>
<?php // load the pagination. ?>
<?php echo $this->pagination->getListFooter(); ?>
<input type="hidden" name="task" value="">
<input type="hidden" name="boxchecked" value="0">
<?php echo HTMLHelper::_('form.token'); ?>
<?php endif; ?>
</div>
</form>

View File

@ -0,0 +1,40 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_users
*
* @copyright (C) 2009 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
defined('_JEXEC') or die;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Layout\LayoutHelper;
use Joomla\CMS\Router\Route;
/** @var Joomla\CMS\WebAsset\WebAssetManager $wa */
$wa = $this->document->getWebAssetManager();
$wa->useScript('keepalive')
->useScript('form.validate');
$this->useCoreUI = true;
?>
<form action="<?php echo Route::_('index.php?option=com_users&layout=edit&id=' . (int) $this->item->id); ?>" method="post" name="adminForm" id="group-form" aria-label="<?php echo Text::_('COM_USERS_GROUP_FORM_' . ((int) $this->item->id === 0 ? 'NEW' : 'EDIT'), true); ?>" class="main-card form-validate">
<?php echo HTMLHelper::_('uitab.startTabSet', 'myTab', ['active' => 'details', 'recall' => true, 'breakpoint' => 768]); ?>
<?php echo HTMLHelper::_('uitab.addTab', 'myTab', 'details', Text::_('COM_USERS_USERGROUP_DETAILS')); ?>
<div class="form-grid">
<?php echo $this->form->renderField('title'); ?>
<?php echo $this->form->renderField('parent_id'); ?>
</div>
<?php echo HTMLHelper::_('uitab.endTab'); ?>
<?php $this->ignore_fieldsets = ['group_details']; ?>
<?php echo LayoutHelper::render('joomla.edit.params', $this); ?>
<?php echo HTMLHelper::_('uitab.endTabSet'); ?>
<input type="hidden" name="task" value="">
<?php echo HTMLHelper::_('form.token'); ?>
</form>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<metadata>
<layout title="COM_USERS_GROUP_VIEW_EDIT_TITLE">
<message>
<![CDATA[COM_USERS_GROUP_VIEW_EDIT_DESC]]>
</message>
</layout>
</metadata>

View File

@ -0,0 +1,144 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_users
*
* @copyright (C) 2009 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
defined('_JEXEC') or die;
use Joomla\CMS\Access\Access;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Layout\LayoutHelper;
use Joomla\CMS\Router\Route;
$user = $this->getCurrentUser();
$listOrder = $this->escape($this->state->get('list.ordering'));
$listDirn = $this->escape($this->state->get('list.direction'));
Text::script('COM_USERS_GROUPS_CONFIRM_DELETE', true);
/** @var Joomla\CMS\WebAsset\WebAssetManager $wa */
$wa = $this->document->getWebAssetManager();
$wa->useScript('com_users.admin-users-groups')
->useScript('multiselect')
->useScript('table.columns');
?>
<form action="<?php echo Route::_('index.php?option=com_users&view=groups'); ?>" method="post" name="adminForm" id="adminForm">
<div class="row">
<div class="col-md-12">
<div id="j-main-container" class="j-main-container">
<?php echo LayoutHelper::render('joomla.searchtools.default', ['view' => $this, 'options' => ['filterButton' => false]]); ?>
<?php if (empty($this->items)) : ?>
<div class="alert alert-info">
<span class="icon-info-circle" aria-hidden="true"></span><span class="visually-hidden"><?php echo Text::_('INFO'); ?></span>
<?php echo Text::_('JGLOBAL_NO_MATCHING_RESULTS'); ?>
</div>
<?php else : ?>
<table class="table" id="groupList">
<caption class="visually-hidden">
<?php echo Text::_('COM_USERS_GROUPS_TABLE_CAPTION'); ?>,
<span id="orderedBy"><?php echo Text::_('JGLOBAL_SORTED_BY'); ?> </span>,
<span id="filteredBy"><?php echo Text::_('JGLOBAL_FILTERED_BY'); ?></span>
</caption>
<thead>
<tr>
<td class="w-1 text-center">
<?php echo HTMLHelper::_('grid.checkall'); ?>
</td>
<th scope="col">
<?php echo HTMLHelper::_('searchtools.sort', 'COM_USERS_HEADING_GROUP_TITLE', 'a.title', $listDirn, $listOrder); ?>
</th>
<th scope="col" class="w-10 text-center">
<?php echo Text::_('COM_USERS_DEBUG_PERMISSIONS'); ?>
</th>
<th scope="col" class="w-10 text-center d-none d-md-table-cell">
<span class="icon-check" aria-hidden="true"></span>
<?php echo Text::_('COM_USERS_COUNT_ENABLED_USERS'); ?>
</th>
<th scope="col" class="w-10 text-center d-none d-md-table-cell">
<span class="icon-times" aria-hidden="true"></span>
<?php echo Text::_('COM_USERS_COUNT_DISABLED_USERS'); ?>
</th>
<th scope="col" class="w-5 d-none d-md-table-cell">
<?php echo HTMLHelper::_('searchtools.sort', 'JGRID_HEADING_ID', 'a.id', $listDirn, $listOrder); ?>
</th>
</tr>
</thead>
<tbody>
<?php foreach ($this->items as $i => $item) :
$canCreate = $user->authorise('core.create', 'com_users');
$canEdit = $user->authorise('core.edit', 'com_users');
// If this group is super admin and this user is not super admin, $canEdit is false
if (!$user->authorise('core.admin') && Access::checkGroup($item->id, 'core.admin')) {
$canEdit = false;
}
$canChange = $user->authorise('core.edit.state', 'com_users');
?>
<tr class="row<?php echo $i % 2; ?>">
<td class="text-center" data-usercount="<?php echo $item->user_count; ?>">
<?php if ($canEdit) : ?>
<?php echo HTMLHelper::_('grid.id', $i, $item->id, false, 'cid', 'cb', $item->title); ?>
<?php endif; ?>
</td>
<th scope="row">
<?php echo LayoutHelper::render('joomla.html.treeprefix', ['level' => $item->level + 1]); ?>
<?php if ($canEdit) : ?>
<a href="<?php echo Route::_('index.php?option=com_users&task=group.edit&id=' . $item->id); ?>" title="<?php echo Text::_('JACTION_EDIT'); ?> <?php echo $this->escape($item->title); ?>">
<?php echo $this->escape($item->title); ?></a>
<?php else : ?>
<?php echo $this->escape($item->title); ?>
<?php endif; ?>
</th>
<td class="text-center btns">
<a href="<?php echo Route::_('index.php?option=com_users&view=debuggroup&group_id=' . (int) $item->id); ?>">
<span class="icon-list" aria-hidden="true"></span>
<span class="visually-hidden"><?php echo Text::_('COM_USERS_DEBUG_PERMISSIONS'); ?></span>
</a>
</td>
<td class="text-center btns itemnumber d-none d-md-table-cell">
<a class="btn <?php echo $item->count_enabled > 0 ? 'btn-success' : 'btn-secondary'; ?>"
href="<?php echo Route::_('index.php?option=com_users&view=users&filter[group_id]=' . (int) $item->id . '&filter[state]=0'); ?>"
aria-describedby="tip-enabled<?php echo $i; ?>">
<?php echo $item->count_enabled; ?>
</a>
<div role="tooltip" id="tip-enabled<?php echo $i; ?>">
<?php echo Text::_('COM_USERS_COUNT_ENABLED_USERS'); ?>
</div>
</td>
<td class="text-center btns itemnumber d-none d-md-table-cell">
<a class="btn <?php echo $item->count_disabled > 0 ? 'btn-danger' : 'btn-secondary'; ?>"
href="<?php echo Route::_('index.php?option=com_users&view=users&filter[group_id]=' . (int) $item->id . '&filter[state]=1'); ?>"
aria-describedby="tip-blocked<?php echo $i; ?>">
<?php echo $item->count_disabled; ?>
</a>
<div role="tooltip" id="tip-blocked<?php echo $i; ?>">
<?php echo Text::_('COM_USERS_COUNT_DISABLED_USERS'); ?>
</div>
</td>
<td class="d-none d-md-table-cell">
<?php echo (int) $item->id; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php // load the pagination. ?>
<?php echo $this->pagination->getListFooter(); ?>
<?php endif; ?>
<input type="hidden" name="task" value="">
<input type="hidden" name="boxchecked" value="0">
<?php echo HTMLHelper::_('form.token'); ?>
</div>
</div>
</div>
</form>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<metadata>
<layout title="COM_USERS_GROUPS_VIEW_DEFAULT_TITLE">
<message>
<![CDATA[COM_USERS_GROUPS_VIEW_DEFAULT_DESC]]>
</message>
</layout>
</metadata>

View File

@ -0,0 +1,54 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_users
*
* @copyright (C) 2009 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
defined('_JEXEC') or die;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route;
/** @var Joomla\CMS\WebAsset\WebAssetManager $wa */
$wa = $this->document->getWebAssetManager();
$wa->useScript('keepalive')
->useScript('form.validate');
?>
<form action="<?php echo Route::_('index.php?option=com_users&id=' . (int) $this->item->id); ?>" method="post" name="adminForm" id="level-form" aria-label="<?php echo Text::_('COM_USERS_LEVEL_FORM_' . ((int) $this->item->id === 0 ? 'NEW' : 'EDIT'), true); ?>" class="form-validate main-card">
<?php echo HTMLHelper::_('uitab.startTabSet', 'myTab', ['active' => 'details', 'recall' => true, 'breakpoint' => 768]); ?>
<?php echo HTMLHelper::_('uitab.addTab', 'myTab', 'details', Text::_('COM_USERS_LEVEL_DETAILS')); ?>
<fieldset class="options-form">
<legend><?php echo Text::_('COM_USERS_LEVEL_DETAILS'); ?></legend>
<div class="control-group">
<div class="control-label">
<?php echo $this->form->getLabel('title'); ?>
</div>
<div class="controls">
<?php echo $this->form->getInput('title'); ?>
</div>
</div>
</fieldset>
<?php echo HTMLHelper::_('uitab.endTab'); ?>
<?php echo HTMLHelper::_('uitab.addTab', 'myTab', 'groups', Text::_('COM_USERS_USER_GROUPS_HAVING_ACCESS')); ?>
<fieldset class="options-form">
<legend><?php echo Text::_('COM_USERS_USER_GROUPS_HAVING_ACCESS'); ?></legend>
<div>
<?php echo HTMLHelper::_('access.usergroups', 'jform[rules]', $this->item->rules, true); ?>
</div>
</fieldset>
<?php echo HTMLHelper::_('uitab.endTab'); ?>
<?php echo HTMLHelper::_('uitab.endTabSet'); ?>
<input type="hidden" name="task" value="">
<?php echo HTMLHelper::_('form.token'); ?>
</form>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<metadata>
<layout title="COM_USERS_LEVEL_VIEW_EDIT_TITLE">
<message>
<![CDATA[COM_USERS_LEVEL_VIEW_EDIT_DESC]]>
</message>
</layout>
</metadata>

View File

@ -0,0 +1,143 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_users
*
* @copyright (C) 2009 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
defined('_JEXEC') or die;
use Joomla\CMS\Access\Access;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Layout\LayoutHelper;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Session\Session;
use Joomla\Component\Users\Administrator\Helper\UsersHelper;
/** @var \Joomla\CMS\WebAsset\WebAssetManager $wa */
$wa = $this->document->getWebAssetManager();
$wa->useScript('table.columns')
->useScript('multiselect');
$user = $this->getCurrentUser();
$listOrder = $this->escape($this->state->get('list.ordering'));
$listDirn = $this->escape($this->state->get('list.direction'));
$saveOrder = $listOrder == 'a.ordering';
if ($saveOrder && !empty($this->items)) {
$saveOrderingUrl = 'index.php?option=com_users&task=levels.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1';
HTMLHelper::_('draggablelist.draggable');
}
?>
<form action="<?php echo Route::_('index.php?option=com_users&view=levels'); ?>" method="post" id="adminForm" name="adminForm">
<div class="row">
<div class="col-md-12">
<div id="j-main-container" class="j-main-container">
<?php echo LayoutHelper::render('joomla.searchtools.default', ['view' => $this, 'options' => ['filterButton' => false]]); ?>
<?php if (empty($this->items)) : ?>
<div class="alert alert-info">
<span class="icon-info-circle" aria-hidden="true"></span><span class="visually-hidden"><?php echo Text::_('INFO'); ?></span>
<?php echo Text::_('JGLOBAL_NO_MATCHING_RESULTS'); ?>
</div>
<?php else : ?>
<table class="table" id="levelList">
<caption class="visually-hidden">
<?php echo Text::_('COM_USERS_LEVELS_TABLE_CAPTION'); ?>,
<span id="orderedBy"><?php echo Text::_('JGLOBAL_SORTED_BY'); ?> </span>,
<span id="filteredBy"><?php echo Text::_('JGLOBAL_FILTERED_BY'); ?></span>
</caption>
<thead>
<tr>
<td class="w-1 text-center">
<?php echo HTMLHelper::_('grid.checkall'); ?>
</td>
<th scope="col" class="w-1 text-center d-none d-md-table-cell">
<?php echo HTMLHelper::_('searchtools.sort', '', 'a.ordering', $listDirn, $listOrder, null, 'asc', 'JGRID_HEADING_ORDERING', 'icon-sort'); ?>
</th>
<th scope="col">
<?php echo HTMLHelper::_('searchtools.sort', 'COM_USERS_HEADING_LEVEL_NAME', 'a.title', $listDirn, $listOrder); ?>
</th>
<th scope="col" class="d-none d-md-table-cell">
<?php echo Text::_('COM_USERS_USER_GROUPS_HAVING_ACCESS'); ?>
</th>
<th scope="col" class="w-1 d-none d-md-table-cell">
<?php echo HTMLHelper::_('searchtools.sort', 'JGRID_HEADING_ID', 'a.id', $listDirn, $listOrder); ?>
</th>
</tr>
</thead>
<tbody<?php if ($saveOrder) :
?> class="js-draggable" data-url="<?php echo $saveOrderingUrl; ?>" data-direction="<?php echo strtolower($listDirn); ?>"<?php
endif; ?>>
<?php $count = count($this->items); ?>
<?php foreach ($this->items as $i => $item) :
$ordering = ($listOrder == 'a.ordering');
$canCreate = $user->authorise('core.create', 'com_users');
$canEdit = $user->authorise('core.edit', 'com_users');
$canChange = $user->authorise('core.edit.state', 'com_users');
// Decode level groups
$groups = json_decode($item->rules);
// If this group is super admin and this user is not super admin, $canEdit is false
if (!$this->getCurrentUser()->authorise('core.admin') && $groups && Access::checkGroup($groups[0], 'core.admin')) {
$canEdit = false;
$canChange = false;
}
?>
<tr class="row<?php echo $i % 2; ?>">
<td class="text-center">
<?php if ($canEdit) : ?>
<?php echo HTMLHelper::_('grid.id', $i, $item->id, false, 'cid', 'cb', $item->title); ?>
<?php endif; ?>
</td>
<td class="text-center d-none d-md-table-cell">
<?php
$iconClass = '';
if (!$canChange) {
$iconClass = ' inactive';
} elseif (!$saveOrder) {
$iconClass = ' inactive" title="' . Text::_('JORDERINGDISABLED');
}
?>
<span class="sortable-handler<?php echo $iconClass ?>">
<span class="icon-ellipsis-v" aria-hidden="true"></span>
</span>
<?php if ($canChange && $saveOrder) : ?>
<input type="text" name="order[]" size="5" value="<?php echo $item->ordering; ?>" class="width-20 text-area-order hidden">
<?php endif; ?>
</td>
<th scope="row">
<?php if ($canEdit) : ?>
<a href="<?php echo Route::_('index.php?option=com_users&task=level.edit&id=' . $item->id); ?>" title="<?php echo Text::_('JACTION_EDIT'); ?> <?php echo $this->escape($item->title); ?>">
<?php echo $this->escape($item->title); ?></a>
<?php else : ?>
<?php echo $this->escape($item->title); ?>
<?php endif; ?>
</th>
<td class="d-none d-md-table-cell">
<?php echo UsersHelper::getVisibleByGroups($item->rules); ?>
</td>
<td class="d-none d-md-table-cell">
<?php echo (int) $item->id; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php // load the pagination. ?>
<?php echo $this->pagination->getListFooter(); ?>
<?php endif; ?>
<input type="hidden" name="task" value="">
<input type="hidden" name="boxchecked" value="0">
<?php echo HTMLHelper::_('form.token'); ?>
</div>
</div>
</div>
</form>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<metadata>
<layout title="COM_USERS_LEVELS_VIEW_DEFAULT_TITLE">
<message>
<![CDATA[COM_USERS_LEVELS_VIEW_DEFAULT_DESC]]>
</message>
</layout>
</metadata>

View File

@ -0,0 +1,62 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_users
*
* @copyright (C) 2009 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
defined('_JEXEC') or die;
use Joomla\CMS\Component\ComponentHelper;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route;
/** @var Joomla\Component\Users\Administrator\View\Mail\HtmlView $this */
/** @var Joomla\CMS\WebAsset\WebAssetManager $wa */
$wa = $this->document->getWebAssetManager();
$wa->useScript('keepalive')
->useScript('form.validate');
$comUserParams = ComponentHelper::getParams('com_users');
?>
<form action="<?php echo Route::_('index.php?option=com_users&view=mail'); ?>" name="adminForm" method="post" id="mail-form" aria-label="<?php echo Text::_('COM_USERS_MASSMAIL_FORM_NEW'); ?>" class="main-card p-4 form-validate">
<div class="row">
<div class="col-lg-9">
<div class="control-group">
<?php echo $this->form->getLabel('subject'); ?>
<span class="input-group">
<?php if (!empty($comUserParams->get('mailSubjectPrefix'))) : ?>
<span class="input-group-text"><?php echo $comUserParams->get('mailSubjectPrefix'); ?></span>
<?php endif; ?>
<?php echo $this->form->getInput('subject'); ?>
</span>
</div>
<div class="control-group">
<?php echo $this->form->getLabel('message'); ?>
<?php echo $this->form->getInput('message'); ?>
<?php if (!empty($comUserParams->get('mailBodySuffix'))) : ?>
<div class="mt-1 card">
<div class="card-body">
<?php echo $comUserParams->get('mailBodySuffix'); ?>
</div>
</div>
<?php endif; ?>
</div>
</div>
<div class="col-lg-3">
<?php echo $this->form->renderField('recurse'); ?>
<?php echo $this->form->renderField('mode'); ?>
<?php echo $this->form->renderField('disabled'); ?>
<?php echo $this->form->renderField('bcc'); ?>
<?php echo $this->form->renderField('group'); ?>
</div>
</div>
<input type="hidden" name="task" value="">
<?php echo HTMLHelper::_('form.token'); ?>
</form>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<metadata>
<layout title="COM_USERS_MAIL_VIEW_DEFAULT_TITLE">
<message>
<![CDATA[COM_USERS_MAIL_VIEW_DEFAULT_DESC]]>
</message>
</layout>
</metadata>

View File

@ -0,0 +1,80 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_users
*
* @copyright (C) 2022 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
// Prevent direct access
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route;
use Joomla\Component\Users\Administrator\View\Method\HtmlView;
/** @var HtmlView $this */
HTMLHelper::_('bootstrap.tooltip', '.hasTooltip');
$cancelURL = Route::_('index.php?option=com_users&task=methods.display&user_id=' . $this->user->id);
if (!empty($this->returnURL)) {
$cancelURL = $this->escape(base64_decode($this->returnURL));
}
if ($this->record->method != 'backupcodes') {
throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403);
}
?>
<h2>
<?php echo Text::_('COM_USERS_USER_BACKUPCODES') ?>
</h2>
<p class="text-muted">
<?php echo Text::_('COM_USERS_USER_BACKUPCODES_DESC') ?>
</p>
<table class="table table-striped">
<?php for ($i = 0; $i < (count($this->backupCodes) / 2); $i++) : ?>
<tr>
<td>
<?php if (!empty($this->backupCodes[2 * $i])) : ?>
<?php // This is a Key emoji; we can hide it from screen readers ?>
<span aria-hidden="true">&#128273;</span>
<?php echo $this->backupCodes[2 * $i] ?>
<?php endif; ?>
</td>
<td>
<?php if (!empty($this->backupCodes[1 + 2 * $i])) : ?>
<?php // This is a Key emoji; we can hide it from screen readers ?>
<span aria-hidden="true">&#128273;</span>
<?php echo $this->backupCodes[1 + 2 * $i] ?>
<?php endif ;?>
</td>
</tr>
<?php endfor; ?>
</table>
<div class="alert alert-info">
<span class="icon-info-circle" aria-hidden="true"></span>
<?php echo Text::_('COM_USERS_MFA_BACKUPCODES_RESET_INFO'); ?>
</div>
<div class="d-sm-none">
<a class="btn btn-danger" href="<?php echo Route::_(sprintf("index.php?option=com_users&task=method.regenerateBackupCodes&user_id=%s&%s=1%s", $this->user->id, Factory::getApplication()->getFormToken(), empty($this->returnURL) ? '' : '&returnurl=' . $this->returnURL)) ?>">
<span class="icon icon-refresh" aria-hidden="true"></span>
<?php echo Text::_('COM_USERS_MFA_BACKUPCODES_RESET'); ?>
</a>
<a href="<?php echo $cancelURL ?>"
class="btn btn-secondary">
<span class="icon icon-cancel-2 icon-ban-circle"></span>
<?php echo Text::_('JCANCEL'); ?>
</a>
</div>

View File

@ -0,0 +1,185 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_users
*
* @copyright (C) 2022 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
// Prevent direct access
defined('_JEXEC') or die;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route;
use Joomla\Component\Users\Administrator\View\Method\HtmlView;
use Joomla\Utilities\ArrayHelper;
/** @var HtmlView $this */
$cancelURL = Route::_('index.php?option=com_users&task=methods.display&user_id=' . $this->user->id);
if (!empty($this->returnURL)) {
$cancelURL = $this->escape(base64_decode($this->returnURL));
}
$recordId = (int) $this->record->id ?? 0;
$method = $this->record->method ?? $this->getModel()->getState('method');
$userId = (int) $this->user->id ?? 0;
$headingLevel = 2;
$hideSubmit = !$this->renderOptions['show_submit'] && !$this->isEditExisting
?>
<div class="card card-body">
<form action="<?php echo Route::_(sprintf("index.php?option=com_users&task=method.save&id=%d&method=%s&user_id=%d", $recordId, $method, $userId)) ?>"
class="form form-horizontal" id="com-users-method-edit" method="post">
<?php echo HTMLHelper::_('form.token') ?>
<?php if (!empty($this->returnURL)) : ?>
<input type="hidden" name="returnurl" value="<?php echo $this->escape($this->returnURL) ?>">
<?php endif; ?>
<?php if (!empty($this->renderOptions['hidden_data'])) : ?>
<?php foreach ($this->renderOptions['hidden_data'] as $key => $value) : ?>
<input type="hidden" name="<?php echo $this->escape($key) ?>" value="<?php echo $this->escape($value) ?>">
<?php endforeach; ?>
<?php endif; ?>
<?php if (!empty($this->title)) : ?>
<?php if (!empty($this->renderOptions['help_url'])) : ?>
<span class="float-end">
<a href="<?php echo $this->renderOptions['help_url'] ?>"
class="btn btn-sm btn-dark"
target="_blank"
>
<span class="icon icon-question-sign" aria-hidden="true"></span>
<span class="visually-hidden"><?php echo Text::_('JHELP') ?></span>
</a>
</span>
<?php endif;?>
<h<?php echo $headingLevel ?> id="com-users-method-edit-head">
<?php echo Text::_($this->title) ?>
</h<?php echo $headingLevel ?>>
<?php $headingLevel++ ?>
<?php endif; ?>
<div class="row">
<label class="col-sm-3 col-form-label"
for="com-users-method-edit-title">
<?php echo Text::_('COM_USERS_MFA_EDIT_FIELD_TITLE'); ?>
</label>
<div class="col-sm-9">
<input type="text"
class="form-control"
id="com-users-method-edit-title"
name="title"
value="<?php echo $this->escape($this->record->title) ?>"
aria-describedby="com-users-method-edit-help">
<p class="form-text" id="com-users-method-edit-help">
<?php echo $this->escape(Text::_('COM_USERS_MFA_EDIT_FIELD_TITLE_DESC')) ?>
</p>
</div>
</div>
<div class="row">
<div class="col-sm-9 offset-sm-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="com-users-is-default-method" <?php echo $this->record->default ? 'checked="checked"' : ''; ?> name="default">
<label class="form-check-label" for="com-users-is-default-method">
<?php echo Text::_('COM_USERS_MFA_EDIT_FIELD_DEFAULT'); ?>
</label>
</div>
</div>
</div>
<?php if (!empty($this->renderOptions['pre_message'])) : ?>
<div class="com-users-method-edit-pre-message text-muted mt-4 mb-3">
<?php echo $this->renderOptions['pre_message'] ?>
</div>
<?php endif; ?>
<?php if (!empty($this->renderOptions['tabular_data'])) : ?>
<div class="com-users-method-edit-tabular-container">
<?php if (!empty($this->renderOptions['table_heading'])) : ?>
<h<?php echo $headingLevel ?> class="h3 border-bottom mb-3">
<?php echo $this->renderOptions['table_heading'] ?>
</h<?php echo $headingLevel ?>>
<?php endif; ?>
<table class="table table-striped">
<tbody>
<?php foreach ($this->renderOptions['tabular_data'] as $cell1 => $cell2) : ?>
<tr>
<td>
<?php echo $cell1 ?>
</td>
<td>
<?php echo $cell2 ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
<?php if ($this->renderOptions['field_type'] == 'custom') : ?>
<?php echo $this->renderOptions['html']; ?>
<?php endif; ?>
<div class="row mb-3 <?php echo $this->renderOptions['input_type'] === 'hidden' ? 'd-none' : '' ?>">
<?php if ($this->renderOptions['label']) : ?>
<label class="col-sm-3 col-form-label" for="com-users-method-code">
<?php echo $this->renderOptions['label']; ?>
</label>
<?php endif; ?>
<div class="col-sm-9" <?php echo $this->renderOptions['label'] ? '' : 'offset-sm-3' ?>>
<?php
$attributes = array_merge(
[
'type' => $this->renderOptions['input_type'],
'name' => 'code',
'value' => $this->escape($this->renderOptions['input_value']),
'id' => 'com-users-method-code',
'class' => 'form-control',
'aria-describedby' => 'com-users-method-code-help',
],
$this->renderOptions['input_attributes']
);
if (strpos($attributes['class'], 'form-control') === false) {
$attributes['class'] .= ' form-control';
}
?>
<input <?php echo ArrayHelper::toString($attributes) ?>>
<p class="form-text" id="com-users-method-code-help">
<?php echo $this->escape($this->renderOptions['placeholder']) ?>
</p>
</div>
</div>
<div class="container d-sm-none">
<div class="row mb-3">
<div class="col-sm-9 offset-sm-3">
<button type="submit"
id="user-mfa-edit-save"
class="btn btn-primary me-3 <?php echo $hideSubmit ? 'd-none' : '' ?> <?php echo $this->renderOptions['submit_class'] ?>">
<span class="<?php echo $this->renderOptions['submit_icon'] ?>" aria-hidden="true"></span>
<?php echo Text::_($this->renderOptions['submit_text']); ?>
</button>
<a href="<?php echo $cancelURL ?>"
id="user-mfa-edit-cancel"
class="btn btn-sm btn-danger">
<span class="icon icon-cancel-2" aria-hidden="true"></span>
<?php echo Text::_('JCANCEL'); ?>
</a>
</div>
</div>
</div>
<?php if (!empty($this->renderOptions['post_message'])) : ?>
<div class="com-users-method-edit-post-message text-muted">
<?php echo $this->renderOptions['post_message'] ?>
</div>
<?php endif; ?>
</form>
</div>

View File

@ -0,0 +1,54 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_users
*
* @copyright (C) 2022 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
// Prevent direct access
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route;
use Joomla\Component\Users\Administrator\View\Methods\HtmlView;
/** @var HtmlView $this */
?>
<div id="com-users-methods-list">
<div id="com-users-methods-reset-container" class="d-flex align-items-center border border-1 rounded-3 p-2">
<div id="com-users-methods-reset-message" class="flex-grow-1">
<?php echo Text::_('COM_USERS_MFA_LIST_STATUS_' . ($this->mfaActive ? 'ON' : 'OFF')) ?>
</div>
<?php if ($this->mfaActive) : ?>
<div>
<a href="<?php echo Route::_('index.php?option=com_users&task=methods.disable&' . Factory::getApplication()->getFormToken() . '=1' . ($this->returnURL ? '&returnurl=' . $this->escape(urlencode($this->returnURL)) : '') . '&user_id=' . $this->user->id) ?>"
class="btn btn-danger btn-sm">
<?php echo Text::_('COM_USERS_MFA_LIST_REMOVEALL'); ?>
</a>
</div>
<?php endif; ?>
</div>
<?php if (!count($this->methods)) : ?>
<div id="com-users-methods-list-instructions" class="alert alert-info mt-2">
<span class="icon icon-info-circle" aria-hidden="true"></span>
<?php echo Text::_('COM_USERS_MFA_LIST_INSTRUCTIONS'); ?>
</div>
<?php elseif ($this->isMandatoryMFASetup) : ?>
<div class="alert alert-info my-3">
<h3 class="alert-heading">
<?php echo Text::_('COM_USERS_MFA_MANDATORY_NOTICE_HEAD') ?>
</h3>
<p>
<?php echo Text::_('COM_USERS_MFA_MANDATORY_NOTICE_BODY') ?>
</p>
</div>
<?php endif ?>
<?php $this->setLayout('list');
echo $this->loadTemplate(); ?>
</div>

View File

@ -0,0 +1,50 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_users
*
* @copyright (C) 2022 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
// Prevent direct access
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route;
use Joomla\Component\Users\Administrator\View\Methods\HtmlView;
/** @var HtmlView $this */
$headingLevel = 2;
?>
<div id="com-users-methods-list">
<?php if (!$this->isAdmin) : ?>
<h<?php echo $headingLevel ?> id="com-users-methods-list-head">
<?php echo Text::_('COM_USERS_MFA_FIRSTTIME_PAGE_HEAD'); ?>
</h<?php echo $headingLevel++ ?>>
<?php endif; ?>
<div id="com-users-methods-list-instructions" class="alert alert-info">
<h<?php echo $headingLevel ?> class="alert-heading">
<span class="fa fa-shield-alt" aria-hidden="true"></span>
<?php echo Text::_('COM_USERS_MFA_FIRSTTIME_INSTRUCTIONS_HEAD'); ?>
</h<?php echo $headingLevel ?>>
<p>
<?php echo Text::_('COM_USERS_MFA_FIRSTTIME_INSTRUCTIONS_WHATITDOES'); ?>
</p>
<a href="<?php echo Route::_(
'index.php?option=com_users&task=methods.doNotShowThisAgain' .
($this->returnURL ? '&returnurl=' . $this->escape(urlencode($this->returnURL)) : '') .
'&user_id=' . $this->user->id .
'&' . Factory::getApplication()->getFormToken() . '=1'
)?>"
class="btn btn-danger w-100">
<?php echo Text::_('COM_USERS_MFA_FIRSTTIME_NOTINTERESTED'); ?>
</a>
</div>
<?php $this->setLayout('list');
echo $this->loadTemplate(); ?>
</div>

View File

@ -0,0 +1,144 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_users
*
* @copyright (C) 2022 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
// Prevent direct access
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Uri\Uri;
use Joomla\Component\Users\Administrator\Helper\Mfa as MfaHelper;
use Joomla\Component\Users\Administrator\Model\MethodsModel;
use Joomla\Component\Users\Administrator\View\Methods\HtmlView;
/** @var HtmlView $this */
HTMLHelper::_('bootstrap.tooltip', '.hasTooltip');
/** @var MethodsModel $model */
$model = $this->getModel();
$this->document->getWebAssetManager()->useScript('com_users.two-factor-list');
$canAddEdit = MfaHelper::canAddEditMethod($this->user);
$canDelete = MfaHelper::canDeleteMethod($this->user);
?>
<div id="com-users-methods-list-container">
<?php foreach ($this->methods as $methodName => $method) :
$methodClass = 'com-users-methods-list-method-name-' . htmlentities($method['name'])
. ($this->defaultMethod == $methodName ? ' com-users-methods-list-method-default' : '');
?>
<div class="com-users-methods-list-method <?php echo $methodClass?> <?php echo count($method['active']) ? 'com-users-methods-list-method-active' : '' ?>">
<div class="com-users-methods-list-method-header">
<div class="com-users-methods-list-method-image">
<img src="<?php echo Uri::root() . $method['image'] ?>"
alt="<?php echo $this->escape($method['display']) ?>"
class="img-fluid"
>
</div>
<div class="com-users-methods-list-method-title">
<h3>
<span class="me-1 flex-grow-1">
<?php echo $method['display'] ?>
</span>
<?php if ($this->defaultMethod == $methodName) : ?>
<span id="com-users-methods-list-method-default-tag" class="badge bg-info p-2 fs-4 me-1">
<?php echo Text::_('COM_USERS_MFA_LIST_DEFAULTTAG') ?>
</span>
<?php endif; ?>
</h3>
</div>
</div>
<div class="com-users-methods-list-method-records-container">
<div class="com-users-methods-list-method-info">
<?php echo $method['shortinfo'] ?>
</div>
<?php if (count($method['active'])) : ?>
<div class="com-users-methods-list-method-records pt-2 my-2">
<?php foreach ($method['active'] as $record) : ?>
<div class="com-users-methods-list-method-record d-flex flex-row flex-wrap justify-content-start border-top py-2">
<div class="com-users-methods-list-method-record-info flex-grow-1 d-flex flex-column align-items-start gap-1">
<?php if ($methodName === 'backupcodes') : ?>
<?php if ($canAddEdit) : ?>
<div class="alert alert-info mt-1 w-100">
<?php echo Text::sprintf('COM_USERS_MFA_BACKUPCODES_PRINT_PROMPT_HEAD', Route::_('index.php?option=com_users&task=method.edit&id=' . (int) $record->id . ($this->returnURL ? '&returnurl=' . $this->escape(urlencode($this->returnURL)) : '') . '&user_id=' . $this->user->id)) ?>
</div>
<?php endif ?>
<?php else : ?>
<h4 class="com-users-methods-list-method-record-title-container mb-1 fs-3">
<?php if ($record->default) : ?>
<span id="com-users-methods-list-method-default-badge-small"
class="text-warning me-1 hasTooltip"
title="<?php echo $this->escape(Text::_('COM_USERS_MFA_LIST_DEFAULTTAG')) ?>">
<span class="icon icon-star" aria-hidden="true"></span>
<span class="visually-hidden"><?php echo $this->escape(Text::_('COM_USERS_MFA_LIST_DEFAULTTAG')) ?></span>
</span>
<?php endif; ?>
<span class="com-users-methods-list-method-record-title fw-bold">
<?php echo $this->escape($record->title); ?>
</span>
</h4>
<?php endif; ?>
<div class="com-users-methods-list-method-record-lastused my-1 d-flex flex-row flex-wrap justify-content-start gap-5 text-muted w-100">
<span class="com-users-methods-list-method-record-createdon">
<?php echo Text::sprintf('COM_USERS_MFA_LBL_CREATEDON', $model->formatRelative($record->created_on)) ?>
</span>
<span class="com-users-methods-list-method-record-lastused-date">
<?php echo Text::sprintf('COM_USERS_MFA_LBL_LASTUSED', $model->formatRelative($record->last_used)) ?>
</span>
</div>
</div>
<?php if ($methodName !== 'backupcodes' && ($canAddEdit || $canDelete)) : ?>
<div class="com-users-methods-list-method-record-actions my-2 d-flex flex-row flex-wrap justify-content-center align-content-center align-items-start">
<?php if ($canAddEdit) : ?>
<a class="com-users-methods-list-method-record-edit btn btn-secondary btn-sm mx-1 hasTooltip"
href="<?php echo Route::_('index.php?option=com_users&task=method.edit&id=' . (int) $record->id . ($this->returnURL ? '&returnurl=' . $this->escape(urlencode($this->returnURL)) : '') . '&user_id=' . $this->user->id)?>"
title="<?php echo Text::_('JACTION_EDIT') ?> <?php echo $this->escape($record->title); ?>">
<span class="icon icon-pencil" aria-hidden="true"></span>
<span class="visually-hidden"><?php echo Text::_('JACTION_EDIT') ?> <?php echo $this->escape($record->title); ?></span>
</a>
<?php endif ?>
<?php if ($method['canDisable'] && $canDelete) : ?>
<a class="com-users-methods-list-method-record-delete btn btn-danger btn-sm mx-1 hasTooltip"
href="<?php echo Route::_('index.php?option=com_users&task=method.delete&id=' . (int) $record->id . ($this->returnURL ? '&returnurl=' . $this->escape(urlencode($this->returnURL)) : '') . '&user_id=' . $this->user->id . '&' . Factory::getApplication()->getFormToken() . '=1')?>"
title="<?php echo Text::_('JACTION_DELETE') ?> <?php echo $this->escape($record->title); ?>">
<span class="icon icon-trash" aria-hidden="true"></span>
<span class="visually-hidden"><?php echo Text::_('JACTION_DELETE') ?> <?php echo $this->escape($record->title); ?></span>
</a>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php if ($canAddEdit && (empty($method['active']) || $method['allowMultiple'])) : ?>
<div class="com-users-methods-list-method-addnew-container border-top pt-2">
<a href="<?php echo Route::_('index.php?option=com_users&task=method.add&method=' . $this->escape(urlencode($method['name'])) . ($this->returnURL ? '&returnurl=' . $this->escape(urlencode($this->returnURL)) : '') . '&user_id=' . $this->user->id)?>"
class="com-users-methods-list-method-addnew btn btn-primary btn-sm"
>
<span class="icon-plus-2" aria-hidden="true"></span>
<?php echo Text::sprintf('COM_USERS_MFA_ADD_AUTHENTICATOR_OF_TYPE', $method['display']) ?>
</a>
</div>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
</div>

Some files were not shown because too many files have changed in this diff Show More