aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--config/config.exs2
-rw-r--r--config/ejabberd.exs169
-rw-r--r--config/ejabberd.yml667
-rw-r--r--ejabberd.yml.example10
-rw-r--r--include/ejabberd_service.hrl20
-rw-r--r--include/mod_muc_room.hrl10
-rw-r--r--include/ns.hrl2
-rw-r--r--lib/ejabberd/config/attr.ex119
-rw-r--r--lib/ejabberd/config/config.ex145
-rw-r--r--lib/ejabberd/config/ejabberd_hook.ex23
-rw-r--r--lib/ejabberd/config/ejabberd_module.ex70
-rw-r--r--lib/ejabberd/config/logger/ejabberd_logger.ex32
-rw-r--r--lib/ejabberd/config/opts_formatter.ex46
-rw-r--r--lib/ejabberd/config/store.ex55
-rw-r--r--lib/ejabberd/config/validator/validation.ex40
-rw-r--r--lib/ejabberd/config/validator/validator_attrs.ex28
-rw-r--r--lib/ejabberd/config/validator/validator_dependencies.ex30
-rw-r--r--lib/ejabberd/config/validator/validator_utility.ex30
-rw-r--r--lib/ejabberd/config_util.ex18
-rw-r--r--lib/ejabberd/module.ex19
-rw-r--r--lib/mix/tasks/deps.tree.ex94
-rw-r--r--lib/mod_presence_demo.ex12
-rw-r--r--mix.exs10
-rw-r--r--mix.lock12
-rw-r--r--rebar.config2
-rw-r--r--src/acl.erl4
-rw-r--r--src/ejabberd_app.erl28
-rw-r--r--src/ejabberd_c2s.erl88
-rw-r--r--src/ejabberd_config.erl38
-rw-r--r--src/ejabberd_http.erl7
-rw-r--r--src/ejabberd_http_bind.erl3
-rw-r--r--src/ejabberd_http_ws.erl3
-rw-r--r--src/ejabberd_oauth.erl2
-rw-r--r--src/ejabberd_service.erl161
-rw-r--r--src/jlib.erl9
-rw-r--r--src/mod_delegation.erl538
-rw-r--r--src/mod_http_api.erl25
-rw-r--r--src/mod_muc.erl26
-rw-r--r--src/mod_muc_admin.erl67
-rw-r--r--src/mod_muc_room.erl445
-rw-r--r--src/mod_privilege.erl363
-rw-r--r--src/mod_pubsub.erl2
-rw-r--r--src/node_flat_sql.erl5
-rw-r--r--src/node_mb.erl3
-rw-r--r--src/node_mb_sql.erl151
-rw-r--r--test/ejabberd_commands_mock_test.exs1
-rw-r--r--test/ejabberd_oauth_mock.exs5
-rw-r--r--test/elixir-config/attr_test.exs87
-rw-r--r--test/elixir-config/config_test.exs65
-rw-r--r--test/elixir-config/ejabberd_logger.exs49
-rw-r--r--test/elixir-config/shared/ejabberd.exs31
-rw-r--r--test/elixir-config/shared/ejabberd_different_from_default.exs9
-rw-r--r--test/elixir-config/shared/ejabberd_for_validation.exs20
-rw-r--r--test/elixir-config/validation_test.exs32
54 files changed, 3616 insertions, 316 deletions
diff --git a/config/config.exs b/config/config.exs
index 4d3783480..0d1a3c720 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -4,7 +4,7 @@ use Mix.Config
config :ejabberd,
file: "config/ejabberd.yml",
log_path: 'log/ejabberd.log'
-
+
# Customize Mnesia directory:
config :mnesia,
dir: 'mnesiadb/'
diff --git a/config/ejabberd.exs b/config/ejabberd.exs
new file mode 100644
index 000000000..05c2b5d83
--- /dev/null
+++ b/config/ejabberd.exs
@@ -0,0 +1,169 @@
+defmodule Ejabberd.ConfigFile do
+ use Ejabberd.Config
+
+ def start do
+ [loglevel: 4,
+ log_rotate_size: 10485760,
+ log_rotate_date: "",
+ log_rotate_count: 1,
+ log_rate_limit: 100,
+ auth_method: :internal,
+ max_fsm_queue: 1000,
+ language: "en",
+ allow_contrib_modules: true,
+ hosts: ["localhost"],
+ shaper: shaper,
+ acl: acl,
+ access: access]
+ end
+
+ defp shaper do
+ [normal: 1000,
+ fast: 50000,
+ max_fsm_queue: 1000]
+ end
+
+ defp acl do
+ [local:
+ [user_regexp: "", loopback: [ip: "127.0.0.0/8"]]]
+ end
+
+ defp access do
+ [max_user_sessions: [all: 10],
+ max_user_offline_messages: [admin: 5000, all: 100],
+ local: [local: :allow],
+ c2s: [blocked: :deny, all: :allow],
+ c2s_shaper: [admin: :none, all: :normal],
+ s2s_shaper: [all: :fast],
+ announce: [admin: :allow],
+ configure: [admin: :allow],
+ muc_admin: [admin: :allow],
+ muc_create: [local: :allow],
+ muc: [all: :allow],
+ pubsub_createnode: [local: :allow],
+ register: [all: :allow],
+ trusted_network: [loopback: :allow]]
+ end
+
+ listen :ejabberd_c2s do
+ @opts [
+ port: 5222,
+ max_stanza_size: 65536,
+ shaper: :c2s_shaper,
+ access: :c2s]
+ end
+
+ listen :ejabberd_s2s_in do
+ @opts [port: 5269]
+ end
+
+ listen :ejabberd_http do
+ @opts [
+ port: 5280,
+ web_admin: true,
+ http_poll: true,
+ http_bind: true,
+ captcha: true]
+ end
+
+ module :mod_adhoc do
+ end
+
+ module :mod_announce do
+ @opts [access: :announce]
+ end
+
+ module :mod_blocking do
+ end
+
+ module :mod_caps do
+ end
+
+ module :mod_carboncopy do
+ end
+
+ module :mod_client_state do
+ @opts [
+ drop_chat_states: true,
+ queue_presence: false]
+ end
+
+ module :mod_configure do
+ end
+
+ module :mod_disco do
+ end
+
+ module :mod_irc do
+ end
+
+ module :mod_http_bind do
+ end
+
+ module :mod_last do
+ end
+
+ module :mod_muc do
+ @opts [
+ access: :muc,
+ access_create: :muc_create,
+ access_persistent: :muc_create,
+ access_admin: :muc_admin]
+ end
+
+ module :mod_offline do
+ @opts [access_max_user_messages: :max_user_offline_messages]
+ end
+
+ module :mod_ping do
+ end
+
+ module :mod_privacy do
+ end
+
+ module :mod_private do
+ end
+
+ module :mod_pubsub do
+ @opts [
+ access_createnode: :pubsub_createnode,
+ ignore_pep_from_offline: true,
+ last_item_cache: true,
+ plugins: ["flat", "hometree", "pep"]]
+ end
+
+ module :mod_register do
+ @opts [welcome_message: [
+ subject: "Welcome!",
+ body: "Hi.\nWelcome to this XMPP Server",
+ ip_access: :trusted_network,
+ access: :register]]
+ end
+
+ module :mod_roster do
+ end
+
+ module :mod_shared_roster do
+ end
+
+ module :mod_stats do
+ end
+
+ module :mod_time do
+ end
+
+ module :mod_version do
+ end
+
+ # Example of how to define a hook, called when the event
+ # specified is triggered.
+ #
+ # @event: Name of the event
+ # @opts: Params are optional. Available: :host and :priority.
+ # If missing, defaults are used. (host: :global | priority: 50)
+ # @callback Could be an anonymous function or a callback from a module,
+ # use the &ModuleName.function/arity format for that.
+ hook :register_user, [host: "localhost"], fn(user, server) ->
+ info("User registered: #{user} on #{server}")
+ end
+end
diff --git a/config/ejabberd.yml b/config/ejabberd.yml
new file mode 100644
index 000000000..80fc3c622
--- /dev/null
+++ b/config/ejabberd.yml
@@ -0,0 +1,667 @@
+###
+### ejabberd configuration file
+###
+###
+
+### The parameters used in this configuration file are explained in more detail
+### in the ejabberd Installation and Operation Guide.
+### Please consult the Guide in case of doubts, it is included with
+### your copy of ejabberd, and is also available online at
+### http://www.process-one.net/en/ejabberd/docs/
+
+### The configuration file is written in YAML.
+### Refer to http://en.wikipedia.org/wiki/YAML for the brief description.
+### However, ejabberd treats different literals as different types:
+###
+### - unquoted or single-quoted strings. They are called "atoms".
+### Example: dog, 'Jupiter', '3.14159', YELLOW
+###
+### - numeric literals. Example: 3, -45.0, .0
+###
+### - quoted or folded strings.
+### Examples of quoted string: "Lizzard", "orange".
+### Example of folded string:
+### > Art thou not Romeo,
+### and a Montague?
+
+### =======
+### LOGGING
+
+##
+## loglevel: Verbosity of log files generated by ejabberd.
+## 0: No ejabberd log at all (not recommended)
+## 1: Critical
+## 2: Error
+## 3: Warning
+## 4: Info
+## 5: Debug
+##
+loglevel: 4
+
+##
+## rotation: Describe how to rotate logs. Either size and/or date can trigger
+## log rotation. Setting count to N keeps N rotated logs. Setting count to 0
+## does not disable rotation, it instead rotates the file and keeps no previous
+## versions around. Setting size to X rotate log when it reaches X bytes.
+## To disable rotation set the size to 0 and the date to ""
+## Date syntax is taken from the syntax newsyslog uses in newsyslog.conf.
+## Some examples:
+## $D0 rotate every night at midnight
+## $D23 rotate every day at 23:00 hr
+## $W0D23 rotate every week on Sunday at 23:00 hr
+## $W5D16 rotate every week on Friday at 16:00 hr
+## $M1D0 rotate on the first day of every month at midnight
+## $M5D6 rotate on every 5th day of the month at 6:00 hr
+##
+log_rotate_size: 10485760
+log_rotate_date: ""
+log_rotate_count: 1
+
+##
+## overload protection: If you want to limit the number of messages per second
+## allowed from error_logger, which is a good idea if you want to avoid a flood
+## of messages when system is overloaded, you can set a limit.
+## 100 is ejabberd's default.
+log_rate_limit: 100
+
+##
+## watchdog_admins: Only useful for developers: if an ejabberd process
+## consumes a lot of memory, send live notifications to these XMPP
+## accounts.
+##
+## watchdog_admins:
+## - "bob@example.com"
+
+
+### ================
+### SERVED HOSTNAMES
+
+##
+## hosts: Domains served by ejabberd.
+## You can define one or several, for example:
+## hosts:
+## - "example.net"
+## - "example.com"
+## - "example.org"
+##
+hosts:
+ - "localhost"
+
+##
+## route_subdomains: Delegate subdomains to other XMPP servers.
+## For example, if this ejabberd serves example.org and you want
+## to allow communication with an XMPP server called im.example.org.
+##
+## route_subdomains: s2s
+
+### ===============
+### LISTENING PORTS
+
+##
+## listen: The ports ejabberd will listen on, which service each is handled
+## by and what options to start it with.
+##
+listen:
+ -
+ port: 5222
+ module: ejabberd_c2s
+ ##
+ ## If TLS is compiled in and you installed a SSL
+ ## certificate, specify the full path to the
+ ## file and uncomment these lines:
+ ##
+ ## certfile: "/path/to/ssl.pem"
+ ## starttls: true
+ ##
+ ## To enforce TLS encryption for client connections,
+ ## use this instead of the "starttls" option:
+ ##
+ ## starttls_required: true
+ ##
+ ## Custom OpenSSL options
+ ##
+ ## protocol_options:
+ ## - "no_sslv3"
+ ## - "no_tlsv1"
+ max_stanza_size: 65536
+ shaper: c2s_shaper
+ access: c2s
+ -
+ port: 5269
+ module: ejabberd_s2s_in
+ ##
+ ## ejabberd_service: Interact with external components (transports, ...)
+ ##
+ ## -
+ ## port: 8888
+ ## module: ejabberd_service
+ ## access: all
+ ## shaper_rule: fast
+ ## ip: "127.0.0.1"
+ ## hosts:
+ ## "icq.example.org":
+ ## password: "secret"
+ ## "sms.example.org":
+ ## password: "secret"
+
+ ##
+ ## ejabberd_stun: Handles STUN Binding requests
+ ##
+ ## -
+ ## port: 3478
+ ## transport: udp
+ ## module: ejabberd_stun
+
+ ##
+ ## To handle XML-RPC requests that provide admin credentials:
+ ##
+ ## -
+ ## port: 4560
+ ## module: ejabberd_xmlrpc
+ -
+ port: 5280
+ module: ejabberd_http
+ ## request_handlers:
+ ## "/pub/archive": mod_http_fileserver
+ web_admin: true
+ http_poll: true
+ http_bind: true
+ ## register: true
+ captcha: true
+
+##
+## s2s_use_starttls: Enable STARTTLS + Dialback for S2S connections.
+## Allowed values are: false optional required required_trusted
+## You must specify a certificate file.
+##
+## s2s_use_starttls: optional
+
+##
+## s2s_certfile: Specify a certificate file.
+##
+## s2s_certfile: "/path/to/ssl.pem"
+
+## Custom OpenSSL options
+##
+## s2s_protocol_options:
+## - "no_sslv3"
+## - "no_tlsv1"
+
+##
+## domain_certfile: Specify a different certificate for each served hostname.
+##
+## host_config:
+## "example.org":
+## domain_certfile: "/path/to/example_org.pem"
+## "example.com":
+## domain_certfile: "/path/to/example_com.pem"
+
+##
+## S2S whitelist or blacklist
+##
+## Default s2s policy for undefined hosts.
+##
+## s2s_access: s2s
+
+##
+## Outgoing S2S options
+##
+## Preferred address families (which to try first) and connect timeout
+## in milliseconds.
+##
+## outgoing_s2s_families:
+## - ipv4
+## - ipv6
+## outgoing_s2s_timeout: 10000
+
+### ==============
+### AUTHENTICATION
+
+##
+## auth_method: Method used to authenticate the users.
+## The default method is the internal.
+## If you want to use a different method,
+## comment this line and enable the correct ones.
+##
+auth_method: internal
+
+##
+## Store the plain passwords or hashed for SCRAM:
+## auth_password_format: plain
+## auth_password_format: scram
+##
+## Define the FQDN if ejabberd doesn't detect it:
+## fqdn: "server3.example.com"
+
+##
+## Authentication using external script
+## Make sure the script is executable by ejabberd.
+##
+## auth_method: external
+## extauth_program: "/path/to/authentication/script"
+
+##
+## Authentication using ODBC
+## Remember to setup a database in the next section.
+##
+## auth_method: odbc
+
+##
+## Authentication using PAM
+##
+## auth_method: pam
+## pam_service: "pamservicename"
+
+##
+## Authentication using LDAP
+##
+## auth_method: ldap
+##
+## List of LDAP servers:
+## ldap_servers:
+## - "localhost"
+##
+## Encryption of connection to LDAP servers:
+## ldap_encrypt: none
+## ldap_encrypt: tls
+##
+## Port to connect to on LDAP servers:
+## ldap_port: 389
+## ldap_port: 636
+##
+## LDAP manager:
+## ldap_rootdn: "dc=example,dc=com"
+##
+## Password of LDAP manager:
+## ldap_password: "******"
+##
+## Search base of LDAP directory:
+## ldap_base: "dc=example,dc=com"
+##
+## LDAP attribute that holds user ID:
+## ldap_uids:
+## - "mail": "%u@mail.example.org"
+##
+## LDAP filter:
+## ldap_filter: "(objectClass=shadowAccount)"
+
+##
+## Anonymous login support:
+## auth_method: anonymous
+## anonymous_protocol: sasl_anon | login_anon | both
+## allow_multiple_connections: true | false
+##
+## host_config:
+## "public.example.org":
+## auth_method: anonymous
+## allow_multiple_connections: false
+## anonymous_protocol: sasl_anon
+##
+## To use both anonymous and internal authentication:
+##
+## host_config:
+## "public.example.org":
+## auth_method:
+## - internal
+## - anonymous
+
+### ==============
+### DATABASE SETUP
+
+## ejabberd by default uses the internal Mnesia database,
+## so you do not necessarily need this section.
+## This section provides configuration examples in case
+## you want to use other database backends.
+## Please consult the ejabberd Guide for details on database creation.
+
+##
+## MySQL server:
+##
+## odbc_type: mysql
+## odbc_server: "server"
+## odbc_database: "database"
+## odbc_username: "username"
+## odbc_password: "password"
+##
+## If you want to specify the port:
+## odbc_port: 1234
+
+##
+## PostgreSQL server:
+##
+## odbc_type: pgsql
+## odbc_server: "server"
+## odbc_database: "database"
+## odbc_username: "username"
+## odbc_password: "password"
+##
+## If you want to specify the port:
+## odbc_port: 1234
+##
+## If you use PostgreSQL, have a large database, and need a
+## faster but inexact replacement for "select count(*) from users"
+##
+## pgsql_users_number_estimate: true
+
+##
+## ODBC compatible or MSSQL server:
+##
+## odbc_type: odbc
+## odbc_server: "DSN=ejabberd;UID=ejabberd;PWD=ejabberd"
+
+##
+## Number of connections to open to the database for each virtual host
+##
+## odbc_pool_size: 10
+
+##
+## Interval to make a dummy SQL request to keep the connections to the
+## database alive. Specify in seconds: for example 28800 means 8 hours
+##
+## odbc_keepalive_interval: undefined
+
+### ===============
+### TRAFFIC SHAPERS
+
+shaper:
+ ##
+ ## The "normal" shaper limits traffic speed to 1000 B/s
+ ##
+ normal: 1000
+
+ ##
+ ## The "fast" shaper limits traffic speed to 50000 B/s
+ ##
+ fast: 50000
+
+##
+## This option specifies the maximum number of elements in the queue
+## of the FSM. Refer to the documentation for details.
+##
+max_fsm_queue: 1000
+
+###. ====================
+###' ACCESS CONTROL LISTS
+acl:
+ ##
+ ## The 'admin' ACL grants administrative privileges to XMPP accounts.
+ ## You can put here as many accounts as you want.
+ ##
+ ## admin:
+ ## user:
+ ## - "aleksey": "localhost"
+ ## - "ermine": "example.org"
+ ##
+ ## Blocked users
+ ##
+ ## blocked:
+ ## user:
+ ## - "baduser": "example.org"
+ ## - "test"
+
+ ## Local users: don't modify this.
+ ##
+ local:
+ user_regexp: ""
+
+ ##
+ ## More examples of ACLs
+ ##
+ ## jabberorg:
+ ## server:
+ ## - "jabber.org"
+ ## aleksey:
+ ## user:
+ ## - "aleksey": "jabber.ru"
+ ## test:
+ ## user_regexp: "^test"
+ ## user_glob: "test*"
+
+ ##
+ ## Loopback network
+ ##
+ loopback:
+ ip:
+ - "127.0.0.0/8"
+
+ ##
+ ## Bad XMPP servers
+ ##
+ ## bad_servers:
+ ## server:
+ ## - "xmpp.zombie.org"
+ ## - "xmpp.spam.com"
+
+##
+## Define specific ACLs in a virtual host.
+##
+## host_config:
+## "localhost":
+## acl:
+## admin:
+## user:
+## - "bob-local": "localhost"
+
+### ============
+### ACCESS RULES
+access:
+ ## Maximum number of simultaneous sessions allowed for a single user:
+ max_user_sessions:
+ all: 10
+ ## Maximum number of offline messages that users can have:
+ max_user_offline_messages:
+ admin: 5000
+ all: 100
+ ## This rule allows access only for local users:
+ local:
+ local: allow
+ ## Only non-blocked users can use c2s connections:
+ c2s:
+ blocked: deny
+ all: allow
+ ## For C2S connections, all users except admins use the "normal" shaper
+ c2s_shaper:
+ admin: none
+ all: normal
+ ## All S2S connections use the "fast" shaper
+ s2s_shaper:
+ all: fast
+ ## Only admins can send announcement messages:
+ announce:
+ admin: allow
+ ## Only admins can use the configuration interface:
+ configure:
+ admin: allow
+ ## Admins of this server are also admins of the MUC service:
+ muc_admin:
+ admin: allow
+ ## Only accounts of the local ejabberd server can create rooms:
+ muc_create:
+ local: allow
+ ## All users are allowed to use the MUC service:
+ muc:
+ all: allow
+ ## Only accounts on the local ejabberd server can create Pubsub nodes:
+ pubsub_createnode:
+ local: allow
+ ## In-band registration allows registration of any possible username.
+ ## To disable in-band registration, replace 'allow' with 'deny'.
+ register:
+ all: allow
+ ## Only allow to register from localhost
+ trusted_network:
+ loopback: allow
+ ## Do not establish S2S connections with bad servers
+ ## s2s:
+ ## bad_servers: deny
+ ## all: allow
+
+## By default the frequency of account registrations from the same IP
+## is limited to 1 account every 10 minutes. To disable, specify: infinity
+## registration_timeout: 600
+
+##
+## Define specific Access Rules in a virtual host.
+##
+## host_config:
+## "localhost":
+## access:
+## c2s:
+## admin: allow
+## all: deny
+## register:
+## all: deny
+
+### ================
+### DEFAULT LANGUAGE
+
+##
+## language: Default language used for server messages.
+##
+language: "en"
+
+##
+## Set a different default language in a virtual host.
+##
+## host_config:
+## "localhost":
+## language: "ru"
+
+### =======
+### CAPTCHA
+
+##
+## Full path to a script that generates the image.
+##
+## captcha_cmd: "/lib/ejabberd/priv/bin/captcha.sh"
+
+##
+## Host for the URL and port where ejabberd listens for CAPTCHA requests.
+##
+## captcha_host: "example.org:5280"
+
+##
+## Limit CAPTCHA calls per minute for JID/IP to avoid DoS.
+##
+## captcha_limit: 5
+
+### =======
+### MODULES
+
+##
+## Modules enabled in all ejabberd virtual hosts.
+##
+modules:
+ mod_adhoc: {}
+ ## mod_admin_extra: {}
+ mod_announce: # recommends mod_adhoc
+ access: announce
+ mod_blocking: {} # requires mod_privacy
+ mod_caps: {}
+ mod_carboncopy: {}
+ mod_client_state:
+ drop_chat_states: true
+ queue_presence: false
+ mod_configure: {} # requires mod_adhoc
+ mod_disco: {}
+ ## mod_echo: {}
+ mod_irc: {}
+ mod_http_bind: {}
+ ## mod_http_fileserver:
+ ## docroot: "/var/www"
+ ## accesslog: "/var/log/ejabberd/access.log"
+ mod_last: {}
+ mod_muc:
+ ## host: "conference.@HOST@"
+ access: muc
+ access_create: muc_create
+ access_persistent: muc_create
+ access_admin: muc_admin
+ ## mod_muc_log: {}
+ mod_offline:
+ access_max_user_messages: max_user_offline_messages
+ mod_ping: {}
+ ## mod_pres_counter:
+ ## count: 5
+ ## interval: 60
+ mod_privacy: {}
+ mod_private: {}
+ ## mod_proxy65: {}
+ mod_pubsub:
+ access_createnode: pubsub_createnode
+ ## reduces resource comsumption, but XEP incompliant
+ ignore_pep_from_offline: true
+ ## XEP compliant, but increases resource comsumption
+ ## ignore_pep_from_offline: false
+ last_item_cache: false
+ plugins:
+ - "flat"
+ - "hometree"
+ - "pep" # pep requires mod_caps
+ mod_register:
+ ##
+ ## Protect In-Band account registrations with CAPTCHA.
+ ##
+ ## captcha_protected: true
+
+ ##
+ ## Set the minimum informational entropy for passwords.
+ ##
+ ## password_strength: 32
+
+ ##
+ ## After successful registration, the user receives
+ ## a message with this subject and body.
+ ##
+ welcome_message:
+ subject: "Welcome!"
+ body: |-
+ Hi.
+ Welcome to this XMPP server.
+
+ ##
+ ## When a user registers, send a notification to
+ ## these XMPP accounts.
+ ##
+ ## registration_watchers:
+ ## - "admin1@example.org"
+
+ ##
+ ## Only clients in the server machine can register accounts
+ ##
+ ip_access: trusted_network
+
+ ##
+ ## Local c2s or remote s2s users cannot register accounts
+ ##
+ ## access_from: deny
+
+ access: register
+ mod_roster: {}
+ mod_shared_roster: {}
+ mod_stats: {}
+ mod_time: {}
+ mod_vcard: {}
+ mod_version: {}
+
+##
+## Enable modules with custom options in a specific virtual host
+##
+## host_config:
+## "localhost":
+## modules:
+## mod_echo:
+## host: "mirror.localhost"
+
+##
+## Enable modules management via ejabberdctl for installation and
+## uninstallation of public/private contributed modules
+## (enabled by default)
+##
+
+allow_contrib_modules: true
+
+### Local Variables:
+### mode: yaml
+### End:
+### vim: set filetype=yaml tabstop=8
diff --git a/ejabberd.yml.example b/ejabberd.yml.example
index 72439e5e1..dae839fdc 100644
--- a/ejabberd.yml.example
+++ b/ejabberd.yml.example
@@ -147,6 +147,15 @@ listen:
## access: all
## shaper_rule: fast
## ip: "127.0.0.1"
+ ## privilege_access:
+ ## roster: "both"
+ ## message: "outgoing"
+ ## presence: "roster"
+ ## delegations:
+ ## "urn:xmpp:mam:1":
+ ## filtering: ["node"]
+ ## "http://jabber.org/protocol/pubsub":
+ ## filtering: []
## hosts:
## "icq.example.org":
## password: "secret"
@@ -580,6 +589,7 @@ modules:
mod_carboncopy: {}
mod_client_state: {}
mod_configure: {} # requires mod_adhoc
+ ##mod_delegation: {} # for xep0356
mod_disco: {}
## mod_echo: {}
mod_irc: {}
diff --git a/include/ejabberd_service.hrl b/include/ejabberd_service.hrl
new file mode 100644
index 000000000..7cd3b6943
--- /dev/null
+++ b/include/ejabberd_service.hrl
@@ -0,0 +1,20 @@
+-include("ejabberd.hrl").
+-include("logger.hrl").
+-include("jlib.hrl").
+
+-type filter_attr() :: {binary(), [binary()]}.
+
+-record(state,
+ {socket :: ejabberd_socket:socket_state(),
+ sockmod = ejabberd_socket :: ejabberd_socket | ejabberd_frontend_socket,
+ streamid = <<"">> :: binary(),
+ host_opts = dict:new() :: ?TDICT,
+ host = <<"">> :: binary(),
+ access :: atom(),
+ check_from = true :: boolean(),
+ server_hosts = ?MYHOSTS :: [binary()],
+ privilege_access :: [attr()],
+ delegations :: [filter_attr()],
+ last_pres = dict:new() :: ?TDICT}).
+
+-type(state() :: #state{} ).
diff --git a/include/mod_muc_room.hrl b/include/mod_muc_room.hrl
index d985f3f3b..551da7285 100644
--- a/include/mod_muc_room.hrl
+++ b/include/mod_muc_room.hrl
@@ -77,11 +77,15 @@
jid :: jid(),
nick :: binary(),
role :: role(),
- is_subscriber = false :: boolean(),
- subscriptions = [] :: [binary()],
+ %%is_subscriber = false :: boolean(),
+ %%subscriptions = [] :: [binary()],
last_presence :: xmlel()
}).
+-record(subscriber, {jid :: jid(),
+ nick = <<>> :: binary(),
+ nodes = [] :: [binary()]}).
+
-record(activity,
{
message_time = 0 :: integer(),
@@ -101,6 +105,8 @@
jid = #jid{} :: jid(),
config = #config{} :: config(),
users = (?DICT):new() :: ?TDICT,
+ subscribers = (?DICT):new() :: ?TDICT,
+ subscriber_nicks = (?DICT):new() :: ?TDICT,
last_voice_request_time = treap:empty() :: treap:treap(),
robots = (?DICT):new() :: ?TDICT,
nicks = (?DICT):new() :: ?TDICT,
diff --git a/include/ns.hrl b/include/ns.hrl
index a150746e7..3dbc765b0 100644
--- a/include/ns.hrl
+++ b/include/ns.hrl
@@ -164,6 +164,8 @@
-define(NS_MIX_NODES_PARTICIPANTS, <<"urn:xmpp:mix:nodes:participants">>).
-define(NS_MIX_NODES_SUBJECT, <<"urn:xmpp:mix:nodes:subject">>).
-define(NS_MIX_NODES_CONFIG, <<"urn:xmpp:mix:nodes:config">>).
+-define(NS_PRIVILEGE, <<"urn:xmpp:privilege:1">>).
+-define(NS_DELEGATION, <<"urn:xmpp:delegation:1">>).
-define(NS_MUCSUB, <<"urn:xmpp:mucsub:0">>).
-define(NS_MUCSUB_NODES_PRESENCE, <<"urn:xmpp:mucsub:nodes:presence">>).
-define(NS_MUCSUB_NODES_MESSAGES, <<"urn:xmpp:mucsub:nodes:messages">>).
diff --git a/lib/ejabberd/config/attr.ex b/lib/ejabberd/config/attr.ex
new file mode 100644
index 000000000..9d17b157d
--- /dev/null
+++ b/lib/ejabberd/config/attr.ex
@@ -0,0 +1,119 @@
+defmodule Ejabberd.Config.Attr do
+ @moduledoc """
+ Module used to work with the attributes parsed from
+ an elixir block (do...end).
+
+ Contains functions for extracting attrs from a block
+ and validation.
+ """
+
+ @type attr :: {atom(), any()}
+
+ @attr_supported [
+ active:
+ [type: :boolean, default: true],
+ git:
+ [type: :string, default: ""],
+ name:
+ [type: :string, default: ""],
+ opts:
+ [type: :list, default: []],
+ dependency:
+ [type: :list, default: []]
+ ]
+
+ @doc """
+ Takes a block with annotations and extracts the list
+ of attributes.
+ """
+ @spec extract_attrs_from_block_with_defaults(any()) :: [attr]
+ def extract_attrs_from_block_with_defaults(block) do
+ block
+ |> extract_attrs_from_block
+ |> put_into_list_if_not_already
+ |> insert_default_attrs_if_missing
+ end
+
+ @doc """
+ Takes an attribute or a list of attrs and validate them.
+
+ Returns a {:ok, attr} or {:error, attr, cause} for each of the attributes.
+ """
+ @spec validate([attr]) :: [{:ok, attr}] | [{:error, attr, atom()}]
+ def validate(attrs) when is_list(attrs), do: Enum.map(attrs, &valid_attr?/1)
+ def validate(attr), do: validate([attr]) |> List.first
+
+ @doc """
+ Returns the type of an attribute, given its name.
+ """
+ @spec get_type_for_attr(atom()) :: atom()
+ def get_type_for_attr(attr_name) do
+ @attr_supported
+ |> Keyword.get(attr_name)
+ |> Keyword.get(:type)
+ end
+
+ @doc """
+ Returns the default value for an attribute, given its name.
+ """
+ @spec get_default_for_attr(atom()) :: any()
+ def get_default_for_attr(attr_name) do
+ @attr_supported
+ |> Keyword.get(attr_name)
+ |> Keyword.get(:default)
+ end
+
+ # Private API
+
+ # Given an elixir block (do...end) returns a list with the annotations
+ # or a single annotation.
+ @spec extract_attrs_from_block(any()) :: [attr] | attr
+ defp extract_attrs_from_block({:__block__, [], attrs}), do: Enum.map(attrs, &extract_attrs_from_block/1)
+ defp extract_attrs_from_block({:@, _, [attrs]}), do: extract_attrs_from_block(attrs)
+ defp extract_attrs_from_block({attr_name, _, [value]}), do: {attr_name, value}
+ defp extract_attrs_from_block(nil), do: []
+
+ # In case extract_attrs_from_block returns a single attribute,
+ # then put it into a list. (Ensures attrs are always into a list).
+ @spec put_into_list_if_not_already([attr] | attr) :: [attr]
+ defp put_into_list_if_not_already(attrs) when is_list(attrs), do: attrs
+ defp put_into_list_if_not_already(attr), do: [attr]
+
+ # Given a list of attributes, it inserts the missing attribute with their
+ # default value.
+ @spec insert_default_attrs_if_missing([attr]) :: [attr]
+ defp insert_default_attrs_if_missing(attrs) do
+ Enum.reduce @attr_supported, attrs, fn({attr_name, _}, acc) ->
+ case Keyword.has_key?(acc, attr_name) do
+ true -> acc
+ false -> Keyword.put(acc, attr_name, get_default_for_attr(attr_name))
+ end
+ end
+ end
+
+ # Given an attribute, validates it and return a tuple with
+ # {:ok, attr} or {:error, attr, cause}
+ @spec valid_attr?(attr) :: {:ok, attr} | {:error, attr, atom()}
+ defp valid_attr?({attr_name, param} = attr) do
+ case Keyword.get(@attr_supported, attr_name) do
+ nil -> {:error, attr, :attr_not_supported}
+ [{:type, param_type} | _] -> case is_of_type?(param, param_type) do
+ true -> {:ok, attr}
+ false -> {:error, attr, :type_not_supported}
+ end
+ end
+ end
+
+ # Given an attribute value and a type, it returns a true
+ # if the value its of the type specified, false otherwise.
+
+ # Usefoul for checking if an attr value respects the type
+ # specified for the annotation.
+ @spec is_of_type?(any(), atom()) :: boolean()
+ defp is_of_type?(param, type) when type == :boolean and is_boolean(param), do: true
+ defp is_of_type?(param, type) when type == :string and is_bitstring(param), do: true
+ defp is_of_type?(param, type) when type == :list and is_list(param), do: true
+ defp is_of_type?(param, type) when type == :atom and is_atom(param), do: true
+ defp is_of_type?(_param, type) when type == :any, do: true
+ defp is_of_type?(_, _), do: false
+end
diff --git a/lib/ejabberd/config/config.ex b/lib/ejabberd/config/config.ex
new file mode 100644
index 000000000..4d1270bc1
--- /dev/null
+++ b/lib/ejabberd/config/config.ex
@@ -0,0 +1,145 @@
+defmodule Ejabberd.Config do
+ @moduledoc """
+ Base module for configuration file.
+
+ Imports macros for the config DSL and contains functions
+ for working/starting the configuration parsed.
+ """
+
+ alias Ejabberd.Config.EjabberdModule
+ alias Ejabberd.Config.Attr
+ alias Ejabberd.Config.EjabberdLogger
+
+ defmacro __using__(_opts) do
+ quote do
+ import Ejabberd.Config, only: :macros
+ import Ejabberd.Logger
+
+ @before_compile Ejabberd.Config
+ end
+ end
+
+ # Validate the modules parsed and log validation errors at compile time.
+ # Could be also possible to interrupt the compilation&execution by throwing
+ # an exception if necessary.
+ def __before_compile__(_env) do
+ get_modules_parsed_in_order
+ |> EjabberdModule.validate
+ |> EjabberdLogger.log_errors
+ end
+
+ @doc """
+ Given the path of the config file, it evaluates it.
+ """
+ def init(file_path, force \\ false) do
+ init_already_executed = Ejabberd.Config.Store.get(:module_name) != []
+
+ case force do
+ true ->
+ Ejabberd.Config.Store.stop
+ Ejabberd.Config.Store.start_link
+ do_init(file_path)
+ false ->
+ if not init_already_executed, do: do_init(file_path)
+ end
+ end
+
+ @doc """
+ Returns a list with all the opts, formatted for ejabberd.
+ """
+ def get_ejabberd_opts do
+ get_general_opts
+ |> Dict.put(:modules, get_modules_parsed_in_order())
+ |> Dict.put(:listeners, get_listeners_parsed_in_order())
+ |> Ejabberd.Config.OptsFormatter.format_opts_for_ejabberd
+ end
+
+ @doc """
+ Register the hooks defined inside the elixir config file.
+ """
+ def start_hooks do
+ get_hooks_parsed_in_order()
+ |> Enum.each(&Ejabberd.Config.EjabberdHook.start/1)
+ end
+
+ ###
+ ### MACROS
+ ###
+
+ defmacro listen(module, do: block) do
+ attrs = Attr.extract_attrs_from_block_with_defaults(block)
+
+ quote do
+ Ejabberd.Config.Store.put(:listeners, %EjabberdModule{
+ module: unquote(module),
+ attrs: unquote(attrs)
+ })
+ end
+ end
+
+ defmacro module(module, do: block) do
+ attrs = Attr.extract_attrs_from_block_with_defaults(block)
+
+ quote do
+ Ejabberd.Config.Store.put(:modules, %EjabberdModule{
+ module: unquote(module),
+ attrs: unquote(attrs)
+ })
+ end
+ end
+
+ defmacro hook(hook_name, opts, fun) do
+ quote do
+ Ejabberd.Config.Store.put(:hooks, %Ejabberd.Config.EjabberdHook{
+ hook: unquote(hook_name),
+ opts: unquote(opts),
+ fun: unquote(fun)
+ })
+ end
+ end
+
+ # Private API
+
+ defp do_init(file_path) do
+ # File evaluation
+ Code.eval_file(file_path) |> extract_and_store_module_name()
+
+ # Getting start/0 config
+ Ejabberd.Config.Store.get(:module_name)
+ |> case do
+ nil -> IO.puts "[ ERR ] Configuration module not found."
+ [module] -> call_start_func_and_store_data(module)
+ end
+
+ # Fetching git modules and install them
+ get_modules_parsed_in_order()
+ |> EjabberdModule.fetch_git_repos
+ end
+
+ # Returns the modules from the store
+ defp get_modules_parsed_in_order,
+ do: Ejabberd.Config.Store.get(:modules) |> Enum.reverse
+
+ # Returns the listeners from the store
+ defp get_listeners_parsed_in_order,
+ do: Ejabberd.Config.Store.get(:listeners) |> Enum.reverse
+
+ defp get_hooks_parsed_in_order,
+ do: Ejabberd.Config.Store.get(:hooks) |> Enum.reverse
+
+ # Returns the general config options
+ defp get_general_opts,
+ do: Ejabberd.Config.Store.get(:general) |> List.first
+
+ # Gets the general ejabberd options calling
+ # the start/0 function and stores them.
+ defp call_start_func_and_store_data(module) do
+ opts = apply(module, :start, [])
+ Ejabberd.Config.Store.put(:general, opts)
+ end
+
+ # Stores the configuration module name
+ defp extract_and_store_module_name({{:module, mod, _bytes, _}, _}) do
+ Ejabberd.Config.Store.put(:module_name, mod)
+ end
+end
diff --git a/lib/ejabberd/config/ejabberd_hook.ex b/lib/ejabberd/config/ejabberd_hook.ex
new file mode 100644
index 000000000..8b7858d23
--- /dev/null
+++ b/lib/ejabberd/config/ejabberd_hook.ex
@@ -0,0 +1,23 @@
+defmodule Ejabberd.Config.EjabberdHook do
+ @moduledoc """
+ Module containing functions for manipulating
+ ejabberd hooks.
+ """
+
+ defstruct hook: nil, opts: [], fun: nil
+
+ alias Ejabberd.Config.EjabberdHook
+
+ @type t :: %EjabberdHook{}
+
+ @doc """
+ Register a hook to ejabberd.
+ """
+ @spec start(EjabberdHook.t) :: none
+ def start(%EjabberdHook{hook: hook, opts: opts, fun: fun}) do
+ host = Keyword.get(opts, :host, :global)
+ priority = Keyword.get(opts, :priority, 50)
+
+ :ejabberd_hooks.add(hook, host, fun, priority)
+ end
+end
diff --git a/lib/ejabberd/config/ejabberd_module.ex b/lib/ejabberd/config/ejabberd_module.ex
new file mode 100644
index 000000000..4de9a302e
--- /dev/null
+++ b/lib/ejabberd/config/ejabberd_module.ex
@@ -0,0 +1,70 @@
+defmodule Ejabberd.Config.EjabberdModule do
+ @moduledoc """
+ Module representing a module block in the configuration file.
+ It offers functions for validation and for starting the modules.
+
+ Warning: The name is EjabberdModule to not collide with
+ the already existing Elixir.Module.
+ """
+
+ @type t :: %{module: atom, attrs: [Attr.t]}
+
+ defstruct [:module, :attrs]
+
+ alias Ejabberd.Config.EjabberdModule
+ alias Ejabberd.Config.Attr
+ alias Ejabberd.Config.Validation
+
+ @doc """
+ Given a list of modules / single module
+ it runs different validators on them.
+
+ For each module, returns a {:ok, mod} or {:error, mod, errors}
+ """
+ def validate(modules) do
+ Validation.validate(modules)
+ end
+
+ @doc """
+ Given a list of modules, it takes only the ones with
+ a git attribute and tries to fetch the repo,
+ then, it install them through :ext_mod.install/1
+ """
+ @spec fetch_git_repos([EjabberdModule.t]) :: none()
+ def fetch_git_repos(modules) do
+ modules
+ |> Enum.filter(&is_git_module?/1)
+ |> Enum.each(&fetch_and_install_git_module/1)
+ end
+
+ # Private API
+
+ defp is_git_module?(%EjabberdModule{attrs: attrs}) do
+ case Keyword.get(attrs, :git) do
+ "" -> false
+ repo -> String.match?(repo, ~r/((git|ssh|http(s)?)|(git@[\w\.]+))(:(\/\/)?)([\w\.@\:\/\-~]+)(\.git)(\/)?/)
+ end
+ end
+
+ defp fetch_and_install_git_module(%EjabberdModule{attrs: attrs}) do
+ repo = Keyword.get(attrs, :git)
+ mod_name = case Keyword.get(attrs, :name) do
+ "" -> infer_mod_name_from_git_url(repo)
+ name -> name
+ end
+
+ path = "#{:ext_mod.modules_dir()}/sources/ejabberd-contrib\/#{mod_name}"
+ fetch_and_store_repo_source_if_not_exists(path, repo)
+ :ext_mod.install(mod_name) # Have to check if overwrites an already present mod
+ end
+
+ defp fetch_and_store_repo_source_if_not_exists(path, repo) do
+ unless File.exists?(path) do
+ IO.puts "[info] Fetching: #{repo}"
+ :os.cmd('git clone #{repo} #{path}')
+ end
+ end
+
+ defp infer_mod_name_from_git_url(repo),
+ do: String.split(repo, "/") |> List.last |> String.replace(".git", "")
+end
diff --git a/lib/ejabberd/config/logger/ejabberd_logger.ex b/lib/ejabberd/config/logger/ejabberd_logger.ex
new file mode 100644
index 000000000..270fbfaa6
--- /dev/null
+++ b/lib/ejabberd/config/logger/ejabberd_logger.ex
@@ -0,0 +1,32 @@
+defmodule Ejabberd.Config.EjabberdLogger do
+ @moduledoc """
+ Module used to log validation errors given validated modules
+ given validated modules.
+ """
+
+ alias Ejabberd.Config.EjabberdModule
+
+ @doc """
+ Given a list of modules validated, in the form of {:ok, mod} or
+ {:error, mod, errors}, it logs to the user the errors found.
+ """
+ @spec log_errors([EjabberdModule.t]) :: [EjabberdModule.t]
+ def log_errors(modules_validated) when is_list(modules_validated) do
+ Enum.each modules_validated, &do_log_errors/1
+ modules_validated
+ end
+
+ defp do_log_errors({:ok, _mod}), do: nil
+ defp do_log_errors({:error, _mod, errors}), do: Enum.each errors, &do_log_errors/1
+ defp do_log_errors({:attribute, errors}), do: Enum.each errors, &log_attribute_error/1
+ defp do_log_errors({:dependency, errors}), do: Enum.each errors, &log_dependency_error/1
+
+ defp log_attribute_error({{attr_name, val}, :attr_not_supported}), do:
+ IO.puts "[ WARN ] Annotation @#{attr_name} is not supported."
+
+ defp log_attribute_error({{attr_name, val}, :type_not_supported}), do:
+ IO.puts "[ WARN ] Annotation @#{attr_name} with value #{inspect val} is not supported (type mismatch)."
+
+ defp log_dependency_error({module, :not_found}), do:
+ IO.puts "[ WARN ] Module #{inspect module} was not found, but is required as a dependency."
+end
diff --git a/lib/ejabberd/config/opts_formatter.ex b/lib/ejabberd/config/opts_formatter.ex
new file mode 100644
index 000000000..b7010ddfe
--- /dev/null
+++ b/lib/ejabberd/config/opts_formatter.ex
@@ -0,0 +1,46 @@
+defmodule Ejabberd.Config.OptsFormatter do
+ @moduledoc """
+ Module for formatting options parsed into the format
+ ejabberd uses.
+ """
+
+ alias Ejabberd.Config.EjabberdModule
+
+ @doc """
+ Takes a keyword list with keys corresponding to
+ the keys requested by the ejabberd config (ex: modules: mods)
+ and formats them to be correctly evaluated by ejabberd.
+
+ Look at how Config.get_ejabberd_opts/0 is constructed for
+ more informations.
+ """
+ @spec format_opts_for_ejabberd([{atom(), any()}]) :: list()
+ def format_opts_for_ejabberd(opts) do
+ opts
+ |> format_attrs_for_ejabberd
+ end
+
+ defp format_attrs_for_ejabberd(opts) when is_list(opts),
+ do: Enum.map opts, &format_attrs_for_ejabberd/1
+
+ defp format_attrs_for_ejabberd({:listeners, mods}),
+ do: {:listen, format_listeners_for_ejabberd(mods)}
+
+ defp format_attrs_for_ejabberd({:modules, mods}),
+ do: {:modules, format_mods_for_ejabberd(mods)}
+
+ defp format_attrs_for_ejabberd({key, opts}) when is_atom(key),
+ do: {key, opts}
+
+ defp format_mods_for_ejabberd(mods) do
+ Enum.map mods, fn %EjabberdModule{module: mod, attrs: attrs} ->
+ {mod, attrs[:opts]}
+ end
+ end
+
+ defp format_listeners_for_ejabberd(mods) do
+ Enum.map mods, fn %EjabberdModule{module: mod, attrs: attrs} ->
+ Keyword.put(attrs[:opts], :module, mod)
+ end
+ end
+end
diff --git a/lib/ejabberd/config/store.ex b/lib/ejabberd/config/store.ex
new file mode 100644
index 000000000..72beea64c
--- /dev/null
+++ b/lib/ejabberd/config/store.ex
@@ -0,0 +1,55 @@
+defmodule Ejabberd.Config.Store do
+ @moduledoc """
+ Module used for storing the modules parsed from
+ the configuration file.
+
+ Example:
+ - Store.put(:modules, mod1)
+ - Store.put(:modules, mod2)
+
+ - Store.get(:modules) :: [mod1, mod2]
+
+ Be carefoul: when retrieving data you get them
+ in the order inserted into the store, which normally
+ is the reversed order of how the modules are specified
+ inside the configuration file. To resolve this just use
+ a Enum.reverse/1.
+ """
+
+ @name __MODULE__
+
+ def start_link do
+ Agent.start_link(fn -> %{} end, name: @name)
+ end
+
+ @doc """
+ Stores a value based on the key. If the key already exists,
+ then it inserts the new element, maintaining all the others.
+ It uses a list for this.
+ """
+ @spec put(atom, any) :: :ok
+ def put(key, val) do
+ Agent.update @name, &Map.update(&1, key, [val], fn coll ->
+ [val | coll]
+ end)
+ end
+
+ @doc """
+ Gets a value based on the key passed.
+ Returns always a list.
+ """
+ @spec get(atom) :: [any]
+ def get(key) do
+ Agent.get @name, &Map.get(&1, key, [])
+ end
+
+ @doc """
+ Stops the store.
+ It uses Agent.stop underneath, so be aware that exit
+ could be called.
+ """
+ @spec stop() :: :ok
+ def stop do
+ Agent.stop @name
+ end
+end
diff --git a/lib/ejabberd/config/validator/validation.ex b/lib/ejabberd/config/validator/validation.ex
new file mode 100644
index 000000000..2fe00361a
--- /dev/null
+++ b/lib/ejabberd/config/validator/validation.ex
@@ -0,0 +1,40 @@
+defmodule Ejabberd.Config.Validation do
+ @moduledoc """
+ Module used to validate a list of modules.
+ """
+
+ @type mod_validation :: {[EjabberdModule.t], EjabberdModule.t, map}
+ @type mod_validation_result :: {:ok, EjabberdModule.t} | {:error, EjabberdModule.t, map}
+
+ alias Ejabberd.Config.EjabberdModule
+ alias Ejabberd.Config.Attr
+ alias Ejabberd.Config.Validator
+ alias Ejabberd.Config.ValidatorUtility
+
+ @doc """
+ Given a module or a list of modules it runs validators on them
+ and returns {:ok, mod} or {:error, mod, errors}, for each
+ of them.
+ """
+ @spec validate([EjabberdModule.t] | EjabberdModule.t) :: [mod_validation_result]
+ def validate(modules) when is_list(modules), do: Enum.map(modules, &do_validate(modules, &1))
+ def validate(module), do: validate([module])
+
+ # Private API
+
+ @spec do_validate([EjabberdModule.t], EjabberdModule.t) :: mod_validation_result
+ defp do_validate(modules, mod) do
+ {modules, mod, %{}}
+ |> Validator.Attrs.validate
+ |> Validator.Dependencies.validate
+ |> resolve_validation_result
+ end
+
+ @spec resolve_validation_result(mod_validation) :: mod_validation_result
+ defp resolve_validation_result({_modules, mod, errors}) do
+ case errors do
+ err when err == %{} -> {:ok, mod}
+ err -> {:error, mod, err}
+ end
+ end
+end
diff --git a/lib/ejabberd/config/validator/validator_attrs.ex b/lib/ejabberd/config/validator/validator_attrs.ex
new file mode 100644
index 000000000..94117ab21
--- /dev/null
+++ b/lib/ejabberd/config/validator/validator_attrs.ex
@@ -0,0 +1,28 @@
+defmodule Ejabberd.Config.Validator.Attrs do
+ @moduledoc """
+ Validator module used to validate attributes.
+ """
+
+ # TODO: Duplicated from validator.ex !!!
+ @type mod_validation :: {[EjabberdModule.t], EjabberdModule.t, map}
+
+ import Ejabberd.Config.ValidatorUtility
+ alias Ejabberd.Config.Attr
+
+ @doc """
+ Given a module (with the form used for validation)
+ it runs Attr.validate/1 on each attribute and
+ returns the validation tuple with the errors updated, if found.
+ """
+ @spec validate(mod_validation) :: mod_validation
+ def validate({modules, mod, errors}) do
+ errors = Enum.reduce mod.attrs, errors, fn(attr, err) ->
+ case Attr.validate(attr) do
+ {:ok, attr} -> err
+ {:error, attr, cause} -> put_error(err, :attribute, {attr, cause})
+ end
+ end
+
+ {modules, mod, errors}
+ end
+end
diff --git a/lib/ejabberd/config/validator/validator_dependencies.ex b/lib/ejabberd/config/validator/validator_dependencies.ex
new file mode 100644
index 000000000..d44c8a136
--- /dev/null
+++ b/lib/ejabberd/config/validator/validator_dependencies.ex
@@ -0,0 +1,30 @@
+defmodule Ejabberd.Config.Validator.Dependencies do
+ @moduledoc """
+ Validator module used to validate dependencies specified
+ with the @dependency annotation.
+ """
+
+ # TODO: Duplicated from validator.ex !!!
+ @type mod_validation :: {[EjabberdModule.t], EjabberdModule.t, map}
+ import Ejabberd.Config.ValidatorUtility
+
+ @doc """
+ Given a module (with the form used for validation)
+ it checks if the @dependency annotation is respected and
+ returns the validation tuple with the errors updated, if found.
+ """
+ @spec validate(mod_validation) :: mod_validation
+ def validate({modules, mod, errors}) do
+ module_names = extract_module_names(modules)
+ dependencies = mod.attrs[:dependency]
+
+ errors = Enum.reduce dependencies, errors, fn(req_module, err) ->
+ case req_module in module_names do
+ true -> err
+ false -> put_error(err, :dependency, {req_module, :not_found})
+ end
+ end
+
+ {modules, mod, errors}
+ end
+end
diff --git a/lib/ejabberd/config/validator/validator_utility.ex b/lib/ejabberd/config/validator/validator_utility.ex
new file mode 100644
index 000000000..17805f748
--- /dev/null
+++ b/lib/ejabberd/config/validator/validator_utility.ex
@@ -0,0 +1,30 @@
+defmodule Ejabberd.Config.ValidatorUtility do
+ @moduledoc """
+ Module used as a base validator for validation modules.
+ Imports utility functions for working with validation structures.
+ """
+
+ alias Ejabberd.Config.EjabberdModule
+
+ @doc """
+ Inserts an error inside the errors collection, for the given key.
+ If the key doesn't exists then it creates an empty collection
+ and inserts the value passed.
+ """
+ @spec put_error(map, atom, any) :: map
+ def put_error(errors, key, val) do
+ Map.update errors, key, [val], fn coll ->
+ [val | coll]
+ end
+ end
+
+ @doc """
+ Given a list of modules it extracts and returns a list
+ of the module names (which are Elixir.Module).
+ """
+ @spec extract_module_names(EjabberdModule.t) :: [atom]
+ def extract_module_names(modules) when is_list(modules) do
+ modules
+ |> Enum.map(&Map.get(&1, :module))
+ end
+end
diff --git a/lib/ejabberd/config_util.ex b/lib/ejabberd/config_util.ex
new file mode 100644
index 000000000..6592104a2
--- /dev/null
+++ b/lib/ejabberd/config_util.ex
@@ -0,0 +1,18 @@
+defmodule Ejabberd.ConfigUtil do
+ @moduledoc """
+ Module containing utility functions for
+ the config file.
+ """
+
+ @doc """
+ Returns true when the config file is based on elixir.
+ """
+ @spec is_elixir_config(list) :: boolean
+ def is_elixir_config(filename) when is_list(filename) do
+ is_elixir_config(to_string(filename))
+ end
+
+ def is_elixir_config(filename) do
+ String.ends_with?(filename, "exs")
+ end
+end
diff --git a/lib/ejabberd/module.ex b/lib/ejabberd/module.ex
new file mode 100644
index 000000000..9fb3f040c
--- /dev/null
+++ b/lib/ejabberd/module.ex
@@ -0,0 +1,19 @@
+defmodule Ejabberd.Module do
+
+ defmacro __using__(opts) do
+ logger_enabled = Keyword.get(opts, :logger, true)
+
+ quote do
+ @behaviour :gen_mod
+ import Ejabberd.Module
+
+ unquote(if logger_enabled do
+ quote do: import Ejabberd.Logger
+ end)
+ end
+ end
+
+ # gen_mod callbacks
+ def depends(_host, _opts), do: []
+ def mod_opt_type(_), do: []
+end
diff --git a/lib/mix/tasks/deps.tree.ex b/lib/mix/tasks/deps.tree.ex
new file mode 100644
index 000000000..94cb85a50
--- /dev/null
+++ b/lib/mix/tasks/deps.tree.ex
@@ -0,0 +1,94 @@
+defmodule Mix.Tasks.Ejabberd.Deps.Tree do
+ use Mix.Task
+
+ alias Ejabberd.Config.EjabberdModule
+
+ @shortdoc "Lists all ejabberd modules and their dependencies"
+
+ @moduledoc """
+ Lists all ejabberd modules and their dependencies.
+
+ The project must have ejabberd as a dependency.
+ """
+
+ def run(_argv) do
+ # First we need to start manually the store to be available
+ # during the compilation of the config file.
+ Ejabberd.Config.Store.start_link
+ Ejabberd.Config.init(:ejabberd_config.get_ejabberd_config_path())
+
+ Mix.shell.info "ejabberd modules"
+
+ Ejabberd.Config.Store.get(:modules)
+ |> Enum.reverse # Because of how mods are stored inside the store
+ |> format_mods
+ |> Mix.shell.info
+ end
+
+ defp format_mods(mods) when is_list(mods) do
+ deps_tree = build_dependency_tree(mods)
+ mods_used_as_dependency = get_mods_used_as_dependency(deps_tree)
+
+ keep_only_mods_not_used_as_dep(deps_tree, mods_used_as_dependency)
+ |> format_mods_into_string
+ end
+
+ defp build_dependency_tree(mods) do
+ Enum.map mods, fn %EjabberdModule{module: mod, attrs: attrs} ->
+ deps = attrs[:dependency]
+ build_dependency_tree(mods, mod, deps)
+ end
+ end
+
+ defp build_dependency_tree(mods, mod, []), do: %{module: mod, dependency: []}
+ defp build_dependency_tree(mods, mod, deps) when is_list(deps) do
+ dependencies = Enum.map deps, fn dep ->
+ dep_deps = get_dependencies_of_mod(mods, dep)
+ build_dependency_tree(mods, dep, dep_deps)
+ end
+
+ %{module: mod, dependency: dependencies}
+ end
+
+ defp get_mods_used_as_dependency(mods) when is_list(mods) do
+ Enum.reduce mods, [], fn(mod, acc) ->
+ case mod do
+ %{dependency: []} -> acc
+ %{dependency: deps} -> get_mod_names(deps) ++ acc
+ end
+ end
+ end
+
+ defp get_mod_names([]), do: []
+ defp get_mod_names(mods) when is_list(mods), do: Enum.map(mods, &get_mod_names/1) |> List.flatten
+ defp get_mod_names(%{module: mod, dependency: deps}), do: [mod | get_mod_names(deps)]
+
+ defp keep_only_mods_not_used_as_dep(mods, mods_used_as_dep) do
+ Enum.filter mods, fn %{module: mod} ->
+ not mod in mods_used_as_dep
+ end
+ end
+
+ defp get_dependencies_of_mod(deps, mod_name) do
+ Enum.find(deps, &(Map.get(&1, :module) == mod_name))
+ |> Map.get(:attrs)
+ |> Keyword.get(:dependency)
+ end
+
+ defp format_mods_into_string(mods), do: format_mods_into_string(mods, 0)
+ defp format_mods_into_string([], _indentation), do: ""
+ defp format_mods_into_string(mods, indentation) when is_list(mods) do
+ Enum.reduce mods, "", fn(mod, acc) ->
+ acc <> format_mods_into_string(mod, indentation)
+ end
+ end
+
+ defp format_mods_into_string(%{module: mod, dependency: deps}, 0) do
+ "\n├── #{mod}" <> format_mods_into_string(deps, 2)
+ end
+
+ defp format_mods_into_string(%{module: mod, dependency: deps}, indentation) do
+ spaces = Enum.reduce 0..indentation, "", fn(_, acc) -> " " <> acc end
+ "\n│#{spaces}└── #{mod}" <> format_mods_into_string(deps, indentation + 4)
+ end
+end
diff --git a/lib/mod_presence_demo.ex b/lib/mod_presence_demo.ex
index ba5abe90e..09bf58405 100644
--- a/lib/mod_presence_demo.ex
+++ b/lib/mod_presence_demo.ex
@@ -1,16 +1,15 @@
defmodule ModPresenceDemo do
- import Ejabberd.Logger # this allow using info, error, etc for logging
- @behaviour :gen_mod
+ use Ejabberd.Module
def start(host, _opts) do
info('Starting ejabberd module Presence Demo')
- Ejabberd.Hooks.add(:set_presence_hook, host, __ENV__.module, :on_presence, 50)
+ Ejabberd.Hooks.add(:set_presence_hook, host, __MODULE__, :on_presence, 50)
:ok
end
def stop(host) do
info('Stopping ejabberd module Presence Demo')
- Ejabberd.Hooks.delete(:set_presence_hook, host, __ENV__.module, :on_presence, 50)
+ Ejabberd.Hooks.delete(:set_presence_hook, host, __MODULE__, :on_presence, 50)
:ok
end
@@ -18,9 +17,4 @@ defmodule ModPresenceDemo do
info('Receive presence for #{user}')
:none
end
-
- # gen_mod callbacks
- def depends(_host, _opts), do: []
- def mod_opt_type(_), do: []
-
end
diff --git a/mix.exs b/mix.exs
index 2c83a319b..ee4b60fb2 100644
--- a/mix.exs
+++ b/mix.exs
@@ -3,7 +3,7 @@ defmodule Ejabberd.Mixfile do
def project do
[app: :ejabberd,
- version: "16.06.0",
+ version: "16.08.0",
description: description,
elixir: "~> 1.2",
elixirc_paths: ["lib"],
@@ -29,7 +29,7 @@ defmodule Ejabberd.Mixfile do
included_applications: [:lager, :mnesia, :p1_utils, :cache_tab,
:fast_tls, :stringprep, :fast_xml,
:stun, :fast_yaml, :ezlib, :iconv,
- :esip, :jiffy, :p1_oauth2, :p1_xmlrpc, :eredis,
+ :esip, :jiffy, :p1_oauth2, :eredis,
:p1_mysql, :p1_pgsql, :sqlite3]]
end
@@ -40,7 +40,7 @@ defmodule Ejabberd.Mixfile do
end
defp deps do
- [{:lager, "~> 3.0.0"},
+ [{:lager, "~> 3.2"},
{:p1_utils, "~> 1.0"},
{:cache_tab, "~> 1.0"},
{:stringprep, "~> 1.0"},
@@ -51,7 +51,6 @@ defmodule Ejabberd.Mixfile do
{:esip, "~> 1.0"},
{:jiffy, "~> 0.14.7"},
{:p1_oauth2, "~> 0.6.1"},
- {:p1_xmlrpc, "~> 1.15"},
{:p1_mysql, "~> 1.0"},
{:p1_pgsql, "~> 1.1"},
{:sqlite3, "~> 1.1"},
@@ -61,7 +60,8 @@ defmodule Ejabberd.Mixfile do
{:exrm, "~> 1.0.0", only: :dev},
# relx is used by exrm. Lock version as for now, ejabberd doesn not compile fine with
# version 3.20:
- {:relx, "~> 3.19.0", only: :dev},
+ {:relx, "~> 3.21", only: :dev},
+ {:ex_doc, ">= 0.0.0", only: :dev},
{:meck, "~> 0.8.4", only: :test},
{:moka, github: "processone/moka", tag: "1.0.5c", only: :test}]
end
diff --git a/mix.lock b/mix.lock
index 17be772ea..fc2cdc924 100644
--- a/mix.lock
+++ b/mix.lock
@@ -1,29 +1,29 @@
%{"bbmustache": {:hex, :bbmustache, "1.0.4", "7ba94f971c5afd7b6617918a4bb74705e36cab36eb84b19b6a1b7ee06427aa38", [:rebar], []},
"cache_tab": {:hex, :cache_tab, "1.0.4", "3fd2b1ab40c36e7830a4e09e836c6b0fa89191cd4e5fd471873e4eb42f5cd37c", [:rebar3], [{:p1_utils, "1.0.5", [hex: :p1_utils, optional: false]}]},
"cf": {:hex, :cf, "0.2.1", "69d0b1349fd4d7d4dc55b7f407d29d7a840bf9a1ef5af529f1ebe0ce153fc2ab", [:rebar3], []},
+ "earmark": {:hex, :earmark, "1.0.1", "2c2cd903bfdc3de3f189bd9a8d4569a075b88a8981ded9a0d95672f6e2b63141", [:mix], []},
"eredis": {:hex, :eredis, "1.0.8", "ab4fda1c4ba7fbe6c19c26c249dc13da916d762502c4b4fa2df401a8d51c5364", [:rebar], []},
- "erlware_commons": {:hex, :erlware_commons, "0.19.0", "7b43caf2c91950c5f60dc20451e3c3afba44d3d4f7f27bcdc52469285a5a3e70", [:rebar3], [{:cf, "0.2.1", [hex: :cf, optional: false]}]},
+ "erlware_commons": {:hex, :erlware_commons, "0.21.0", "a04433071ad7d112edefc75ac77719dd3e6753e697ac09428fc83d7564b80b15", [:rebar3], [{:cf, "0.2.1", [hex: :cf, optional: false]}]},
"esip": {:hex, :esip, "1.0.8", "69885a6c07964aabc6c077fe1372aa810a848bd3d9a415b160dabdce9c7a79b5", [:rebar3], [{:fast_tls, "1.0.7", [hex: :fast_tls, optional: false]}, {:p1_utils, "1.0.5", [hex: :p1_utils, optional: false]}, {:stun, "1.0.7", [hex: :stun, optional: false]}]},
+ "ex_doc": {:hex, :ex_doc, "0.13.0", "aa2f8fe4c6136a2f7cfc0a7e06805f82530e91df00e2bff4b4362002b43ada65", [:mix], [{:earmark, "~> 1.0", [hex: :earmark, optional: false]}]},
"exrm": {:hex, :exrm, "1.0.8", "5aa8990cdfe300282828b02cefdc339e235f7916388ce99f9a1f926a9271a45d", [:mix], [{:relx, "~> 3.5", [hex: :relx, optional: false]}]},
"ezlib": {:hex, :ezlib, "1.0.1", "add8b2770a1a70c174aaea082b4a8668c0c7fdb03ee6cc81c6c68d3a6c3d767d", [:rebar3], []},
"fast_tls": {:hex, :fast_tls, "1.0.7", "9b72ecfcdcad195ab072c196fab8334f49d8fea76bf1a51f536d69e7527d902a", [:rebar3], [{:p1_utils, "1.0.5", [hex: :p1_utils, optional: false]}]},
"fast_xml": {:hex, :fast_xml, "1.1.15", "6d23eb7f874e1357cf80a48d75a7bd0c8f6318029dc4b70122e9f54911f57f83", [:rebar3], [{:p1_utils, "1.0.5", [hex: :p1_utils, optional: false]}]},
"fast_yaml": {:hex, :fast_yaml, "1.0.6", "3fe6feb7935ae8028b337e53e1db29e73ad3bca8041108f6a8f73b7175ece75c", [:rebar3], [{:p1_utils, "1.0.5", [hex: :p1_utils, optional: false]}]},
"getopt": {:hex, :getopt, "0.8.2", "b17556db683000ba50370b16c0619df1337e7af7ecbf7d64fbf8d1d6bce3109b", [:rebar], []},
- "goldrush": {:hex, :goldrush, "0.1.7", "349a351d17c71c2fdaa18a6c2697562abe136fec945f147b381f0cf313160228", [:rebar3], []},
+ "goldrush": {:hex, :goldrush, "0.1.8", "2024ba375ceea47e27ea70e14d2c483b2d8610101b4e852ef7f89163cdb6e649", [:rebar3], []},
"iconv": {:hex, :iconv, "1.0.2", "a0792f06ab4b5ea1b5bb49789405739f1281a91c44cf3879cb70e4d777666217", [:rebar3], [{:p1_utils, "1.0.5", [hex: :p1_utils, optional: false]}]},
"jiffy": {:hex, :jiffy, "0.14.7", "9f33b893edd6041ceae03bc1e50b412e858cc80b46f3d7535a7a9940a79a1c37", [:rebar, :make], []},
- "lager": {:hex, :lager, "3.0.2", "25dc81bc3659b62f5ab9bd073e97ddd894fc4c242019fccef96f3889d7366c97", [:rebar3], [{:goldrush, "0.1.7", [hex: :goldrush, optional: false]}]},
+ "lager": {:hex, :lager, "3.2.1", "eef4e18b39e4195d37606d9088ea05bf1b745986cf8ec84f01d332456fe88d17", [:rebar3], [{:goldrush, "0.1.8", [hex: :goldrush, optional: false]}]},
"meck": {:hex, :meck, "0.8.4", "59ca1cd971372aa223138efcf9b29475bde299e1953046a0c727184790ab1520", [:rebar, :make], []},
"moka": {:git, "https://github.com/processone/moka.git", "3eed3a6dd7dedb70a6cd18f86c7561a18626eb3b", [tag: "1.0.5c"]},
"p1_mysql": {:hex, :p1_mysql, "1.0.1", "d2be1cfc71bb4f1391090b62b74c3f5cb8e7a45b0076b8cb290cd6b2856c581b", [:rebar3], []},
"p1_oauth2": {:hex, :p1_oauth2, "0.6.1", "4e021250cc198c538b097393671a41e7cebf463c248980320e038fe0316eb56b", [:rebar3], []},
"p1_pgsql": {:hex, :p1_pgsql, "1.1.0", "ca525c42878eac095e5feb19563acc9915c845648f48fdec7ba6266c625d4ac7", [:rebar3], []},
"p1_utils": {:hex, :p1_utils, "1.0.5", "3e698354fdc1fea5491d991457b0cb986c0a00a47d224feb841dc3ec82b9f721", [:rebar3], []},
- "p1_xmlrpc": {:hex, :p1_xmlrpc, "1.15.1", "a382b62dc21bb372281c2488f99294d84f2b4020ed0908a1c4ad710ace3cf35a", [:rebar3], []},
- "pc": {:hex, :pc, "1.2.0", "5e07731d1f8bf997a8d0271510983e570f910b42cd59bf612e664ad6dc35742e", [:rebar3], []},
"providers": {:hex, :providers, "1.6.0", "db0e2f9043ae60c0155205fcd238d68516331d0e5146155e33d1e79dc452964a", [:rebar3], [{:getopt, "0.8.2", [hex: :getopt, optional: false]}]},
- "relx": {:hex, :relx, "3.19.0", "286dd5244b4786f56aac75d5c8e2d1fb4cfd306810d4ec8548f3ae1b3aadb8f7", [:rebar3], [{:bbmustache, "1.0.4", [hex: :bbmustache, optional: false]}, {:cf, "0.2.1", [hex: :cf, optional: false]}, {:erlware_commons, "0.19.0", [hex: :erlware_commons, optional: false]}, {:getopt, "0.8.2", [hex: :getopt, optional: false]}, {:providers, "1.6.0", [hex: :providers, optional: false]}]},
+ "relx": {:hex, :relx, "3.21.0", "91e1ea9f09b4edfda8461901f4b5c5e0226e43ec161e147eeab29f7761df6eb5", [:rebar3], [{:bbmustache, "1.0.4", [hex: :bbmustache, optional: false]}, {:cf, "0.2.1", [hex: :cf, optional: false]}, {:erlware_commons, "0.21.0", [hex: :erlware_commons, optional: false]}, {:getopt, "0.8.2", [hex: :getopt, optional: false]}, {:providers, "1.6.0", [hex: :providers, optional: false]}]},
"samerlib": {:git, "https://github.com/processone/samerlib", "fbbba035b1548ac4e681df00d61bf609645333a0", [tag: "0.8.0c"]},
"sqlite3": {:hex, :sqlite3, "1.1.5", "794738b6d07b6d36ec6d42492cb9d629bad9cf3761617b8b8d728e765db19840", [:rebar3], []},
"stringprep": {:hex, :stringprep, "1.0.6", "1cf1c439eb038aa590da5456e019f86afbfbfeb5a2d37b6e5f873041624c6701", [:rebar3], [{:p1_utils, "1.0.5", [hex: :p1_utils, optional: false]}]},
diff --git a/rebar.config b/rebar.config
index ab5562858..3c9610021 100644
--- a/rebar.config
+++ b/rebar.config
@@ -18,7 +18,6 @@
{fast_yaml, ".*", {git, "https://github.com/processone/fast_yaml", {tag, "1.0.6"}}},
{jiffy, ".*", {git, "https://github.com/davisp/jiffy", {tag, "0.14.7"}}},
{p1_oauth2, ".*", {git, "https://github.com/processone/p1_oauth2", {tag, "0.6.1"}}},
- {p1_xmlrpc, ".*", {git, "https://github.com/processone/p1_xmlrpc", {tag, "1.15.1"}}},
{luerl, ".*", {git, "https://github.com/rvirding/luerl", {tag, "v0.2"}}},
{if_var_true, mysql, {p1_mysql, ".*", {git, "https://github.com/processone/p1_mysql",
{tag, "1.0.1"}}}},
@@ -75,6 +74,7 @@
{if_var_true, debug, debug_info},
{if_var_true, roster_gateway_workaround, {d, 'ROSTER_GATWAY_WORKAROUND'}},
{if_var_match, db_type, mssql, {d, 'mssql'}},
+ {if_var_true, elixir, {d, 'ELIXIR_ENABLED'}},
{if_var_true, erlang_deprecated_types, {d, 'ERL_DEPRECATED_TYPES'}},
{if_var_true, hipe, native},
{src_dirs, [asn1, src,
diff --git a/src/acl.erl b/src/acl.erl
index 897996976..d3f9afe38 100644
--- a/src/acl.erl
+++ b/src/acl.erl
@@ -262,6 +262,7 @@ normalize_spec(Spec) ->
{server, S} -> {server, nameprep(S)};
{resource, R} -> {resource, resourceprep(R)};
{server_regexp, SR} -> {server_regexp, b(SR)};
+ {resource_regexp, R} -> {resource_regexp, b(R)};
{server_glob, S} -> {server_glob, b(S)};
{resource_glob, R} -> {resource_glob, b(R)};
{ip, {Net, Mask}} -> {ip, {Net, Mask}};
@@ -686,7 +687,8 @@ transform_options({acl, Name, Type}, Opts) ->
{server_regexp, SR} -> {server_regexp, [b(SR)]};
{server_glob, S} -> {server_glob, [b(S)]};
{ip, S} -> {ip, [b(S)]};
- {resource_glob, R} -> {resource_glob, [b(R)]}
+ {resource_glob, R} -> {resource_glob, [b(R)]};
+ {resource_regexp, R} -> {resource_regexp, [b(R)]}
end,
[{acl, [{Name, [T]}]}|Opts];
transform_options({access, Name, Rules}, Opts) ->
diff --git a/src/ejabberd_app.erl b/src/ejabberd_app.erl
index 6f0b97fa3..890ab6f90 100644
--- a/src/ejabberd_app.erl
+++ b/src/ejabberd_app.erl
@@ -56,6 +56,7 @@ start(normal, _Args) ->
ejabberd_admin:start(),
gen_mod:start(),
ext_mod:start(),
+ setup_if_elixir_conf_used(),
ejabberd_config:start(),
set_settings_from_config(),
acl:start(),
@@ -75,6 +76,8 @@ start(normal, _Args) ->
ejabberd_oauth:start(),
gen_mod:start_modules(),
ejabberd_listener:start_listeners(),
+ ejabberd_service:start(),
+ register_elixir_config_hooks(),
?INFO_MSG("ejabberd ~s is started in the node ~p", [?VERSION, node()]),
Sup;
start(_, _) ->
@@ -239,8 +242,25 @@ opt_type(modules) ->
end;
opt_type(_) -> [cluster_nodes, loglevel, modules, net_ticktime].
-start_elixir_application() ->
- case application:ensure_started(elixir) of
- ok -> ok;
- {error, _Msg} -> ?ERROR_MSG("Elixir application not started.", [])
+setup_if_elixir_conf_used() ->
+ case ejabberd_config:is_using_elixir_config() of
+ true -> 'Elixir.Ejabberd.Config.Store':start_link();
+ false -> ok
+ end.
+
+register_elixir_config_hooks() ->
+ case ejabberd_config:is_using_elixir_config() of
+ true -> 'Elixir.Ejabberd.Config':start_hooks();
+ false -> ok
end.
+
+start_elixir_application() ->
+ case ejabberd_config:is_elixir_enabled() of
+ true ->
+ case application:ensure_started(elixir) of
+ ok -> ok;
+ {error, _Msg} -> ?ERROR_MSG("Elixir application not started.", [])
+ end;
+ _ ->
+ ok
+ end.
diff --git a/src/ejabberd_c2s.erl b/src/ejabberd_c2s.erl
index 73cc57247..226c5e0da 100644
--- a/src/ejabberd_c2s.erl
+++ b/src/ejabberd_c2s.erl
@@ -32,6 +32,7 @@
-protocol({xep, 78, '2.5'}).
-protocol({xep, 138, '2.0'}).
-protocol({xep, 198, '1.3'}).
+-protocol({xep, 356, '7.1'}).
-update_info({update, 0}).
@@ -48,6 +49,7 @@
send_element/2,
socket_type/0,
get_presence/1,
+ get_last_presence/1,
get_aux_field/2,
set_aux_field/3,
del_aux_field/2,
@@ -116,9 +118,12 @@
mgmt_pending_since,
mgmt_timeout,
mgmt_max_timeout,
+ mgmt_ack_timeout,
+ mgmt_ack_timer,
mgmt_resend,
mgmt_stanzas_in = 0,
mgmt_stanzas_out = 0,
+ mgmt_stanzas_req = 0,
ask_offline = true,
lang = <<"">>}).
@@ -217,6 +222,9 @@ socket_type() -> xml_stream.
get_presence(FsmRef) ->
(?GEN_FSM):sync_send_all_state_event(FsmRef,
{get_presence}, 1000).
+get_last_presence(FsmRef) ->
+ (?GEN_FSM):sync_send_all_state_event(FsmRef,
+ {get_last_presence}, 1000).
get_aux_field(Key, #state{aux_fields = Opts}) ->
case lists:keysearch(Key, 1, Opts) of
@@ -329,13 +337,18 @@ init([{SockMod, Socket}, Opts]) ->
_ -> 1000
end,
ResumeTimeout = case proplists:get_value(resume_timeout, Opts) of
- Timeout when is_integer(Timeout), Timeout >= 0 -> Timeout;
+ RTimeo when is_integer(RTimeo), RTimeo >= 0 -> RTimeo;
_ -> 300
end,
MaxResumeTimeout = case proplists:get_value(max_resume_timeout, Opts) of
Max when is_integer(Max), Max >= ResumeTimeout -> Max;
_ -> ResumeTimeout
end,
+ AckTimeout = case proplists:get_value(ack_timeout, Opts) of
+ ATimeo when is_integer(ATimeo), ATimeo > 0 -> ATimeo * 1000;
+ infinity -> undefined;
+ _ -> 60000
+ end,
ResendOnTimeout = case proplists:get_value(resend_on_timeout, Opts) of
Resend when is_boolean(Resend) -> Resend;
if_offline -> if_offline;
@@ -359,6 +372,7 @@ init([{SockMod, Socket}, Opts]) ->
mgmt_max_queue = MaxAckQueue,
mgmt_timeout = ResumeTimeout,
mgmt_max_timeout = MaxResumeTimeout,
+ mgmt_ack_timeout = AckTimeout,
mgmt_resend = ResendOnTimeout},
{ok, wait_for_stream, StateData, ?C2S_OPEN_TIMEOUT}.
@@ -1332,6 +1346,15 @@ handle_sync_event({get_presence}, _From, StateName,
Resource = StateData#state.resource,
Reply = {User, Resource, Show, Status},
fsm_reply(Reply, StateName, StateData);
+handle_sync_event({get_last_presence}, _From, StateName,
+ StateData) ->
+ User = StateData#state.user,
+ Server = StateData#state.server,
+ PresLast = StateData#state.pres_last,
+ Resource = StateData#state.resource,
+ Reply = {User, Server, Resource, PresLast},
+ fsm_reply(Reply, StateName, StateData);
+
handle_sync_event(get_subscribed, _From, StateName,
StateData) ->
Subscribed = (?SETS):to_list(StateData#state.pres_f),
@@ -1775,6 +1798,11 @@ handle_info({set_resume_timeout, Timeout}, StateName, StateData) ->
fsm_next_state(StateName, StateData#state{mgmt_timeout = Timeout});
handle_info(dont_ask_offline, StateName, StateData) ->
fsm_next_state(StateName, StateData#state{ask_offline = false});
+handle_info(close, StateName, StateData) ->
+ ?DEBUG("Timeout waiting for stream management acknowledgement of ~s",
+ [jid:to_string(StateData#state.jid)]),
+ close(self()),
+ fsm_next_state(StateName, StateData#state{mgmt_ack_timer = undefined});
handle_info({_Ref, {resume, OldStateData}}, StateName, StateData) ->
%% This happens if the resume_session/1 request timed out; the new session
%% now receives the late response.
@@ -1910,8 +1938,8 @@ send_stanza(StateData, Stanza) when StateData#state.csi_state == inactive ->
send_stanza(StateData, Stanza) when StateData#state.mgmt_state == pending ->
mgmt_queue_add(StateData, Stanza);
send_stanza(StateData, Stanza) when StateData#state.mgmt_state == active ->
- NewStateData = send_stanza_and_ack_req(StateData, Stanza),
- mgmt_queue_add(NewStateData, Stanza);
+ NewStateData = mgmt_queue_add(StateData, Stanza),
+ mgmt_send_stanza(NewStateData, Stanza);
send_stanza(StateData, Stanza) ->
send_element(StateData, Stanza),
StateData.
@@ -2499,6 +2527,12 @@ fsm_next_state(wait_for_resume, #state{mgmt_pending_since = undefined,
sid = SID, jid = JID, ip = IP,
conn = Conn, auth_module = AuthModule,
server = Host} = StateData) ->
+ case StateData of
+ #state{mgmt_ack_timer = undefined} ->
+ ok;
+ #state{mgmt_ack_timer = Timer} ->
+ erlang:cancel_timer(Timer)
+ end,
?INFO_MSG("Waiting for resumption of stream for ~s",
[jid:to_string(JID)]),
Info = [{ip, IP}, {conn, Conn}, {auth_module, AuthModule}],
@@ -2779,7 +2813,8 @@ handle_r(StateData) ->
handle_a(StateData, Attrs) ->
case catch jlib:binary_to_integer(fxml:get_attr_s(<<"h">>, Attrs)) of
H when is_integer(H), H >= 0 ->
- check_h_attribute(StateData, H);
+ NewStateData = check_h_attribute(StateData, H),
+ maybe_renew_ack_request(NewStateData);
_ ->
?DEBUG("Ignoring invalid ACK element from ~s",
[jid:to_string(StateData#state.jid)]),
@@ -2878,16 +2913,45 @@ update_num_stanzas_in(#state{mgmt_state = MgmtState} = StateData, El)
update_num_stanzas_in(StateData, _El) ->
StateData.
-send_stanza_and_ack_req(StateData, Stanza) ->
- AckReq = #xmlel{name = <<"r">>,
- attrs = [{<<"xmlns">>, StateData#state.mgmt_xmlns}],
- children = []},
- case send_element(StateData, Stanza) == ok andalso
- send_element(StateData, AckReq) == ok of
+mgmt_send_stanza(StateData, Stanza) ->
+ case send_element(StateData, Stanza) of
+ ok ->
+ maybe_request_ack(StateData);
+ _ ->
+ StateData#state{mgmt_state = pending}
+ end.
+
+maybe_request_ack(#state{mgmt_ack_timer = undefined} = StateData) ->
+ request_ack(StateData);
+maybe_request_ack(StateData) ->
+ StateData.
+
+request_ack(#state{mgmt_xmlns = Xmlns,
+ mgmt_ack_timeout = AckTimeout} = StateData) ->
+ AckReq = #xmlel{name = <<"r">>, attrs = [{<<"xmlns">>, Xmlns}]},
+ case {send_element(StateData, AckReq), AckTimeout} of
+ {ok, undefined} ->
+ ok;
+ {ok, Timeout} ->
+ Timer = erlang:send_after(Timeout, self(), close),
+ StateData#state{mgmt_ack_timer = Timer,
+ mgmt_stanzas_req = StateData#state.mgmt_stanzas_out};
+ _ ->
+ StateData#state{mgmt_state = pending}
+ end.
+
+maybe_renew_ack_request(#state{mgmt_ack_timer = undefined} = StateData) ->
+ StateData;
+maybe_renew_ack_request(#state{mgmt_ack_timer = Timer,
+ mgmt_queue = Queue,
+ mgmt_stanzas_out = NumStanzasOut,
+ mgmt_stanzas_req = NumStanzasReq} = StateData) ->
+ erlang:cancel_timer(Timer),
+ case NumStanzasReq < NumStanzasOut andalso not queue:is_empty(Queue) of
true ->
- StateData;
+ request_ack(StateData#state{mgmt_ack_timer = undefined});
false ->
- StateData#state{mgmt_state = pending}
+ StateData#state{mgmt_ack_timer = undefined}
end.
mgmt_queue_add(StateData, El) ->
diff --git a/src/ejabberd_config.erl b/src/ejabberd_config.erl
index 87a918704..6ca6a40a8 100644
--- a/src/ejabberd_config.erl
+++ b/src/ejabberd_config.erl
@@ -33,10 +33,12 @@
get_option/2, get_option/3, add_option/2, has_option/1,
get_vh_by_auth_method/1, is_file_readable/1,
get_version/0, get_myhosts/0, get_mylang/0,
+ get_ejabberd_config_path/0, is_using_elixir_config/0,
prepare_opt_val/4, convert_table_to_binary/5,
transform_options/1, collect_options/1, default_db/2,
convert_to_yaml/1, convert_to_yaml/2, v_db/2,
- env_binary_to_list/2, opt_type/1, may_hide_data/1]).
+ env_binary_to_list/2, opt_type/1, may_hide_data/1,
+ is_elixir_enabled/0]).
-export([start/2]).
@@ -147,7 +149,18 @@ read_file(File) ->
{include_modules_configs, true}]).
read_file(File, Opts) ->
- Terms1 = get_plain_terms_file(File, Opts),
+ Terms1 = case is_elixir_enabled() of
+ true ->
+ case 'Elixir.Ejabberd.ConfigUtil':is_elixir_config(File) of
+ true ->
+ 'Elixir.Ejabberd.Config':init(File),
+ 'Elixir.Ejabberd.Config':get_ejabberd_opts();
+ false ->
+ get_plain_terms_file(File, Opts)
+ end;
+ false ->
+ get_plain_terms_file(File, Opts)
+ end,
Terms_macros = case proplists:get_bool(replace_macros, Opts) of
true -> replace_macros(Terms1);
false -> Terms1
@@ -318,7 +331,9 @@ get_absolute_path(File) ->
File;
relative ->
{ok, Dir} = file:get_cwd(),
- filename:absname_join(Dir, File)
+ filename:absname_join(Dir, File);
+ volumerelative ->
+ filename:absname(File)
end.
@@ -1040,6 +1055,23 @@ replace_modules(Modules) ->
%% Elixir module naming
%% ====================
+-ifdef(ELIXIR_ENABLED).
+is_elixir_enabled() ->
+ true.
+-else.
+is_elixir_enabled() ->
+ false.
+-endif.
+
+is_using_elixir_config() ->
+ case is_elixir_enabled() of
+ true ->
+ Config = get_ejabberd_config_path(),
+ 'Elixir.Ejabberd.ConfigUtil':is_elixir_config(Config);
+ false ->
+ false
+ end.
+
%% If module name start with uppercase letter, this is an Elixir module:
is_elixir_module(Module) ->
case atom_to_list(Module) of
diff --git a/src/ejabberd_http.erl b/src/ejabberd_http.erl
index a79f26305..31f80be78 100644
--- a/src/ejabberd_http.erl
+++ b/src/ejabberd_http.erl
@@ -145,9 +145,14 @@ init({SockMod, Socket}, Opts) ->
DefinedHandlers = gen_mod:get_opt(
request_handlers, Opts,
fun(Hs) ->
+ Hs1 = lists:map(fun
+ ({Mod, Path}) when is_atom(Mod) -> {Path, Mod};
+ ({Path, Mod}) -> {Path, Mod}
+ end, Hs),
+
[{str:tokens(
iolist_to_binary(Path), <<"/">>),
- Mod} || {Path, Mod} <- Hs]
+ Mod} || {Path, Mod} <- Hs1]
end, []),
RequestHandlers = DefinedHandlers ++ Captcha ++ Register ++
Admin ++ Bind ++ XMLRPC,
diff --git a/src/ejabberd_http_bind.erl b/src/ejabberd_http_bind.erl
index ea8cd792f..628119e6f 100644
--- a/src/ejabberd_http_bind.erl
+++ b/src/ejabberd_http_bind.erl
@@ -338,8 +338,9 @@ handle_session_start(Pid, XmppDomain, Sid, Rid, Attrs,
init([Sid, Key, IP, HOpts]) ->
?DEBUG("started: ~p", [{Sid, Key, IP}]),
Opts1 = ejabberd_c2s_config:get_c2s_limits(),
- SOpts = lists:filtermap(fun({stream_managment, _}) -> true;
+ SOpts = lists:filtermap(fun({stream_management, _}) -> true;
({max_ack_queue, _}) -> true;
+ ({ack_timeout, _}) -> true;
({resume_timeout, _}) -> true;
({max_resume_timeout, _}) -> true;
({resend_on_timeout, _}) -> true;
diff --git a/src/ejabberd_http_ws.erl b/src/ejabberd_http_ws.erl
index e66cf33a5..e76e8689a 100644
--- a/src/ejabberd_http_ws.erl
+++ b/src/ejabberd_http_ws.erl
@@ -112,8 +112,9 @@ socket_handoff(LocalPath, Request, Socket, SockMod, Buf, Opts) ->
%%% Internal
init([{#ws{ip = IP, http_opts = HOpts}, _} = WS]) ->
- SOpts = lists:filtermap(fun({stream_managment, _}) -> true;
+ SOpts = lists:filtermap(fun({stream_management, _}) -> true;
({max_ack_queue, _}) -> true;
+ ({ack_timeout, _}) -> true;
({resume_timeout, _}) -> true;
({max_resume_timeout, _}) -> true;
({resend_on_timeout, _}) -> true;
diff --git a/src/ejabberd_oauth.erl b/src/ejabberd_oauth.erl
index 36c679cb6..4541190ad 100644
--- a/src/ejabberd_oauth.erl
+++ b/src/ejabberd_oauth.erl
@@ -66,7 +66,7 @@
%% * Using the command line and oauth_issue_token command, the token is generated in behalf of ejabberd' sysadmin
%% (as it has access to ejabberd command line).
--define(EXPIRE, 31536000).
+-define(EXPIRE, 4294967).
start() ->
DBMod = get_db_backend(),
diff --git a/src/ejabberd_service.erl b/src/ejabberd_service.erl
index 9d72b17b4..9dd7c831e 100644
--- a/src/ejabberd_service.erl
+++ b/src/ejabberd_service.erl
@@ -36,7 +36,7 @@
-behaviour(?GEN_FSM).
%% External exports
--export([start/2, start_link/2, send_text/2,
+-export([start/0, start/2, start_link/2, send_text/2,
send_element/2, socket_type/0, transform_listen_option/2]).
-export([init/1, wait_for_stream/2,
@@ -44,19 +44,10 @@
handle_event/3, handle_sync_event/4, code_change/4,
handle_info/3, terminate/3, print_state/1, opt_type/1]).
--include("ejabberd.hrl").
--include("logger.hrl").
+-include("ejabberd_service.hrl").
+-include("mod_privacy.hrl").
--include("jlib.hrl").
-
--record(state,
- {socket :: ejabberd_socket:socket_state(),
- sockmod = ejabberd_socket :: ejabberd_socket | ejabberd_frontend_socket,
- streamid = <<"">> :: binary(),
- host_opts = dict:new() :: ?TDICT,
- host = <<"">> :: binary(),
- access :: atom(),
- check_from = true :: boolean()}).
+-export([get_delegated_ns/1]).
%-define(DBGFSM, true).
@@ -99,6 +90,15 @@
%%%----------------------------------------------------------------------
%%% API
%%%----------------------------------------------------------------------
+
+%% for xep-0355
+%% table contans records like {namespace, fitering attributes, pid(),
+%% host, disco info for general case, bare jid disco info }
+
+start() ->
+ ets:new(delegated_namespaces, [named_table, public]),
+ ets:new(hooks_tmp, [named_table, public]).
+
start(SockData, Opts) ->
supervisor:start_child(ejabberd_service_sup,
[SockData, Opts]).
@@ -109,6 +109,9 @@ start_link(SockData, Opts) ->
socket_type() -> xml_stream.
+get_delegated_ns(FsmRef) ->
+ (?GEN_FSM):sync_send_all_state_event(FsmRef, {get_delegated_ns}).
+
%%%----------------------------------------------------------------------
%%% Callback functions from gen_fsm
%%%----------------------------------------------------------------------
@@ -141,6 +144,21 @@ init([{SockMod, Socket}, Opts]) ->
p1_sha:sha(crypto:rand_bytes(20))),
dict:from_list([{global, Pass}])
end,
+ %% privilege access to entities data
+ PrivAccess = case lists:keysearch(privilege_access, 1, Opts) of
+ {value, {_, PrivAcc}} -> PrivAcc;
+ _ -> []
+ end,
+ Delegations = case lists:keyfind(delegations, 1, Opts) of
+ {delegations, Del} ->
+ lists:foldl(
+ fun({Ns, FiltAttr}, D) when Ns /= ?NS_DELEGATION ->
+ Attr = proplists:get_value(filtering, FiltAttr, []),
+ D ++ [{Ns, Attr}];
+ (_Deleg, D) -> D
+ end, [], Del);
+ false -> []
+ end,
Shaper = case lists:keysearch(shaper_rule, 1, Opts) of
{value, {_, S}} -> S;
_ -> none
@@ -154,8 +172,9 @@ init([{SockMod, Socket}, Opts]) ->
SockMod:change_shaper(Socket, Shaper),
{ok, wait_for_stream,
#state{socket = Socket, sockmod = SockMod,
- streamid = new_id(), host_opts = HostOpts,
- access = Access, check_from = CheckFrom}}.
+ streamid = new_id(), host_opts = HostOpts, access = Access,
+ check_from = CheckFrom, privilege_access = PrivAccess,
+ delegations = Delegations}}.
%%----------------------------------------------------------------------
%% Func: StateName/2
@@ -227,8 +246,31 @@ wait_for_handshake({xmlstreamelement, El}, StateData) ->
[H]),
ejabberd_hooks:run(component_connected,
[H])
- end, dict:fetch_keys(StateData#state.host_opts)),
- {next_state, stream_established, StateData};
+ end, dict:fetch_keys(StateData#state.host_opts)),
+
+ mod_privilege:advertise_permissions(StateData),
+ DelegatedNs = mod_delegation:advertise_delegations(StateData),
+
+ RosterAccess = proplists:get_value(roster,
+ StateData#state.privilege_access),
+
+ case proplists:get_value(presence,
+ StateData#state.privilege_access) of
+ <<"managed_entity">> ->
+ mod_privilege:initial_presences(StateData),
+ Fun = mod_privilege:process_presence(self()),
+ add_hooks(user_send_packet, Fun);
+ <<"roster">> when (RosterAccess == <<"both">>) or
+ (RosterAccess == <<"get">>) ->
+ mod_privilege:initial_presences(StateData),
+ Fun = mod_privilege:process_presence(self()),
+ add_hooks(user_send_packet, Fun),
+ Fun2 = mod_privilege:process_roster_presence(self()),
+ add_hooks(s2s_receive_packet, Fun2);
+ _ -> ok
+ end,
+ {next_state, stream_established,
+ StateData#state{delegations = DelegatedNs}};
_ ->
send_text(StateData, ?INVALID_HANDSHAKE_ERR),
{stop, normal, StateData}
@@ -276,11 +318,12 @@ stream_established({xmlstreamelement, El}, StateData) ->
<<"">> -> error;
_ -> jid:from_string(To)
end,
- if ((Name == <<"iq">>) or (Name == <<"message">>) or
- (Name == <<"presence">>))
- and (ToJID /= error)
- and (FromJID /= error) ->
- ejabberd_router:route(FromJID, ToJID, NewEl);
+ if (Name == <<"iq">>) and (ToJID /= error) and (FromJID /= error) ->
+ mod_privilege:process_iq(StateData, FromJID, ToJID, NewEl);
+ (Name == <<"presence">>) and (ToJID /= error) and (FromJID /= error) ->
+ ejabberd_router:route(FromJID, ToJID, NewEl);
+ (Name == <<"message">>) and (ToJID /= error) and (FromJID /= error) ->
+ mod_privilege:process_message(StateData, FromJID, ToJID, NewEl);
true ->
Lang = fxml:get_tag_attr_s(<<"xml:lang">>, El),
Txt = <<"Incorrect stanza name or from/to JID">>,
@@ -330,8 +373,11 @@ handle_event(_Event, StateName, StateData) ->
%% {stop, Reason, NewStateData} |
%% {stop, Reason, Reply, NewStateData}
%%----------------------------------------------------------------------
-handle_sync_event(_Event, _From, StateName,
- StateData) ->
+handle_sync_event({get_delegated_ns}, _From, StateName, StateData) ->
+ Reply = {StateData#state.host, StateData#state.delegations},
+ {reply, Reply, StateName, StateData};
+
+handle_sync_event(_Event, _From, StateName, StateData) ->
Reply = ok, {reply, Reply, StateName, StateData}.
code_change(_OldVsn, StateName, StateData, _Extra) ->
@@ -370,6 +416,36 @@ handle_info({route, From, To, Packet}, StateName,
ejabberd_router:route_error(To, From, Err, Packet)
end,
{next_state, StateName, StateData};
+
+handle_info({user_presence, Packet, From},
+ stream_established, StateData) ->
+ To = jid:from_string(StateData#state.host),
+ PacketNew = jlib:replace_from_to(From, To, Packet),
+ send_element(StateData, PacketNew),
+ {next_state, stream_established, StateData};
+
+handle_info({roster_presence, Packet, From},
+ stream_established, StateData) ->
+ %% check that current presence stanza is equivalent to last
+ PresenceNew = jlib:remove_attr(<<"to">>, Packet),
+ Dict = StateData#state.last_pres,
+ LastPresence =
+ try dict:fetch(From, Dict)
+ catch _:_ ->
+ undefined
+ end,
+ case mod_privilege:compare_presences(LastPresence, PresenceNew) of
+ false ->
+ #xmlel{attrs = Attrs} = PresenceNew,
+ Presence = PresenceNew#xmlel{attrs = [{<<"to">>, StateData#state.host} | Attrs]},
+ send_element(StateData, Presence),
+ DictNew = dict:store(From, PresenceNew, Dict),
+ StateDataNew = StateData#state{last_pres = DictNew},
+ {next_state, stream_established, StateDataNew};
+ _ ->
+ {next_state, stream_established, StateData}
+ end;
+
handle_info(Info, StateName, StateData) ->
?ERROR_MSG("Unexpected info: ~p", [Info]),
{next_state, StateName, StateData}.
@@ -388,7 +464,26 @@ terminate(Reason, StateName, StateData) ->
ejabberd_hooks:run(component_disconnected,
[StateData#state.host, Reason])
end,
- dict:fetch_keys(StateData#state.host_opts));
+ dict:fetch_keys(StateData#state.host_opts)),
+
+ lists:foreach(fun({Ns, _FilterAttr}) ->
+ ets:delete(delegated_namespaces, Ns),
+ remove_iq_handlers(Ns)
+ end, StateData#state.delegations),
+
+ RosterAccess = proplists:get_value(roster, StateData#state.privilege_access),
+ case proplists:get_value(presence, StateData#state.privilege_access) of
+ <<"managed_entity">> ->
+ Fun = mod_privilege:process_presence(self()),
+ remove_hooks(user_send_packet, Fun);
+ <<"roster">> when (RosterAccess == <<"both">>) or
+ (RosterAccess == <<"get">>) ->
+ Fun = mod_privilege:process_presence(self()),
+ remove_hooks(user_send_packet, Fun),
+ Fun2 = mod_privilege:process_roster_presence(self()),
+ remove_hooks(s2s_receive_packet, Fun2);
+ _ -> ok
+ end;
_ -> ok
end,
(StateData#state.sockmod):close(StateData#state.socket),
@@ -448,3 +543,19 @@ fsm_limit_opts(Opts) ->
opt_type(max_fsm_queue) ->
fun (I) when is_integer(I), I > 0 -> I end;
opt_type(_) -> [max_fsm_queue].
+
+remove_iq_handlers(Ns) ->
+ lists:foreach(fun(Host) ->
+ gen_iq_handler:remove_iq_handler(ejabberd_local, Host, Ns),
+ gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, Ns)
+ end, ?MYHOSTS).
+
+add_hooks(Hook, Fun) ->
+ lists:foreach(fun(Host) ->
+ ejabberd_hooks:add(Hook, Host,Fun, 100)
+ end, ?MYHOSTS).
+
+remove_hooks(Hook, Fun) ->
+ lists:foreach(fun(Host) ->
+ ejabberd_hooks:delete(Hook, Host, Fun, 100)
+ end, ?MYHOSTS).
diff --git a/src/jlib.erl b/src/jlib.erl
index 4bc9b0055..3384e670e 100644
--- a/src/jlib.erl
+++ b/src/jlib.erl
@@ -371,15 +371,20 @@ iq_type_to_string(error) -> <<"error">>.
-spec iq_to_xml(IQ :: iq()) -> xmlel().
iq_to_xml(#iq{id = ID, type = Type, sub_el = SubEl}) ->
+ Children =
+ if
+ is_list(SubEl) -> SubEl;
+ true -> [SubEl]
+ end,
if ID /= <<"">> ->
#xmlel{name = <<"iq">>,
attrs =
[{<<"id">>, ID}, {<<"type">>, iq_type_to_string(Type)}],
- children = SubEl};
+ children = Children};
true ->
#xmlel{name = <<"iq">>,
attrs = [{<<"type">>, iq_type_to_string(Type)}],
- children = SubEl}
+ children = Children}
end.
-spec parse_xdata_submit(El :: xmlel()) ->
diff --git a/src/mod_delegation.erl b/src/mod_delegation.erl
new file mode 100644
index 000000000..f2d1a13b5
--- /dev/null
+++ b/src/mod_delegation.erl
@@ -0,0 +1,538 @@
+%%%--------------------------------------------------------------------------------------
+%%% File : mod_delegation.erl
+%%% Author : Anna Mukharram <amuhar3@gmail.com>
+%%% Purpose : This module is an implementation for XEP-0355: Namespace Delegation
+%%%--------------------------------------------------------------------------------------
+
+-module(mod_delegation).
+
+-author('amuhar3@gmail.com').
+
+-behaviour(gen_mod).
+
+-protocol({xep, 0355, '0.3'}).
+
+-export([start/2, stop/1, depends/2, mod_opt_type/1]).
+
+-export([advertise_delegations/1, process_iq/3,
+ disco_local_features/5, disco_sm_features/5,
+ disco_local_identity/5, disco_sm_identity/5, disco_info/5, clean/0]).
+
+-include_lib("stdlib/include/ms_transform.hrl").
+
+-include("ejabberd_service.hrl").
+
+-define(CLEAN_INTERVAL, timer:minutes(10)).
+
+%%%--------------------------------------------------------------------------------------
+%%% API
+%%%--------------------------------------------------------------------------------------
+
+start(Host, _Opts) ->
+ mod_disco:register_feature(Host, ?NS_DELEGATION),
+ %% start timer for hooks_tmp table cleaning
+ timer:apply_after(?CLEAN_INTERVAL, ?MODULE, clean, []),
+
+ ejabberd_hooks:add(disco_local_features, Host, ?MODULE,
+ disco_local_features, 500), %% This hook should be the last
+ ejabberd_hooks:add(disco_local_identity, Host, ?MODULE,
+ disco_local_identity, 500),
+ ejabberd_hooks:add(disco_sm_identity, Host, ?MODULE,
+ disco_sm_identity, 500),
+ ejabberd_hooks:add(disco_sm_features, Host, ?MODULE,
+ disco_sm_features, 500),
+ ejabberd_hooks:add(disco_info, Host, ?MODULE,
+ disco_info, 500).
+
+
+stop(Host) ->
+ mod_disco:unregister_feature(Host, ?NS_DELEGATION),
+ ejabberd_hooks:delete(disco_local_features, Host, ?MODULE,
+ disco_local_features, 500),
+ ejabberd_hooks:delete(disco_local_identity, Host, ?MODULE,
+ disco_local_identity, 500),
+ ejabberd_hooks:delete(disco_sm_identity, Host, ?MODULE,
+ disco_sm_identity, 500),
+ ejabberd_hooks:delete(disco_sm_features, Host, ?MODULE,
+ disco_sm_features, 500),
+ ejabberd_hooks:delete(disco_info, Host, ?MODULE,
+ disco_info, 500).
+
+depends(_Host, _Opts) -> [].
+
+mod_opt_type(_Opt) -> [].
+
+%%%--------------------------------------------------------------------------------------
+%%% 4.2 Functions to advertise service of delegated namespaces
+%%%--------------------------------------------------------------------------------------
+attribute_tag(Attrs) ->
+ lists:map(fun(Attr) ->
+ #xmlel{name = <<"attribute">>, attrs = [{<<"name">> , Attr}]}
+ end, Attrs).
+
+delegations(From, To, Delegations) ->
+ {Elem0, DelegatedNs} =
+ lists:foldl(fun({Ns, FiltAttr}, {Acc, AccNs}) ->
+ case ets:insert_new(delegated_namespaces,
+ {Ns, FiltAttr, self(), To, {}, {}}) of
+ true ->
+ Attrs =
+ if
+ FiltAttr == [] ->
+ ?DEBUG("namespace ~s is delegated to ~s with"
+ " no filtering attributes ~n",[Ns, To]),
+ [];
+ true ->
+ ?DEBUG("namespace ~s is delegated to ~s with"
+ " ~p filtering attributes ~n",[Ns, To, FiltAttr]),
+ attribute_tag(FiltAttr)
+ end,
+ add_iq_handlers(Ns),
+ {[#xmlel{name = <<"delegated">>,
+ attrs = [{<<"namespace">>, Ns}],
+ children = Attrs}| Acc], [{Ns, FiltAttr}|AccNs]};
+ false -> {Acc, AccNs}
+ end
+ end, {[], []}, Delegations),
+ case Elem0 of
+ [] -> {ignore, DelegatedNs};
+ _ ->
+ Elem1 = #xmlel{name = <<"delegation">>,
+ attrs = [{<<"xmlns">>, ?NS_DELEGATION}],
+ children = Elem0},
+ Id = randoms:get_string(),
+ {#xmlel{name = <<"message">>,
+ attrs = [{<<"id">>, Id}, {<<"from">>, From}, {<<"to">>, To}],
+ children = [Elem1]}, DelegatedNs}
+ end.
+
+add_iq_handlers(Ns) ->
+ lists:foreach(fun(Host) ->
+ IQDisc =
+ gen_mod:get_module_opt(Host, ?MODULE, iqdisc,
+ fun gen_iq_handler:check_type/1, one_queue),
+ gen_iq_handler:add_iq_handler(ejabberd_sm, Host,
+ Ns, ?MODULE,
+ process_iq, IQDisc),
+ gen_iq_handler:add_iq_handler(ejabberd_local, Host,
+ Ns, ?MODULE,
+ process_iq, IQDisc)
+ end, ?MYHOSTS).
+
+advertise_delegations(#state{delegations = []}) -> [];
+advertise_delegations(StateData) ->
+ {Delegated, DelegatedNs} =
+ delegations(?MYNAME, StateData#state.host, StateData#state.delegations),
+ if
+ Delegated /= ignore ->
+ ejabberd_service:send_element(StateData, Delegated),
+ % server asks available features for delegated namespaces
+ disco_info(StateData#state{delegations = DelegatedNs});
+ true -> ok
+ end,
+ DelegatedNs.
+
+%%%--------------------------------------------------------------------------------------
+%%% Delegated namespaces hook
+%%%--------------------------------------------------------------------------------------
+
+check_filter_attr([], _Children) -> true;
+check_filter_attr(_FilterAttr, []) -> false;
+check_filter_attr(FilterAttr, [#xmlel{} = Stanza|_]) ->
+ Attrs = proplists:get_keys(Stanza#xmlel.attrs),
+ lists:all(fun(Attr) ->
+ lists:member(Attr, Attrs)
+ end, FilterAttr);
+check_filter_attr(_FilterAttr, _Children) -> false.
+
+-spec get_client_server([attr()]) -> {jid(), jid()}.
+
+get_client_server(Attrs) ->
+ Client = fxml:get_attr_s(<<"from">>, Attrs),
+ ClientJID = jid:from_string(Client),
+ ServerJID = jid:from_string(ClientJID#jid.lserver),
+ {ClientJID, ServerJID}.
+
+decapsulate_result(#xmlel{children = []}) -> ok;
+decapsulate_result(#xmlel{children = Children}) ->
+ decapsulate_result0(Children).
+
+decapsulate_result0([]) -> ok;
+decapsulate_result0([#xmlel{name = <<"delegation">>,
+ attrs = [{<<"xmlns">>, ?NS_DELEGATION}]} = Packet]) ->
+ decapsulate_result1(Packet#xmlel.children);
+decapsulate_result0(_Children) -> ok.
+
+decapsulate_result1([]) -> ok;
+decapsulate_result1([#xmlel{name = <<"forwarded">>,
+ attrs = [{<<"xmlns">>, ?NS_FORWARD}]} = Packet]) ->
+ decapsulate_result2(Packet#xmlel.children);
+decapsulate_result1(_Children) -> ok.
+
+decapsulate_result2([]) -> ok;
+decapsulate_result2([#xmlel{name = <<"iq">>, attrs = Attrs} = Packet]) ->
+ Ns = fxml:get_attr_s(<<"xmlns">>, Attrs),
+ if
+ Ns /= <<"jabber:client">> ->
+ ok;
+ true -> Packet
+ end;
+decapsulate_result2(_Children) -> ok.
+
+-spec check_iq(xmlel(), xmlel()) -> xmlel() | ignore.
+
+check_iq(#xmlel{attrs = Attrs} = Packet,
+ #xmlel{attrs = AttrsOrigin} = OriginPacket) ->
+ % Id attribute of OriginPacket Must be equil to Packet Id attribute
+ Id1 = fxml:get_attr_s(<<"id">>, Attrs),
+ Id2 = fxml:get_attr_s(<<"id">>, AttrsOrigin),
+ % From attribute of OriginPacket Must be equil to Packet To attribute
+ From = fxml:get_attr_s(<<"from">>, AttrsOrigin),
+ To = fxml:get_attr_s(<<"to">>, Attrs),
+ % Type attribute Must be error or result
+ Type = fxml:get_attr_s(<<"type">>, Attrs),
+ if
+ ((Type == <<"result">>) or (Type == <<"error">>)),
+ Id1 == Id2, To == From ->
+ NewPacket = jlib:remove_attr(<<"xmlns">>, Packet),
+ %% We can send the decapsulated stanza from Server to Client (To)
+ NewPacket;
+ true ->
+ %% service-unavailable
+ Err = jlib:make_error_reply(OriginPacket, ?ERR_SERVICE_UNAVAILABLE),
+ Err
+ end;
+check_iq(_Packet, _OriginPacket) -> ignore.
+
+-spec manage_service_result(atom(), atom(), binary(), xmlel()) -> ok.
+
+manage_service_result(HookRes, HookErr, Service, OriginPacket) ->
+ fun(Packet) ->
+ {ClientJID, ServerJID} = get_client_server(OriginPacket#xmlel.attrs),
+ Server = ClientJID#jid.lserver,
+
+ ets:delete(hooks_tmp, {HookRes, Server}),
+ ets:delete(hooks_tmp, {HookErr, Server}),
+ % Check Packet "from" attribute
+ % It Must be equil to current service host
+ From = fxml:get_attr_s(<<"from">> , Packet#xmlel.attrs),
+ if
+ From == Service ->
+ % decapsulate iq result
+ ResultIQ = decapsulate_result(Packet),
+ ServResponse = check_iq(ResultIQ, OriginPacket),
+ if
+ ServResponse /= ignore ->
+ ejabberd_router:route(ServerJID, ClientJID, ServResponse);
+ true -> ok
+ end;
+ true ->
+ % service unavailable
+ Err = jlib:make_error_reply(OriginPacket, ?ERR_SERVICE_UNAVAILABLE),
+ ejabberd_router:route(ServerJID, ClientJID, Err)
+ end
+ end.
+
+-spec manage_service_error(atom(), atom(), xmlel()) -> ok.
+
+manage_service_error(HookRes, HookErr, OriginPacket) ->
+ fun(_Packet) ->
+ {ClientJID, ServerJID} = get_client_server(OriginPacket#xmlel.attrs),
+ Server = ClientJID#jid.lserver,
+ ets:delete(hooks_tmp, {HookRes, Server}),
+ ets:delete(hooks_tmp, {HookErr, Server}),
+ Err = jlib:make_error_reply(OriginPacket, ?ERR_SERVICE_UNAVAILABLE),
+ ejabberd_router:route(ServerJID, ClientJID, Err)
+ end.
+
+
+-spec forward_iq(binary(), binary(), xmlel()) -> ok.
+
+forward_iq(Server, Service, Packet) ->
+ Elem0 = #xmlel{name = <<"forwarded">>,
+ attrs = [{<<"xmlns">>, ?NS_FORWARD}], children = [Packet]},
+ Elem1 = #xmlel{name = <<"delegation">>,
+ attrs = [{<<"xmlns">>, ?NS_DELEGATION}], children = [Elem0]},
+ Id = randoms:get_string(),
+ Elem2 = #xmlel{name = <<"iq">>,
+ attrs = [{<<"from">>, Server}, {<<"to">>, Service},
+ {<<"type">>, <<"set">>}, {<<"id">>, Id}],
+ children = [Elem1]},
+
+ HookRes = {iq, result, Id},
+ HookErr = {iq, error, Id},
+
+ FunRes = manage_service_result(HookRes, HookErr, Service, Packet),
+ FunErr = manage_service_error(HookRes, HookErr, Packet),
+
+ Timestamp = p1_time_compat:system_time(seconds),
+ ets:insert(hooks_tmp, {{HookRes, Server}, FunRes, Timestamp}),
+ ets:insert(hooks_tmp, {{HookErr, Server}, FunErr, Timestamp}),
+
+ From = jid:make(<<"">>, Server, <<"">>),
+ To = jid:make(<<"">>, Service, <<"">>),
+ ejabberd_router:route(From, To, Elem2).
+
+process_iq(From, #jid{lresource = <<"">>} = To,
+ #iq{type = Type, xmlns = XMLNS} = IQ) ->
+ %% check if stanza directed to server
+ %% or directed to the bare JID of the sender
+ case ((Type == get) or (Type == set)) of
+ true ->
+ Packet = jlib:iq_to_xml(IQ),
+ #xmlel{name = <<"iq">>, attrs = Attrs, children = Children} = Packet,
+ AttrsNew = [{<<"xmlns">>, <<"jabber:client">>} | Attrs],
+ AttrsNew2 = jlib:replace_from_to_attrs(jid:to_string(From),
+ jid:to_string(To), AttrsNew),
+ case ets:lookup(delegated_namespaces, XMLNS) of
+ [{XMLNS, FiltAttr, _Pid, ServiceHost, _, _}] ->
+ case check_filter_attr(FiltAttr, Children) of
+ true ->
+ forward_iq(From#jid.server, ServiceHost,
+ Packet#xmlel{attrs = AttrsNew2});
+ _ -> ok
+ end;
+ [] -> ok
+ end,
+ ignore;
+ _ ->
+ ignore
+ end;
+process_iq(_From, _To, _IQ) -> ignore.
+
+%%%--------------------------------------------------------------------------------------
+%%% 7. Discovering Support
+%%%--------------------------------------------------------------------------------------
+
+decapsulate_features(#xmlel{attrs = Attrs} = Packet, Node) ->
+ case fxml:get_attr_s(<<"node">>, Attrs) of
+ Node ->
+ PREFIX = << ?NS_DELEGATION/binary, "::" >>,
+ Size = byte_size(PREFIX),
+ BARE_PREFIX = << ?NS_DELEGATION/binary, ":bare:" >>,
+ SizeBare = byte_size(BARE_PREFIX),
+
+ Features = [Feat || #xmlel{attrs = [{<<"var">>, Feat}]} <-
+ fxml:get_subtags(Packet, <<"feature">>)],
+
+ Identity = [I || I <- fxml:get_subtags(Packet, <<"identity">>)],
+
+ Exten = [I || I <- fxml:get_subtags_with_xmlns(Packet, <<"x">>, ?NS_XDATA)],
+
+ case Node of
+ << PREFIX:Size/binary, NS/binary >> ->
+ ets:update_element(delegated_namespaces, NS,
+ {5, {Features, Identity, Exten}});
+ << BARE_PREFIX:SizeBare/binary, NS/binary >> ->
+ ets:update_element(delegated_namespaces, NS,
+ {6, {Features, Identity, Exten}});
+ _ -> ok
+ end;
+ _ -> ok
+ end;
+decapsulate_features(_Packet, _Node) -> ok.
+
+-spec disco_result(atom(), atom(), binary()) -> ok.
+
+disco_result(HookRes, HookErr, Node) ->
+ fun(Packet) ->
+ Tag = fxml:get_subtag_with_xmlns(Packet, <<"query">>, ?NS_DISCO_INFO),
+ decapsulate_features(Tag, Node),
+
+ ets:delete(hooks_tmp, {HookRes, ?MYNAME}),
+ ets:delete(hooks_tmp, {HookErr, ?MYNAME})
+ end.
+
+-spec disco_error(atom(), atom()) -> ok.
+
+disco_error(HookRes, HookErr) ->
+ fun(_Packet) ->
+ ets:delete(hooks_tmp, {HookRes, ?MYNAME}),
+ ets:delete(hooks_tmp, {HookErr, ?MYNAME})
+ end.
+
+-spec disco_info(state()) -> ok.
+
+disco_info(StateData) ->
+ disco_info(StateData, <<"::">>),
+ disco_info(StateData, <<":bare:">>).
+
+-spec disco_info(state(), binary()) -> ok.
+
+disco_info(StateData, Sep) ->
+ lists:foreach(fun({Ns, _FilterAttr}) ->
+ Id = randoms:get_string(),
+ Node = << ?NS_DELEGATION/binary, Sep/binary, Ns/binary >>,
+
+ HookRes = {iq, result, Id},
+ HookErr = {iq, error, Id},
+
+ FunRes = disco_result(HookRes, HookErr, Node),
+ FunErr = disco_error(HookRes, HookErr),
+
+ Timestamp = p1_time_compat:system_time(seconds),
+ ets:insert(hooks_tmp, {{HookRes, ?MYNAME}, FunRes, Timestamp}),
+ ets:insert(hooks_tmp, {{HookErr, ?MYNAME}, FunErr, Timestamp}),
+
+ Tag = #xmlel{name = <<"query">>,
+ attrs = [{<<"xmlns">>, ?NS_DISCO_INFO},
+ {<<"node">>, Node}],
+ children = []},
+ DiscoReq = #xmlel{name = <<"iq">>,
+ attrs = [{<<"type">>, <<"get">>}, {<<"id">>, Id},
+ {<<"from">>, ?MYNAME},
+ {<<"to">>, StateData#state.host }],
+ children = [Tag]},
+ ejabberd_service:send_element(StateData, DiscoReq)
+
+ end, StateData#state.delegations).
+
+
+disco_features(Acc, Bare) ->
+ Fun = fun(Feat) ->
+ ets:foldl(fun({Ns, _, _, _, _, _}, A) ->
+ A or str:prefix(Ns, Feat)
+ end, false, delegated_namespaces)
+ end,
+ % delete feature namespace which is delegated to service
+ Features = lists:filter(fun ({{Feature, _Host}}) ->
+ not Fun(Feature);
+ (Feature) when is_binary(Feature) ->
+ not Fun(Feature)
+ end, Acc),
+ % add service features
+ FeaturesList =
+ ets:foldl(fun({_, _, _, _, {Feats, _, _}, {FeatsBare, _, _}}, A) ->
+ if
+ Bare -> A ++ FeatsBare;
+ true -> A ++ Feats
+ end;
+ (_, A) -> A
+ end, Features, delegated_namespaces),
+ {result, FeaturesList}.
+
+disco_identity(Acc, Bare) ->
+ % filter delegated identites
+ Fun = fun(Ident) ->
+ ets:foldl(fun({_, _, _, _, {_ , I, _}, {_ , IBare, _}}, A) ->
+ Identity =
+ if
+ Bare -> IBare;
+ true -> I
+ end,
+ (fxml:get_attr_s(<<"category">> , Ident) ==
+ fxml:get_attr_s(<<"category">>, Identity)) and
+ (fxml:get_attr_s(<<"type">> , Ident) ==
+ fxml:get_attr_s(<<"type">>, Identity)) or A;
+ (_, A) -> A
+ end, false, delegated_namespaces)
+ end,
+
+ Identities =
+ lists:filter(fun (#xmlel{attrs = Attrs}) ->
+ not Fun(Attrs)
+ end, Acc),
+ % add service features
+ ets:foldl(fun({_, _, _, _, {_, I, _}, {_, IBare, _}}, A) ->
+ if
+ Bare -> A ++ IBare;
+ true -> A ++ I
+ end;
+ (_, A) -> A
+ end, Identities, delegated_namespaces).
+
+%% xmlns from value element
+
+-spec get_field_value([xmlel()]) -> binary().
+
+get_field_value([]) -> <<"">>;
+get_field_value([Elem| Elems]) ->
+ case (fxml:get_attr_s(<<"var">>, Elem#xmlel.attrs) == <<"FORM_TYPE">>) and
+ (fxml:get_attr_s(<<"type">>, Elem#xmlel.attrs) == <<"hidden">>) of
+ true ->
+ Ns = fxml:get_subtag_cdata(Elem, <<"value">>),
+ if
+ Ns /= <<"">> -> Ns;
+ true -> get_field_value(Elems)
+ end;
+ _ -> get_field_value(Elems)
+ end.
+
+get_info(Acc, Bare) ->
+ Fun = fun(Feat) ->
+ ets:foldl(fun({Ns, _, _, _, _, _}, A) ->
+ (A or str:prefix(Ns, Feat))
+ end, false, delegated_namespaces)
+ end,
+ Exten = lists:filter(fun(Xmlel) ->
+ Tags = fxml:get_subtags(Xmlel, <<"field">>),
+ case get_field_value(Tags) of
+ <<"">> -> true;
+ Value -> not Fun(Value)
+ end
+ end, Acc),
+ ets:foldl(fun({_, _, _, _, {_, _, Ext}, {_, _, ExtBare}}, A) ->
+ if
+ Bare -> A ++ ExtBare;
+ true -> A ++ Ext
+ end;
+ (_, A) -> A
+ end, Exten, delegated_namespaces).
+
+%% 7.2.1 General Case
+
+disco_local_features({error, _Error} = Acc, _From, _To, _Node, _Lang) ->
+ Acc;
+disco_local_features(Acc, _From, _To, <<>>, _Lang) ->
+ FeatsOld = case Acc of
+ {result, I} -> I;
+ _ -> []
+ end,
+ disco_features(FeatsOld, false);
+disco_local_features(Acc, _From, _To, _Node, _Lang) ->
+ Acc.
+
+disco_local_identity(Acc, _From, _To, <<>>, _Lang) ->
+ disco_identity(Acc, false);
+disco_local_identity(Acc, _From, _To, _Node, _Lang) ->
+ Acc.
+
+%% 7.2.2 Rediction Of Bare JID Disco Info
+
+disco_sm_features({error, ?ERR_ITEM_NOT_FOUND}, _From,
+ #jid{lresource = <<"">>}, <<>>, _Lang) ->
+ disco_features([], true);
+disco_sm_features({error, _Error} = Acc, _From, _To, _Node, _Lang) ->
+ Acc;
+disco_sm_features(Acc, _From, #jid{lresource = <<"">>}, <<>>, _Lang) ->
+ FeatsOld = case Acc of
+ {result, I} -> I;
+ _ -> []
+ end,
+ disco_features(FeatsOld, true);
+disco_sm_features(Acc, _From, _To, _Node, _Lang) ->
+ Acc.
+
+disco_sm_identity(Acc, _From, #jid{lresource = <<"">>}, <<>>, _Lang) ->
+ disco_identity(Acc, true);
+disco_sm_identity(Acc, _From, _To, _Node, _Lang) ->
+ Acc.
+
+disco_info(Acc, #jid{}, #jid{lresource = <<"">>}, <<>>, _Lang) ->
+ get_info(Acc, true);
+disco_info(Acc, _Host, _Mod, <<>>, _Lang) ->
+ get_info(Acc, false);
+disco_info(Acc, _Host, _Mod, _Node, _Lang) ->
+ Acc.
+
+%% clean hooks_tmp table
+
+clean() ->
+ ?DEBUG("cleaning ~p ETS table~n", [hooks_tmp]),
+ Now = p1_time_compat:system_time(seconds),
+ catch ets:select_delete(hooks_tmp,
+ ets:fun2ms(fun({_, _, Timestamp}) ->
+ Now - 300 >= Timestamp
+ end)),
+ %% start timer for table cleaning
+ timer:apply_after(?CLEAN_INTERVAL, ?MODULE, clean, []).
diff --git a/src/mod_http_api.erl b/src/mod_http_api.erl
index 73e6f7e4e..dbca82375 100644
--- a/src/mod_http_api.erl
+++ b/src/mod_http_api.erl
@@ -101,7 +101,7 @@
-define(AC_ALLOW_HEADERS,
{<<"Access-Control-Allow-Headers">>,
- <<"Content-Type">>}).
+ <<"Content-Type, Authorization, X-Admin">>}).
-define(AC_MAX_AGE,
{<<"Access-Control-Max-Age">>, <<"86400">>}).
@@ -259,8 +259,10 @@ process([Call], #request{method = 'GET', q = Data, ip = IP} = Req) ->
?DEBUG("Bad Request: ~p ~p", [_Error, erlang:get_stacktrace()]),
badrequest_response()
end;
-process([], #request{method = 'OPTIONS', data = <<>>}) ->
+process([_Call], #request{method = 'OPTIONS', data = <<>>}) ->
{200, ?OPTIONS_HEADER, []};
+process(_, #request{method = 'OPTIONS'}) ->
+ {400, ?OPTIONS_HEADER, []};
process(_Path, Request) ->
?DEBUG("Bad Request: no handler ~p", [Request]),
json_error(400, 40, <<"Missing command name.">>).
@@ -385,6 +387,20 @@ format_args(Args, ArgsFormat) ->
L when is_list(L) -> exit({additional_unused_args, L})
end.
+format_arg({Elements},
+ {list, {_ElementDefName, {tuple, [{_Tuple1N, Tuple1S}, {_Tuple2N, Tuple2S}]} = Tuple}})
+ when is_list(Elements) andalso
+ (Tuple1S == binary orelse Tuple1S == string) ->
+ lists:map(fun({F1, F2}) ->
+ {format_arg(F1, Tuple1S), format_arg(F2, Tuple2S)};
+ ({Val}) when is_list(Val) ->
+ format_arg({Val}, Tuple)
+ end, Elements);
+format_arg(Elements,
+ {list, {_ElementDefName, {list, _} = ElementDefFormat}})
+ when is_list(Elements) ->
+ [{format_arg(Element, ElementDefFormat)}
+ || Element <- Elements];
format_arg(Elements,
{list, {_ElementDefName, ElementDefFormat}})
when is_list(Elements) ->
@@ -492,6 +508,11 @@ format_result({Code, Text}, {Name, restuple}) ->
{[{<<"res">>, Code == true orelse Code == ok},
{<<"text">>, iolist_to_binary(Text)}]}};
+format_result(Code, {Name, restuple}) ->
+ {jlib:atom_to_binary(Name),
+ {[{<<"res">>, Code == true orelse Code == ok},
+ {<<"text">>, <<"">>}]}};
+
format_result(Els, {Name, {list, {_, {tuple, [{_, atom}, _]}} = Fmt}}) ->
{jlib:atom_to_binary(Name), {[format_result(El, Fmt) || El <- Els]}};
diff --git a/src/mod_muc.erl b/src/mod_muc.erl
index 571f85926..4eb129a87 100644
--- a/src/mod_muc.erl
+++ b/src/mod_muc.erl
@@ -488,9 +488,8 @@ do_route1(Host, ServerHost, Access, HistorySize, RoomShaper,
_ ->
case mnesia:dirty_read(muc_online_room, {Room, Host}) of
[] ->
- Type = fxml:get_attr_s(<<"type">>, Attrs),
- case {Name, Type} of
- {<<"presence">>, <<"">>} ->
+ case is_create_request(Packet) of
+ true ->
case check_user_can_create_room(ServerHost,
AccessCreate, From, Room) and
check_create_roomid(ServerHost, Room) of
@@ -508,7 +507,7 @@ do_route1(Host, ServerHost, Access, HistorySize, RoomShaper,
Packet, ?ERRT_FORBIDDEN(Lang, ErrText)),
ejabberd_router:route(To, From, Err)
end;
- _ ->
+ false ->
Lang = fxml:get_attr_s(<<"xml:lang">>, Attrs),
ErrText = <<"Conference room does not exist">>,
Err = jlib:make_error_reply(Packet,
@@ -523,6 +522,22 @@ do_route1(Host, ServerHost, Access, HistorySize, RoomShaper,
end
end.
+-spec is_create_request(xmlel()) -> boolean().
+is_create_request(#xmlel{name = <<"presence">>} = Packet) ->
+ <<"">> == fxml:get_tag_attr_s(<<"type">>, Packet);
+is_create_request(#xmlel{name = <<"iq">>} = Packet) ->
+ case jlib:iq_query_info(Packet) of
+ #iq{type = set, xmlns = ?NS_MUCSUB,
+ sub_el = #xmlel{name = <<"subscribe">>}} ->
+ true;
+ #iq{type = get, xmlns = ?NS_MUC_OWNER, sub_el = SubEl} ->
+ [] == fxml:remove_cdata(SubEl#xmlel.children);
+ _ ->
+ false
+ end;
+is_create_request(_) ->
+ false.
+
check_user_can_create_room(ServerHost, AccessCreate,
From, _RoomID) ->
case acl:match_rule(ServerHost, AccessCreate, From) of
@@ -705,10 +720,11 @@ get_vh_rooms(Host, #rsm_in{max=M, direction=Direction, id=I, index=Index})->
get_subscribed_rooms(ServerHost, Host, From) ->
Rooms = get_rooms(ServerHost, Host),
+ BareFrom = jid:remove_resource(From),
lists:flatmap(
fun(#muc_room{name_host = {Name, _}, opts = Opts}) ->
Subscribers = proplists:get_value(subscribers, Opts, []),
- case lists:keymember(From, 1, Subscribers) of
+ case lists:keymember(BareFrom, 1, Subscribers) of
true -> [jid:make(Name, Host, <<>>)];
false -> []
end;
diff --git a/src/mod_muc_admin.erl b/src/mod_muc_admin.erl
index e1d48cdab..e334dca2b 100644
--- a/src/mod_muc_admin.erl
+++ b/src/mod_muc_admin.erl
@@ -13,6 +13,7 @@
-export([start/2, stop/1, depends/2, muc_online_rooms/1,
muc_unregister_nick/1, create_room/3, destroy_room/2,
+ create_room_with_opts/4,
create_rooms_file/1, destroy_rooms_file/1,
rooms_unused_list/2, rooms_unused_destroy/2,
get_user_rooms/2, get_room_occupants/2,
@@ -20,7 +21,7 @@
change_room_option/4, get_room_options/2,
set_room_affiliation/4, get_room_affiliations/2,
web_menu_main/2, web_page_main/2, web_menu_host/3,
- subscribe_room/4, unsubscribe_room/2,
+ subscribe_room/4, unsubscribe_room/2, get_subscribers/2,
web_page_host/3, mod_opt_type/1, get_commands_spec/0]).
-include("ejabberd.hrl").
@@ -88,6 +89,18 @@ get_commands_spec() ->
module = ?MODULE, function = create_rooms_file,
args = [{file, string}],
result = {res, rescode}},
+ #ejabberd_commands{name = create_room_with_opts, tags = [muc_room],
+ desc = "Create a MUC room name@service in host with given options",
+ module = ?MODULE, function = create_room_with_opts,
+ args = [{name, binary}, {service, binary},
+ {host, binary},
+ {options, {list,
+ {option, {tuple,
+ [{name, binary},
+ {value, binary}
+ ]}}
+ }}],
+ result = {res, rescode}},
#ejabberd_commands{name = destroy_rooms_file, tags = [muc],
desc = "Destroy the rooms indicated in file",
longdesc = "Provide one room JID per line.",
@@ -163,6 +176,11 @@ get_commands_spec() ->
module = ?MODULE, function = unsubscribe_room,
args = [{user, binary}, {room, binary}],
result = {res, rescode}},
+ #ejabberd_commands{name = get_subscribers, tags = [muc_room],
+ desc = "List subscribers of a MUC conference",
+ module = ?MODULE, function = get_subscribers,
+ args = [{name, binary}, {service, binary}],
+ result = {subscribers, {list, {jid, string}}}},
#ejabberd_commands{name = set_room_affiliation, tags = [muc_room],
desc = "Change an affiliation in a MUC room",
module = ?MODULE, function = set_room_affiliation,
@@ -411,15 +429,23 @@ prepare_room_info(Room_info) ->
%% ok | error
%% @doc Create a room immediately with the default options.
create_room(Name1, Host1, ServerHost) ->
+ create_room_with_opts(Name1, Host1, ServerHost, []).
+
+create_room_with_opts(Name1, Host1, ServerHost, CustomRoomOpts) ->
Name = jid:nodeprep(Name1),
Host = jid:nodeprep(Host1),
%% Get the default room options from the muc configuration
DefRoomOpts = gen_mod:get_module_opt(ServerHost, mod_muc,
default_room_options, fun(X) -> X end, []),
+ %% Change default room options as required
+ FormattedRoomOpts = [format_room_option(Opt, Val) || {Opt, Val}<-CustomRoomOpts],
+ RoomOpts = lists:ukeymerge(1,
+ lists:keysort(1, FormattedRoomOpts),
+ lists:keysort(1, DefRoomOpts)),
%% Store the room on the server, it is not started yet though at this point
- mod_muc:store_room(ServerHost, Host, Name, DefRoomOpts),
+ mod_muc:store_room(ServerHost, Host, Name, RoomOpts),
%% Get all remaining mod_muc parameters that might be utilized
Access = gen_mod:get_module_opt(ServerHost, mod_muc, access, fun(X) -> X end, all),
@@ -440,7 +466,7 @@ create_room(Name1, Host1, ServerHost) ->
Name,
HistorySize,
RoomShaper,
- DefRoomOpts),
+ RoomOpts),
{atomic, ok} = register_room(Host, Name, Pid),
ok;
_ ->
@@ -759,12 +785,20 @@ send_direct_invitation(FromJid, UserJid, XmlEl) ->
%% the option to change (for example title or max_users),
%% and the value to assign to the new option.
%% For example:
-%% change_room_option("testroom", "conference.localhost", "title", "Test Room")
-change_room_option(Name, Service, Option, Value) when is_atom(Option) ->
- Pid = get_room_pid(Name, Service),
- {ok, _} = change_room_option(Pid, Option, Value),
- ok;
+%% change_room_option(<<"testroom">>, <<"conference.localhost">>, <<"title">>, <<"Test Room">>)
change_room_option(Name, Service, OptionString, ValueString) ->
+ case get_room_pid(Name, Service) of
+ room_not_found ->
+ room_not_found;
+ Pid ->
+ {Option, Value} = format_room_option(OptionString, ValueString),
+ Config = get_room_config(Pid),
+ Config2 = change_option(Option, Value, Config),
+ {ok, _} = gen_fsm:sync_send_all_state_event(Pid, {change_config, Config2}),
+ ok
+ end.
+
+format_room_option(OptionString, ValueString) ->
Option = jlib:binary_to_atom(OptionString),
Value = case Option of
title -> ValueString;
@@ -775,12 +809,7 @@ change_room_option(Name, Service, OptionString, ValueString) ->
max_users -> jlib:binary_to_integer(ValueString);
_ -> jlib:binary_to_atom(ValueString)
end,
- change_room_option(Name, Service, Option, Value).
-
-change_room_option(Pid, Option, Value) ->
- Config = get_room_config(Pid),
- Config2 = change_option(Option, Value, Config),
- gen_fsm:sync_send_all_state_event(Pid, {change_config, Config2}).
+ {Option, Value}.
%% @doc Get the Pid of an existing MUC room, or 'room_not_found'.
get_room_pid(Name, Service) ->
@@ -800,6 +829,7 @@ change_option(Option, Value, Config) ->
allow_private_messages -> Config#config{allow_private_messages = Value};
allow_private_messages_from_visitors -> Config#config{allow_private_messages_from_visitors = Value};
allow_query_users -> Config#config{allow_query_users = Value};
+ allow_subscription -> Config#config{allow_subscription = Value};
allow_user_invites -> Config#config{allow_user_invites = Value};
allow_visitor_nickchange -> Config#config{allow_visitor_nickchange = Value};
allow_visitor_status -> Config#config{allow_visitor_status = Value};
@@ -954,6 +984,15 @@ unsubscribe_room(User, Room) ->
throw({error, "Malformed room JID"})
end.
+get_subscribers(Name, Host) ->
+ case get_room_pid(Name, Host) of
+ Pid when is_pid(Pid) ->
+ {ok, JIDList} = gen_fsm:sync_send_all_state_event(Pid, get_subscribers),
+ [jid:to_string(jid:remove_resource(J)) || J <- JIDList];
+ _ ->
+ throw({error, "The room does not exist"})
+ end.
+
make_opts(StateData) ->
Config = StateData#state.config,
[
diff --git a/src/mod_muc_room.erl b/src/mod_muc_room.erl
index e5ed4cc68..ac2f5daf1 100644
--- a/src/mod_muc_room.erl
+++ b/src/mod_muc_room.erl
@@ -139,7 +139,8 @@ normal_state({route, From, <<"">>,
StateData) ->
Lang = fxml:get_attr_s(<<"xml:lang">>, Attrs),
case is_user_online(From, StateData) orelse
- is_user_allowed_message_nonparticipant(From, StateData)
+ is_subscriber(From, StateData) orelse
+ is_user_allowed_message_nonparticipant(From, StateData)
of
true ->
case fxml:get_attr_s(<<"type">>, Attrs) of
@@ -252,20 +253,13 @@ normal_state({route, From, <<"">>,
Err = jlib:make_error_reply(Packet, Error),
ejabberd_router:route(StateData#state.jid, From, Err),
{next_state, normal_state, StateData};
- IJID ->
+ IJIDs ->
Config = StateData#state.config,
case Config#config.members_only of
true ->
- case get_affiliation(IJID, StateData) of
- none ->
- NSD = set_affiliation(IJID, member,
- StateData),
- send_affiliation(IJID, member,
- StateData),
- store_room(NSD),
- {next_state, normal_state, NSD};
- _ -> {next_state, normal_state, StateData}
- end;
+ NSD = process_invitees(IJIDs, StateData),
+ store_room(NSD),
+ {next_state, normal_state, NSD};
false -> {next_state, normal_state, StateData}
end
end;
@@ -527,7 +521,8 @@ normal_state({route, From, ToNick,
continue_delivery ->
case
{(StateData#state.config)#config.allow_private_messages,
- is_user_online(From, StateData)}
+ is_user_online(From, StateData) orelse
+ is_subscriber(From, StateData)}
of
{true, true} ->
case Type of
@@ -562,9 +557,7 @@ normal_state({route, From, ToNick,
PmFromVisitors == anyone;
(PmFromVisitors == moderators) and
DstIsModerator ->
- {ok, #user{nick = FromNick}} =
- (?DICT):find(jid:tolower(From),
- StateData#state.users),
+ {FromNick, _} = get_participant_data(From, StateData),
FromNickJID =
jid:replace_resource(StateData#state.jid,
FromNick),
@@ -678,7 +671,7 @@ handle_event({service_message, Msg}, _StateName,
children = [{xmlcdata, Msg}]}]},
send_wrapped_multiple(
StateData#state.jid,
- StateData#state.users,
+ get_users_and_subscribers(StateData),
MessagePkt,
?NS_MUCSUB_NODES_MESSAGES,
StateData),
@@ -749,6 +742,10 @@ handle_sync_event({change_state, NewStateData}, _From,
handle_sync_event({process_item_change, Item, UJID}, _From, StateName, StateData) ->
NSD = process_item_change(Item, StateData, UJID),
{reply, {ok, NSD}, StateName, NSD};
+handle_sync_event(get_subscribers, _From, StateName, StateData) ->
+ JIDs = lists:map(fun jid:make/1,
+ ?DICT:fetch_keys(StateData#state.subscribers)),
+ {reply, {ok, JIDs}, StateName, StateData};
handle_sync_event({muc_subscribe, From, Nick, Nodes}, _From,
StateName, StateData) ->
SubEl = #xmlel{name = <<"subscribe">>,
@@ -928,7 +925,7 @@ terminate(Reason, _StateName, StateData) ->
end,
tab_remove_online_user(LJID, StateData)
end,
- [], StateData#state.users),
+ [], get_users_and_subscribers(StateData)),
add_to_log(room_existence, stopped, StateData),
mod_muc:room_destroyed(StateData#state.host, StateData#state.room, self(),
StateData#state.server_host),
@@ -945,11 +942,12 @@ process_groupchat_message(From,
#xmlel{name = <<"message">>, attrs = Attrs} = Packet,
StateData) ->
Lang = fxml:get_attr_s(<<"xml:lang">>, Attrs),
- case is_user_online(From, StateData) orelse
+ IsSubscriber = is_subscriber(From, StateData),
+ case is_user_online(From, StateData) orelse IsSubscriber orelse
is_user_allowed_message_nonparticipant(From, StateData)
of
true ->
- {FromNick, Role, IsSubscriber} = get_participant_data(From, StateData),
+ {FromNick, Role} = get_participant_data(From, StateData),
if (Role == moderator) or (Role == participant) or IsSubscriber or
((StateData#state.config)#config.moderated == false) ->
Subject = check_subject(Packet),
@@ -992,7 +990,7 @@ process_groupchat_message(From,
end,
send_wrapped_multiple(
jid:replace_resource(StateData#state.jid, FromNick),
- StateData#state.users,
+ get_users_and_subscribers(StateData),
NewPacket, Node, NewStateData1),
NewStateData2 = case has_body_or_subject(NewPacket) of
true ->
@@ -1060,9 +1058,16 @@ get_participant_data(From, StateData) ->
case (?DICT):find(jid:tolower(From),
StateData#state.users)
of
- {ok, #user{nick = FromNick, role = Role, is_subscriber = IsSubscriber}} ->
- {FromNick, Role, IsSubscriber};
- error -> {<<"">>, moderator, false}
+ {ok, #user{nick = FromNick, role = Role}} ->
+ {FromNick, Role};
+ error ->
+ case ?DICT:find(jid:tolower(jid:remove_resource(From)),
+ StateData#state.subscribers) of
+ {ok, #subscriber{nick = FromNick}} ->
+ {FromNick, none};
+ error ->
+ {<<"">>, moderator}
+ end
end.
process_presence(From, Nick,
@@ -1070,7 +1075,6 @@ process_presence(From, Nick,
StateData) ->
Type0 = fxml:get_attr_s(<<"type">>, Attrs0),
IsOnline = is_user_online(From, StateData),
- IsSubscriber = is_subscriber(From, StateData),
if Type0 == <<"">>;
IsOnline and ((Type0 == <<"unavailable">>) or (Type0 == <<"error">>)) ->
case ejabberd_hooks:run_fold(muc_filter_presence,
@@ -1107,7 +1111,7 @@ process_presence(From, Nick,
Status_el ->
fxml:get_tag_cdata(Status_el)
end,
- remove_online_user(From, NewState, IsSubscriber, Reason);
+ remove_online_user(From, NewState, Reason);
<<"error">> ->
ErrorText = <<"It is not allowed to send error messages to the"
" room. The participant (~s) has sent an error "
@@ -1164,27 +1168,15 @@ process_presence(From, Nick,
From, Err),
StateData;
_ ->
- case is_initial_presence(From, StateData) of
- true ->
- subscriber_becomes_available(
- From, Nick, Packet, StateData);
- false ->
- change_nick(From, Nick, StateData)
- end
+ change_nick(From, Nick, StateData)
end;
_NotNickChange ->
- case is_initial_presence(From, StateData) of
- true ->
- subscriber_becomes_available(
- From, Nick, Packet, StateData);
- false ->
- Stanza = maybe_strip_status_from_presence(
- From, Packet, StateData),
- NewState = add_user_presence(From, Stanza,
- StateData),
- send_new_presence(From, NewState, StateData),
- NewState
- end
+ Stanza = maybe_strip_status_from_presence(
+ From, Packet, StateData),
+ NewState = add_user_presence(From, Stanza,
+ StateData),
+ send_new_presence(From, NewState, StateData),
+ NewState
end
end
end,
@@ -1202,21 +1194,10 @@ maybe_strip_status_from_presence(From, Packet, StateData) ->
_Allowed -> Packet
end.
-subscriber_becomes_available(From, Nick, Packet, StateData) ->
- Stanza = maybe_strip_status_from_presence(From, Packet, StateData),
- State1 = add_user_presence(From, Stanza, StateData),
- Aff = get_affiliation(From, State1),
- Role = get_default_role(Aff, State1),
- State2 = set_role(From, Role, State1),
- State3 = set_nick(From, Nick, State2),
- send_existing_presences(From, State3),
- send_initial_presence(From, State3, StateData),
- State3.
-
close_room_if_temporary_and_empty(StateData1) ->
case not (StateData1#state.config)#config.persistent
- andalso (?DICT):to_list(StateData1#state.users) == []
- of
+ andalso (?DICT):size(StateData1#state.users) == 0
+ andalso (?DICT):size(StateData1#state.subscribers) == 0 of
true ->
?INFO_MSG("Destroyed MUC room ~s because it's temporary "
"and empty",
@@ -1226,18 +1207,39 @@ close_room_if_temporary_and_empty(StateData1) ->
_ -> {next_state, normal_state, StateData1}
end.
+get_users_and_subscribers(StateData) ->
+ OnlineSubscribers = ?DICT:fold(
+ fun(LJID, _, Acc) ->
+ LBareJID = jid:remove_resource(LJID),
+ case is_subscriber(LBareJID, StateData) of
+ true ->
+ ?SETS:add_element(LBareJID, Acc);
+ false ->
+ Acc
+ end
+ end, ?SETS:new(), StateData#state.users),
+ ?DICT:fold(
+ fun(LBareJID, #subscriber{nick = Nick}, Acc) ->
+ case ?SETS:is_element(LBareJID, OnlineSubscribers) of
+ false ->
+ ?DICT:store(LBareJID,
+ #user{jid = jid:make(LBareJID),
+ nick = Nick,
+ role = none,
+ last_presence = undefined},
+ Acc);
+ true ->
+ Acc
+ end
+ end, StateData#state.users, StateData#state.subscribers).
+
is_user_online(JID, StateData) ->
LJID = jid:tolower(JID),
(?DICT):is_key(LJID, StateData#state.users).
is_subscriber(JID, StateData) ->
- LJID = jid:tolower(JID),
- case (?DICT):find(LJID, StateData#state.users) of
- {ok, #user{is_subscriber = IsSubscriber}} ->
- IsSubscriber;
- _ ->
- false
- end.
+ LJID = jid:tolower(jid:remove_resource(JID)),
+ (?DICT):is_key(LJID, StateData#state.subscribers).
%% Check if the user is occupant of the room, or at least is an admin or owner.
is_occupant_or_admin(JID, StateData) ->
@@ -1414,7 +1416,6 @@ make_reason(Packet, From, StateData, Reason1) ->
iolist_to_binary(io_lib:format(Reason1, [FromNick, Condition])).
expulse_participant(Packet, From, StateData, Reason1) ->
- IsSubscriber = is_subscriber(From, StateData),
Reason2 = make_reason(Packet, From, StateData, Reason1),
NewState = add_user_presence_un(From,
#xmlel{name = <<"presence">>,
@@ -1429,7 +1430,7 @@ expulse_participant(Packet, From, StateData, Reason1) ->
Reason2}]}]},
StateData),
send_new_presence(From, NewState, StateData),
- remove_online_user(From, NewState, IsSubscriber).
+ remove_online_user(From, NewState).
set_affiliation(JID, Affiliation, StateData) ->
set_affiliation(JID, Affiliation, StateData, <<"">>).
@@ -1709,8 +1710,7 @@ prepare_room_queue(StateData) ->
{empty, _} -> StateData
end.
-update_online_user(JID, #user{nick = Nick, subscriptions = Nodes,
- is_subscriber = IsSubscriber} = User, StateData) ->
+update_online_user(JID, #user{nick = Nick} = User, StateData) ->
LJID = jid:tolower(JID),
Nicks1 = case (?DICT):find(LJID, StateData#state.users) of
{ok, #user{nick = OldNick}} ->
@@ -1729,9 +1729,7 @@ update_online_user(JID, #user{nick = Nick, subscriptions = Nodes,
[LJID], Nicks1),
Users = (?DICT):update(LJID,
fun(U) ->
- U#user{nick = Nick,
- subscriptions = Nodes,
- is_subscriber = IsSubscriber}
+ U#user{nick = Nick}
end, User, StateData#state.users),
NewStateData = StateData#state{users = Users, nicks = Nicks},
case {?DICT:find(LJID, StateData#state.users),
@@ -1743,32 +1741,29 @@ update_online_user(JID, #user{nick = Nick, subscriptions = Nodes,
end,
NewStateData.
-add_online_user(JID, Nick, Role, IsSubscriber, Nodes, StateData) ->
+set_subscriber(JID, Nick, Nodes, StateData) ->
+ BareJID = jid:remove_resource(JID),
+ LBareJID = jid:tolower(BareJID),
+ Subscribers = ?DICT:store(LBareJID,
+ #subscriber{jid = BareJID,
+ nick = Nick,
+ nodes = Nodes},
+ StateData#state.subscribers),
+ Nicks = ?DICT:store(Nick, [LBareJID], StateData#state.subscriber_nicks),
+ NewStateData = StateData#state{subscribers = Subscribers,
+ subscriber_nicks = Nicks},
+ store_room(NewStateData),
+ NewStateData.
+
+add_online_user(JID, Nick, Role, StateData) ->
tab_add_online_user(JID, StateData),
- User = #user{jid = JID, nick = Nick, role = Role,
- is_subscriber = IsSubscriber, subscriptions = Nodes},
- StateData1 = update_online_user(JID, User, StateData),
- if IsSubscriber ->
- store_room(StateData1);
- true ->
- ok
- end,
- StateData1.
+ User = #user{jid = JID, nick = Nick, role = Role},
+ update_online_user(JID, User, StateData).
-remove_online_user(JID, StateData, IsSubscriber) ->
- remove_online_user(JID, StateData, IsSubscriber, <<"">>).
+remove_online_user(JID, StateData) ->
+ remove_online_user(JID, StateData, <<"">>).
-remove_online_user(JID, StateData, _IsSubscriber = true, _Reason) ->
- LJID = jid:tolower(JID),
- Users = case (?DICT):find(LJID, StateData#state.users) of
- {ok, U} ->
- (?DICT):store(LJID, U#user{last_presence = undefined},
- StateData#state.users);
- error ->
- StateData#state.users
- end,
- StateData#state{users = Users};
-remove_online_user(JID, StateData, _IsSubscriber, Reason) ->
+remove_online_user(JID, StateData, Reason) ->
LJID = jid:tolower(JID),
{ok, #user{nick = Nick}} = (?DICT):find(LJID,
StateData#state.users),
@@ -1841,10 +1836,13 @@ add_user_presence_un(JID, Presence, StateData) ->
%% Find and return a list of the full JIDs of the users of Nick.
%% Return jid record.
find_jids_by_nick(Nick, StateData) ->
- case (?DICT):find(Nick, StateData#state.nicks) of
- {ok, [User]} -> [jid:make(User)];
- {ok, Users} -> [jid:make(LJID) || LJID <- Users];
- error -> false
+ Nicks = ?DICT:merge(fun(_, Val, _) -> Val end,
+ StateData#state.nicks,
+ StateData#state.subscriber_nicks),
+ case (?DICT):find(Nick, Nicks) of
+ {ok, [User]} -> [jid:make(User)];
+ {ok, Users} -> [jid:make(LJID) || LJID <- Users];
+ error -> false
end.
%% Find and return the full JID of the user of Nick with
@@ -1911,7 +1909,14 @@ is_nick_change(JID, Nick, StateData) ->
end.
nick_collision(User, Nick, StateData) ->
- UserOfNick = find_jid_by_nick(Nick, StateData),
+ UserOfNick = case find_jid_by_nick(Nick, StateData) of
+ false ->
+ case ?DICT:find(Nick, StateData#state.subscriber_nicks) of
+ {ok, [J]} -> J;
+ error -> false
+ end;
+ J -> J
+ end,
(UserOfNick /= false andalso
jid:remove_resource(jid:tolower(UserOfNick))
/= jid:remove_resource(jid:tolower(User))).
@@ -2023,8 +2028,7 @@ add_new_user(From, Nick,
NewState = add_user_presence(
From, Packet,
add_online_user(From, Nick, Role,
- IsSubscribeRequest,
- Nodes, StateData)),
+ StateData)),
send_existing_presences(From, NewState),
send_initial_presence(From, NewState, StateData),
Shift = count_stanza_shift(Nick, Els, NewState),
@@ -2034,9 +2038,7 @@ add_new_user(From, Nick,
end,
NewState;
true ->
- add_online_user(From, Nick, none,
- IsSubscribeRequest,
- Nodes, StateData)
+ set_subscriber(From, Nick, Nodes, StateData)
end,
ResultState =
case NewStateData#state.just_created of
@@ -2276,15 +2278,6 @@ presence_broadcast_allowed(JID, StateData) ->
Role = get_role(JID, StateData),
lists:member(Role, (StateData#state.config)#config.presence_broadcast).
-is_initial_presence(From, StateData) ->
- LJID = jid:tolower(From),
- case (?DICT):find(LJID, StateData#state.users) of
- {ok, #user{last_presence = Pres}} when Pres /= undefined ->
- false;
- _ ->
- true
- end.
-
send_initial_presence(NJID, StateData, OldStateData) ->
send_new_presence1(NJID, <<"">>, true, StateData, OldStateData).
@@ -2378,7 +2371,7 @@ send_new_presence1(NJID, Reason, IsInitialPresence, StateData, OldStateData) ->
true ->
[{LNJID, UserInfo}];
false ->
- (?DICT):to_list(StateData#state.users)
+ (?DICT):to_list(get_users_and_subscribers(StateData))
end,
lists:foreach(
fun({LUJID, Info}) ->
@@ -2434,7 +2427,7 @@ send_new_presence1(NJID, Reason, IsInitialPresence, StateData, OldStateData) ->
send_wrapped(jid:replace_resource(StateData#state.jid, Nick),
Info#user.jid, Packet, Node1, StateData),
Type = fxml:get_tag_attr_s(<<"type">>, Packet),
- IsSubscriber = Info#user.is_subscriber,
+ IsSubscriber = is_subscriber(Info#user.jid, StateData),
IsOccupant = Info#user.last_presence /= undefined,
if (IsSubscriber and not IsOccupant) and
(IsInitialPresence or (Type == <<"unavailable">>)) ->
@@ -2658,19 +2651,20 @@ send_nick_changing(JID, OldNick, StateData,
(_) ->
ok
end,
- (?DICT):to_list(StateData#state.users)).
+ ?DICT:to_list(get_users_and_subscribers(StateData))).
maybe_send_affiliation(JID, Affiliation, StateData) ->
LJID = jid:tolower(JID),
+ Users = get_users_and_subscribers(StateData),
IsOccupant = case LJID of
{LUser, LServer, <<"">>} ->
not (?DICT):is_empty(
(?DICT):filter(fun({U, S, _}, _) ->
U == LUser andalso
S == LServer
- end, StateData#state.users));
+ end, Users));
{_LUser, _LServer, _LResource} ->
- (?DICT):is_key(LJID, StateData#state.users)
+ (?DICT):is_key(LJID, Users)
end,
case IsOccupant of
true ->
@@ -2691,19 +2685,19 @@ send_affiliation(LJID, Affiliation, StateData) ->
children =
[#xmlel{name = <<"item">>,
attrs = ItemAttrs}]}]},
+ Users = get_users_and_subscribers(StateData),
Recipients = case (StateData#state.config)#config.anonymous of
true ->
(?DICT):filter(fun(_, #user{role = moderator}) ->
true;
(_, _) ->
false
- end, StateData#state.users);
+ end, Users);
false ->
- StateData#state.users
+ Users
end,
- send_multiple(StateData#state.jid,
- StateData#state.server_host,
- Recipients, Message).
+ send_wrapped_multiple(StateData#state.jid, Recipients, Message,
+ ?NS_MUCSUB_NODES_AFFILIATIONS, StateData).
status_els(IsInitialPresence, JID, #user{jid = JID}, StateData) ->
Status = case IsInitialPresence of
@@ -3410,7 +3404,7 @@ send_kickban_presence1(MJID, UJID, Reason, Code, Affiliation,
StateData#state.jid, Nick),
send_wrapped(RoomJIDNick, Info#user.jid, Packet,
?NS_MUCSUB_NODES_AFFILIATIONS, StateData),
- IsSubscriber = Info#user.is_subscriber,
+ IsSubscriber = is_subscriber(Info#user.jid, StateData),
IsOccupant = Info#user.last_presence /= undefined,
if (IsSubscriber and not IsOccupant) ->
send_wrapped(RoomJIDNick, Info#user.jid, Packet,
@@ -3419,7 +3413,7 @@ send_kickban_presence1(MJID, UJID, Reason, Code, Affiliation,
ok
end
end,
- (?DICT):to_list(StateData#state.users)).
+ (?DICT):to_list(get_users_and_subscribers(StateData))).
get_actor_nick(<<"">>, _StateData) ->
<<"">>;
@@ -4249,7 +4243,7 @@ send_config_change_info(New, #state{config = Old} = StateData) ->
attrs = [{<<"xmlns">>, ?NS_MUC_USER}],
children = StatusEls}]},
send_wrapped_multiple(StateData#state.jid,
- StateData#state.users,
+ get_users_and_subscribers(StateData),
Message,
?NS_MUCSUB_NODES_CONFIG,
StateData).
@@ -4265,7 +4259,7 @@ remove_nonmembers(StateData) ->
_ -> SD
end
end,
- StateData, (?DICT):to_list(StateData#state.users)).
+ StateData, (?DICT):to_list(get_users_and_subscribers(StateData))).
set_opts([], StateData) -> StateData;
set_opts([{Opt, Val} | Opts], StateData) ->
@@ -4386,14 +4380,17 @@ set_opts([{Opt, Val} | Opts], StateData) ->
StateData#state{config =
(StateData#state.config)#config{allow_subscription = Val}};
subscribers ->
- lists:foldl(
- fun({JID, Nick, Nodes}, State) ->
- User = #user{jid = JID, nick = Nick,
- subscriptions = Nodes,
- is_subscriber = true,
- role = none},
- update_online_user(JID, User, State)
- end, StateData, Val);
+ Subscribers = lists:foldl(
+ fun({JID, Nick, Nodes}, Acc) ->
+ BareJID = jid:remove_resource(JID),
+ ?DICT:store(
+ jid:tolower(BareJID),
+ #subscriber{jid = BareJID,
+ nick = Nick,
+ nodes = Nodes},
+ Acc)
+ end, ?DICT:new(), Val),
+ StateData#state{subscribers = Subscribers};
affiliations ->
StateData#state{affiliations = (?DICT):from_list(Val)};
subject -> StateData#state{subject = Val};
@@ -4408,12 +4405,11 @@ set_opts([{Opt, Val} | Opts], StateData) ->
make_opts(StateData) ->
Config = StateData#state.config,
Subscribers = (?DICT):fold(
- fun(_LJID, #user{is_subscriber = true} = User, Acc) ->
- [{User#user.jid, User#user.nick,
- User#user.subscriptions}|Acc];
- (_, _, Acc) ->
- Acc
- end, [], StateData#state.users),
+ fun(_LJID, Sub, Acc) ->
+ [{Sub#subscriber.jid,
+ Sub#subscriber.nick,
+ Sub#subscriber.nodes}|Acc]
+ end, [], StateData#state.subscribers),
[?MAKE_CONFIG_OPT(title), ?MAKE_CONFIG_OPT(description),
?MAKE_CONFIG_OPT(allow_change_subj),
?MAKE_CONFIG_OPT(allow_query_users),
@@ -4469,7 +4465,7 @@ destroy_room(DEl, StateData) ->
Info#user.jid, Packet,
?NS_MUCSUB_NODES_CONFIG, StateData)
end,
- (?DICT):to_list(StateData#state.users)),
+ (?DICT):to_list(get_users_and_subscribers(StateData))),
case (StateData#state.config)#config.persistent of
true ->
mod_muc:forget_room(StateData#state.server_host,
@@ -4629,9 +4625,9 @@ process_iq_mucsub(From, Packet,
Err = ?ERRT_BAD_REQUEST(Lang, <<"Missing 'nick' attribute">>),
{error, Err};
Nick when Config#config.allow_subscription ->
- LJID = jid:tolower(From),
- case (?DICT):find(LJID, StateData#state.users) of
- {ok, #user{role = Role, nick = Nick1}} when Nick1 /= Nick ->
+ LBareJID = jid:tolower(jid:remove_resource(From)),
+ case (?DICT):find(LBareJID, StateData#state.subscribers) of
+ {ok, #subscriber{nick = Nick1}} when Nick1 /= Nick ->
Nodes = get_subscription_nodes(Packet),
case {nick_collision(From, Nick, StateData),
mod_muc:can_use_nick(StateData#state.server_host,
@@ -4644,14 +4640,12 @@ process_iq_mucsub(From, Packet,
ErrText = <<"That nickname is registered by another person">>,
{error, ?ERRT_CONFLICT(Lang, ErrText)};
_ ->
- NewStateData = add_online_user(
- From, Nick, Role, true, Nodes, StateData),
+ NewStateData = set_subscriber(From, Nick, Nodes, StateData),
{result, subscription_nodes_to_events(Nodes), NewStateData}
end;
- {ok, #user{role = Role}} ->
+ {ok, #subscriber{}} ->
Nodes = get_subscription_nodes(Packet),
- NewStateData = add_online_user(
- From, Nick, Role, true, Nodes, StateData),
+ NewStateData = set_subscriber(From, Nick, Nodes, StateData),
{result, subscription_nodes_to_events(Nodes), NewStateData};
error ->
add_new_user(From, Nick, Packet, StateData)
@@ -4664,53 +4658,44 @@ process_iq_mucsub(From, _Packet,
#iq{type = set,
sub_el = #xmlel{name = <<"unsubscribe">>}},
StateData) ->
- LJID = jid:tolower(From),
- case ?DICT:find(LJID, StateData#state.users) of
- {ok, #user{is_subscriber = true} = User} ->
- NewStateData = remove_subscription(From, User, StateData),
+ LBareJID = jid:tolower(jid:remove_resource(From)),
+ case ?DICT:find(LBareJID, StateData#state.subscribers) of
+ {ok, #subscriber{nick = Nick}} ->
+ Nicks = ?DICT:erase(Nick, StateData#state.subscriber_nicks),
+ Subscribers = ?DICT:erase(LBareJID, StateData#state.subscribers),
+ NewStateData = StateData#state{subscribers = Subscribers,
+ subscriber_nicks = Nicks},
store_room(NewStateData),
{result, [], NewStateData};
- error when From#jid.lresource == <<"">> ->
- {LUser, LServer, _} = LJID,
- NewStateData =
- dict:fold(
- fun({U, S, _}, #user{jid = J, is_subscriber = true} = User,
- AccState) when U == LUser, S == LServer ->
- remove_subscription(J, User, AccState);
- (_, _, AccState) ->
- AccState
- end, StateData, StateData#state.users),
- store_room(NewStateData),
- {result, [], NewStateData};
- _ ->
+ error ->
{result, [], StateData}
end;
-process_iq_mucsub(_From, _Packet, #iq{type = set, lang = Lang}, _StateData) ->
+process_iq_mucsub(From, _Packet,
+ #iq{type = get, lang = Lang,
+ sub_el = #xmlel{name = <<"subscriptions">>}},
+ StateData) ->
+ FAffiliation = get_affiliation(From, StateData),
+ FRole = get_role(From, StateData),
+ if FRole == moderator; FAffiliation == owner; FAffiliation == admin ->
+ Subs = dict:fold(
+ fun(_, #subscriber{jid = J}, Acc) ->
+ SJID = jid:to_string(J),
+ [#xmlel{name = <<"subscription">>,
+ attrs = [{<<"jid">>, SJID}]}|Acc]
+ end, [], StateData#state.subscribers),
+ {result, Subs, StateData};
+ true ->
+ Txt = <<"Moderator privileges required">>,
+ {error, ?ERRT_FORBIDDEN(Lang, Txt)}
+ end;
+process_iq_mucsub(_From, _Packet, #iq{lang = Lang}, _StateData) ->
Txt = <<"Unrecognized subscription command">>,
- {error, ?ERRT_BAD_REQUEST(Lang, Txt)};
-process_iq_mucsub(_From, _Packet, #iq{type = get, lang = Lang}, _StateData) ->
- Txt = <<"Value 'get' of 'type' attribute is not allowed">>,
{error, ?ERRT_BAD_REQUEST(Lang, Txt)}.
-remove_subscription(JID, #user{is_subscriber = true} = User, StateData) ->
- case User#user.last_presence of
- undefined ->
- remove_online_user(JID, StateData, false);
- _ ->
- LJID = jid:tolower(JID),
- Users = ?DICT:store(LJID, User#user{is_subscriber = false},
- StateData#state.users),
- StateData#state{users = Users}
- end;
-remove_subscription(_JID, #user{}, StateData) ->
- StateData.
-
remove_subscriptions(StateData) ->
if not (StateData#state.config)#config.allow_subscription ->
- dict:fold(
- fun(_LJID, User, State) ->
- remove_subscription(User#user.jid, User, State)
- end, StateData, StateData#state.users);
+ StateData#state{subscribers = ?DICT:new(),
+ subscriber_nicks = ?DICT:new()};
true ->
StateData
end.
@@ -4943,18 +4928,35 @@ is_invitation(Els) ->
end,
false, Els).
+process_invitees(Invetees, StateDataIni) ->
+ lists:foldl(
+ fun(IJID, StateData) ->
+ case get_affiliation(IJID, StateData) of
+ none ->
+ NSD = set_affiliation(IJID, member,
+ StateData),
+ send_affiliation(IJID, member,
+ StateData),
+ NSD;
+ _ -> StateData
+ end
+ end,
+ StateDataIni,
+ Invetees).
+
check_invitation(From, Packet, Lang, StateData) ->
FAffiliation = get_affiliation(From, StateData),
CanInvite =
(StateData#state.config)#config.allow_user_invites
orelse
FAffiliation == admin orelse FAffiliation == owner,
- InviteEl = case fxml:get_subtag_with_xmlns(Packet, <<"x">>, ?NS_MUC_USER) of
+
+ InviteEls = case fxml:get_subtag_with_xmlns(Packet, <<"x">>, ?NS_MUC_USER) of
false ->
Txt1 = <<"No 'x' element found">>,
throw({error, ?ERRT_BAD_REQUEST(Lang, Txt1)});
XEl ->
- case fxml:get_subtag(XEl, <<"invite">>) of
+ case fxml:get_subtags(XEl, <<"invite">>) of
false ->
Txt2 = <<"No 'invite' element found">>,
throw({error, ?ERRT_BAD_REQUEST(Lang, Txt2)});
@@ -4962,20 +4964,17 @@ check_invitation(From, Packet, Lang, StateData) ->
InviteEl1
end
end,
- JID = case
- jid:from_string(fxml:get_tag_attr_s(<<"to">>,
- InviteEl))
- of
- error ->
- Txt = <<"Incorrect value of 'to' attribute">>,
- throw({error, ?ERRT_JID_MALFORMED(Lang, Txt)});
- JID1 -> JID1
- end,
case CanInvite of
false ->
Txt3 = <<"Invitations are not allowed in this conference">>,
throw({error, ?ERRT_NOT_ALLOWED(Lang, Txt3)});
true ->
+ process_invitations(From, InviteEls, Lang, StateData)
+ end.
+
+process_invitations(From, InviteEls, Lang, StateData) ->
+ lists:map(
+ fun(InviteEl) ->
Reason = fxml:get_path_s(InviteEl,
[{elem, <<"reason">>}, cdata]),
ContinueEl = case fxml:get_path_s(InviteEl,
@@ -5044,9 +5043,19 @@ check_invitation(From, Packet, Lang, StateData) ->
<<"">>})}],
children = [{xmlcdata, Reason}]},
Body]},
+ JID = case
+ jid:from_string(fxml:get_tag_attr_s(<<"to">>,
+ InviteEl))
+ of
+ error ->
+ Txt = <<"Incorrect value of 'to' attribute">>,
+ throw({error, ?ERRT_JID_MALFORMED(Lang, Txt)});
+ JID1 -> JID1
+ end,
ejabberd_router:route(StateData#state.jid, JID, Msg),
JID
- end.
+ end,
+ InviteEls).
%% Handle a message sent to the room by a non-participant.
%% If it is a decline, send to the inviter.
@@ -5171,18 +5180,26 @@ store_room(StateData) ->
send_wrapped(From, To, Packet, Node, State) ->
LTo = jid:tolower(To),
- case ?DICT:find(LTo, State#state.users) of
- {ok, #user{is_subscriber = true,
- subscriptions = Nodes,
- last_presence = undefined}} ->
- case lists:member(Node, Nodes) of
- true ->
- NewPacket = wrap(From, To, Packet, Node),
- ejabberd_router:route(State#state.jid, To, NewPacket);
- false ->
+ LBareTo = jid:tolower(jid:remove_resource(To)),
+ IsOffline = case ?DICT:find(LTo, State#state.users) of
+ {ok, #user{last_presence = undefined}} -> true;
+ error -> true;
+ _ -> false
+ end,
+ if IsOffline ->
+ case ?DICT:find(LBareTo, State#state.subscribers) of
+ {ok, #subscriber{nodes = Nodes, jid = JID}} ->
+ case lists:member(Node, Nodes) of
+ true ->
+ NewPacket = wrap(From, JID, Packet, Node),
+ ejabberd_router:route(State#state.jid, JID, NewPacket);
+ false ->
+ ok
+ end;
+ _ ->
ok
end;
- _ ->
+ true ->
ejabberd_router:route(From, To, Packet)
end.
@@ -5203,9 +5220,9 @@ wrap(From, To, Packet, Node) ->
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Multicast
-send_multiple(From, Server, Users, Packet) ->
- JIDs = [ User#user.jid || {_, User} <- ?DICT:to_list(Users)],
- ejabberd_router_multicast:route_multicast(From, Server, JIDs, Packet).
+%% send_multiple(From, Server, Users, Packet) ->
+%% JIDs = [ User#user.jid || {_, User} <- ?DICT:to_list(Users)],
+%% ejabberd_router_multicast:route_multicast(From, Server, JIDs, Packet).
send_wrapped_multiple(From, Users, Packet, Node, State) ->
lists:foreach(
diff --git a/src/mod_privilege.erl b/src/mod_privilege.erl
new file mode 100644
index 000000000..af6dacec4
--- /dev/null
+++ b/src/mod_privilege.erl
@@ -0,0 +1,363 @@
+%%%--------------------------------------------------------------------------------------
+%%% File : mod_privilege.erl
+%%% Author : Anna Mukharram <amuhar3@gmail.com>
+%%% Purpose : This module is an implementation for XEP-0356: Privileged Entity
+%%%--------------------------------------------------------------------------------------
+
+-module(mod_privilege).
+
+-author('amuhar3@gmail.com').
+
+-protocol({xep, 0356, '0.2.1'}).
+
+-export([advertise_permissions/1, initial_presences/1, process_presence/1,
+ process_roster_presence/1, compare_presences/2,
+ process_message/4, process_iq/4]).
+
+-include("ejabberd_service.hrl").
+
+-include("mod_privacy.hrl").
+
+%%%--------------------------------------------------------------------------------------
+%%% Functions to advertise services of allowed permission
+%%%--------------------------------------------------------------------------------------
+
+-spec permissions(binary(), binary(), list()) -> xmlel().
+
+permissions(From, To, PrivAccess) ->
+ Perms = lists:map(fun({Access, Type}) ->
+ ?DEBUG("Advertise service ~s of allowed permission: ~s = ~s~n",
+ [To, Access, Type]),
+ #xmlel{name = <<"perm">>,
+ attrs = [{<<"access">>,
+ atom_to_binary(Access,latin1)},
+ {<<"type">>, Type}]}
+ end, PrivAccess),
+ Stanza = #xmlel{name = <<"privilege">>,
+ attrs = [{<<"xmlns">> ,?NS_PRIVILEGE}],
+ children = Perms},
+ Id = randoms:get_string(),
+ #xmlel{name = <<"message">>,
+ attrs = [{<<"id">>, Id}, {<<"from">>, From}, {<<"to">>, To}],
+ children = [Stanza]}.
+
+advertise_permissions(#state{privilege_access = []}) -> ok;
+advertise_permissions(StateData) ->
+ Stanza =
+ permissions(?MYNAME, StateData#state.host, StateData#state.privilege_access),
+ ejabberd_service:send_element(StateData, Stanza).
+
+%%%--------------------------------------------------------------------------------------
+%%% Process presences
+%%%--------------------------------------------------------------------------------------
+
+initial_presences(StateData) ->
+ Pids = ejabberd_sm:get_all_pids(),
+ lists:foreach(
+ fun(Pid) ->
+ {User, Server, Resource, PresenceLast} = ejabberd_c2s:get_last_presence(Pid),
+ From = #jid{user = User, server = Server, resource = Resource},
+ To = jid:from_string(StateData#state.host),
+ PacketNew = jlib:replace_from_to(From, To, PresenceLast),
+ ejabberd_service:send_element(StateData, PacketNew)
+ end, Pids).
+
+%% hook user_send_packet(Packet, C2SState, From, To) -> Packet
+%% for Managed Entity Presence
+process_presence(Pid) ->
+ fun(#xmlel{name = <<"presence">>} = Packet, _C2SState, From, _To) ->
+ case fxml:get_attr_s(<<"type">>, Packet#xmlel.attrs) of
+ T when (T == <<"">>) or (T == <<"unavailable">>) ->
+ Pid ! {user_presence, Packet, From};
+ _ -> ok
+ end,
+ Packet;
+ (Packet, _C2SState, _From, _To) ->
+ Packet
+ end.
+%% s2s_receive_packet(From, To, Packet) -> ok
+%% for Roster Presence
+%% From subscription "from" or "both"
+process_roster_presence(Pid) ->
+ fun(From, To, #xmlel{name = <<"presence">>} = Packet) ->
+ case fxml:get_attr_s(<<"type">>, Packet#xmlel.attrs) of
+ T when (T == <<"">>) or (T == <<"unavailable">>) ->
+ Server = To#jid.server,
+ User = To#jid.user,
+ PrivList = ejabberd_hooks:run_fold(privacy_get_user_list,
+ Server, #userlist{}, [User, Server]),
+ case privacy_check_packet(Server, User, PrivList, From, To, Packet, in) of
+ allow ->
+ Pid ! {roster_presence, Packet, From};
+ _ -> ok
+ end,
+ ok;
+ _ -> ok
+ end;
+ (_From, _To, _Packet) -> ok
+ end.
+
+%%%--------------------------------------------------------------------------------------
+%%% Manage Roster
+%%%--------------------------------------------------------------------------------------
+
+process_iq(StateData, FromJID, ToJID, Packet) ->
+ IQ = jlib:iq_query_or_response_info(Packet),
+ case IQ of
+ #iq{xmlns = ?NS_ROSTER} ->
+ case (ToJID#jid.luser /= <<"">>) and
+ (FromJID#jid.luser == <<"">>) and
+ lists:member(ToJID#jid.lserver, ?MYHOSTS) of
+ true ->
+ AccessType =
+ proplists:get_value(roster, StateData#state.privilege_access, none),
+ case IQ#iq.type of
+ get when (AccessType == <<"both">>) or (AccessType == <<"get">>) ->
+ RosterIQ = roster_management(ToJID, FromJID, IQ),
+ ejabberd_service:send_element(StateData, RosterIQ);
+ set when (AccessType == <<"both">>) or (AccessType == <<"set">>) ->
+ %% check if user ToJID exist
+ #jid{lserver = Server, luser = User} = ToJID,
+ case ejabberd_auth:is_user_exists(User,Server) of
+ true ->
+ ResIQ = roster_management(ToJID, FromJID, IQ),
+ ejabberd_service:send_element(StateData, ResIQ);
+ _ -> ok
+ end;
+ _ ->
+ Err = jlib:make_error_reply(Packet, ?ERR_FORBIDDEN),
+ ejabberd_service:send_element(StateData, Err)
+ end;
+ _ ->
+ ejabberd_router:route(FromJID, ToJID, Packet)
+ end;
+ #iq{type = Type, id = Id} when (Type == error) or (Type == result) -> % for XEP-0355
+ Hook = {iq, Type, Id},
+ Host = ToJID#jid.lserver,
+ case (ToJID#jid.luser == <<"">>) and
+ (FromJID#jid.luser == <<"">>) and
+ lists:member(ToJID#jid.lserver, ?MYHOSTS) of
+ true ->
+ case ets:lookup(hooks_tmp, {Hook, Host}) of
+ [{_, Function, _Timestamp}] ->
+ catch apply(Function, [Packet]);
+ [] ->
+ ejabberd_router:route(FromJID, ToJID, Packet)
+ end;
+ _ ->
+ ejabberd_router:route(FromJID, ToJID, Packet)
+ end;
+ _ ->
+ ejabberd_router:route(FromJID, ToJID, Packet)
+ end.
+
+roster_management(FromJID, ToJID, IQ) ->
+ ResIQ = mod_roster:process_iq(FromJID, FromJID, IQ),
+ ResXml = jlib:iq_to_xml(ResIQ),
+ jlib:replace_from_to(FromJID, ToJID, ResXml).
+
+%%%--------------------------------------------------------------------------------------
+%%% Message permission
+%%%--------------------------------------------------------------------------------------
+
+process_message(StateData, FromJID, ToJID, #xmlel{children = Children} = Packet) ->
+ %% if presence was send from service to server,
+ case lists:member(ToJID#jid.lserver, ?MYHOSTS) and
+ (ToJID#jid.luser == <<"">>) and
+ (FromJID#jid.luser == <<"">>) of %% service
+ true ->
+ %% if stanza contains privilege element
+ case Children of
+ [#xmlel{name = <<"privilege">>,
+ attrs = [{<<"xmlns">>, ?NS_PRIVILEGE}],
+ children = [#xmlel{name = <<"forwarded">>,
+ attrs = [{<<"xmlns">>, ?NS_FORWARD}],
+ children = Children2}]}] ->
+ %% 1 case : privilege service send subscription message
+ %% on behalf of the client
+ %% 2 case : privilege service send message on behalf
+ %% of the client
+ case Children2 of
+ %% it isn't case of 0356 extension
+ [#xmlel{name = <<"presence">>} = Child] ->
+ forward_subscribe(StateData, Child, Packet);
+ [#xmlel{name = <<"message">>} = Child] -> %% xep-0356
+ forward_message(StateData, Child, Packet);
+ _ ->
+ Lang = fxml:get_tag_attr_s(<<"xml:lang">>, Packet),
+ Txt = <<"invalid forwarded element">>,
+ Err = jlib:make_error_reply(Packet, ?ERRT_BAD_REQUEST(Lang, Txt)),
+ ejabberd_service:send_element(StateData, Err)
+ end;
+ _ ->
+ ejabberd_router:route(FromJID, ToJID, Packet)
+ end;
+
+ _ ->
+ ejabberd_router:route(FromJID, ToJID, Packet)
+ end.
+
+forward_subscribe(StateData, Presence, Packet) ->
+ PrivAccess = StateData#state.privilege_access,
+ T = proplists:get_value(roster, PrivAccess, none),
+ Type = fxml:get_attr_s(<<"type">>, Presence#xmlel.attrs),
+ if
+ ((T == <<"both">>) or (T == <<"set">>)) and (Type == <<"subscribe">>) ->
+ From = fxml:get_attr_s(<<"from">>, Presence#xmlel.attrs),
+ FromJ = jid:from_string(From),
+ To = fxml:get_attr_s(<<"to">>, Presence#xmlel.attrs),
+ ToJ = case To of
+ <<"">> -> error;
+ _ -> jid:from_string(To)
+ end,
+ if
+ (ToJ /= error) and (FromJ /= error) ->
+ Server = FromJ#jid.lserver,
+ User = FromJ#jid.luser,
+ case (FromJ#jid.lresource == <<"">>) and
+ lists:member(Server, ?MYHOSTS) of
+ true ->
+ if
+ (Server /= ToJ#jid.lserver) or
+ (User /= ToJ#jid.luser) ->
+ %% 0356 server MUST NOT allow the privileged entity
+ %% to do anything that the managed entity could not do
+ try_roster_subscribe(Server,User, FromJ, ToJ, Presence);
+ true -> %% we don't want presence sent to self
+ ok
+ end;
+ _ ->
+ Err = jlib:make_error_reply(Packet, ?ERR_FORBIDDEN),
+ ejabberd_service:send_element(StateData, Err)
+ end;
+ true ->
+ Lang = fxml:get_tag_attr_s(<<"xml:lang">>, Packet),
+ Txt = <<"Incorrect stanza from/to JID">>,
+ Err = jlib:make_error_reply(Packet, ?ERRT_BAD_REQUEST(Lang, Txt)),
+ ejabberd_service:send_element(StateData, Err)
+ end;
+ true ->
+ Err = jlib:make_error_reply(Packet, ?ERR_FORBIDDEN),
+ ejabberd_service:send_element(StateData, Err)
+ end.
+
+forward_message(StateData, Message, Packet) ->
+ PrivAccess = StateData#state.privilege_access,
+ T = proplists:get_value(message, PrivAccess, none),
+ if
+ (T == <<"outgoing">>) ->
+ From = fxml:get_attr_s(<<"from">>, Message#xmlel.attrs),
+ FromJ = jid:from_string(From),
+ To = fxml:get_attr_s(<<"to">>, Message#xmlel.attrs),
+ ToJ = case To of
+ <<"">> -> FromJ;
+ _ -> jid:from_string(To)
+ end,
+ if
+ (ToJ /= error) and (FromJ /= error) ->
+ Server = FromJ#jid.server,
+ User = FromJ#jid.user,
+ case (FromJ#jid.lresource == <<"">>) and
+ lists:member(Server, ?MYHOSTS) of
+ true ->
+ %% there are no restriction on to attribute
+ PrivList = ejabberd_hooks:run_fold(privacy_get_user_list,
+ Server, #userlist{},
+ [User, Server]),
+ check_privacy_route(Server, User, PrivList,
+ FromJ, ToJ, Message);
+ _ ->
+ Err = jlib:make_error_reply(Packet, ?ERR_FORBIDDEN),
+ ejabberd_service:send_element(StateData, Err)
+ end;
+ true ->
+ Lang = fxml:get_tag_attr_s(<<"xml:lang">>, Packet),
+ Txt = <<"Incorrect stanza from/to JID">>,
+ Err = jlib:make_error_reply(Packet, ?ERRT_BAD_REQUEST(Lang, Txt)),
+ ejabberd_service:send_element(StateData, Err)
+ end;
+ true ->
+ Err = jlib:make_error_reply(Packet,?ERR_FORBIDDEN),
+ ejabberd_service:send_element(StateData, Err)
+ end.
+
+%%%--------------------------------------------------------------------------------------
+%%% helper functions
+%%%--------------------------------------------------------------------------------------
+
+compare_presences(undefined, _Presence) -> false;
+compare_presences(#xmlel{attrs = Attrs, children = Child},
+ #xmlel{attrs = Attrs2, children = Child2}) ->
+ Id1 = fxml:get_attr_s(<<"id">>, Attrs),
+ Id2 = fxml:get_attr_s(<<"id">>, Attrs2),
+ if
+ (Id1 /= Id2) ->
+ false;
+ (Id1 /= <<"">>) and (Id1 == Id2) ->
+ true;
+ true ->
+ case not compare_attrs(Attrs, Attrs2) of
+ true -> false;
+ _ ->
+ compare_elements(Child, Child2)
+ end
+ end.
+
+
+compare_elements([],[]) -> true;
+compare_elements(Tags1, Tags2) when length(Tags1) == length(Tags2) ->
+ compare_tags(Tags1,Tags2);
+compare_elements(_Tags1, _Tags2) -> false.
+
+compare_tags([],[]) -> true;
+compare_tags([{xmlcdata, CData}|Tags1], [{xmlcdata, CData}|Tags2]) ->
+ compare_tags(Tags1, Tags2);
+compare_tags([{xmlcdata, _CData1}|_Tags1], [{xmlcdata, _CData2}|_Tags2]) ->
+ false;
+compare_tags([#xmlel{} = Stanza1|Tags1], [#xmlel{} = Stanza2|Tags2]) ->
+ case (Stanza1#xmlel.name == Stanza2#xmlel.name) and
+ compare_attrs(Stanza1#xmlel.attrs, Stanza2#xmlel.attrs) and
+ compare_tags(Stanza1#xmlel.children, Stanza2#xmlel.children) of
+ true ->
+ compare_tags(Tags1,Tags2);
+ false ->
+ false
+ end.
+
+%% attr() :: {Name, Value}
+-spec compare_attrs([attr()], [attr()]) -> boolean().
+compare_attrs([],[]) -> true;
+compare_attrs(Attrs1, Attrs2) when length(Attrs1) == length(Attrs2) ->
+ lists:foldl(fun(Attr,Acc) -> lists:member(Attr, Attrs2) and Acc end, true, Attrs1);
+compare_attrs(_Attrs1, _Attrs2) -> false.
+
+%% Check if privacy rules allow this delivery
+%% from ejabberd_c2s.erl
+privacy_check_packet(Server, User, PrivList, From, To, Packet , Dir) ->
+ ejabberd_hooks:run_fold(privacy_check_packet,
+ Server, allow, [User, Server, PrivList,
+ {From, To, Packet}, Dir]).
+
+check_privacy_route(Server, User, PrivList, From, To, Packet) ->
+ case privacy_check_packet(Server, User, PrivList, From, To, Packet, out) of
+ allow ->
+ ejabberd_router:route(From, To, Packet);
+ _ -> ok %% who should receive error : service or user?
+ end.
+
+try_roster_subscribe(Server,User, From, To, Packet) ->
+ Access =
+ gen_mod:get_module_opt(Server, mod_roster, access,
+ fun(A) when is_atom(A) -> A end, all),
+ case acl:match_rule(Server, Access, From) of
+ deny ->
+ ok;
+ allow ->
+ ejabberd_hooks:run(roster_out_subscription, Server,
+ [User, Server, To, subscribe]),
+ PrivList = ejabberd_hooks:run_fold(privacy_get_user_list,
+ Server,
+ #userlist{},
+ [User, Server]),
+ check_privacy_route(Server, User, PrivList, From, To, Packet)
+ end.
diff --git a/src/mod_pubsub.erl b/src/mod_pubsub.erl
index 81afc9a06..d64d05737 100644
--- a/src/mod_pubsub.erl
+++ b/src/mod_pubsub.erl
@@ -363,8 +363,6 @@ depends(ServerHost, Opts) ->
%% The default plugin module is implicit.
%% <p>The Erlang code for the plugin is located in a module called
%% <em>node_plugin</em>. The 'node_' prefix is mandatory.</p>
-%% <p>The modules are initialized in alphetical order and the list is checked
-%% and sorted to ensure that each module is initialized only once.</p>
%% <p>See {@link node_hometree:init/1} for an example implementation.</p>
init_plugins(Host, ServerHost, Opts) ->
TreePlugin = tree(Host, gen_mod:get_opt(nodetree, Opts,
diff --git a/src/node_flat_sql.erl b/src/node_flat_sql.erl
index fa4af4d57..86375eaec 100644
--- a/src/node_flat_sql.erl
+++ b/src/node_flat_sql.erl
@@ -946,8 +946,9 @@ select_affiliation_subscriptions(Nidx, JID, JID) ->
select_affiliation_subscriptions(Nidx, JID);
select_affiliation_subscriptions(Nidx, GenKey, SubKey) ->
{result, Affiliation} = get_affiliation(Nidx, GenKey),
- {result, Subscriptions} = get_subscriptions(Nidx, SubKey),
- {Affiliation, Subscriptions}.
+ {result, BareJidSubs} = get_subscriptions(Nidx, GenKey),
+ {result, FullJidSubs} = get_subscriptions(Nidx, SubKey),
+ {Affiliation, BareJidSubs++FullJidSubs}.
update_affiliation(Nidx, JID, Affiliation) ->
J = encode_jid(JID),
diff --git a/src/node_mb.erl b/src/node_mb.erl
index 6c7f09780..3399422e5 100644
--- a/src/node_mb.erl
+++ b/src/node_mb.erl
@@ -38,6 +38,7 @@
%%% plugins:
%%% - "flat"
%%% - "pep" # Requires mod_caps.
+%%% - "mb"
%%% pep_mapping:
%%% "urn:xmpp:microblog:0": "mb"
%%% </pre></p>
@@ -154,7 +155,7 @@ set_subscriptions(Nidx, Owner, Subscription, SubId) ->
node_pep:set_subscriptions(Nidx, Owner, Subscription, SubId).
get_pending_nodes(Host, Owner) ->
- node_hometree:get_pending_nodes(Host, Owner).
+ node_pep:get_pending_nodes(Host, Owner).
get_states(Nidx) ->
node_pep:get_states(Nidx).
diff --git a/src/node_mb_sql.erl b/src/node_mb_sql.erl
new file mode 100644
index 000000000..a3fdf3aed
--- /dev/null
+++ b/src/node_mb_sql.erl
@@ -0,0 +1,151 @@
+%%%----------------------------------------------------------------------
+%%% File : node_mb_sql.erl
+%%% Author : Holger Weiss <holger@zedat.fu-berlin.de>
+%%% Purpose : PEP microblogging (XEP-0277) plugin with SQL backend
+%%% Created : 6 Sep 2016 by Holger Weiss <holger@zedat.fu-berlin.de>
+%%%
+%%%
+%%% ejabberd, Copyright (C) 2016 ProcessOne
+%%%
+%%% This program is free software; you can redistribute it and/or
+%%% modify it under the terms of the GNU General Public License as
+%%% published by the Free Software Foundation; either version 2 of the
+%%% License, or (at your option) any later version.
+%%%
+%%% This program is distributed in the hope that it will be useful,
+%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
+%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%%% General Public License for more details.
+%%%
+%%% You should have received a copy of the GNU General Public License along
+%%% with this program; if not, write to the Free Software Foundation, Inc.,
+%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+%%%
+%%%----------------------------------------------------------------------
+
+-module(node_mb_sql).
+-behaviour(gen_pubsub_node).
+-author('holger@zedat.fu-berlin.de').
+
+-include("pubsub.hrl").
+-include("jlib.hrl").
+
+-export([init/3, terminate/2, options/0, features/0,
+ create_node_permission/6, create_node/2, delete_node/1,
+ purge_node/2, subscribe_node/8, unsubscribe_node/4,
+ publish_item/7, delete_item/4, remove_extra_items/3,
+ get_entity_affiliations/2, get_node_affiliations/1,
+ get_affiliation/2, set_affiliation/3,
+ get_entity_subscriptions/2, get_node_subscriptions/1,
+ get_subscriptions/2, set_subscriptions/4,
+ get_pending_nodes/2, get_states/1, get_state/2,
+ set_state/1, get_items/7, get_items/3, get_item/7,
+ get_item/2, set_item/1, get_item_name/3, node_to_path/1,
+ path_to_node/1]).
+
+init(Host, ServerHost, Opts) ->
+ node_pep_sql:init(Host, ServerHost, Opts).
+
+terminate(Host, ServerHost) ->
+ node_pep_sql:terminate(Host, ServerHost), ok.
+
+options() ->
+ [{sql, true}, {rsm, true} | node_mb:options()].
+
+features() ->
+ [<<"rsm">> | node_mb:features()].
+
+create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access) ->
+ node_pep_sql:create_node_permission(Host, ServerHost, Node, ParentNode,
+ Owner, Access).
+
+create_node(Nidx, Owner) ->
+ node_pep_sql:create_node(Nidx, Owner).
+
+delete_node(Removed) ->
+ node_pep_sql:delete_node(Removed).
+
+subscribe_node(Nidx, Sender, Subscriber, AccessModel, SendLast,
+ PresenceSubscription, RosterGroup, Options) ->
+ node_pep_sql:subscribe_node(Nidx, Sender, Subscriber, AccessModel, SendLast,
+ PresenceSubscription, RosterGroup, Options).
+
+unsubscribe_node(Nidx, Sender, Subscriber, SubId) ->
+ node_pep_sql:unsubscribe_node(Nidx, Sender, Subscriber, SubId).
+
+publish_item(Nidx, Publisher, Model, MaxItems, ItemId, Payload, PubOpts) ->
+ node_pep_sql:publish_item(Nidx, Publisher, Model, MaxItems, ItemId,
+ Payload, PubOpts).
+
+remove_extra_items(Nidx, MaxItems, ItemIds) ->
+ node_pep_sql:remove_extra_items(Nidx, MaxItems, ItemIds).
+
+delete_item(Nidx, Publisher, PublishModel, ItemId) ->
+ node_pep_sql:delete_item(Nidx, Publisher, PublishModel, ItemId).
+
+purge_node(Nidx, Owner) ->
+ node_pep_sql:purge_node(Nidx, Owner).
+
+get_entity_affiliations(Host, Owner) ->
+ node_pep_sql:get_entity_affiliations(Host, Owner).
+
+get_node_affiliations(Nidx) ->
+ node_pep_sql:get_node_affiliations(Nidx).
+
+get_affiliation(Nidx, Owner) ->
+ node_pep_sql:get_affiliation(Nidx, Owner).
+
+set_affiliation(Nidx, Owner, Affiliation) ->
+ node_pep_sql:set_affiliation(Nidx, Owner, Affiliation).
+
+get_entity_subscriptions(Host, Owner) ->
+ node_pep_sql:get_entity_subscriptions(Host, Owner).
+
+get_node_subscriptions(Nidx) ->
+ node_pep_sql:get_node_subscriptions(Nidx).
+
+get_subscriptions(Nidx, Owner) ->
+ node_pep_sql:get_subscriptions(Nidx, Owner).
+
+set_subscriptions(Nidx, Owner, Subscription, SubId) ->
+ node_pep_sql:set_subscriptions(Nidx, Owner, Subscription, SubId).
+
+get_pending_nodes(Host, Owner) ->
+ node_pep_sql:get_pending_nodes(Host, Owner).
+
+get_states(Nidx) ->
+ node_pep_sql:get_states(Nidx).
+
+get_state(Nidx, JID) ->
+ node_pep_sql:get_state(Nidx, JID).
+
+set_state(State) ->
+ node_pep_sql:set_state(State).
+
+get_items(Nidx, From, RSM) ->
+ node_pep_sql:get_items(Nidx, From, RSM).
+
+get_items(Nidx, JID, AccessModel, PresenceSubscription, RosterGroup, SubId,
+ RSM) ->
+ node_pep_sql:get_items(Nidx, JID, AccessModel, PresenceSubscription,
+ RosterGroup, SubId, RSM).
+
+get_item(Nidx, ItemId) ->
+ node_pep_sql:get_item(Nidx, ItemId).
+
+get_item(Nidx, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup,
+ SubId) ->
+ node_pep_sql:get_item(Nidx, ItemId, JID, AccessModel, PresenceSubscription,
+ RosterGroup, SubId).
+
+set_item(Item) ->
+ node_pep_sql:set_item(Item).
+
+get_item_name(Host, Node, Id) ->
+ node_pep_sql:get_item_name(Host, Node, Id).
+
+node_to_path(Node) ->
+ node_pep_sql:node_to_path(Node).
+
+path_to_node(Path) ->
+ node_pep_sql:path_to_node(Path).
diff --git a/test/ejabberd_commands_mock_test.exs b/test/ejabberd_commands_mock_test.exs
index 9d33d7573..785e74cd7 100644
--- a/test/ejabberd_commands_mock_test.exs
+++ b/test/ejabberd_commands_mock_test.exs
@@ -58,6 +58,7 @@ defmodule EjabberdCommandsMockTest do
setup do
:meck.unload
:meck.new(@module, [:non_strict])
+ :mnesia.delete_table(:ejabberd_commands)
:ejabberd_commands.init
end
diff --git a/test/ejabberd_oauth_mock.exs b/test/ejabberd_oauth_mock.exs
index e6a34f65e..965bff1e6 100644
--- a/test/ejabberd_oauth_mock.exs
+++ b/test/ejabberd_oauth_mock.exs
@@ -26,7 +26,10 @@ defmodule EjabberdOauthMock do
:mnesia.start
:mnesia.create_table(:oauth_token,
[ram_copies: [node],
- attributes: [:oauth_token, :us, :scope, :expire]])
+ attributes: [:oauth_token, :us, :scope, :expire]])
+ :application.start(:cache_tab)
+ :cache_tab.new(:oauth_token,
+ [{:max_size, 1000}, {:life_time, 3600}])
end
def get_token(user, domain, command, expiration \\ 3600) do
diff --git a/test/elixir-config/attr_test.exs b/test/elixir-config/attr_test.exs
new file mode 100644
index 000000000..c5cab5bd8
--- /dev/null
+++ b/test/elixir-config/attr_test.exs
@@ -0,0 +1,87 @@
+defmodule Ejabberd.Config.AttrTest do
+ use ExUnit.Case, async: true
+
+ alias Ejabberd.Config.Attr
+
+ test "extract attrs from single line block" do
+ block = quote do
+ @active false
+ end
+
+ block_res = Attr.extract_attrs_from_block_with_defaults(block)
+ assert {:active, false} in block_res
+ end
+
+ test "extract attrs from multi line block" do
+ block = quote do
+ @active false
+ @opts [http: true]
+ end
+
+ block_res = Attr.extract_attrs_from_block_with_defaults(block)
+ assert {:active, false} in block_res
+ assert {:opts, [http: true]} in block_res
+ end
+
+ test "inserts correctly defaults attr when missing in block" do
+ block = quote do
+ @active false
+ @opts [http: true]
+ end
+
+ block_res = Attr.extract_attrs_from_block_with_defaults(block)
+
+ assert {:active, false} in block_res
+ assert {:git, ""} in block_res
+ assert {:name, ""} in block_res
+ assert {:opts, [http: true]} in block_res
+ assert {:dependency, []} in block_res
+ end
+
+ test "inserts all defaults attr when passed an empty block" do
+ block = quote do
+ end
+
+ block_res = Attr.extract_attrs_from_block_with_defaults(block)
+
+ assert {:active, true} in block_res
+ assert {:git, ""} in block_res
+ assert {:name, ""} in block_res
+ assert {:opts, []} in block_res
+ assert {:dependency, []} in block_res
+ end
+
+ test "validates attrs and returns errors, if any" do
+ block = quote do
+ @not_supported_attr true
+ @active "false"
+ @opts [http: true]
+ end
+
+ block_res =
+ block
+ |> Attr.extract_attrs_from_block_with_defaults
+ |> Attr.validate
+
+ assert {:ok, {:opts, [http: true]}} in block_res
+ assert {:ok, {:git, ""}} in block_res
+ assert {:error, {:not_supported_attr, true}, :attr_not_supported} in block_res
+ assert {:error, {:active, "false"}, :type_not_supported} in block_res
+ end
+
+ test "returns the correct type for an attribute" do
+ assert :boolean == Attr.get_type_for_attr(:active)
+ assert :string == Attr.get_type_for_attr(:git)
+ assert :string == Attr.get_type_for_attr(:name)
+ assert :list == Attr.get_type_for_attr(:opts)
+ assert :list == Attr.get_type_for_attr(:dependency)
+ end
+
+ test "returns the correct default for an attribute" do
+ assert true == Attr.get_default_for_attr(:active)
+ assert "" == Attr.get_default_for_attr(:git)
+ assert "" == Attr.get_default_for_attr(:name)
+ assert [] == Attr.get_default_for_attr(:opts)
+ assert [] == Attr.get_default_for_attr(:dependency)
+ end
+end
diff --git a/test/elixir-config/config_test.exs b/test/elixir-config/config_test.exs
new file mode 100644
index 000000000..c359c49c3
--- /dev/null
+++ b/test/elixir-config/config_test.exs
@@ -0,0 +1,65 @@
+defmodule Ejabberd.ConfigTest do
+ use ExUnit.Case
+
+ alias Ejabberd.Config
+ alias Ejabberd.Config.Store
+
+ setup_all do
+ pid = Process.whereis(Ejabberd.Config.Store)
+ unless pid != nil and Process.alive?(pid) do
+ Store.start_link
+
+ File.cd("test/elixir-config/shared")
+ config_file_path = File.cwd! <> "/ejabberd.exs"
+ Config.init(config_file_path)
+ end
+
+ {:ok, %{}}
+ end
+
+ test "extracts successfully the module name from config file" do
+ assert [Ejabberd.ConfigFile] == Store.get(:module_name)
+ end
+
+ test "extracts successfully general opts from config file" do
+ [general] = Store.get(:general)
+ shaper = [normal: 1000, fast: 50000, max_fsm_queue: 1000]
+ assert [loglevel: 4, language: "en", hosts: ["localhost"], shaper: shaper] == general
+ end
+
+ test "extracts successfully listeners from config file" do
+ [listen] = Store.get(:listeners)
+ assert :ejabberd_c2s == listen.module
+ assert [port: 5222, max_stanza_size: 65536, shaper: :c2s_shaper, access: :c2s] == listen.attrs[:opts]
+ end
+
+ test "extracts successfully modules from config file" do
+ [module] = Store.get(:modules)
+ assert :mod_adhoc == module.module
+ assert [] == module.attrs[:opts]
+ end
+
+ test "extracts successfully hooks from config file" do
+ [register_hook] = Store.get(:hooks)
+
+ assert :register_user == register_hook.hook
+ assert [host: "localhost"] == register_hook.opts
+ assert is_function(register_hook.fun)
+ end
+
+ # TODO: When enalbed, this test causes the evaluation of a different config file, so
+ # the other tests, that uses the store, are compromised because the data is different.
+ # So, until a good way is found, this test should remain disabed.
+ #
+ # test "init/2 with force:true re-initializes the config store with new data" do
+ # config_file_path = File.cwd! <> "/ejabberd_different_from_default.exs"
+ # Config.init(config_file_path, true)
+ #
+ # assert [Ejabberd.ConfigFile] == Store.get(:module_name)
+ # assert [[loglevel: 4, language: "en", hosts: ["localhost"]]] == Store.get(:general)
+ # assert [] == Store.get(:modules)
+ # assert [] == Store.get(:listeners)
+ #
+ # Store.stop
+ # end
+end
diff --git a/test/elixir-config/ejabberd_logger.exs b/test/elixir-config/ejabberd_logger.exs
new file mode 100644
index 000000000..d13f79aa6
--- /dev/null
+++ b/test/elixir-config/ejabberd_logger.exs
@@ -0,0 +1,49 @@
+defmodule Ejabberd.Config.EjabberdLoggerTest do
+ use ExUnit.Case
+
+ import ExUnit.CaptureIO
+
+ alias Ejabberd.Config
+ alias Ejabberd.Config.Store
+ alias Ejabberd.Config.Validation
+ alias Ejabberd.Config.EjabberdLogger
+
+ setup_all do
+ pid = Process.whereis(Ejabberd.Config.Store)
+ unless pid != nil and Process.alive?(pid) do
+ Store.start_link
+
+ File.cd("test/elixir-config/shared")
+ config_file_path = File.cwd! <> "/ejabberd_for_validation.exs"
+ Config.init(config_file_path)
+ end
+
+ {:ok, %{}}
+ end
+
+ test "outputs correctly when attr is not supported" do
+ error_msg = "[ WARN ] Annotation @attr_not_supported is not supported.\n"
+
+ [_mod_irc, _mod_configure, mod_time] = Store.get(:modules)
+ fun = fn ->
+ mod_time
+ |> Validation.validate
+ |> EjabberdLogger.log_errors
+ end
+
+ assert capture_io(fun) == error_msg
+ end
+
+ test "outputs correctly when dependency is not found" do
+ error_msg = "[ WARN ] Module :mod_adhoc was not found, but is required as a dependency.\n"
+
+ [_mod_irc, mod_configure, _mod_time] = Store.get(:modules)
+ fun = fn ->
+ mod_configure
+ |> Validation.validate
+ |> EjabberdLogger.log_errors
+ end
+
+ assert capture_io(fun) == error_msg
+ end
+end
diff --git a/test/elixir-config/shared/ejabberd.exs b/test/elixir-config/shared/ejabberd.exs
new file mode 100644
index 000000000..5d0243bb5
--- /dev/null
+++ b/test/elixir-config/shared/ejabberd.exs
@@ -0,0 +1,31 @@
+defmodule Ejabberd.ConfigFile do
+ use Ejabberd.Config
+
+ def start do
+ [loglevel: 4,
+ language: "en",
+ hosts: ["localhost"],
+ shaper: shaper]
+ end
+
+ defp shaper do
+ [normal: 1000,
+ fast: 50000,
+ max_fsm_queue: 1000]
+ end
+
+ listen :ejabberd_c2s do
+ @opts [
+ port: 5222,
+ max_stanza_size: 65536,
+ shaper: :c2s_shaper,
+ access: :c2s]
+ end
+
+ module :mod_adhoc do
+ end
+
+ hook :register_user, [host: "localhost"], fn(user, server) ->
+ info("User registered: #{user} on #{server}")
+ end
+end
diff --git a/test/elixir-config/shared/ejabberd_different_from_default.exs b/test/elixir-config/shared/ejabberd_different_from_default.exs
new file mode 100644
index 000000000..a39409683
--- /dev/null
+++ b/test/elixir-config/shared/ejabberd_different_from_default.exs
@@ -0,0 +1,9 @@
+defmodule Ejabberd.ConfigFile do
+ use Ejabberd.Config
+
+ def start do
+ [loglevel: 4,
+ language: "en",
+ hosts: ["localhost"]]
+ end
+end
diff --git a/test/elixir-config/shared/ejabberd_for_validation.exs b/test/elixir-config/shared/ejabberd_for_validation.exs
new file mode 100644
index 000000000..8c0196c7e
--- /dev/null
+++ b/test/elixir-config/shared/ejabberd_for_validation.exs
@@ -0,0 +1,20 @@
+defmodule Ejabberd.ConfigFile do
+ use Ejabberd.Config
+
+ def start do
+ [loglevel: 4,
+ language: "en",
+ hosts: ["localhost"]]
+ end
+
+ module :mod_time do
+ @attr_not_supported true
+ end
+
+ module :mod_configure do
+ @dependency [:mod_adhoc]
+ end
+
+ module :mod_irc do
+ end
+end
diff --git a/test/elixir-config/validation_test.exs b/test/elixir-config/validation_test.exs
new file mode 100644
index 000000000..1df775966
--- /dev/null
+++ b/test/elixir-config/validation_test.exs
@@ -0,0 +1,32 @@
+defmodule Ejabberd.Config.ValidationTest do
+ use ExUnit.Case
+
+ alias Ejabberd.Config
+ alias Ejabberd.Config.Store
+ alias Ejabberd.Config.Validation
+
+ setup_all do
+ pid = Process.whereis(Ejabberd.Config.Store)
+ unless pid != nil and Process.alive?(pid) do
+ Store.start_link
+
+ File.cd("test/elixir-config/shared")
+ config_file_path = File.cwd! <> "/ejabberd_for_validation.exs"
+ Config.init(config_file_path)
+ end
+
+ {:ok, %{}}
+ end
+
+ test "validates correctly the modules" do
+ [mod_irc, mod_configure, mod_time] = Store.get(:modules)
+
+ [{:error, _mod, errors}] = Validation.validate(mod_configure)
+ assert %{dependency: [mod_adhoc: :not_found]} == errors
+
+ [{:error, _mod, errors}] = Validation.validate(mod_time)
+ assert %{attribute: [{{:attr_not_supported, true}, :attr_not_supported}]} == errors
+
+ [{:ok, _mod}] = Validation.validate(mod_irc)
+ end
+end