From ee7d3c7030f01a84364b8590f36d63e739aa61cd Mon Sep 17 00:00:00 2001 From: Badlop Date: Mon, 15 Apr 2013 12:03:14 +0200 Subject: [PATCH] Initial import from ejabberd-modules SVN --- README.txt | 60 + atom_pubsub/Emakefile | 4 + atom_pubsub/README | 116 + atom_pubsub/build.bat | 1 + atom_pubsub/build.sh | 2 + atom_pubsub/src/atom_microblog.erl | 367 + atom_pubsub/src/atom_pubsub.erl | 374 + atom_pubsub/src/mod_couch.erl | 24 + bfile/LICENCE | 27 + bfile/Makefile | 23 + bfile/README | 28 + bfile/c_src/FILE_drv.c | 490 ++ bfile/c_src/Makefile | 42 + bfile/config/Makefile | 21 + bfile/config/acconfig.h | 269 + bfile/config/aclocal.m4 | 32 + bfile/config/config.guess | 1354 ++++ bfile/config/config.h.in | 319 + bfile/config/config.sub | 1460 ++++ bfile/config/configure.in | 88 + bfile/config/include.mk.in | 75 + bfile/config/install-sh | 250 + bfile/src/Makefile | 25 + bfile/src/bfile.app.src | 7 + bfile/src/bfile.erl | 224 + bfile/tests/read.erl | 19 + bfile/tests/readold.erl | 18 + bfile/tests/write.erl | 17 + bfile/tests/writeold.erl | 16 + bfile/vsn.mk | 1 + dns/src/dns.erl | 381 + ejabberd-dev/README.txt | 5 + ejabberd-dev/include/adhoc.hrl | 36 + ejabberd-dev/include/ejabberd.hrl | 64 + ejabberd-dev/include/ejabberd_commands.hrl | 52 + ejabberd-dev/include/ejabberd_config.hrl | 28 + ejabberd-dev/include/ejabberd_ctl.hrl | 25 + ejabberd-dev/include/jlib.hrl | 336 + ejabberd-dev/include/mod_muc/mod_muc_room.hrl | 90 + ejabberd-dev/include/mod_pubsub/pubsub.hrl | 193 + ejabberd-dev/include/mod_roster.hrl | 33 + ejabberd-dev/include/web/ejabberd_http.hrl | 34 + .../include/web/ejabberd_web_admin.hrl | 76 + ejabberd-dev/src/gen_mod.erl | 236 + ejabberdPrefs/English.lproj/InfoPlist.strings | Bin 0 -> 532 bytes .../ejabberdPref.nib/classes.nib | 30 + .../English.lproj/ejabberdPref.nib/info.nib | 14 + .../ejabberdPref.nib/keyedobjects.nib | Bin 0 -> 8128 bytes ejabberdPrefs/Info.plist | 30 + ejabberdPrefs/LICENSE | 18 + ejabberdPrefs/MPL | 470 ++ ejabberdPrefs/config.txt | 1 + ejabberdPrefs/ejabberd.plist | 16 + .../ejabberd.xcodeproj/project.pbxproj | 439 ++ ejabberdPrefs/ejabberdController.h | 34 + ejabberdPrefs/ejabberdController.m | 125 + ejabberdPrefs/ejabberdPref.h | 19 + ejabberdPrefs/ejabberdPref.m | 19 + ejabberdPrefs/ejabberdPref.tiff | Bin 0 -> 1086 bytes ejabberdPrefs/ejabberd_Prefix.pch | 8 + ejabberdPrefs/instance_started.png | Bin 0 -> 4422 bytes ejabberdPrefs/instance_stopped.png | Bin 0 -> 4339 bytes ejabberdPrefs/logo_ejabberd.png | Bin 0 -> 4501 bytes ejabberdPrefs/version.plist | 16 + ejabberd_xmlrpc/COPYING | 343 + ejabberd_xmlrpc/ChangeLog | 130 + ejabberd_xmlrpc/Emakefile | 2 + ejabberd_xmlrpc/README.txt | 499 ++ ejabberd_xmlrpc/build.bat | 1 + ejabberd_xmlrpc/build.sh | 2 + ejabberd_xmlrpc/src/ejabberd_xmlrpc.erl | 467 ++ extract-mod-translations.sh | 27 + ircd/Emakefile | 1 + ircd/README.txt | 83 + ircd/build.bat | 1 + ircd/build.sh | 2 + ircd/src/ejabberd_ircd.erl | 885 +++ jorge/COPYING | 339 + jorge/README | 44 + jorge/calendar_view.php | 679 ++ jorge/chat_map.php | 152 + jorge/class.db.php | 3096 ++++++++ jorge/class.ejabberd_xmlrpc.php | 257 + jorge/class.helper.php | 358 + jorge/class.roster.php | 147 + jorge/class.sessions.php | 68 + jorge/config.php.inc | 99 + jorge/contacts.php | 191 + jorge/export.php | 208 + jorge/favicon.ico | Bin 0 -> 9662 bytes jorge/favorites.php | 178 + jorge/footer.php | 126 + jorge/func.php | 932 +++ jorge/headers.php | 371 + jorge/help.php | 29 + jorge/img/apple-logo.png | Bin 0 -> 1410 bytes jorge/img/bak2a.png | Bin 0 -> 251 bytes jorge/img/bak2b.png | Bin 0 -> 231 bytes jorge/img/bar_bg.png | Bin 0 -> 213 bytes jorge/img/bar_new.png | Bin 0 -> 366 bytes jorge/img/bell-bak.png | Bin 0 -> 171 bytes jorge/img/bell-down.png | Bin 0 -> 131 bytes jorge/img/cal_bck.png | Bin 0 -> 674 bytes jorge/img/cal_bck2.png | Bin 0 -> 436 bytes jorge/img/cal_bck_bot.png | Bin 0 -> 167 bytes jorge/img/cal_bck_left.png | Bin 0 -> 394 bytes jorge/img/cal_bck_left2.png | Bin 0 -> 176 bytes jorge/img/cal_bck_right.png | Bin 0 -> 406 bytes jorge/img/cal_bck_right2.png | Bin 0 -> 173 bytes jorge/img/cal_bck_top.gif | Bin 0 -> 52 bytes jorge/img/cal_corn_11.png | Bin 0 -> 290 bytes jorge/img/cal_corn_12.png | Bin 0 -> 298 bytes jorge/img/cal_corn_21.png | Bin 0 -> 341 bytes jorge/img/cal_corn_22.png | Bin 0 -> 355 bytes jorge/img/closed.png | Bin 0 -> 423 bytes jorge/img/indicator.gif | Bin 0 -> 1553 bytes jorge/img/jorge_logo.png | Bin 0 -> 11845 bytes jorge/img/list.png | Bin 0 -> 179 bytes jorge/img/loader.gif | Bin 0 -> 875 bytes jorge/img/logo_jabster2.png | Bin 0 -> 13517 bytes jorge/img/open.png | Bin 0 -> 510 bytes jorge/img/shadow.png | Bin 0 -> 4648 bytes jorge/img/spinner.gif | Bin 0 -> 2573 bytes jorge/index.php | 384 + jorge/install/jorge.sql | 182 + jorge/jquery.autocomplete.css | 48 + jorge/lang/core.php | 44 + jorge/lang/eng.php | 329 + jorge/lang/pol.php | 330 + jorge/lib/dimensions.js | 320 + jorge/lib/hl.js | 179 + jorge/lib/iecanvas.htc | 380 + jorge/lib/jquery.autocomplete.pack.js | 13 + jorge/lib/jquery.bgiframe.min.js | 10 + jorge/lib/jquery.flot.pack.js | 1 + jorge/lib/jquery.pack.js | 11 + jorge/lib/jquery.quicksearch.js | 317 + jorge/lib/jquery.tooltip.js | 310 + jorge/lib/recaptchalib.php | 277 + jorge/lib/simpletreemenu.js | 133 + jorge/logger.php | 242 + jorge/main.php | 536 ++ jorge/mod_logdb/erlang-mysql_userflag.diff | 180 + .../mod_logdb/patch-src-mod_logdb-2.0.3.diff | 6647 +++++++++++++++++ ...ch-src-mod_logdb-disable_ejabberd_ctl.diff | 65 + jorge/mod_logdb/readme.txt | 48 + jorge/my_links.php | 193 + jorge/not_enabled.php | 105 + jorge/search_v2.php | 593 ++ jorge/settings.php | 330 + jorge/simpletree.css | 28 + jorge/stats.php | 285 + jorge/style.css | 403 + jorge/tools/gen_stats.php | 55 + jorge/tools/optimize_tables.php | 63 + jorge/tools/trash_cleanup.php | 116 + jorge/trash.php | 167 + jorge/upper.php | 490 ++ mod_admin_extra/COPYING | 343 + mod_admin_extra/ChangeLog | 117 + mod_admin_extra/Emakefile | 2 + mod_admin_extra/README.txt | 86 + mod_admin_extra/build.bat | 1 + mod_admin_extra/build.sh | 2 + mod_admin_extra/src/mod_admin_extra.erl | 1578 ++++ mod_admin_extra/src/mod_ecomm_test.erl | 427 ++ mod_archive/Emakefile | 5 + mod_archive/LICENSE.txt | 1 + mod_archive/README.txt | 73 + mod_archive/build.bat | 1 + mod_archive/build.sh | 2 + mod_archive/src/mod_archive.erl | 1076 +++ mod_archive/src/mod_archive_odbc.erl | 2570 +++++++ mod_archive/src/mod_archive_odbc_mysql.sql | 83 + mod_archive/src/mod_archive_odbc_pgsql.sql | 72 + mod_archive/src/mod_archive_odbc_sqlite3.sql | 70 + mod_archive/src/mod_archive_sql.erl | 1379 ++++ mod_archive/src/mod_archive_webview.erl | 553 ++ mod_archive/src/msgs/mod_archive_webview.pot | 226 + .../src/msgs/pl.mod_archive_webview.po | 226 + mod_archive/src/pg_mod_archive.sql | 60 + mod_cron/COPYING | 343 + mod_cron/ChangeLog | 33 + mod_cron/Emakefile | 2 + mod_cron/README.txt | 76 + mod_cron/build.bat | 1 + mod_cron/build.sh | 2 + mod_cron/src/mod_cron.erl | 181 + mod_ctlextra/COPYING | 343 + mod_ctlextra/ChangeLog | 92 + mod_ctlextra/Emakefile | 2 + mod_ctlextra/README.txt | 74 + mod_ctlextra/build.bat | 1 + mod_ctlextra/build.sh | 2 + mod_ctlextra/src/mod_ctlextra.erl | 895 +++ mod_irc/Emakefile | 4 + mod_irc/LICENSE.txt | 343 + mod_irc/Makefile | 20 + mod_irc/README.txt | 121 + mod_irc/build.bat | 1 + mod_irc/build.sh | 2 + mod_irc/src/iconv.erl | 94 + mod_irc/src/iconv_erl.c | 155 + mod_irc/src/mod_irc.erl | 1016 +++ mod_irc/src/mod_irc_connection.erl | 1348 ++++ mod_log_chat/Emakefile | 3 + mod_log_chat/LICENSE.txt | 510 ++ mod_log_chat/Makefile | 14 + mod_log_chat/README.txt | 29 + mod_log_chat/TODO | 3 + mod_log_chat/build.bat | 1 + mod_log_chat/build.sh | 2 + mod_log_chat/conf/ejabberd.conf.sample | 28 + mod_log_chat/src/mod_log_chat.erl | 261 + mod_logsession/COPYING | 343 + mod_logsession/ChangeLog | 10 + mod_logsession/Emakefile | 2 + mod_logsession/README.txt | 81 + mod_logsession/build.bat | 1 + mod_logsession/build.sh | 2 + mod_logsession/src/failed_auth.patch | 44 + mod_logsession/src/mod_logsession.erl | 153 + mod_logxml/COPYING | 343 + mod_logxml/ChangeLog | 37 + mod_logxml/Emakefile | 2 + mod_logxml/README.txt | 95 + mod_logxml/build.bat | 1 + mod_logxml/build.sh | 2 + mod_logxml/src/mod_logxml.erl | 266 + mod_muc_admin/COPYING | 343 + mod_muc_admin/ChangeLog | 80 + mod_muc_admin/Emakefile | 2 + mod_muc_admin/README.txt | 46 + mod_muc_admin/build.bat | 1 + mod_muc_admin/build.sh | 2 + mod_muc_admin/src/mod_muc_admin.erl | 881 +++ mod_muc_log_http/COPYING | 343 + mod_muc_log_http/ChangeLog | 41 + mod_muc_log_http/Emakefile | 2 + mod_muc_log_http/README.txt | 56 + mod_muc_log_http/build.bat | 1 + mod_muc_log_http/build.sh | 2 + mod_muc_log_http/src/mod_muc_log_http.erl | 256 + mod_multicast/COPYING | 343 + mod_multicast/ChangeLog | 72 + mod_multicast/Emakefile | 6 + mod_multicast/README.txt | 99 + mod_multicast/build.bat | 1 + mod_multicast/build.sh | 2 + mod_multicast/src/ejabberd.app | 81 + mod_multicast/src/ejabberd_c2s.erl | 1863 +++++ .../src/ejabberd_router_multicast.erl | 210 + mod_multicast/src/ejabberd_sup.erl | 174 + mod_multicast/src/mod_muc_room.erl | 3005 ++++++++ mod_multicast/src/mod_multicast.erl | 1302 ++++ mod_openid/Emakefile | 1 + mod_openid/README | 34 + mod_openid/build.bat | 1 + mod_openid/build.sh | 2 + mod_openid/src/mod_openid.erl | 251 + mod_profile/Emakefile | 2 + mod_profile/README.txt | 27 + mod_profile/build.bat | 1 + mod_profile/build.sh | 2 + mod_profile/src/mod_profile.erl | 303 + mod_register_web/ChangeLog | 37 + mod_register_web/Emakefile | 2 + mod_register_web/README.txt | 77 + mod_register_web/build.bat | 1 + mod_register_web/build.sh | 2 + mod_register_web/src/mod_register_web.erl | 584 ++ .../src/msgs/es.mod_register_web.msg | 24 + .../src/msgs/es.mod_register_web.po | 126 + .../src/msgs/mod_register_web.pot | 114 + .../src/msgs/ru.mod_register_web.msg | 24 + .../src/msgs/ru.mod_register_web.po | 126 + .../src/msgs/uk.mod_register_web.msg | 24 + .../src/msgs/uk.mod_register_web.po | 119 + mod_rest/Emakefile | 1 + mod_rest/README.txt | 252 + mod_rest/build.bat | 1 + mod_rest/build.sh | 2 + mod_rest/src/mod_rest.erl | 159 + mod_s2s_log/ChangeLog | 11 + mod_s2s_log/Emakefile | 2 + mod_s2s_log/README.txt | 14 + mod_s2s_log/build.bat | 1 + mod_s2s_log/build.sh | 2 + mod_s2s_log/src/mod_s2s_log.erl | 112 + mod_shcommands/COPYING | 343 + mod_shcommands/ChangeLog | 13 + mod_shcommands/Emakefile | 2 + mod_shcommands/README.txt | 71 + mod_shcommands/build.bat | 1 + mod_shcommands/build.sh | 2 + mod_shcommands/src/mod_shcommands.erl | 156 + mod_statsdx/COPYING | 343 + mod_statsdx/ChangeLog | 60 + mod_statsdx/Emakefile | 3 + mod_statsdx/README.txt | 95 + mod_statsdx/build.bat | 1 + mod_statsdx/build.sh | 2 + mod_statsdx/src/mod_stats2file.erl | 409 + mod_statsdx/src/mod_statsdx.erl | 1699 +++++ mod_webpresence/AUTHORS | 46 + mod_webpresence/COPYING | 343 + mod_webpresence/ChangeLog | 169 + mod_webpresence/Emakefile | 2 + mod_webpresence/README.txt | 276 + mod_webpresence/TODO.txt | 25 + mod_webpresence/build.bat | 1 + mod_webpresence/build.sh | 2 + mod_webpresence/conf/ejabberd.cfg.example | 4 + .../data/pixmaps/3d-dot1/available.gif | Bin 0 -> 9215 bytes mod_webpresence/data/pixmaps/3d-dot1/away.gif | Bin 0 -> 9338 bytes mod_webpresence/data/pixmaps/3d-dot1/chat.gif | Bin 0 -> 9215 bytes mod_webpresence/data/pixmaps/3d-dot1/dnd.gif | Bin 0 -> 9382 bytes .../data/pixmaps/3d-dot1/unavailable.gif | Bin 0 -> 9251 bytes mod_webpresence/data/pixmaps/3d-dot1/xa.gif | Bin 0 -> 9366 bytes .../data/pixmaps/3d-dot2/available.gif | Bin 0 -> 3650 bytes mod_webpresence/data/pixmaps/3d-dot2/away.gif | Bin 0 -> 3091 bytes mod_webpresence/data/pixmaps/3d-dot2/chat.gif | Bin 0 -> 3091 bytes mod_webpresence/data/pixmaps/3d-dot2/dnd.gif | Bin 0 -> 3091 bytes .../data/pixmaps/3d-dot2/unavailable.gif | Bin 0 -> 3145 bytes mod_webpresence/data/pixmaps/3d-dot2/xa.gif | Bin 0 -> 3091 bytes .../data/pixmaps/alphamod/available.png | Bin 0 -> 550 bytes .../data/pixmaps/alphamod/away.png | Bin 0 -> 556 bytes .../data/pixmaps/alphamod/chat.png | Bin 0 -> 536 bytes mod_webpresence/data/pixmaps/alphamod/dnd.png | Bin 0 -> 483 bytes .../data/pixmaps/alphamod/unavailable.png | Bin 0 -> 545 bytes mod_webpresence/data/pixmaps/alphamod/xa.png | Bin 0 -> 531 bytes .../data/pixmaps/amibulb/available.png | Bin 0 -> 592 bytes mod_webpresence/data/pixmaps/amibulb/away.png | Bin 0 -> 603 bytes mod_webpresence/data/pixmaps/amibulb/chat.png | Bin 0 -> 560 bytes mod_webpresence/data/pixmaps/amibulb/dnd.png | Bin 0 -> 549 bytes .../data/pixmaps/amibulb/unavailable.png | Bin 0 -> 515 bytes mod_webpresence/data/pixmaps/amibulb/xa.png | Bin 0 -> 583 bytes .../data/pixmaps/amigadu/available.gif | Bin 0 -> 993 bytes mod_webpresence/data/pixmaps/amigadu/away.gif | Bin 0 -> 1012 bytes mod_webpresence/data/pixmaps/amigadu/chat.gif | Bin 0 -> 1001 bytes mod_webpresence/data/pixmaps/amigadu/dnd.gif | Bin 0 -> 997 bytes .../data/pixmaps/amigadu/unavailable.gif | Bin 0 -> 993 bytes mod_webpresence/data/pixmaps/amigadu/xa.gif | Bin 0 -> 1011 bytes .../data/pixmaps/amiglobe/available.png | Bin 0 -> 831 bytes .../data/pixmaps/amiglobe/away.png | Bin 0 -> 771 bytes .../data/pixmaps/amiglobe/chat.png | Bin 0 -> 752 bytes mod_webpresence/data/pixmaps/amiglobe/dnd.png | Bin 0 -> 752 bytes .../data/pixmaps/amiglobe/unavailable.png | Bin 0 -> 567 bytes mod_webpresence/data/pixmaps/amiglobe/xa.png | Bin 0 -> 725 bytes .../data/pixmaps/amiicq/available.gif | Bin 0 -> 1080 bytes mod_webpresence/data/pixmaps/amiicq/away.gif | Bin 0 -> 1071 bytes mod_webpresence/data/pixmaps/amiicq/chat.gif | Bin 0 -> 1063 bytes mod_webpresence/data/pixmaps/amiicq/dnd.gif | Bin 0 -> 1063 bytes .../data/pixmaps/amiicq/unavailable.gif | Bin 0 -> 1078 bytes mod_webpresence/data/pixmaps/amiicq/xa.gif | Bin 0 -> 1062 bytes .../data/pixmaps/amimsn/available.png | Bin 0 -> 808 bytes mod_webpresence/data/pixmaps/amimsn/away.png | Bin 0 -> 689 bytes mod_webpresence/data/pixmaps/amimsn/chat.png | Bin 0 -> 732 bytes mod_webpresence/data/pixmaps/amimsn/dnd.png | Bin 0 -> 719 bytes .../data/pixmaps/amimsn/unavailable.png | Bin 0 -> 737 bytes mod_webpresence/data/pixmaps/amimsn/xa.png | Bin 0 -> 730 bytes .../data/pixmaps/amistellar/available.gif | Bin 0 -> 612 bytes .../data/pixmaps/amistellar/away.gif | Bin 0 -> 678 bytes .../data/pixmaps/amistellar/chat.gif | Bin 0 -> 564 bytes .../data/pixmaps/amistellar/dnd.gif | Bin 0 -> 554 bytes .../data/pixmaps/amistellar/unavailable.gif | Bin 0 -> 527 bytes .../data/pixmaps/amistellar/xa.gif | Bin 0 -> 631 bytes .../data/pixmaps/amitlen/available.gif | Bin 0 -> 979 bytes mod_webpresence/data/pixmaps/amitlen/away.gif | Bin 0 -> 1011 bytes mod_webpresence/data/pixmaps/amitlen/chat.gif | Bin 0 -> 993 bytes mod_webpresence/data/pixmaps/amitlen/dnd.gif | Bin 0 -> 989 bytes .../data/pixmaps/amitlen/unavailable.gif | Bin 0 -> 979 bytes mod_webpresence/data/pixmaps/amitlen/xa.gif | Bin 0 -> 1008 bytes .../data/pixmaps/bulb/available.gif | Bin 0 -> 3304 bytes mod_webpresence/data/pixmaps/bulb/away.gif | Bin 0 -> 665 bytes mod_webpresence/data/pixmaps/bulb/chat.gif | Bin 0 -> 640 bytes mod_webpresence/data/pixmaps/bulb/dnd.gif | Bin 0 -> 3274 bytes .../data/pixmaps/bulb/unavailable.gif | Bin 0 -> 662 bytes mod_webpresence/data/pixmaps/bulb/xa.gif | Bin 0 -> 3323 bytes .../data/pixmaps/chrome-pl/available.gif | Bin 0 -> 428 bytes .../data/pixmaps/chrome-pl/away.gif | Bin 0 -> 397 bytes .../data/pixmaps/chrome-pl/chat.gif | Bin 0 -> 475 bytes .../data/pixmaps/chrome-pl/dnd.gif | Bin 0 -> 367 bytes .../data/pixmaps/chrome-pl/unavailable.gif | Bin 0 -> 420 bytes mod_webpresence/data/pixmaps/chrome-pl/xa.gif | Bin 0 -> 436 bytes .../data/pixmaps/crystal/available.png | Bin 0 -> 701 bytes mod_webpresence/data/pixmaps/crystal/away.png | Bin 0 -> 821 bytes mod_webpresence/data/pixmaps/crystal/chat.png | Bin 0 -> 810 bytes mod_webpresence/data/pixmaps/crystal/dnd.png | Bin 0 -> 771 bytes .../data/pixmaps/crystal/unavailable.png | Bin 0 -> 397 bytes mod_webpresence/data/pixmaps/crystal/xa.png | Bin 0 -> 805 bytes .../data/pixmaps/crystal2/available.gif | Bin 0 -> 589 bytes .../data/pixmaps/crystal2/away.gif | Bin 0 -> 838 bytes .../data/pixmaps/crystal2/chat.gif | Bin 0 -> 714 bytes mod_webpresence/data/pixmaps/crystal2/dnd.gif | Bin 0 -> 701 bytes .../data/pixmaps/crystal2/unavailable.gif | Bin 0 -> 550 bytes mod_webpresence/data/pixmaps/crystal2/xa.gif | Bin 0 -> 738 bytes .../data/pixmaps/crystal3/available.gif | Bin 0 -> 877 bytes .../data/pixmaps/crystal3/away.gif | Bin 0 -> 897 bytes .../data/pixmaps/crystal3/chat.gif | Bin 0 -> 810 bytes mod_webpresence/data/pixmaps/crystal3/dnd.gif | Bin 0 -> 766 bytes .../data/pixmaps/crystal3/unavailable.gif | Bin 0 -> 636 bytes mod_webpresence/data/pixmaps/crystal3/xa.gif | Bin 0 -> 783 bytes .../data/pixmaps/dcraven/available.png | Bin 0 -> 772 bytes mod_webpresence/data/pixmaps/dcraven/away.png | Bin 0 -> 757 bytes mod_webpresence/data/pixmaps/dcraven/chat.png | Bin 0 -> 896 bytes mod_webpresence/data/pixmaps/dcraven/dnd.png | Bin 0 -> 933 bytes .../data/pixmaps/dcraven/unavailable.png | Bin 0 -> 772 bytes mod_webpresence/data/pixmaps/dcraven/xa.png | Bin 0 -> 764 bytes .../data/pixmaps/dudes/available.gif | Bin 0 -> 201 bytes mod_webpresence/data/pixmaps/dudes/away.gif | Bin 0 -> 113 bytes mod_webpresence/data/pixmaps/dudes/chat.gif | Bin 0 -> 143 bytes mod_webpresence/data/pixmaps/dudes/dnd.gif | Bin 0 -> 145 bytes .../data/pixmaps/dudes/unavailable.gif | Bin 0 -> 95 bytes mod_webpresence/data/pixmaps/dudes/xa.gif | Bin 0 -> 111 bytes .../data/pixmaps/frickenhuge/available.gif | Bin 0 -> 1988 bytes .../data/pixmaps/frickenhuge/away.gif | Bin 0 -> 2013 bytes .../data/pixmaps/frickenhuge/chat.gif | Bin 0 -> 1988 bytes .../data/pixmaps/frickenhuge/dnd.gif | Bin 0 -> 2014 bytes .../data/pixmaps/frickenhuge/unavailable.gif | Bin 0 -> 1832 bytes .../data/pixmaps/frickenhuge/xa.gif | Bin 0 -> 1999 bytes .../data/pixmaps/gabber/available.gif | Bin 0 -> 325 bytes mod_webpresence/data/pixmaps/gabber/away.gif | Bin 0 -> 325 bytes mod_webpresence/data/pixmaps/gabber/chat.gif | Bin 0 -> 248 bytes mod_webpresence/data/pixmaps/gabber/dnd.gif | Bin 0 -> 237 bytes .../data/pixmaps/gabber/unavailable.gif | Bin 0 -> 220 bytes mod_webpresence/data/pixmaps/gabber/xa.gif | Bin 0 -> 249 bytes mod_webpresence/data/pixmaps/gg/available.gif | Bin 0 -> 389 bytes mod_webpresence/data/pixmaps/gg/away.gif | Bin 0 -> 517 bytes mod_webpresence/data/pixmaps/gg/chat.gif | Bin 0 -> 389 bytes mod_webpresence/data/pixmaps/gg/dnd.gif | Bin 0 -> 517 bytes .../data/pixmaps/gg/unavailable.gif | Bin 0 -> 445 bytes mod_webpresence/data/pixmaps/gg/xa.gif | Bin 0 -> 517 bytes .../data/pixmaps/gnome/available.png | Bin 0 -> 594 bytes mod_webpresence/data/pixmaps/gnome/away.png | Bin 0 -> 543 bytes mod_webpresence/data/pixmaps/gnome/chat.png | Bin 0 -> 556 bytes mod_webpresence/data/pixmaps/gnome/dnd.png | Bin 0 -> 462 bytes .../data/pixmaps/gnome/unavailable.png | Bin 0 -> 507 bytes mod_webpresence/data/pixmaps/gnome/xa.png | Bin 0 -> 630 bytes .../data/pixmaps/gossip/available.png | Bin 0 -> 739 bytes mod_webpresence/data/pixmaps/gossip/away.png | Bin 0 -> 606 bytes mod_webpresence/data/pixmaps/gossip/chat.png | Bin 0 -> 652 bytes mod_webpresence/data/pixmaps/gossip/dnd.png | Bin 0 -> 888 bytes .../data/pixmaps/gossip/unavailable.png | Bin 0 -> 524 bytes mod_webpresence/data/pixmaps/gossip/xa.png | Bin 0 -> 767 bytes .../data/pixmaps/gota/available.png | Bin 0 -> 695 bytes mod_webpresence/data/pixmaps/gota/away.png | Bin 0 -> 693 bytes mod_webpresence/data/pixmaps/gota/chat.png | Bin 0 -> 878 bytes mod_webpresence/data/pixmaps/gota/dnd.png | Bin 0 -> 626 bytes .../data/pixmaps/gota/unavailable.png | Bin 0 -> 504 bytes mod_webpresence/data/pixmaps/gota/xa.png | Bin 0 -> 598 bytes .../data/pixmaps/gtalk/available.png | Bin 0 -> 1007 bytes mod_webpresence/data/pixmaps/gtalk/away.png | Bin 0 -> 1047 bytes mod_webpresence/data/pixmaps/gtalk/chat.png | Bin 0 -> 407 bytes mod_webpresence/data/pixmaps/gtalk/dnd.png | Bin 0 -> 1000 bytes .../data/pixmaps/gtalk/unavailable.png | Bin 0 -> 1016 bytes mod_webpresence/data/pixmaps/gtalk/xa.png | Bin 0 -> 406 bytes .../data/pixmaps/icq/available.gif | Bin 0 -> 209 bytes mod_webpresence/data/pixmaps/icq/away.gif | Bin 0 -> 209 bytes mod_webpresence/data/pixmaps/icq/chat.gif | Bin 0 -> 209 bytes mod_webpresence/data/pixmaps/icq/dnd.gif | Bin 0 -> 209 bytes .../data/pixmaps/icq/unavailable.gif | Bin 0 -> 209 bytes mod_webpresence/data/pixmaps/icq/xa.gif | Bin 0 -> 209 bytes .../data/pixmaps/invision/available.gif | Bin 0 -> 1928 bytes .../data/pixmaps/invision/away.gif | Bin 0 -> 1946 bytes .../data/pixmaps/invision/chat.gif | Bin 0 -> 1928 bytes mod_webpresence/data/pixmaps/invision/dnd.gif | Bin 0 -> 1957 bytes .../data/pixmaps/invision/unavailable.gif | Bin 0 -> 1962 bytes mod_webpresence/data/pixmaps/invision/xa.gif | Bin 0 -> 1976 bytes .../data/pixmaps/jabberbulb/available.png | Bin 0 -> 709 bytes .../data/pixmaps/jabberbulb/away.png | Bin 0 -> 972 bytes .../data/pixmaps/jabberbulb/chat.png | Bin 0 -> 926 bytes .../data/pixmaps/jabberbulb/dnd.png | Bin 0 -> 867 bytes .../data/pixmaps/jabberbulb/unavailable.png | Bin 0 -> 660 bytes .../data/pixmaps/jabberbulb/xa.png | Bin 0 -> 895 bytes .../pixmaps/jsf-jabber-text/available.png | Bin 0 -> 493 bytes .../data/pixmaps/jsf-jabber-text/away.png | Bin 0 -> 529 bytes .../data/pixmaps/jsf-jabber-text/chat.png | Bin 0 -> 525 bytes .../data/pixmaps/jsf-jabber-text/dnd.png | Bin 0 -> 519 bytes .../pixmaps/jsf-jabber-text/unavailable.png | Bin 0 -> 527 bytes .../data/pixmaps/jsf-jabber-text/xa.png | Bin 0 -> 519 bytes .../data/pixmaps/jsf-text/available.png | Bin 0 -> 1114 bytes .../data/pixmaps/jsf-text/away.png | Bin 0 -> 858 bytes .../data/pixmaps/jsf-text/chat.png | Bin 0 -> 1036 bytes mod_webpresence/data/pixmaps/jsf-text/dnd.png | Bin 0 -> 935 bytes .../data/pixmaps/jsf-text/unavailable.png | Bin 0 -> 1027 bytes mod_webpresence/data/pixmaps/jsf-text/xa.png | Bin 0 -> 1014 bytes .../data/pixmaps/jsf-text2/available.png | Bin 0 -> 1066 bytes .../data/pixmaps/jsf-text2/away.png | Bin 0 -> 978 bytes .../data/pixmaps/jsf-text2/chat.png | Bin 0 -> 936 bytes .../data/pixmaps/jsf-text2/dnd.png | Bin 0 -> 1120 bytes .../data/pixmaps/jsf-text2/unavailable.png | Bin 0 -> 865 bytes mod_webpresence/data/pixmaps/jsf-text2/xa.png | Bin 0 -> 893 bytes .../data/pixmaps/koniczynek/available.gif | Bin 0 -> 539 bytes .../data/pixmaps/koniczynek/away.gif | Bin 0 -> 532 bytes .../data/pixmaps/koniczynek/chat.gif | Bin 0 -> 539 bytes .../data/pixmaps/koniczynek/dnd.gif | Bin 0 -> 501 bytes .../data/pixmaps/koniczynek/unavailable.gif | Bin 0 -> 505 bytes .../data/pixmaps/koniczynek/xa.gif | Bin 0 -> 516 bytes .../data/pixmaps/licq/available.gif | Bin 0 -> 1060 bytes mod_webpresence/data/pixmaps/licq/away.gif | Bin 0 -> 1060 bytes mod_webpresence/data/pixmaps/licq/chat.gif | Bin 0 -> 331 bytes mod_webpresence/data/pixmaps/licq/dnd.gif | Bin 0 -> 1060 bytes .../data/pixmaps/licq/unavailable.gif | Bin 0 -> 647 bytes mod_webpresence/data/pixmaps/licq/xa.gif | Bin 0 -> 1060 bytes .../data/pixmaps/nahuatl/available.gif | Bin 0 -> 356 bytes mod_webpresence/data/pixmaps/nahuatl/away.gif | Bin 0 -> 449 bytes mod_webpresence/data/pixmaps/nahuatl/chat.gif | Bin 0 -> 455 bytes mod_webpresence/data/pixmaps/nahuatl/dnd.gif | Bin 0 -> 399 bytes .../data/pixmaps/nahuatl/unavailable.gif | Bin 0 -> 346 bytes mod_webpresence/data/pixmaps/nahuatl/xa.gif | Bin 0 -> 396 bytes .../data/pixmaps/nuvola/available.png | Bin 0 -> 766 bytes mod_webpresence/data/pixmaps/nuvola/away.png | Bin 0 -> 741 bytes mod_webpresence/data/pixmaps/nuvola/chat.png | Bin 0 -> 702 bytes mod_webpresence/data/pixmaps/nuvola/dnd.png | Bin 0 -> 677 bytes .../data/pixmaps/nuvola/unavailable.png | Bin 0 -> 719 bytes mod_webpresence/data/pixmaps/nuvola/xa.png | Bin 0 -> 751 bytes .../data/pixmaps/phpbb/available.gif | Bin 0 -> 1525 bytes mod_webpresence/data/pixmaps/phpbb/away.gif | Bin 0 -> 1450 bytes mod_webpresence/data/pixmaps/phpbb/chat.gif | Bin 0 -> 1576 bytes mod_webpresence/data/pixmaps/phpbb/dnd.gif | Bin 0 -> 1555 bytes .../data/pixmaps/phpbb/unavailable.gif | Bin 0 -> 1525 bytes mod_webpresence/data/pixmaps/phpbb/xa.gif | Bin 0 -> 1525 bytes .../data/pixmaps/pogoda/available.gif | Bin 0 -> 907 bytes mod_webpresence/data/pixmaps/pogoda/away.gif | Bin 0 -> 754 bytes mod_webpresence/data/pixmaps/pogoda/chat.gif | Bin 0 -> 833 bytes mod_webpresence/data/pixmaps/pogoda/dnd.gif | Bin 0 -> 723 bytes .../data/pixmaps/pogoda/unavailable.gif | Bin 0 -> 994 bytes mod_webpresence/data/pixmaps/pogoda/xa.gif | Bin 0 -> 890 bytes .../data/pixmaps/psi/available.gif | Bin 0 -> 210 bytes mod_webpresence/data/pixmaps/psi/away.gif | Bin 0 -> 345 bytes mod_webpresence/data/pixmaps/psi/chat.gif | Bin 0 -> 276 bytes mod_webpresence/data/pixmaps/psi/dnd.gif | Bin 0 -> 344 bytes .../data/pixmaps/psi/unavailable.gif | Bin 0 -> 245 bytes mod_webpresence/data/pixmaps/psi/xa.gif | Bin 0 -> 322 bytes .../data/pixmaps/simplebulb/available.png | Bin 0 -> 791 bytes .../data/pixmaps/simplebulb/away.png | Bin 0 -> 782 bytes .../data/pixmaps/simplebulb/chat.png | Bin 0 -> 832 bytes .../data/pixmaps/simplebulb/dnd.png | Bin 0 -> 824 bytes .../data/pixmaps/simplebulb/unavailable.png | Bin 0 -> 766 bytes .../data/pixmaps/simplebulb/xa.png | Bin 0 -> 811 bytes .../data/pixmaps/stellar/available.png | Bin 0 -> 229 bytes mod_webpresence/data/pixmaps/stellar/away.png | Bin 0 -> 243 bytes mod_webpresence/data/pixmaps/stellar/chat.png | Bin 0 -> 248 bytes mod_webpresence/data/pixmaps/stellar/dnd.png | Bin 0 -> 250 bytes .../data/pixmaps/stellar/unavailable.png | Bin 0 -> 204 bytes mod_webpresence/data/pixmaps/stellar/xa.png | Bin 0 -> 202 bytes .../data/pixmaps/stellar2/available.gif | Bin 0 -> 849 bytes .../data/pixmaps/stellar2/away.gif | Bin 0 -> 861 bytes .../data/pixmaps/stellar2/chat.gif | Bin 0 -> 988 bytes mod_webpresence/data/pixmaps/stellar2/dnd.gif | Bin 0 -> 899 bytes .../data/pixmaps/stellar2/unavailable.gif | Bin 0 -> 690 bytes mod_webpresence/data/pixmaps/stellar2/xa.gif | Bin 0 -> 857 bytes .../data/pixmaps/sun/available.png | Bin 0 -> 458 bytes mod_webpresence/data/pixmaps/sun/away.png | Bin 0 -> 724 bytes mod_webpresence/data/pixmaps/sun/chat.png | Bin 0 -> 454 bytes mod_webpresence/data/pixmaps/sun/dnd.png | Bin 0 -> 381 bytes .../data/pixmaps/sun/unavailable.png | Bin 0 -> 417 bytes mod_webpresence/data/pixmaps/sun/xa.png | Bin 0 -> 371 bytes .../data/pixmaps/tux/available.gif | Bin 0 -> 1504 bytes mod_webpresence/data/pixmaps/tux/away.gif | Bin 0 -> 1615 bytes mod_webpresence/data/pixmaps/tux/chat.gif | Bin 0 -> 1504 bytes mod_webpresence/data/pixmaps/tux/dnd.gif | Bin 0 -> 1604 bytes .../data/pixmaps/tux/unavailable.gif | Bin 0 -> 1538 bytes mod_webpresence/data/pixmaps/tux/xa.gif | Bin 0 -> 1615 bytes .../data/pixmaps/wpkontakt/available.gif | Bin 0 -> 813 bytes .../data/pixmaps/wpkontakt/away.gif | Bin 0 -> 832 bytes .../data/pixmaps/wpkontakt/chat.gif | Bin 0 -> 712 bytes .../data/pixmaps/wpkontakt/dnd.gif | Bin 0 -> 819 bytes .../data/pixmaps/wpkontakt/unavailable.gif | Bin 0 -> 756 bytes mod_webpresence/data/pixmaps/wpkontakt/xa.gif | Bin 0 -> 797 bytes .../data/pixmaps/yipee/available.gif | Bin 0 -> 29271 bytes mod_webpresence/data/pixmaps/yipee/away.gif | Bin 0 -> 29514 bytes mod_webpresence/data/pixmaps/yipee/chat.gif | Bin 0 -> 30437 bytes mod_webpresence/data/pixmaps/yipee/dnd.gif | Bin 0 -> 29783 bytes .../data/pixmaps/yipee/unavailable.gif | Bin 0 -> 29045 bytes mod_webpresence/data/pixmaps/yipee/xa.gif | Bin 0 -> 29216 bytes .../data/pixmaps/zyx/available.png | Bin 0 -> 833 bytes mod_webpresence/data/pixmaps/zyx/away.png | Bin 0 -> 932 bytes mod_webpresence/data/pixmaps/zyx/chat.png | Bin 0 -> 907 bytes mod_webpresence/data/pixmaps/zyx/dnd.png | Bin 0 -> 841 bytes .../data/pixmaps/zyx/unavailable.png | Bin 0 -> 841 bytes mod_webpresence/data/pixmaps/zyx/xa.png | Bin 0 -> 949 bytes mod_webpresence/src/mod_webpresence.erl | 1075 +++ .../src/msgs/es.mod_webpresence.msg | 30 + .../src/msgs/es.mod_webpresence.po | 146 + mod_webpresence/src/msgs/mod_webpresence.pot | 137 + .../src/msgs/wa.mod_webpresence.msg | 30 + .../src/msgs/wa.mod_webpresence.po | 147 + mod_xmlrpc/COPYING | 343 + mod_xmlrpc/ChangeLog | 98 + mod_xmlrpc/Emakefile | 2 + mod_xmlrpc/README.txt | 296 + mod_xmlrpc/build.bat | 1 + mod_xmlrpc/build.sh | 2 + mod_xmlrpc/src/mod_xmlrpc.erl | 488 ++ .../OpenfireExporter/changelog.html | 192 + .../classes/wildfire-user-schema.xsd | 79 + .../OpenfireExporter/classes/xep-0227.xsd | 47 + .../OpenfireExporter/lib/openfire.jar | Bin 0 -> 6416495 bytes .../OpenfireExporter/lib/readme-lib.txt | 18 + .../OpenfireExporter/logo_large.gif | Bin 0 -> 977 bytes .../OpenfireExporter/logo_small.gif | Bin 0 -> 600 bytes openfire-export/OpenfireExporter/plugin.xml | 22 + openfire-export/OpenfireExporter/readme.html | 262 + .../plugin/openfire/ExporterXEP227.java | 477 ++ .../plugin/openfire/UserSchemaValidator.java | 84 + .../jivesoftware/openfire/PrivateStorage.java | 326 + .../OpenfireExporter/src/web/export-file.jsp | 17 + .../src/web/export-user-data.jsp | 137 + .../src/web/images/error-16x16.gif | Bin 0 -> 1050 bytes .../src/web/images/info-16x16.gif | Bin 0 -> 594 bytes .../src/web/images/success-16x16.gif | Bin 0 -> 1016 bytes .../src/web/import-export-selection.jsp | 42 + .../src/web/import-user-data.jsp | 168 + 615 files changed, 68838 insertions(+) create mode 100644 README.txt create mode 100644 atom_pubsub/Emakefile create mode 100644 atom_pubsub/README create mode 100644 atom_pubsub/build.bat create mode 100755 atom_pubsub/build.sh create mode 100644 atom_pubsub/src/atom_microblog.erl create mode 100644 atom_pubsub/src/atom_pubsub.erl create mode 100644 atom_pubsub/src/mod_couch.erl create mode 100644 bfile/LICENCE create mode 100644 bfile/Makefile create mode 100644 bfile/README create mode 100644 bfile/c_src/FILE_drv.c create mode 100644 bfile/c_src/Makefile create mode 100644 bfile/config/Makefile create mode 100644 bfile/config/acconfig.h create mode 100644 bfile/config/aclocal.m4 create mode 100755 bfile/config/config.guess create mode 100644 bfile/config/config.h.in create mode 100755 bfile/config/config.sub create mode 100644 bfile/config/configure.in create mode 100644 bfile/config/include.mk.in create mode 100755 bfile/config/install-sh create mode 100644 bfile/src/Makefile create mode 100644 bfile/src/bfile.app.src create mode 100644 bfile/src/bfile.erl create mode 100644 bfile/tests/read.erl create mode 100644 bfile/tests/readold.erl create mode 100644 bfile/tests/write.erl create mode 100644 bfile/tests/writeold.erl create mode 100644 bfile/vsn.mk create mode 100644 dns/src/dns.erl create mode 100644 ejabberd-dev/README.txt create mode 100644 ejabberd-dev/include/adhoc.hrl create mode 100644 ejabberd-dev/include/ejabberd.hrl create mode 100644 ejabberd-dev/include/ejabberd_commands.hrl create mode 100644 ejabberd-dev/include/ejabberd_config.hrl create mode 100644 ejabberd-dev/include/ejabberd_ctl.hrl create mode 100644 ejabberd-dev/include/jlib.hrl create mode 100644 ejabberd-dev/include/mod_muc/mod_muc_room.hrl create mode 100644 ejabberd-dev/include/mod_pubsub/pubsub.hrl create mode 100644 ejabberd-dev/include/mod_roster.hrl create mode 100644 ejabberd-dev/include/web/ejabberd_http.hrl create mode 100644 ejabberd-dev/include/web/ejabberd_web_admin.hrl create mode 100644 ejabberd-dev/src/gen_mod.erl create mode 100644 ejabberdPrefs/English.lproj/InfoPlist.strings create mode 100644 ejabberdPrefs/English.lproj/ejabberdPref.nib/classes.nib create mode 100644 ejabberdPrefs/English.lproj/ejabberdPref.nib/info.nib create mode 100644 ejabberdPrefs/English.lproj/ejabberdPref.nib/keyedobjects.nib create mode 100644 ejabberdPrefs/Info.plist create mode 100644 ejabberdPrefs/LICENSE create mode 100644 ejabberdPrefs/MPL create mode 100644 ejabberdPrefs/config.txt create mode 100644 ejabberdPrefs/ejabberd.plist create mode 100644 ejabberdPrefs/ejabberd.xcodeproj/project.pbxproj create mode 100644 ejabberdPrefs/ejabberdController.h create mode 100644 ejabberdPrefs/ejabberdController.m create mode 100644 ejabberdPrefs/ejabberdPref.h create mode 100644 ejabberdPrefs/ejabberdPref.m create mode 100644 ejabberdPrefs/ejabberdPref.tiff create mode 100644 ejabberdPrefs/ejabberd_Prefix.pch create mode 100644 ejabberdPrefs/instance_started.png create mode 100644 ejabberdPrefs/instance_stopped.png create mode 100644 ejabberdPrefs/logo_ejabberd.png create mode 100644 ejabberdPrefs/version.plist create mode 100644 ejabberd_xmlrpc/COPYING create mode 100644 ejabberd_xmlrpc/ChangeLog create mode 100644 ejabberd_xmlrpc/Emakefile create mode 100644 ejabberd_xmlrpc/README.txt create mode 100644 ejabberd_xmlrpc/build.bat create mode 100755 ejabberd_xmlrpc/build.sh create mode 100644 ejabberd_xmlrpc/src/ejabberd_xmlrpc.erl create mode 100755 extract-mod-translations.sh create mode 100644 ircd/Emakefile create mode 100644 ircd/README.txt create mode 100644 ircd/build.bat create mode 100755 ircd/build.sh create mode 100644 ircd/src/ejabberd_ircd.erl create mode 100644 jorge/COPYING create mode 100644 jorge/README create mode 100644 jorge/calendar_view.php create mode 100644 jorge/chat_map.php create mode 100644 jorge/class.db.php create mode 100644 jorge/class.ejabberd_xmlrpc.php create mode 100644 jorge/class.helper.php create mode 100644 jorge/class.roster.php create mode 100644 jorge/class.sessions.php create mode 100644 jorge/config.php.inc create mode 100644 jorge/contacts.php create mode 100644 jorge/export.php create mode 100644 jorge/favicon.ico create mode 100644 jorge/favorites.php create mode 100644 jorge/footer.php create mode 100644 jorge/func.php create mode 100644 jorge/headers.php create mode 100644 jorge/help.php create mode 100644 jorge/img/apple-logo.png create mode 100644 jorge/img/bak2a.png create mode 100644 jorge/img/bak2b.png create mode 100644 jorge/img/bar_bg.png create mode 100644 jorge/img/bar_new.png create mode 100644 jorge/img/bell-bak.png create mode 100644 jorge/img/bell-down.png create mode 100644 jorge/img/cal_bck.png create mode 100644 jorge/img/cal_bck2.png create mode 100644 jorge/img/cal_bck_bot.png create mode 100644 jorge/img/cal_bck_left.png create mode 100644 jorge/img/cal_bck_left2.png create mode 100644 jorge/img/cal_bck_right.png create mode 100644 jorge/img/cal_bck_right2.png create mode 100644 jorge/img/cal_bck_top.gif create mode 100644 jorge/img/cal_corn_11.png create mode 100644 jorge/img/cal_corn_12.png create mode 100644 jorge/img/cal_corn_21.png create mode 100644 jorge/img/cal_corn_22.png create mode 100644 jorge/img/closed.png create mode 100644 jorge/img/indicator.gif create mode 100644 jorge/img/jorge_logo.png create mode 100644 jorge/img/list.png create mode 100644 jorge/img/loader.gif create mode 100644 jorge/img/logo_jabster2.png create mode 100644 jorge/img/open.png create mode 100644 jorge/img/shadow.png create mode 100644 jorge/img/spinner.gif create mode 100644 jorge/index.php create mode 100644 jorge/install/jorge.sql create mode 100644 jorge/jquery.autocomplete.css create mode 100644 jorge/lang/core.php create mode 100644 jorge/lang/eng.php create mode 100644 jorge/lang/pol.php create mode 100644 jorge/lib/dimensions.js create mode 100644 jorge/lib/hl.js create mode 100644 jorge/lib/iecanvas.htc create mode 100644 jorge/lib/jquery.autocomplete.pack.js create mode 100644 jorge/lib/jquery.bgiframe.min.js create mode 100644 jorge/lib/jquery.flot.pack.js create mode 100644 jorge/lib/jquery.pack.js create mode 100644 jorge/lib/jquery.quicksearch.js create mode 100644 jorge/lib/jquery.tooltip.js create mode 100644 jorge/lib/recaptchalib.php create mode 100644 jorge/lib/simpletreemenu.js create mode 100644 jorge/logger.php create mode 100644 jorge/main.php create mode 100644 jorge/mod_logdb/erlang-mysql_userflag.diff create mode 100644 jorge/mod_logdb/patch-src-mod_logdb-2.0.3.diff create mode 100644 jorge/mod_logdb/patch-src-mod_logdb-disable_ejabberd_ctl.diff create mode 100644 jorge/mod_logdb/readme.txt create mode 100644 jorge/my_links.php create mode 100644 jorge/not_enabled.php create mode 100644 jorge/search_v2.php create mode 100644 jorge/settings.php create mode 100644 jorge/simpletree.css create mode 100644 jorge/stats.php create mode 100644 jorge/style.css create mode 100644 jorge/tools/gen_stats.php create mode 100644 jorge/tools/optimize_tables.php create mode 100644 jorge/tools/trash_cleanup.php create mode 100644 jorge/trash.php create mode 100644 jorge/upper.php create mode 100644 mod_admin_extra/COPYING create mode 100644 mod_admin_extra/ChangeLog create mode 100644 mod_admin_extra/Emakefile create mode 100644 mod_admin_extra/README.txt create mode 100644 mod_admin_extra/build.bat create mode 100755 mod_admin_extra/build.sh create mode 100644 mod_admin_extra/src/mod_admin_extra.erl create mode 100644 mod_admin_extra/src/mod_ecomm_test.erl create mode 100644 mod_archive/Emakefile create mode 100644 mod_archive/LICENSE.txt create mode 100644 mod_archive/README.txt create mode 100644 mod_archive/build.bat create mode 100755 mod_archive/build.sh create mode 100644 mod_archive/src/mod_archive.erl create mode 100644 mod_archive/src/mod_archive_odbc.erl create mode 100644 mod_archive/src/mod_archive_odbc_mysql.sql create mode 100644 mod_archive/src/mod_archive_odbc_pgsql.sql create mode 100644 mod_archive/src/mod_archive_odbc_sqlite3.sql create mode 100644 mod_archive/src/mod_archive_sql.erl create mode 100644 mod_archive/src/mod_archive_webview.erl create mode 100644 mod_archive/src/msgs/mod_archive_webview.pot create mode 100644 mod_archive/src/msgs/pl.mod_archive_webview.po create mode 100644 mod_archive/src/pg_mod_archive.sql create mode 100644 mod_cron/COPYING create mode 100644 mod_cron/ChangeLog create mode 100644 mod_cron/Emakefile create mode 100644 mod_cron/README.txt create mode 100644 mod_cron/build.bat create mode 100755 mod_cron/build.sh create mode 100644 mod_cron/src/mod_cron.erl create mode 100644 mod_ctlextra/COPYING create mode 100644 mod_ctlextra/ChangeLog create mode 100644 mod_ctlextra/Emakefile create mode 100644 mod_ctlextra/README.txt create mode 100644 mod_ctlextra/build.bat create mode 100755 mod_ctlextra/build.sh create mode 100644 mod_ctlextra/src/mod_ctlextra.erl create mode 100644 mod_irc/Emakefile create mode 100644 mod_irc/LICENSE.txt create mode 100644 mod_irc/Makefile create mode 100644 mod_irc/README.txt create mode 100644 mod_irc/build.bat create mode 100755 mod_irc/build.sh create mode 100644 mod_irc/src/iconv.erl create mode 100644 mod_irc/src/iconv_erl.c create mode 100644 mod_irc/src/mod_irc.erl create mode 100644 mod_irc/src/mod_irc_connection.erl create mode 100644 mod_log_chat/Emakefile create mode 100644 mod_log_chat/LICENSE.txt create mode 100644 mod_log_chat/Makefile create mode 100644 mod_log_chat/README.txt create mode 100644 mod_log_chat/TODO create mode 100644 mod_log_chat/build.bat create mode 100755 mod_log_chat/build.sh create mode 100644 mod_log_chat/conf/ejabberd.conf.sample create mode 100644 mod_log_chat/src/mod_log_chat.erl create mode 100644 mod_logsession/COPYING create mode 100644 mod_logsession/ChangeLog create mode 100644 mod_logsession/Emakefile create mode 100644 mod_logsession/README.txt create mode 100644 mod_logsession/build.bat create mode 100755 mod_logsession/build.sh create mode 100644 mod_logsession/src/failed_auth.patch create mode 100644 mod_logsession/src/mod_logsession.erl create mode 100644 mod_logxml/COPYING create mode 100644 mod_logxml/ChangeLog create mode 100644 mod_logxml/Emakefile create mode 100644 mod_logxml/README.txt create mode 100644 mod_logxml/build.bat create mode 100755 mod_logxml/build.sh create mode 100644 mod_logxml/src/mod_logxml.erl create mode 100644 mod_muc_admin/COPYING create mode 100644 mod_muc_admin/ChangeLog create mode 100644 mod_muc_admin/Emakefile create mode 100644 mod_muc_admin/README.txt create mode 100644 mod_muc_admin/build.bat create mode 100755 mod_muc_admin/build.sh create mode 100644 mod_muc_admin/src/mod_muc_admin.erl create mode 100644 mod_muc_log_http/COPYING create mode 100644 mod_muc_log_http/ChangeLog create mode 100644 mod_muc_log_http/Emakefile create mode 100644 mod_muc_log_http/README.txt create mode 100644 mod_muc_log_http/build.bat create mode 100755 mod_muc_log_http/build.sh create mode 100644 mod_muc_log_http/src/mod_muc_log_http.erl create mode 100644 mod_multicast/COPYING create mode 100644 mod_multicast/ChangeLog create mode 100644 mod_multicast/Emakefile create mode 100644 mod_multicast/README.txt create mode 100644 mod_multicast/build.bat create mode 100755 mod_multicast/build.sh create mode 100644 mod_multicast/src/ejabberd.app create mode 100644 mod_multicast/src/ejabberd_c2s.erl create mode 100644 mod_multicast/src/ejabberd_router_multicast.erl create mode 100644 mod_multicast/src/ejabberd_sup.erl create mode 100644 mod_multicast/src/mod_muc_room.erl create mode 100644 mod_multicast/src/mod_multicast.erl create mode 100644 mod_openid/Emakefile create mode 100644 mod_openid/README create mode 100644 mod_openid/build.bat create mode 100755 mod_openid/build.sh create mode 100644 mod_openid/src/mod_openid.erl create mode 100644 mod_profile/Emakefile create mode 100644 mod_profile/README.txt create mode 100644 mod_profile/build.bat create mode 100755 mod_profile/build.sh create mode 100644 mod_profile/src/mod_profile.erl create mode 100644 mod_register_web/ChangeLog create mode 100644 mod_register_web/Emakefile create mode 100644 mod_register_web/README.txt create mode 100644 mod_register_web/build.bat create mode 100755 mod_register_web/build.sh create mode 100644 mod_register_web/src/mod_register_web.erl create mode 100644 mod_register_web/src/msgs/es.mod_register_web.msg create mode 100644 mod_register_web/src/msgs/es.mod_register_web.po create mode 100644 mod_register_web/src/msgs/mod_register_web.pot create mode 100644 mod_register_web/src/msgs/ru.mod_register_web.msg create mode 100644 mod_register_web/src/msgs/ru.mod_register_web.po create mode 100644 mod_register_web/src/msgs/uk.mod_register_web.msg create mode 100644 mod_register_web/src/msgs/uk.mod_register_web.po create mode 100644 mod_rest/Emakefile create mode 100644 mod_rest/README.txt create mode 100644 mod_rest/build.bat create mode 100755 mod_rest/build.sh create mode 100644 mod_rest/src/mod_rest.erl create mode 100644 mod_s2s_log/ChangeLog create mode 100644 mod_s2s_log/Emakefile create mode 100644 mod_s2s_log/README.txt create mode 100644 mod_s2s_log/build.bat create mode 100755 mod_s2s_log/build.sh create mode 100644 mod_s2s_log/src/mod_s2s_log.erl create mode 100644 mod_shcommands/COPYING create mode 100644 mod_shcommands/ChangeLog create mode 100644 mod_shcommands/Emakefile create mode 100644 mod_shcommands/README.txt create mode 100644 mod_shcommands/build.bat create mode 100755 mod_shcommands/build.sh create mode 100644 mod_shcommands/src/mod_shcommands.erl create mode 100644 mod_statsdx/COPYING create mode 100644 mod_statsdx/ChangeLog create mode 100644 mod_statsdx/Emakefile create mode 100644 mod_statsdx/README.txt create mode 100644 mod_statsdx/build.bat create mode 100755 mod_statsdx/build.sh create mode 100644 mod_statsdx/src/mod_stats2file.erl create mode 100644 mod_statsdx/src/mod_statsdx.erl create mode 100644 mod_webpresence/AUTHORS create mode 100644 mod_webpresence/COPYING create mode 100644 mod_webpresence/ChangeLog create mode 100644 mod_webpresence/Emakefile create mode 100644 mod_webpresence/README.txt create mode 100644 mod_webpresence/TODO.txt create mode 100644 mod_webpresence/build.bat create mode 100755 mod_webpresence/build.sh create mode 100644 mod_webpresence/conf/ejabberd.cfg.example create mode 100644 mod_webpresence/data/pixmaps/3d-dot1/available.gif create mode 100644 mod_webpresence/data/pixmaps/3d-dot1/away.gif create mode 100644 mod_webpresence/data/pixmaps/3d-dot1/chat.gif create mode 100644 mod_webpresence/data/pixmaps/3d-dot1/dnd.gif create mode 100644 mod_webpresence/data/pixmaps/3d-dot1/unavailable.gif create mode 100644 mod_webpresence/data/pixmaps/3d-dot1/xa.gif create mode 100644 mod_webpresence/data/pixmaps/3d-dot2/available.gif create mode 100644 mod_webpresence/data/pixmaps/3d-dot2/away.gif create mode 100644 mod_webpresence/data/pixmaps/3d-dot2/chat.gif create mode 100644 mod_webpresence/data/pixmaps/3d-dot2/dnd.gif create mode 100644 mod_webpresence/data/pixmaps/3d-dot2/unavailable.gif create mode 100644 mod_webpresence/data/pixmaps/3d-dot2/xa.gif create mode 100644 mod_webpresence/data/pixmaps/alphamod/available.png create mode 100644 mod_webpresence/data/pixmaps/alphamod/away.png create mode 100644 mod_webpresence/data/pixmaps/alphamod/chat.png create mode 100644 mod_webpresence/data/pixmaps/alphamod/dnd.png create mode 100644 mod_webpresence/data/pixmaps/alphamod/unavailable.png create mode 100644 mod_webpresence/data/pixmaps/alphamod/xa.png create mode 100644 mod_webpresence/data/pixmaps/amibulb/available.png create mode 100644 mod_webpresence/data/pixmaps/amibulb/away.png create mode 100644 mod_webpresence/data/pixmaps/amibulb/chat.png create mode 100644 mod_webpresence/data/pixmaps/amibulb/dnd.png create mode 100644 mod_webpresence/data/pixmaps/amibulb/unavailable.png create mode 100644 mod_webpresence/data/pixmaps/amibulb/xa.png create mode 100644 mod_webpresence/data/pixmaps/amigadu/available.gif create mode 100644 mod_webpresence/data/pixmaps/amigadu/away.gif create mode 100644 mod_webpresence/data/pixmaps/amigadu/chat.gif create mode 100644 mod_webpresence/data/pixmaps/amigadu/dnd.gif create mode 100644 mod_webpresence/data/pixmaps/amigadu/unavailable.gif create mode 100644 mod_webpresence/data/pixmaps/amigadu/xa.gif create mode 100644 mod_webpresence/data/pixmaps/amiglobe/available.png create mode 100644 mod_webpresence/data/pixmaps/amiglobe/away.png create mode 100644 mod_webpresence/data/pixmaps/amiglobe/chat.png create mode 100644 mod_webpresence/data/pixmaps/amiglobe/dnd.png create mode 100644 mod_webpresence/data/pixmaps/amiglobe/unavailable.png create mode 100644 mod_webpresence/data/pixmaps/amiglobe/xa.png create mode 100644 mod_webpresence/data/pixmaps/amiicq/available.gif create mode 100644 mod_webpresence/data/pixmaps/amiicq/away.gif create mode 100644 mod_webpresence/data/pixmaps/amiicq/chat.gif create mode 100644 mod_webpresence/data/pixmaps/amiicq/dnd.gif create mode 100644 mod_webpresence/data/pixmaps/amiicq/unavailable.gif create mode 100644 mod_webpresence/data/pixmaps/amiicq/xa.gif create mode 100644 mod_webpresence/data/pixmaps/amimsn/available.png create mode 100644 mod_webpresence/data/pixmaps/amimsn/away.png create mode 100644 mod_webpresence/data/pixmaps/amimsn/chat.png create mode 100644 mod_webpresence/data/pixmaps/amimsn/dnd.png create mode 100644 mod_webpresence/data/pixmaps/amimsn/unavailable.png create mode 100644 mod_webpresence/data/pixmaps/amimsn/xa.png create mode 100644 mod_webpresence/data/pixmaps/amistellar/available.gif create mode 100644 mod_webpresence/data/pixmaps/amistellar/away.gif create mode 100644 mod_webpresence/data/pixmaps/amistellar/chat.gif create mode 100644 mod_webpresence/data/pixmaps/amistellar/dnd.gif create mode 100644 mod_webpresence/data/pixmaps/amistellar/unavailable.gif create mode 100644 mod_webpresence/data/pixmaps/amistellar/xa.gif create mode 100644 mod_webpresence/data/pixmaps/amitlen/available.gif create mode 100644 mod_webpresence/data/pixmaps/amitlen/away.gif create mode 100644 mod_webpresence/data/pixmaps/amitlen/chat.gif create mode 100644 mod_webpresence/data/pixmaps/amitlen/dnd.gif create mode 100644 mod_webpresence/data/pixmaps/amitlen/unavailable.gif create mode 100644 mod_webpresence/data/pixmaps/amitlen/xa.gif create mode 100644 mod_webpresence/data/pixmaps/bulb/available.gif create mode 100644 mod_webpresence/data/pixmaps/bulb/away.gif create mode 100644 mod_webpresence/data/pixmaps/bulb/chat.gif create mode 100644 mod_webpresence/data/pixmaps/bulb/dnd.gif create mode 100644 mod_webpresence/data/pixmaps/bulb/unavailable.gif create mode 100644 mod_webpresence/data/pixmaps/bulb/xa.gif create mode 100644 mod_webpresence/data/pixmaps/chrome-pl/available.gif create mode 100644 mod_webpresence/data/pixmaps/chrome-pl/away.gif create mode 100644 mod_webpresence/data/pixmaps/chrome-pl/chat.gif create mode 100644 mod_webpresence/data/pixmaps/chrome-pl/dnd.gif create mode 100644 mod_webpresence/data/pixmaps/chrome-pl/unavailable.gif create mode 100644 mod_webpresence/data/pixmaps/chrome-pl/xa.gif create mode 100644 mod_webpresence/data/pixmaps/crystal/available.png create mode 100644 mod_webpresence/data/pixmaps/crystal/away.png create mode 100644 mod_webpresence/data/pixmaps/crystal/chat.png create mode 100644 mod_webpresence/data/pixmaps/crystal/dnd.png create mode 100644 mod_webpresence/data/pixmaps/crystal/unavailable.png create mode 100644 mod_webpresence/data/pixmaps/crystal/xa.png create mode 100644 mod_webpresence/data/pixmaps/crystal2/available.gif create mode 100644 mod_webpresence/data/pixmaps/crystal2/away.gif create mode 100644 mod_webpresence/data/pixmaps/crystal2/chat.gif create mode 100644 mod_webpresence/data/pixmaps/crystal2/dnd.gif create mode 100644 mod_webpresence/data/pixmaps/crystal2/unavailable.gif create mode 100644 mod_webpresence/data/pixmaps/crystal2/xa.gif create mode 100644 mod_webpresence/data/pixmaps/crystal3/available.gif create mode 100644 mod_webpresence/data/pixmaps/crystal3/away.gif create mode 100644 mod_webpresence/data/pixmaps/crystal3/chat.gif create mode 100644 mod_webpresence/data/pixmaps/crystal3/dnd.gif create mode 100644 mod_webpresence/data/pixmaps/crystal3/unavailable.gif create mode 100644 mod_webpresence/data/pixmaps/crystal3/xa.gif create mode 100644 mod_webpresence/data/pixmaps/dcraven/available.png create mode 100644 mod_webpresence/data/pixmaps/dcraven/away.png create mode 100644 mod_webpresence/data/pixmaps/dcraven/chat.png create mode 100644 mod_webpresence/data/pixmaps/dcraven/dnd.png create mode 100644 mod_webpresence/data/pixmaps/dcraven/unavailable.png create mode 100644 mod_webpresence/data/pixmaps/dcraven/xa.png create mode 100644 mod_webpresence/data/pixmaps/dudes/available.gif create mode 100644 mod_webpresence/data/pixmaps/dudes/away.gif create mode 100644 mod_webpresence/data/pixmaps/dudes/chat.gif create mode 100644 mod_webpresence/data/pixmaps/dudes/dnd.gif create mode 100644 mod_webpresence/data/pixmaps/dudes/unavailable.gif create mode 100644 mod_webpresence/data/pixmaps/dudes/xa.gif create mode 100644 mod_webpresence/data/pixmaps/frickenhuge/available.gif create mode 100644 mod_webpresence/data/pixmaps/frickenhuge/away.gif create mode 100644 mod_webpresence/data/pixmaps/frickenhuge/chat.gif create mode 100644 mod_webpresence/data/pixmaps/frickenhuge/dnd.gif create mode 100644 mod_webpresence/data/pixmaps/frickenhuge/unavailable.gif create mode 100644 mod_webpresence/data/pixmaps/frickenhuge/xa.gif create mode 100644 mod_webpresence/data/pixmaps/gabber/available.gif create mode 100644 mod_webpresence/data/pixmaps/gabber/away.gif create mode 100644 mod_webpresence/data/pixmaps/gabber/chat.gif create mode 100644 mod_webpresence/data/pixmaps/gabber/dnd.gif create mode 100644 mod_webpresence/data/pixmaps/gabber/unavailable.gif create mode 100644 mod_webpresence/data/pixmaps/gabber/xa.gif create mode 100644 mod_webpresence/data/pixmaps/gg/available.gif create mode 100644 mod_webpresence/data/pixmaps/gg/away.gif create mode 100644 mod_webpresence/data/pixmaps/gg/chat.gif create mode 100644 mod_webpresence/data/pixmaps/gg/dnd.gif create mode 100644 mod_webpresence/data/pixmaps/gg/unavailable.gif create mode 100644 mod_webpresence/data/pixmaps/gg/xa.gif create mode 100644 mod_webpresence/data/pixmaps/gnome/available.png create mode 100644 mod_webpresence/data/pixmaps/gnome/away.png create mode 100644 mod_webpresence/data/pixmaps/gnome/chat.png create mode 100644 mod_webpresence/data/pixmaps/gnome/dnd.png create mode 100644 mod_webpresence/data/pixmaps/gnome/unavailable.png create mode 100644 mod_webpresence/data/pixmaps/gnome/xa.png create mode 100644 mod_webpresence/data/pixmaps/gossip/available.png create mode 100644 mod_webpresence/data/pixmaps/gossip/away.png create mode 100644 mod_webpresence/data/pixmaps/gossip/chat.png create mode 100644 mod_webpresence/data/pixmaps/gossip/dnd.png create mode 100644 mod_webpresence/data/pixmaps/gossip/unavailable.png create mode 100644 mod_webpresence/data/pixmaps/gossip/xa.png create mode 100644 mod_webpresence/data/pixmaps/gota/available.png create mode 100644 mod_webpresence/data/pixmaps/gota/away.png create mode 100644 mod_webpresence/data/pixmaps/gota/chat.png create mode 100644 mod_webpresence/data/pixmaps/gota/dnd.png create mode 100644 mod_webpresence/data/pixmaps/gota/unavailable.png create mode 100644 mod_webpresence/data/pixmaps/gota/xa.png create mode 100644 mod_webpresence/data/pixmaps/gtalk/available.png create mode 100644 mod_webpresence/data/pixmaps/gtalk/away.png create mode 100644 mod_webpresence/data/pixmaps/gtalk/chat.png create mode 100644 mod_webpresence/data/pixmaps/gtalk/dnd.png create mode 100644 mod_webpresence/data/pixmaps/gtalk/unavailable.png create mode 100644 mod_webpresence/data/pixmaps/gtalk/xa.png create mode 100644 mod_webpresence/data/pixmaps/icq/available.gif create mode 100644 mod_webpresence/data/pixmaps/icq/away.gif create mode 100644 mod_webpresence/data/pixmaps/icq/chat.gif create mode 100644 mod_webpresence/data/pixmaps/icq/dnd.gif create mode 100644 mod_webpresence/data/pixmaps/icq/unavailable.gif create mode 100644 mod_webpresence/data/pixmaps/icq/xa.gif create mode 100644 mod_webpresence/data/pixmaps/invision/available.gif create mode 100644 mod_webpresence/data/pixmaps/invision/away.gif create mode 100644 mod_webpresence/data/pixmaps/invision/chat.gif create mode 100644 mod_webpresence/data/pixmaps/invision/dnd.gif create mode 100644 mod_webpresence/data/pixmaps/invision/unavailable.gif create mode 100644 mod_webpresence/data/pixmaps/invision/xa.gif create mode 100644 mod_webpresence/data/pixmaps/jabberbulb/available.png create mode 100644 mod_webpresence/data/pixmaps/jabberbulb/away.png create mode 100644 mod_webpresence/data/pixmaps/jabberbulb/chat.png create mode 100644 mod_webpresence/data/pixmaps/jabberbulb/dnd.png create mode 100644 mod_webpresence/data/pixmaps/jabberbulb/unavailable.png create mode 100644 mod_webpresence/data/pixmaps/jabberbulb/xa.png create mode 100644 mod_webpresence/data/pixmaps/jsf-jabber-text/available.png create mode 100644 mod_webpresence/data/pixmaps/jsf-jabber-text/away.png create mode 100644 mod_webpresence/data/pixmaps/jsf-jabber-text/chat.png create mode 100644 mod_webpresence/data/pixmaps/jsf-jabber-text/dnd.png create mode 100644 mod_webpresence/data/pixmaps/jsf-jabber-text/unavailable.png create mode 100644 mod_webpresence/data/pixmaps/jsf-jabber-text/xa.png create mode 100644 mod_webpresence/data/pixmaps/jsf-text/available.png create mode 100644 mod_webpresence/data/pixmaps/jsf-text/away.png create mode 100644 mod_webpresence/data/pixmaps/jsf-text/chat.png create mode 100644 mod_webpresence/data/pixmaps/jsf-text/dnd.png create mode 100644 mod_webpresence/data/pixmaps/jsf-text/unavailable.png create mode 100644 mod_webpresence/data/pixmaps/jsf-text/xa.png create mode 100644 mod_webpresence/data/pixmaps/jsf-text2/available.png create mode 100644 mod_webpresence/data/pixmaps/jsf-text2/away.png create mode 100644 mod_webpresence/data/pixmaps/jsf-text2/chat.png create mode 100644 mod_webpresence/data/pixmaps/jsf-text2/dnd.png create mode 100644 mod_webpresence/data/pixmaps/jsf-text2/unavailable.png create mode 100644 mod_webpresence/data/pixmaps/jsf-text2/xa.png create mode 100644 mod_webpresence/data/pixmaps/koniczynek/available.gif create mode 100644 mod_webpresence/data/pixmaps/koniczynek/away.gif create mode 100644 mod_webpresence/data/pixmaps/koniczynek/chat.gif create mode 100644 mod_webpresence/data/pixmaps/koniczynek/dnd.gif create mode 100644 mod_webpresence/data/pixmaps/koniczynek/unavailable.gif create mode 100644 mod_webpresence/data/pixmaps/koniczynek/xa.gif create mode 100644 mod_webpresence/data/pixmaps/licq/available.gif create mode 100644 mod_webpresence/data/pixmaps/licq/away.gif create mode 100644 mod_webpresence/data/pixmaps/licq/chat.gif create mode 100644 mod_webpresence/data/pixmaps/licq/dnd.gif create mode 100644 mod_webpresence/data/pixmaps/licq/unavailable.gif create mode 100644 mod_webpresence/data/pixmaps/licq/xa.gif create mode 100644 mod_webpresence/data/pixmaps/nahuatl/available.gif create mode 100644 mod_webpresence/data/pixmaps/nahuatl/away.gif create mode 100644 mod_webpresence/data/pixmaps/nahuatl/chat.gif create mode 100644 mod_webpresence/data/pixmaps/nahuatl/dnd.gif create mode 100644 mod_webpresence/data/pixmaps/nahuatl/unavailable.gif create mode 100644 mod_webpresence/data/pixmaps/nahuatl/xa.gif create mode 100644 mod_webpresence/data/pixmaps/nuvola/available.png create mode 100644 mod_webpresence/data/pixmaps/nuvola/away.png create mode 100644 mod_webpresence/data/pixmaps/nuvola/chat.png create mode 100644 mod_webpresence/data/pixmaps/nuvola/dnd.png create mode 100644 mod_webpresence/data/pixmaps/nuvola/unavailable.png create mode 100644 mod_webpresence/data/pixmaps/nuvola/xa.png create mode 100644 mod_webpresence/data/pixmaps/phpbb/available.gif create mode 100644 mod_webpresence/data/pixmaps/phpbb/away.gif create mode 100644 mod_webpresence/data/pixmaps/phpbb/chat.gif create mode 100644 mod_webpresence/data/pixmaps/phpbb/dnd.gif create mode 100644 mod_webpresence/data/pixmaps/phpbb/unavailable.gif create mode 100644 mod_webpresence/data/pixmaps/phpbb/xa.gif create mode 100644 mod_webpresence/data/pixmaps/pogoda/available.gif create mode 100644 mod_webpresence/data/pixmaps/pogoda/away.gif create mode 100644 mod_webpresence/data/pixmaps/pogoda/chat.gif create mode 100644 mod_webpresence/data/pixmaps/pogoda/dnd.gif create mode 100644 mod_webpresence/data/pixmaps/pogoda/unavailable.gif create mode 100644 mod_webpresence/data/pixmaps/pogoda/xa.gif create mode 100644 mod_webpresence/data/pixmaps/psi/available.gif create mode 100644 mod_webpresence/data/pixmaps/psi/away.gif create mode 100644 mod_webpresence/data/pixmaps/psi/chat.gif create mode 100644 mod_webpresence/data/pixmaps/psi/dnd.gif create mode 100644 mod_webpresence/data/pixmaps/psi/unavailable.gif create mode 100644 mod_webpresence/data/pixmaps/psi/xa.gif create mode 100644 mod_webpresence/data/pixmaps/simplebulb/available.png create mode 100644 mod_webpresence/data/pixmaps/simplebulb/away.png create mode 100644 mod_webpresence/data/pixmaps/simplebulb/chat.png create mode 100644 mod_webpresence/data/pixmaps/simplebulb/dnd.png create mode 100644 mod_webpresence/data/pixmaps/simplebulb/unavailable.png create mode 100644 mod_webpresence/data/pixmaps/simplebulb/xa.png create mode 100644 mod_webpresence/data/pixmaps/stellar/available.png create mode 100644 mod_webpresence/data/pixmaps/stellar/away.png create mode 100644 mod_webpresence/data/pixmaps/stellar/chat.png create mode 100644 mod_webpresence/data/pixmaps/stellar/dnd.png create mode 100644 mod_webpresence/data/pixmaps/stellar/unavailable.png create mode 100644 mod_webpresence/data/pixmaps/stellar/xa.png create mode 100644 mod_webpresence/data/pixmaps/stellar2/available.gif create mode 100644 mod_webpresence/data/pixmaps/stellar2/away.gif create mode 100644 mod_webpresence/data/pixmaps/stellar2/chat.gif create mode 100644 mod_webpresence/data/pixmaps/stellar2/dnd.gif create mode 100644 mod_webpresence/data/pixmaps/stellar2/unavailable.gif create mode 100644 mod_webpresence/data/pixmaps/stellar2/xa.gif create mode 100644 mod_webpresence/data/pixmaps/sun/available.png create mode 100644 mod_webpresence/data/pixmaps/sun/away.png create mode 100644 mod_webpresence/data/pixmaps/sun/chat.png create mode 100644 mod_webpresence/data/pixmaps/sun/dnd.png create mode 100644 mod_webpresence/data/pixmaps/sun/unavailable.png create mode 100644 mod_webpresence/data/pixmaps/sun/xa.png create mode 100644 mod_webpresence/data/pixmaps/tux/available.gif create mode 100644 mod_webpresence/data/pixmaps/tux/away.gif create mode 100644 mod_webpresence/data/pixmaps/tux/chat.gif create mode 100644 mod_webpresence/data/pixmaps/tux/dnd.gif create mode 100644 mod_webpresence/data/pixmaps/tux/unavailable.gif create mode 100644 mod_webpresence/data/pixmaps/tux/xa.gif create mode 100644 mod_webpresence/data/pixmaps/wpkontakt/available.gif create mode 100644 mod_webpresence/data/pixmaps/wpkontakt/away.gif create mode 100644 mod_webpresence/data/pixmaps/wpkontakt/chat.gif create mode 100644 mod_webpresence/data/pixmaps/wpkontakt/dnd.gif create mode 100644 mod_webpresence/data/pixmaps/wpkontakt/unavailable.gif create mode 100644 mod_webpresence/data/pixmaps/wpkontakt/xa.gif create mode 100644 mod_webpresence/data/pixmaps/yipee/available.gif create mode 100644 mod_webpresence/data/pixmaps/yipee/away.gif create mode 100644 mod_webpresence/data/pixmaps/yipee/chat.gif create mode 100644 mod_webpresence/data/pixmaps/yipee/dnd.gif create mode 100644 mod_webpresence/data/pixmaps/yipee/unavailable.gif create mode 100644 mod_webpresence/data/pixmaps/yipee/xa.gif create mode 100644 mod_webpresence/data/pixmaps/zyx/available.png create mode 100644 mod_webpresence/data/pixmaps/zyx/away.png create mode 100644 mod_webpresence/data/pixmaps/zyx/chat.png create mode 100644 mod_webpresence/data/pixmaps/zyx/dnd.png create mode 100644 mod_webpresence/data/pixmaps/zyx/unavailable.png create mode 100644 mod_webpresence/data/pixmaps/zyx/xa.png create mode 100644 mod_webpresence/src/mod_webpresence.erl create mode 100644 mod_webpresence/src/msgs/es.mod_webpresence.msg create mode 100644 mod_webpresence/src/msgs/es.mod_webpresence.po create mode 100644 mod_webpresence/src/msgs/mod_webpresence.pot create mode 100644 mod_webpresence/src/msgs/wa.mod_webpresence.msg create mode 100644 mod_webpresence/src/msgs/wa.mod_webpresence.po create mode 100644 mod_xmlrpc/COPYING create mode 100644 mod_xmlrpc/ChangeLog create mode 100644 mod_xmlrpc/Emakefile create mode 100644 mod_xmlrpc/README.txt create mode 100644 mod_xmlrpc/build.bat create mode 100755 mod_xmlrpc/build.sh create mode 100644 mod_xmlrpc/src/mod_xmlrpc.erl create mode 100755 openfire-export/OpenfireExporter/changelog.html create mode 100644 openfire-export/OpenfireExporter/classes/wildfire-user-schema.xsd create mode 100644 openfire-export/OpenfireExporter/classes/xep-0227.xsd create mode 100644 openfire-export/OpenfireExporter/lib/openfire.jar create mode 100644 openfire-export/OpenfireExporter/lib/readme-lib.txt create mode 100644 openfire-export/OpenfireExporter/logo_large.gif create mode 100644 openfire-export/OpenfireExporter/logo_small.gif create mode 100755 openfire-export/OpenfireExporter/plugin.xml create mode 100755 openfire-export/OpenfireExporter/readme.html create mode 100755 openfire-export/OpenfireExporter/src/java/net/processone/plugin/openfire/ExporterXEP227.java create mode 100755 openfire-export/OpenfireExporter/src/java/net/processone/plugin/openfire/UserSchemaValidator.java create mode 100644 openfire-export/OpenfireExporter/src/java/org/jivesoftware/openfire/PrivateStorage.java create mode 100755 openfire-export/OpenfireExporter/src/web/export-file.jsp create mode 100755 openfire-export/OpenfireExporter/src/web/export-user-data.jsp create mode 100644 openfire-export/OpenfireExporter/src/web/images/error-16x16.gif create mode 100644 openfire-export/OpenfireExporter/src/web/images/info-16x16.gif create mode 100644 openfire-export/OpenfireExporter/src/web/images/success-16x16.gif create mode 100755 openfire-export/OpenfireExporter/src/web/import-export-selection.jsp create mode 100755 openfire-export/OpenfireExporter/src/web/import-user-data.jsp diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..9a04bc4 --- /dev/null +++ b/README.txt @@ -0,0 +1,60 @@ +ejabberd-modules is a collaborative development area for ejabberd +modules developers and users. + + + For users + ========= + +You need to have Erlang installed. + +To use an ejabberd module coming from this repository: + +- Read the module specific README to see if special steps are needed + to deploy it. + +- Run "./build.sh" or "build.bat" in the root (usually trunk + directory) of the wanted module. + +- Copy generated .beam files from the ebin directory to the directory + where your ejabberd .beam files are. + +- Use the configuration file examples provided in the conf dir to + update your ejabberd.cfg configuration file. + +If during compilation of a module you get an error like: + {"init terminating in do_boot",{undef,[{make,all,[]},... +it means Erlang couldn't find its file make.beam +In Debian and other distributions you can try to install packages like: + erlang-dev erlang-nox erlang-tools + + + For developers + ============== + +The following organisation has been set-up for the development: + +- Each module has its own SVN structure (trunk/branches/tags) to allow + independent versioning. + +- Development and compilation of module should be possible without + ejabberd SVN, as long as developers check-out the ejabberd-dev + module. This module contains include file to make compilation + possible. + +- The module directory structure is usually the following: + README.txt: Module description + LICENSE.txt: License for the module + Emakefile: Erlang makefile to build the module (preferred way, if no + dependencies on C code, as build will thus works on Windows) + doc/: Documentation dir + src/: Source directory + src/msgs/: Directory with translation files (pot, po and msg). + ebin/: empty (Target directory for the build). + conf/: Directory containing example configuration for your module. + build.sh: *nix build script. + build.bat: Windows build script. + +- Module developers should put in the README if the module has + requirements or known incompatibilities with other modules (for + example, by modifying the same main ejabberd modules). + diff --git a/atom_pubsub/Emakefile b/atom_pubsub/Emakefile new file mode 100644 index 0000000..a6c0c01 --- /dev/null +++ b/atom_pubsub/Emakefile @@ -0,0 +1,4 @@ +{'../ejabberd-dev/src/gen_mod', [{outdir, "../ejabberd-dev/ebin"},{i,"../ejabberd-dev/include"}]}. +{'src/atom_microblog', [{outdir, "ebin"},{i,"../ejabberd-dev/include"}]}. +{'src/atom_pubsub', [{outdir, "ebin"},{i,"../ejabberd-dev/include"}]}. +{'src/mod_couch', [{outdir, "ebin"},{i,"../ejabberd-dev/include"}]}. diff --git a/atom_pubsub/README b/atom_pubsub/README new file mode 100644 index 0000000..bed0aef --- /dev/null +++ b/atom_pubsub/README @@ -0,0 +1,116 @@ + + atom_pubsub - the Atom PubSub tunnel + + Author: Eric Cestari http://www.cestari.info/ + Licensed under the same terms as ejabberd (GPL 2) + Requires: ejabberd trunk SVN r1561 or newer; or ejabberd 2.0.3 or newer + + + DESCRIPTION + ----------- + +The atom_pubsub module provides access to all PEP nodes via an AtomPub interface. +Also gives access to tune, mood and geoloc nodes if they exist. + +urn:xmpp:microblog is not a XEP yet, but its latest incarnation can be found here : +http://www.xmpp.org/extensions/inbox/microblogging.html + +AtomPub RFC : http://bitworking.org/projects/atom/rfc5023.html + +For more information refer to: +http://www.cestari.info/2008/6/19/atom-pubsub-module-for-ejabberd +http://www.cestari.info/2008/9/12/atom_pubsub-dead-long-live-atom_microblog + + + INSTALL + ------- + +1. Compile the files executing: ./build.sh + +2. Copy beam files from ebin/ into your ejabberd installation. + +3. Edit ejabberd.cfg and add a request handler in ejabberd_http listener: +{listen, [ + ... + {8080, ejabberd_http, [ + http_poll, + web_admin, + {request_handlers, [{["pep"], atom_microblog}, % Make sure it's "pep", or change it in atom_microblog.erl + {["pubsub"], atom_pubsub}]} % Make sure it's "pubsub", or change it in atom_pubsub.erl + + ]} + ]}. + +4. Restart ejabberd. + + + USAGE + ----- + +URL for the service document for a given user@domain is : +http://:5280/pep// + +The atom_pubsub module provides access to all nodes below the /home/server/user tree. +SVC document : http://:5280/pubsub// + + +# Configuring your PEP nodes + +For your PEP nodes to be published, you need to activate persistence. +Two ways: + +A) For existing nodes : + + + + + + http://jabber.org/protocol/pubsub#node_config + + 1 + 1 + + + + + +B) for new nodes : +It's better to change src/mod_pubsub/node_pep.erl in the options() function: + {persist_items, true}, + {max_items, 1}, +All future PEP nodes will be published with those parameters by default. + + +# What you get + +Full caching support with Etag, Conditional Get, etc. + +Authentication is required for writing, updating via Atom. Use your full JID as username for authentication. + +It expects the payload to be an atom entry, but does not enforce it. + +However, it has to be well formed XML. + + +# Can I have it with OpenFire and Epeios ? + +That's not possible. atom_microblog needs direct access to the pubsub structures. + + + WHAT'S NEXT? + ------------ + +* Better understanding of Atom entries. have better links, implement reply-to + +* Adding a local friend collection for personal use + +* But that may be implemented in mod_pubsub/node_microblog.erl (it will arrive soon) + + + THANKS + ------ + +* johnny_ for testing and giving feedback. +* badlop and C Romain from ProcessOne for feeding ejabberd with my patches, making this code work. + diff --git a/atom_pubsub/build.bat b/atom_pubsub/build.bat new file mode 100644 index 0000000..fff9b3b --- /dev/null +++ b/atom_pubsub/build.bat @@ -0,0 +1 @@ +erl -pa ../../ejabberd-dev/trunk/ebin -pa ebin -make diff --git a/atom_pubsub/build.sh b/atom_pubsub/build.sh new file mode 100755 index 0000000..f0a0cfa --- /dev/null +++ b/atom_pubsub/build.sh @@ -0,0 +1,2 @@ +#!/bin/sh +erl -pa ../ejabberd-dev/ebin -pz ebin -make diff --git a/atom_pubsub/src/atom_microblog.erl b/atom_pubsub/src/atom_microblog.erl new file mode 100644 index 0000000..c00cb58 --- /dev/null +++ b/atom_pubsub/src/atom_microblog.erl @@ -0,0 +1,367 @@ +%% +% This module enables access to the PEP node "urn:xmpp:microblog" via +% an Atompub compliant interface. +% +% +-module(atom_microblog). +-author('eric@ohmforce.com'). +-include("ejabberd.hrl"). +-include("jlib.hrl"). +-include("mod_pubsub/pubsub.hrl"). +-include("web/ejabberd_http.hrl"). +-export([process/2]). + +process([Domain,User|_]=LocalPath, #request{auth = Auth} = Request)-> + case get_auth(Auth) of + %%make sure user belongs to pubsub domain + {User, Domain} -> + out(Request, Request#request.method, LocalPath,User); + _ -> + out(Request, Request#request.method, LocalPath,undefined) + end; + + +process(_LocalPath, _Request)-> + error(404). + +get_host([Domain,User|_Rest])-> {User, Domain, []}. + +get_collection([_Domain,_User, Node|_R])-> + case lists:member(Node, ["mood", "geoloc", "tune"]) of + true -> "http://jabber.org/protocol/"++Node; + false -> Node + end; +get_collection(_)->error. + +get_member([_Domain,_User, _Node, Member]=Uri)-> + [get_host(Uri), get_collection(Uri), Member]. + +base_uri(#request{host=Host, port=Port}, Domain, User)-> + "http://"++Host++":"++i2l(Port)++"/pep/"++Domain++"/"++ User. + +collection_uri(R, Domain, User, Node)-> + Clean=lists:last(string:tokens(Node, "/")), + base_uri(R, Domain, User)++"/"++Clean. + +entry_uri(R, Domain, User, Node, Id)-> + collection_uri(R, Domain, User, Node)++"/"++Id. + +generate_etag(#pubsub_item{modification={_JID, {_, D2, D3}}})->integer_to_list(D3+D2). + +out(_Args, 'POST', [_,_, _], undefined) ->error(401); +out(_Args, 'PUT', [_,_, _], undefined) ->error(401); +out(_Args, 'DELETE', [_,_, _], undefined) ->error(401); + + +%% Service document +out(Args, 'GET', [Domain, UserNode]=Uri, _User) -> + %%Collections = mnesia:dirty_match_object(#pubsub_node{nodeid={get_host(Uri), '_'},_ = '_'}), + case mod_pubsub:tree_action(get_host(Uri), get_nodes, [get_host(Uri)]) of + [] -> error(404); + Collections -> + ?DEBUG("PEP nodes : ~p~n",[Collections]), + {200, [{"Content-Type", "application/atomsvc+xml"}], "" + ++ xml:element_to_string(service(Args,Domain, UserNode, Collections))} + end; + +%% Collection + +out(Args, 'GET', [Domain, User, Node]=Uri, _User) -> + case mod_pubsub:tree_action(get_host(Uri), get_node, [get_host(Uri),get_collection(Uri)]) of + {error, _} -> error(404); + _ -> + Items = lists:sort(fun(X,Y)-> + {_,DateX} = X#pubsub_item.modification, + {_,DateY} = Y#pubsub_item.modification, + DateX > DateY + end, mod_pubsub:get_items( + get_host(Uri), + get_collection(Uri), "")), + case Items of + [] -> ?DEBUG("Items : ~p ~n", [collection(get_collection(Uri), + collection_uri(Args,Domain,User,Node), calendar:now_to_universal_time(erlang:now()), User, "", [])]), + {200, [{"Content-Type", "application/atom+xml"}], + collection(get_collection(Uri), + collection_uri(Args,Domain,User,Node), calendar:now_to_universal_time(erlang:now()), User, "", [])}; + _ -> + #pubsub_item{modification = {_JID,LastDate}} = LastItem = hd(Items), + Etag =generate_etag(LastItem), + IfNoneMatch=proplists:get_value('If-None-Match', Args#request.headers), + if IfNoneMatch==Etag + -> + success(304); + true -> + XMLEntries= [item_to_entry(Args,Node,Entry)||Entry <- Items], + {200, [{"Content-Type", "application/atom+xml"},{"Etag", Etag}], + "" + ++ xml:element_to_string( + collection(get_collection(Uri), collection_uri(Args,Domain,User,Node), + calendar:now_to_universal_time(LastDate), User, "", XMLEntries))} + end + end + end; +%% Add new collection +out(_Args, 'POST', [_Domain, _User], _User)-> error(403); + +out(Args, 'POST', [Domain,User, Node]=Uri, User) -> + %%FIXME Slug + Slug = case lists:keysearch("Slug",3,Args#request.headers) of + false -> uniqid(false) ; + {value, {_,_,_,_,Value}} -> Value + end, + Payload = xml_stream:parse_element(Args#request.data), + case mod_pubsub:publish_item(get_host(Uri), + Domain, + get_collection(Uri), + jlib:make_jid(User,Domain, ""), + Slug, + [Payload]) of + {result, []} -> + ?DEBUG("Publishing to ~p~n",[entry_uri(Args, Domain,User, Node,Slug)]), + {201, [{"location", entry_uri(Args, Domain,User,Node,Slug)}], Payload}; + {error, Error} -> + error(400, Error) + end; +out(_Args, 'POST', [_, _, _], _) -> + {status, 403}; + +%% Atom doc +out(Args, 'GET', [_Domain,_U, Node, _Member]=URI, _User) -> + Failure = fun(_Error)->error(404)end, + Success = fun(Item)-> + Etag =generate_etag(Item), + IfNoneMatch=proplists:get_value('If-None-Match', Args#request.headers), + if IfNoneMatch==Etag + -> + success(304); + true -> + {200, [{"Content-Type", "application/atom+xml"},{"Etag", Etag}], "" + ++ xml:element_to_string(item_to_entry(Args, Node, Item))} + end + end, + get_item(URI, Failure, Success); + + +%% Update doc +out(Args, 'PUT', [Domain,User, _Node, Member]=Uri, User) -> + Payload = xml_stream:parse_element(Args#request.data), + Failure = fun(_Error)->error(404)end, + Success = fun(Item)-> + Etag =generate_etag(Item), + IfMatch=proplists:get_value('If-Match', Args#request.headers), + if IfMatch==Etag + -> + case mod_pubsub:publish_item(get_host(Uri), + Domain, + get_collection(Uri), + jlib:make_jid(User,Domain, ""), + Member, + [Payload]) of + {result, _Result} -> + {200, [{"Content-Type", "application/atom+xml"}],""}; + {error, {xmlelement, "error", [{"code",Code},_],_}} -> + error(Code); + {error, _Error} -> + error(500) + end; + true -> + error(412) %% ressource has been modified since last get for this client. + end + end, + get_item(Uri, Failure, Success); + +%% +out(_Args, 'PUT',_Url, _User) -> + error(401); + +out(_Args, 'DELETE', [Domain,User, _Node, _Member]=Uri, User) -> + case mod_pubsub:delete_item(get_host(Uri), + get_collection(Uri), + jlib:make_jid(User,Domain, ""), + get_member(Uri)) of + {result, _Result} -> + success(200); + {error, {xmlelement, "error", [{"code",Code},_],_}} -> + error(Code); + {error, _Code1} -> + error(500) + end; + +out(_Args, 'DELETE',_Url, _User) -> + error(401); + +out(_, _, _, _) -> + error(403). + +get_item(Uri, Failure, Success)-> + case catch mod_pubsub:node_action(get_host(Uri), + get_collection(Uri), + get_item, + get_member(Uri)) of + {error, Reason} -> + Failure(Reason); + {result, Item} -> + Success(Item) + end. + + + + +item_to_entry(Args,Node,#pubsub_item{itemid={Id,_}, payload=Entry}=Item)-> + [R | _]=xml:remove_cdata(Entry), + item_to_entry(Args, Node, Id, R, Item). + +item_to_entry(Args,Node, Id,{xmlelement, "entry", Attrs, SubEl}, + #pubsub_item{modification={_, Secs}, itemid={Id, {{User, Domain, []},_}}}) -> + Date = calendar:now_to_local_time(Secs), + SubEl2=[{xmlelement, "app:edited", [], [{xmlcdata, w3cdtf(Date)}]}, + {xmlelement, "link",[{"rel", "edit"}, + {"href", entry_uri(Args,Domain,User, Node, Id)}],[] }, + {xmlelement, "id", [],[{xmlcdata, Id}]} + | SubEl], + {xmlelement, "entry", [{"xmlns:app","http://www.w3.org/2007/app"}|Attrs], SubEl2}; + +%% Don't do anything except adding xmlns +item_to_entry(_Args,Node, _Id, {xmlelement, Name, Attrs, Subels}=Element, _Item)-> + case proplists:is_defined("xmlns",Attrs) of + true -> Element; + false -> {xmlelement, Name, [{"xmlns", Node}|Attrs], Subels} + end. + + + +collection(Title, Link, Updated, Author, _Id, Entries)-> + {xmlelement, "feed", [{"xmlns", "http://www.w3.org/2005/Atom"}, + {"xmlns:app", "http://www.w3.org/2007/app"}], [ + {xmlelement, "title", [],[{xmlcdata, Title}]}, + {xmlelement, "updated", [],[{xmlcdata, w3cdtf(Updated)}]}, + {xmlelement, "link", [{"href", Link}], []}, + {xmlelement, "author", [], [ + {xmlelement, "name", [], [{xmlcdata,Author}]} + ]}, + {xmlelement, "title", [],[{xmlcdata, Title}]} | + Entries + ]}. + +service(Args, Domain, User, Collections)-> + {xmlelement, "service", [{"xmlns", "http://www.w3.org/2007/app"}, + {"xmlns:atom", "http://www.w3.org/2005/Atom"}, + {"xmlns:app", "http://www.w3.org/2007/app"}],[ + {xmlelement, "workspace", [],[ + {xmlelement, "atom:title", [],[{xmlcdata,"Feed for "++User++"@"++Domain}]} | + lists:map(fun(#pubsub_node{nodeid={_Server, Id}})-> + {xmlelement, "collection", [{"href", collection_uri(Args,Domain,User, Id)}], [ + {xmlelement, "atom:title", [], [{xmlcdata, Id}]} + ]} + end, Collections) + ]} + ]}. + +%%% lifted from ejabberd_web_admin +get_auth(Auth) -> + case Auth of + {SJID, P} -> + case jlib:string_to_jid(SJID) of + error -> + unauthorized; + #jid{user = U, server = S} -> + case ejabberd_auth:check_password(U, S, P) of + true -> + {U, S}; + false -> + unauthorized + end + end; + _ -> + unauthorized + end. + + + +%% simple output functions +error(404)-> + {404, [], "Not Found"}; +error(403)-> + {403, [], "Forbidden"}; +error(500)-> + {500, [], "Internal server error"}; +error(401)-> + {401, [{"WWW-Authenticate", "basic realm=\"ejabberd\""}],"Unauthorized"}; +error(Code)-> + {Code, [], ""}. +success(200)-> + {200, [], ""}; +success(Code)-> + {Code, [], ""}. +error(Code, Error) when is_list(Error) -> {Code, [], Error}; +error(Code, {xmlelement, "error",_,_}=Error) -> {Code, [], xml:element_to_string(Error)}; +error(Code, _Error) -> {Code, [], "Bad request"}. + + +% Code below is taken (with some modifications) from the yaws webserver, which +% is distributed under the folowing license: +% +% This software (the yaws webserver) is free software. +% Parts of this software is Copyright (c) Claes Wikstrom +% Any use or misuse of the source code is hereby freely allowed. +% +% 1. Redistributions of source code must retain the above copyright +% notice as well as this list of conditions. +% +% 2. Redistributions in binary form must reproduce the above copyright +% notice as well as this list of conditions. +%%% Create W3CDTF (http://www.w3.org/TR/NOTE-datetime) formatted date +%%% w3cdtf(GregSecs) -> "YYYY-MM-DDThh:mm:ssTZD" +%%% +uniqid(false)-> + {T1, T2, T3} = now(), + lists:flatten(io_lib:fwrite("~.16B~.16B~.16B", [T1, T2, T3])); +uniqid(Slug) -> + Slut = string:to_lower(Slug), + S = string:substr(Slut, 1, 9), + {_T1, T2, T3} = now(), + lists:flatten(io_lib:fwrite("~s-~.16B~.16B", [S, T2, T3])). + +w3cdtf(Date) -> %1 Date = calendar:gregorian_seconds_to_datetime(GregSecs), + {{Y, Mo, D},{H, Mi, S}} = Date, + [UDate|_] = calendar:local_time_to_universal_time_dst(Date), + {DiffD,{DiffH,DiffMi,_}}=calendar:time_difference(UDate,Date), + w3cdtf_diff(Y, Mo, D, H, Mi, S, DiffD, DiffH, DiffMi). + +%%% w3cdtf's helper function +w3cdtf_diff(Y, Mo, D, H, Mi, S, _DiffD, DiffH, DiffMi) when DiffH < 12, DiffH /= 0 -> + i2l(Y) ++ "-" ++ add_zero(Mo) ++ "-" ++ add_zero(D) ++ "T" ++ + add_zero(H) ++ ":" ++ add_zero(Mi) ++ ":" ++ + add_zero(S) ++ "+" ++ add_zero(DiffH) ++ ":" ++ add_zero(DiffMi); + +w3cdtf_diff(Y, Mo, D, H, Mi, S, DiffD, DiffH, DiffMi) when DiffH > 12, DiffD == 0 -> + i2l(Y) ++ "-" ++ add_zero(Mo) ++ "-" ++ add_zero(D) ++ "T" ++ + add_zero(H) ++ ":" ++ add_zero(Mi) ++ ":" ++ + add_zero(S) ++ "+" ++ add_zero(DiffH) ++ ":" ++ + add_zero(DiffMi); + +w3cdtf_diff(Y, Mo, D, H, Mi, S, DiffD, DiffH, DiffMi) when DiffH > 12, DiffD /= 0, DiffMi /= 0 -> + i2l(Y) ++ "-" ++ add_zero(Mo) ++ "-" ++ add_zero(D) ++ "T" ++ + add_zero(H) ++ ":" ++ add_zero(Mi) ++ ":" ++ + add_zero(S) ++ "-" ++ add_zero(23-DiffH) ++ + ":" ++ add_zero(60-DiffMi); + +w3cdtf_diff(Y, Mo, D, H, Mi, S, DiffD, DiffH, DiffMi) when DiffH > 12, DiffD /= 0, DiffMi == 0 -> + i2l(Y) ++ "-" ++ add_zero(Mo) ++ "-" ++ add_zero(D) ++ "T" ++ + add_zero(H) ++ ":" ++ add_zero(Mi) ++ ":" ++ + add_zero(S) ++ "-" ++ add_zero(24-DiffH) ++ + ":" ++ add_zero(DiffMi); + +w3cdtf_diff(Y, Mo, D, H, Mi, S, _DiffD, DiffH, _DiffMi) when DiffH == 0 -> + i2l(Y) ++ "-" ++ add_zero(Mo) ++ "-" ++ add_zero(D) ++ "T" ++ + add_zero(H) ++ ":" ++ add_zero(Mi) ++ ":" ++ + add_zero(S) ++ "Z". + +add_zero(I) when is_integer(I) -> add_zero(i2l(I)); +add_zero([A]) -> [$0,A]; +add_zero(L) when is_list(L) -> L. + +i2l(I) when is_integer(I) -> integer_to_list(I); +i2l(L) when is_list(L) -> L. + + diff --git a/atom_pubsub/src/atom_pubsub.erl b/atom_pubsub/src/atom_pubsub.erl new file mode 100644 index 0000000..a7d8bec --- /dev/null +++ b/atom_pubsub/src/atom_pubsub.erl @@ -0,0 +1,374 @@ +%% +% This module enables access to the PEP node "urn:xmpp:microblog" via +% an Atompub compliant interface. +% +% +-module(atom_pubsub). +-author('eric@ohmforce.com'). +-include("ejabberd.hrl"). +-include("jlib.hrl"). +-include("mod_pubsub/pubsub.hrl"). +-include("web/ejabberd_http.hrl"). +-export([process/2]). + +process([Domain,User|_]=LocalPath, #request{auth = Auth} = Request)-> + case get_auth(Auth) of + %%make sure user belongs to pubsub domain + {User, Domain} -> + out(Request, Request#request.method, LocalPath,User); + _ -> + out(Request, Request#request.method, LocalPath,undefined) + end; + + +process(_LocalPath, _Request)-> + error(404). + +get_host([Domain,_User|_Rest])-> "pubsub."++Domain. +get_root([Domain,User|_Rest]) -> ["home", Domain, User]. +get_collection([Domain,User, Node|_Rest])->["home", Domain, User, Node]. +get_item_name([_Domain,_User, _Node, Member]) -> Member. +collection_uri(R, Domain, User, Node) -> + case Node of + ["home", Domain, User|_Rest]-> + base_uri(R, Domain, User)++"/"++lists:last(Node); + _ -> base_uri(R, Domain, User)++"/"++Node + end. +entry_uri(R, Domain, User, Node, Id)-> + collection_uri(R, Domain, User, Node)++"/"++Id. + + + +get_member([_Domain,_User, _Node, Member]=Uri)-> + [get_host(Uri), get_collection(Uri), Member]. + +base_uri(#request{host=Host, port=Port}, Domain, User)-> + "http://"++Host++":"++i2l(Port)++"/pubsub/"++Domain++"/"++ User. + + + + +generate_etag(#pubsub_item{modification={_JID, {_, D2, D3}}})->integer_to_list(D3+D2). + +out(_Args, 'POST', [_,_, _], undefined) ->error(401); +out(_Args, 'PUT', [_,_, _], undefined) ->error(401); +out(_Args, 'DELETE', [_,_, _], undefined) ->error(401); + + +%% Service document +out(Args, 'GET', [Domain, UserNode]=Uri, _User) -> + %%Collections = mnesia:dirty_match_object(#pubsub_node{nodeid={get_host(Uri), '_'},_ = '_'}), + case mod_pubsub:tree_action(get_host(Uri), get_subnodes, [get_host(Uri),get_root(Uri), "" ]) of + [] -> error(404); + Collections -> + {200, [{"Content-Type", "application/atomsvc+xml"}], "" + ++ xml:element_to_string(service(Args,Domain, UserNode, Collections))} + end; + +%% Collection + +out(Args, 'GET', [Domain, User, Node]=Uri, _User) -> + case mod_pubsub:tree_action(get_host(Uri), get_node, [get_host(Uri),get_collection(Uri)]) of + {error, _} -> error(404); + _ -> + Items = lists:sort(fun(X,Y)-> + {_,DateX} = X#pubsub_item.modification, + {_,DateY} = Y#pubsub_item.modification, + DateX > DateY + end, mod_pubsub:get_items( + get_host(Uri), + get_collection(Uri), "")), + case Items of + [] -> ?DEBUG("Items : ~p ~n", [collection(get_collection(Uri), + collection_uri(Args,Domain,User,Node), calendar:now_to_universal_time(erlang:now()), User, "", [])]), + {200, [{"Content-Type", "application/atom+xml"}], + collection(get_collection(Uri), + collection_uri(Args,Domain,User,Node), calendar:now_to_universal_time(erlang:now()), User, "", [])}; + _ -> + #pubsub_item{modification = {_JID,LastDate}} = LastItem = hd(Items), + Etag =generate_etag(LastItem), + IfNoneMatch=proplists:get_value('If-None-Match', Args#request.headers), + if IfNoneMatch==Etag + -> + success(304); + true -> + XMLEntries= [item_to_entry(Args,Node,Entry)||Entry <- Items], + {200, [{"Content-Type", "application/atom+xml"},{"Etag", Etag}], + "" + ++ xml:element_to_string( + collection(get_collection(Uri), collection_uri(Args,Domain,User,Node), + calendar:now_to_universal_time(LastDate), User, "", XMLEntries))} + end + end + end; + +%% Add new collection +out(_Args, 'POST', [_Domain, _User], _User)-> error(403); + +out(Args, 'POST', [Domain,User, Node]=Uri, User) -> + %%FIXME Slug + Slug = case lists:keysearch("Slug",3,Args#request.headers) of + false -> uniqid(false) ; + {value, {_,_,_,_,Value}} -> Value + end, + Payload = xml_stream:parse_element(Args#request.data), + [FilteredPayload]=xml:remove_cdata([Payload]), + + %FilteredPayload2 = case xml:get_subtag(FilteredPayload, "app:edited") -> + % {xmlelement, Name, Attrs, [{cdata, }]} + case mod_pubsub:publish_item(get_host(Uri), + Domain, + get_collection(Uri), + jlib:make_jid(User,Domain, ""), + Slug, + [FilteredPayload]) of + {result, []} -> + ?DEBUG("Publishing to ~p~n",[entry_uri(Args, Domain,User, Node,Slug)]), + {201, [{"location", entry_uri(Args, Domain,User,Node,Slug)}], Payload}; + {error, Error} -> + error(400, Error) + end; +out(_Args, 'POST', [_, _, _], _) -> + {status, 403}; + +%% Atom doc +out(Args, 'GET', [_Domain,_U, Node, _Member]=URI, _User) -> + Failure = fun(_Error)->error(404)end, + Success = fun(Item)-> + Etag =generate_etag(Item), + IfNoneMatch=proplists:get_value('If-None-Match', Args#request.headers), + if IfNoneMatch==Etag + -> + success(304); + true -> + {200, [{"Content-Type", "application/atom+xml"},{"Etag", Etag}], "" + ++ xml:element_to_string(item_to_entry(Args, Node, Item))} + end + end, + get_item(URI, Failure, Success); + + +%% Update doc +out(Args, 'PUT', [Domain,User, _Node, Member]=Uri, User) -> + Payload = xml_stream:parse_element(Args#request.data), + Failure = fun(_Error)->error(404)end, + Success = fun(Item)-> + Etag =generate_etag(Item), + IfMatch=proplists:get_value('If-Match', Args#request.headers), + if IfMatch==Etag + -> + case mod_pubsub:publish_item(get_host(Uri), + Domain, + get_collection(Uri), + jlib:make_jid(User,Domain, ""), + Member, + [Payload]) of + {result, _Result} -> + {200, [{"Content-Type", "application/atom+xml"}],""}; + {error, {xmlelement, "error", [{"code","404"},_],_}} -> + error(404); + {error, _Error} -> + error(500) + end; + true -> + error(412) %% ressource has been modified since last get for this client. + end + end, + get_item(Uri, Failure, Success); + +%% +out(_Args, 'PUT',_Url, _User) -> + error(401); + +out(_Args, 'DELETE', [Domain,User, _Node, _Member]=Uri, User) -> + case mod_pubsub:delete_item(get_host(Uri), + get_collection(Uri), + jlib:make_jid(User,Domain, ""), + get_item_name(Uri)) of + {result, _Result} -> + success(200); + {error, {xmlelement, "error", [{"code","404"},_],_}} -> + error(404); + {error, _Code1} -> + error(500) + end; + +out(_Args, 'DELETE',_Url, _User) -> + error(401); + +out(_, _, _, _) -> + error(403). + +get_item(Uri, Failure, Success)-> + case catch mod_pubsub:node_action(get_host(Uri), + get_collection(Uri), + get_item, + get_member(Uri)) of + {error, Reason} -> + Failure(Reason); + {result, Item} -> + Success(Item) + end. + + + + +item_to_entry(Args,Node,#pubsub_item{itemid={Id,_}, payload=Entry}=Item)-> + [R]=xml:remove_cdata(Entry), + item_to_entry(Args, Node, Id, R, Item). + +item_to_entry(Args,Node, Id,{xmlelement, "entry", Attrs, SubEl}, + #pubsub_item{modification={JID, Secs} }) -> + Date = calendar:now_to_local_time(Secs), + {User, Domain, _}=jlib:jid_tolower(JID), + SubEl2=[{xmlelement, "app:edited", [], [{xmlcdata, w3cdtf(Date)}]}, + {xmlelement, "link",[{"rel", "edit"}, + {"href", entry_uri(Args,Domain,User, Node, Id)}],[] }, + {xmlelement, "id", [],[{xmlcdata, Id}]} + | SubEl], + {xmlelement, "entry", [{"xmlns:app","http://www.w3.org/2007/app"}|Attrs], SubEl2}; + +% Don't do anything except adding xmlns +item_to_entry(_Args,Node, _Id, {xmlelement, Name, Attrs, Subels}=Element, _Item)-> + case proplists:is_defined("xmlns",Attrs) of + true -> Element; + false -> {xmlelement, Name, [{"xmlns", Node}|Attrs], Subels} + end. + + + +collection(Title, Link, Updated, Author, _Id, Entries)-> + {xmlelement, "feed", [{"xmlns", "http://www.w3.org/2005/Atom"}, + {"xmlns:app", "http://www.w3.org/2007/app"}], [ + {xmlelement, "title", [],[{xmlcdata, Title}]}, + {xmlelement, "updated", [],[{xmlcdata, w3cdtf(Updated)}]}, + {xmlelement, "link", [{"href", Link}], []}, + {xmlelement, "author", [], [ + {xmlelement, "name", [], [{xmlcdata,Author}]} + ]}, + {xmlelement, "title", [],[{xmlcdata, Title}]} | + Entries + ]}. + +service(Args, Domain, User, Collections)-> + {xmlelement, "service", [{"xmlns", "http://www.w3.org/2007/app"}, + {"xmlns:atom", "http://www.w3.org/2005/Atom"}, + {"xmlns:app", "http://www.w3.org/2007/app"}],[ + {xmlelement, "workspace", [],[ + {xmlelement, "atom:title", [],[{xmlcdata,"Feed for "++User++"@"++Domain}]} | + lists:map(fun(#pubsub_node{nodeid={_Server, Id}, type=_Type})-> + {xmlelement, "collection", [{"href", collection_uri(Args,Domain,User, Id)}], [ + {xmlelement, "atom:title", [], [{xmlcdata, lists:last(Id)}]} + ]} + end, Collections) + ]} + ]}. + +%%% lifted from ejabberd_web_admin +get_auth(Auth) -> + case Auth of + {SJID, P} -> + case jlib:string_to_jid(SJID) of + error -> + unauthorized; + #jid{user = U, server = S} -> + case ejabberd_auth:check_password(U, S, P) of + true -> + {U, S}; + false -> + unauthorized + end + end; + _ -> + unauthorized + end. + + + +%% simple output functions +error(404)-> + {404, [], "Not Found"}; +error(403)-> + {403, [], "Forbidden"}; +error(500)-> + {500, [], "Internal server error"}; +error(401)-> + {401, [{"WWW-Authenticate", "basic realm=\"ejabberd\""}],"Unauthorized"}; +error(Code)-> + {Code, [], ""}. +success(200)-> + {200, [], ""}; +success(Code)-> + {Code, [], ""}. +error(Code, Error) when is_list(Error) -> {Code, [], Error}; +error(Code, {xmlelement, "error",_,_}=Error) -> {Code, [], xml:element_to_string(Error)}; +error(Code, _Error) -> {Code, [], "Bad request"}. + + +% Code below is taken (with some modifications) from the yaws webserver, which +% is distributed under the folowing license: +% +% This software (the yaws webserver) is free software. +% Parts of this software is Copyright (c) Claes Wikstrom +% Any use or misuse of the source code is hereby freely allowed. +% +% 1. Redistributions of source code must retain the above copyright +% notice as well as this list of conditions. +% +% 2. Redistributions in binary form must reproduce the above copyright +% notice as well as this list of conditions. +%%% Create W3CDTF (http://www.w3.org/TR/NOTE-datetime) formatted date +%%% w3cdtf(GregSecs) -> "YYYY-MM-DDThh:mm:ssTZD" +%%% +uniqid(false)-> + {T1, T2, T3} = now(), + lists:flatten(io_lib:fwrite("~.16B~.16B~.16B", [T1, T2, T3])); +uniqid(Slug) -> + Slut = string:to_lower(Slug), + S = string:substr(Slut, 1, 9), + {_T1, T2, T3} = now(), + lists:flatten(io_lib:fwrite("~s-~.16B~.16B", [S, T2, T3])). + +w3cdtf(Date) -> %1 Date = calendar:gregorian_seconds_to_datetime(GregSecs), + {{Y, Mo, D},{H, Mi, S}} = Date, + [UDate|_] = calendar:local_time_to_universal_time_dst(Date), + {DiffD,{DiffH,DiffMi,_}}=calendar:time_difference(UDate,Date), + w3cdtf_diff(Y, Mo, D, H, Mi, S, DiffD, DiffH, DiffMi). + +%%% w3cdtf's helper function +w3cdtf_diff(Y, Mo, D, H, Mi, S, _DiffD, DiffH, DiffMi) when DiffH < 12, DiffH /= 0 -> + i2l(Y) ++ "-" ++ add_zero(Mo) ++ "-" ++ add_zero(D) ++ "T" ++ + add_zero(H) ++ ":" ++ add_zero(Mi) ++ ":" ++ + add_zero(S) ++ "+" ++ add_zero(DiffH) ++ ":" ++ add_zero(DiffMi); + +w3cdtf_diff(Y, Mo, D, H, Mi, S, DiffD, DiffH, DiffMi) when DiffH > 12, DiffD == 0 -> + i2l(Y) ++ "-" ++ add_zero(Mo) ++ "-" ++ add_zero(D) ++ "T" ++ + add_zero(H) ++ ":" ++ add_zero(Mi) ++ ":" ++ + add_zero(S) ++ "+" ++ add_zero(DiffH) ++ ":" ++ + add_zero(DiffMi); + +w3cdtf_diff(Y, Mo, D, H, Mi, S, DiffD, DiffH, DiffMi) when DiffH > 12, DiffD /= 0, DiffMi /= 0 -> + i2l(Y) ++ "-" ++ add_zero(Mo) ++ "-" ++ add_zero(D) ++ "T" ++ + add_zero(H) ++ ":" ++ add_zero(Mi) ++ ":" ++ + add_zero(S) ++ "-" ++ add_zero(23-DiffH) ++ + ":" ++ add_zero(60-DiffMi); + +w3cdtf_diff(Y, Mo, D, H, Mi, S, DiffD, DiffH, DiffMi) when DiffH > 12, DiffD /= 0, DiffMi == 0 -> + i2l(Y) ++ "-" ++ add_zero(Mo) ++ "-" ++ add_zero(D) ++ "T" ++ + add_zero(H) ++ ":" ++ add_zero(Mi) ++ ":" ++ + add_zero(S) ++ "-" ++ add_zero(24-DiffH) ++ + ":" ++ add_zero(DiffMi); + +w3cdtf_diff(Y, Mo, D, H, Mi, S, _DiffD, DiffH, _DiffMi) when DiffH == 0 -> + i2l(Y) ++ "-" ++ add_zero(Mo) ++ "-" ++ add_zero(D) ++ "T" ++ + add_zero(H) ++ ":" ++ add_zero(Mi) ++ ":" ++ + add_zero(S) ++ "Z". + +add_zero(I) when is_integer(I) -> add_zero(i2l(I)); +add_zero([A]) -> [$0,A]; +add_zero(L) when is_list(L) -> L. + +i2l(I) when is_integer(I) -> integer_to_list(I); +i2l(L) when is_list(L) -> L. + + diff --git a/atom_pubsub/src/mod_couch.erl b/atom_pubsub/src/mod_couch.erl new file mode 100644 index 0000000..d909bd1 --- /dev/null +++ b/atom_pubsub/src/mod_couch.erl @@ -0,0 +1,24 @@ +%%%---------------------------------------------------------------------- +%%% File : mod_couch.erl +%%% Author : Eric Cestari +%%% Purpose : Configures and starts ecouch client +%%% Created : +%%% Id : $Id$ +%%%---------------------------------------------------------------------- + +-module(mod_couch). +-author('eric@ohmforce.com'). +-vsn('0.2.0'). + +-behaviour(gen_mod). + +-export([start/2, stop/1]). + +start(Host, Opts) -> + Server = gen_mod:get_opt(server, Opts, {"127.0.0.1", "5984"}), + inets:start(), + application:set_env(ecouch, Server, {}), + application:start(ecouch). + +stop(_Host) -> + application:stop(ecouch). \ No newline at end of file diff --git a/bfile/LICENCE b/bfile/LICENCE new file mode 100644 index 0000000..b6792f8 --- /dev/null +++ b/bfile/LICENCE @@ -0,0 +1,27 @@ + +Copyright (c) 2006, Claes Wikstrom, klacke@hyber.org +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of "bfile" nor the names of its contributors may be + used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/bfile/Makefile b/bfile/Makefile new file mode 100644 index 0000000..84ef6ae --- /dev/null +++ b/bfile/Makefile @@ -0,0 +1,23 @@ + + +all: + (cd config;$(MAKE)) + (cd src;$(MAKE)) + -(cd c_src;$(MAKE) -k) + $(MAKE) appfile + +clean: + (cd src;$(MAKE) clean) + (cd c_src;$(MAKE) clean) + (cd config; $(MAKE) clean) + + +install: all + (cd c_src; $(MAKE) install) + +conf_clean: + (cd config; $(MAKE) clean) + +appfile: + (cd src;$(MAKE) ../ebin/bfile.app) + diff --git a/bfile/README b/bfile/README new file mode 100644 index 0000000..5390392 --- /dev/null +++ b/bfile/README @@ -0,0 +1,28 @@ +An interface to fast FILE I/O + +It's based on an old and hacked version +of the BSD FILE* + +To install, type make; make install +and it shuld install itself as an app in your +erlang dir. + +See the source src/bfile.erl for API + +Here's an example shell session: + + +2> bfile:load_driver(). +ok +4> {ok, Fd} = bfile:fopen("Makefile", "r"). +{ok,{bfile,#Port<0.98>}} +5> bfile:fgets(Fd). +{line,<<10>>} +6> bfile:fgets(Fd). +{line,<<10>>} +7> bfile:fgets(Fd). +{line,<<97,108,108,58,32,10>>} +14> bfile:fread(Fd, 10000). +{ok,<<10,10,105,110,115,116,97,108,108,58,32,97,108,108,10,9,40,99,100,32,99,95,115,114,99,59,32,...>>} +15> bfile:fread(Fd, 10000). +eof diff --git a/bfile/c_src/FILE_drv.c b/bfile/c_src/FILE_drv.c new file mode 100644 index 0000000..1035da8 --- /dev/null +++ b/bfile/c_src/FILE_drv.c @@ -0,0 +1,490 @@ +/* Interface to stdio buffered FILE io */ +/* author: klacke@kaja.klacke.net */ +/* Created : 22 Nov 1999 by Claes Wikstrom */ + +#include + +#define USE_STDIO + +#ifdef WIN32 +#define USE_STDIO +#include + +#else + +#include +#include +#include +#include +#endif + +#include "erl_driver.h" +#ifndef ERL_DRV_NIL +#include "erl_driver_compat.h" +#endif + +#ifdef USE_STDIO +# include +#else + +# define malloc(s) driver_alloc(s) +# define realloc(p, s) driver_realloc(p, s) +# define free(p) driver_free(p) + +# define BINTERFACE static +# include "bbio.c" +# define FILE bFILE +# define clearerr bclearerr +# define fclose bfclose +# define feof bfeof +# define ferror bferror +# define fflush bfflush +# define fgets bfgets +# define fileno bfileno +# define fopen bfopen +# define fread bfread +# define fseek bfseek +# define ftell bftell +# define fwrite bfwrite +# define getc bgetc +# define ungetc bungetc +#endif + + +#define get_int32(s) ((((unsigned char*) (s))[0] << 24) | \ + (((unsigned char*) (s))[1] << 16) | \ + (((unsigned char*) (s))[2] << 8) | \ + (((unsigned char*) (s))[3])) + +#define put_int32(i, s) {((char*)(s))[0] = (char)((i) >> 24) & 0xff; \ + ((char*)(s))[1] = (char)((i) >> 16) & 0xff; \ + ((char*)(s))[2] = (char)((i) >> 8) & 0xff; \ + ((char*)(s))[3] = (char)((i) & 0xff);} +/* op codes */ + +#define XX_OPEN 'o' +#define XX_CLOSE 'c' +#define XX_READ 'r' +#define XX_WRITE 'w' +#define XX_SEEK 's' +#define XX_TELL 't' +#define XX_TRUNCATE 'T' +#define XX_FLUSH 'f' +#define XX_OEOF 'e' +#define XX_ERROR 'E' +#define XX_GETC 'g' +#define XX_GETS 'G' +#define XX_GETS2 '2' +#define XX_SET_LINEBUF_SIZE 'S' +#define XX_UNGETC 'u' + + + +/* return codes */ +#define XX_VALUE 'v' +#define XX_FLINE 'L' +#define XX_OK 'o' +#define XX_I32 'O' +#define XX_NOLINE 'N' +#define XX_FERROR 'E' +#define XX_REOF 'x' + + +#ifdef WIN32 +#define XX_EINVAL WSAEINVAL + + +#else +#define XX_EINVAL EINVAL + +#endif + + +static ErlDrvData FILE_start(ErlDrvPort port, char *buf); +static void FILE_stop(ErlDrvData drv_data); + +static ErlDrvEntry FILE_driver_entry; + +typedef struct _desc { + ErlDrvPort port; + FILE *fp; + int linebuf_size; +} Desc; + + +static ErlDrvData FILE_start(ErlDrvPort port, char *buf) +{ + Desc *d = (Desc*) driver_alloc(sizeof (Desc)); + + if (d == NULL) + return (ErlDrvData) -1; + d->fp = NULL; + d->port = port; + d->linebuf_size = 255; /* default line size */ + + set_port_control_flags(port, PORT_CONTROL_FLAG_BINARY); + + return (ErlDrvData) d; +} + + +static void FILE_stop(ErlDrvData drv_data) +{ + Desc *d = (Desc*) drv_data; + if (d->fp) + fclose(d->fp); + driver_free(d); +} + + +static char *driver_error(ErlDrvPort port, int err) +{ + char response[256]; /* Response buffer. */ + char* s; + char* t; + ErlDrvBinary* bin; + + bin = driver_alloc_binary(1); + bin->orig_bytes[0] = XX_FERROR; + + response[0] = XX_FERROR; + for (s = erl_errno_id(err), t = bin->orig_bytes + 1; *s; s++, t++) + *t = tolower(*s); + return (char *)bin; +} + +static char *driver_ret32(ErlDrvPort port, unsigned int r) +{ + char ch = XX_I32; + ErlDrvBinary* bin; + + bin = driver_alloc_binary(1); + bin->orig_bytes[0] = ch; + put_int32(r, bin->orig_bytes + 1); + return (char *)bin; +} + +static char *driver_ok(ErlDrvPort port) +{ + char ch = XX_OK; + ErlDrvBinary* bin; + bin = driver_alloc_binary(1); + bin->orig_bytes[0] = ch; + return (char *)bin; +} + +static char *driver_eof(ErlDrvPort port) +{ + char ch = XX_REOF; + ErlDrvBinary* bin; + bin = driver_alloc_binary(1); + bin->orig_bytes[0] = ch; + return (char *)bin; +} + + +static int FILE_control(ErlDrvData drv_data, + unsigned int command, + char *buf, int len, + char **rbuf, int rlen) +{ + Desc *desc = (Desc*) drv_data; + ErlDrvBinary* bin; + + switch (command) { + + case XX_OPEN: { + char file[BUFSIZ]; /* should be FILENAME_MAX */ + char flags[4]; /* at most someething like rb+ */ + char* src; + char* dst; + char* src_end; + char* dst_end; + + if (desc->fp != NULL) { + *rbuf = driver_error(desc->port, XX_EINVAL); + return 1; + } + + /* play it safe ? */ + src = buf; + src_end = buf + len; + + /* get file name */ + dst = file; + dst_end = dst + BUFSIZ; /* make room for a '\0' */ + while((src < src_end) && (dst < dst_end) && (*src != '\0')) + *dst++ = *src++; + if ((src == src_end) || (dst == dst_end)) { + driver_error(desc->port, XX_EINVAL); + } + *dst = *src++; + /* get flags */ + dst = flags; + dst_end = dst + 4; + while((src < src_end) && (dst < dst_end) && (*src != '\0')) + *dst++ = *src++; + if (dst == dst_end) { + *rbuf = driver_error(desc->port, XX_EINVAL); + return 1; + } + *dst = '\0'; + + if (src + 1 != src_end) { + *rbuf = driver_error(desc->port, XX_EINVAL); + return 1; + } + + if ((desc->fp = fopen(file, flags))==NULL) { + *rbuf = driver_error(desc->port, errno); + return 1; + } + *rbuf = driver_ok(desc->port); + return 1; + break; + } + + case XX_WRITE: { + if (fwrite(buf, 1, len, desc->fp) != len) { + *rbuf = driver_error(desc->port, errno); + return 1; + } + *rbuf = driver_ok(desc->port); + return 1; + break; + } + + case XX_READ: { + char ch = XX_VALUE; + int rval; + int sz = get_int32(buf); + + if ((bin = driver_alloc_binary(sz + 1)) == NULL) { + *rbuf = driver_error(desc->port, -1); + return 1; + } + + bin->orig_bytes[0] = ch; + if ((rval = fread(bin->orig_bytes + 1, 1, sz, desc->fp)) != sz) { + if (feof(desc->fp)) { + if (rval == 0) { + driver_free_binary(bin); + *rbuf = driver_eof(desc->port); + return 1; + } + bin = driver_realloc_binary(bin, rval + 1); + *rbuf = (char *)bin; + return 1; + } + driver_free_binary(bin); + *rbuf = driver_error(desc->port, errno); + return 1; + } + *rbuf = (char *)bin; + return 1; + } + + case XX_SEEK: { + int offs = get_int32(buf); + int w = (int) buf[4]; + int whence; + switch (w) { + case 1: whence = SEEK_SET; break; + case 2: whence = SEEK_CUR; break; + case 3: whence = SEEK_END; break; + } + if ((w = fseek(desc->fp, offs, whence)) != 0) { + *rbuf = driver_error(desc->port, errno); + return 1; + } + *rbuf = driver_ok(desc->port); + return 1; + } + + case XX_TELL: { + int offs; + if ((offs = ftell(desc->fp)) == -1) { + *rbuf = driver_error(desc->port, errno); + return 1; + } + *rbuf = driver_ret32(desc->port, offs); + return 1; + break; + } + + case XX_TRUNCATE: { + int fno; + int offs; + /* is this really safe? */ + if (fflush(desc->fp) != 0) { + *rbuf = driver_error(desc->port, errno); + return 1; + } + if ((offs = ftell(desc->fp)) == -1) { + *rbuf = driver_error(desc->port, errno); + return 1; + } + fno = fileno(desc->fp); +#ifdef WIN32 + if (SetEndOfFile((HANDLE)fno) != 0) { + *rbuf = driver_error(desc->port, GetLastError()); + return 1; + } +#else + if (ftruncate(fno, offs) == -1) { + *rbuf = driver_error(desc->port, errno); + return 1; + } +#endif + *rbuf = driver_ok(desc->port); + return 1; + } + + case XX_FLUSH: + if (fflush(desc->fp) != 0) + *rbuf = driver_error(desc->port, errno); + else + *rbuf = driver_ok(desc->port); + return 1; + break; + + case XX_OEOF: + if (feof(desc->fp)) + *rbuf = driver_ret32(desc->port, 1); + else + *rbuf = driver_ret32(desc->port, 0); + return 1; + break; + + case XX_ERROR: + if (ferror(desc->fp)) + *rbuf = driver_ret32(desc->port, 1); + else + *rbuf = driver_ret32(desc->port,0); + return 1; + break; + + case XX_GETC: { + int ch; + if ((ch = getc(desc->fp)) == EOF) { + if (feof(desc->fp)) { + *rbuf = driver_eof(desc->port); + return 1; + } + *rbuf = driver_error(desc->port, errno); + return 1; + } + *rbuf = driver_ret32(desc->port, ch); + return 1; + break; + } + + case XX_SET_LINEBUF_SIZE: { + int sz = get_int32(buf); + desc->linebuf_size = sz; + *rbuf = driver_ok(desc->port); + return 1; + break; + } + + case XX_GETS: + case XX_GETS2: { + int rval; + long cpos1, cpos2; + char header; + + if ((bin = driver_alloc_binary(desc->linebuf_size + 1)) == NULL) { + *rbuf = driver_error(desc->port, -1); + return 1; + } + + if ((cpos1 = ftell(desc->fp)) == -1) { + driver_free_binary(bin); + *rbuf = driver_error(desc->port, errno); + return 1; + } + + if ((fgets(bin->orig_bytes + 1, desc->linebuf_size, + desc->fp)) == NULL) { + driver_free_binary(bin); + if (feof(desc->fp)) { + *rbuf = driver_eof(desc->port); + return 1; + } + *rbuf = driver_error(desc->port, errno); + return 1; + } + if ((cpos2 = ftell(desc->fp)) == -1) { + driver_free_binary(bin); + *rbuf = driver_error(desc->port, errno); + return 1; + } + rval = cpos2 - cpos1; + + if (bin->orig_bytes[rval] == '\n' && + bin->orig_bytes[rval + 1] == 0) { + header = XX_FLINE; + /* GETS keep newline, GETS2 remove newline */ + rval = rval - (command == XX_GETS ? 0 : 1); + } + else + header = XX_NOLINE; + bin->orig_bytes[0] = header; + bin = driver_realloc_binary(bin, rval + 1); + *rbuf = (char *)bin; + return 1; + } + + case XX_UNGETC: { + int ch = buf[0]; + if (ungetc(ch, desc->fp) == EOF) + *rbuf = driver_error(desc->port, errno); + else + *rbuf = driver_ok(desc->port); + return 1; + break; + } + + default: +#ifdef DEBUG + fprintf(stderr, "Unknown opcode %c\n\r", command); +#endif + *rbuf = driver_error(desc->port, XX_EINVAL); + return 1; + break; + } + + +} + +static void FILE_finish() +{ +#ifndef USE_STDIO + /* + * Make sure any remaining buffers are flushed (this is done on exit() by + * the normal stdio). + */ + bbio_cleanup(); +#endif +} + + +/* + * Initialize and return a driver entry struct + */ + +DRIVER_INIT(FILE_drv) +{ + FILE_driver_entry.init = NULL; /* Not used */ + FILE_driver_entry.start = FILE_start; + FILE_driver_entry.stop = FILE_stop; + FILE_driver_entry.output = NULL; + FILE_driver_entry.ready_input = NULL; + FILE_driver_entry.ready_output = NULL; + FILE_driver_entry.driver_name = "FILE_drv"; + FILE_driver_entry.finish = FILE_finish; + FILE_driver_entry.outputv = NULL; + FILE_driver_entry.control = FILE_control; + return &FILE_driver_entry; +} + diff --git a/bfile/c_src/Makefile b/bfile/c_src/Makefile new file mode 100644 index 0000000..3f232d2 --- /dev/null +++ b/bfile/c_src/Makefile @@ -0,0 +1,42 @@ + +include ../config/include.mk + +## don't build this under win32 at all +ifdef WIN32 + +PRIV_FILES = + +else + +PRIV_FILES=../priv/FILE_drv.so + +endif + + +CFLAGS += -I$(ERL_C_INCLUDE_DIR) -I../config -I. + +# +# Targets +# + +all: $(PRIV_FILES) + +clean: + -rm -f $(PRIV_FILES) FILE_drv.o + + +install: + install -d $(ERLDIR)/lib/bfile + cp -r `pwd` $(ERLDIR)/lib/bfile + cp -r `pwd`/../ebin $(ERLDIR)/lib/bfile + cp -r `pwd`/../priv $(ERLDIR)/lib/bfile + + +../priv/FILE_drv.so: FILE_drv.o + $(LD_SHARED) -o $@ FILE_drv.o $(LIBS) + + +FILE_drv.o: FILE_drv.c + $(CC) -o $@ -c -fpic $(CFLAGS) -DDYNAMIC_DRIVER FILE_drv.c + + diff --git a/bfile/config/Makefile b/bfile/config/Makefile new file mode 100644 index 0000000..d82abf9 --- /dev/null +++ b/bfile/config/Makefile @@ -0,0 +1,21 @@ +MK_INCLUDE=include.mk +CONFIG_H=config.h + +all: config.status $(MK_INCLUDE) $(CONFIG_H) + +config.status: configure + ./configure + +$(CONFIG_H) $(MK_INCLUDE): config.status include.mk.in config.h.in + ./config.status + +configure: configure.in + autoheader + autoconf + +clean: + -rm -f config.cache config.log config.status configure \ + $(MK_INCLUDE) $(CONFIG_H) + -rm -rf autom4te.cache + + diff --git a/bfile/config/acconfig.h b/bfile/config/acconfig.h new file mode 100644 index 0000000..aca8d35 --- /dev/null +++ b/bfile/config/acconfig.h @@ -0,0 +1,269 @@ +#ifndef _CONFIG_H_ +#define _CONFIG_H_ + +#ifndef WIN32 + +@TOP@ + +/* + * This file contains prototypes for config.h.in which autoheader + * could not figure by itself. I.e. if you write your own test which + * defines a macro you will probably have to put it here, but if you + * use any standard test AC_* it will be detected by autoheader. + */ + +#undef WIN32 + +/* Define to a string defining your target. ($target in configure.in) */ +#undef CPU_VENDOR_OS + + +/* Define to the full path of the ifconfig program */ +#undef IFCONFIG + +/* Define to the full path of the route program */ +#undef ROUTE + +/* Define to the full path of the arp program */ +#undef ARP + + +/* + * These four below should definately be done away with!! + */ + +/* Define if your target OS is a BSD derivative */ +#undef BSD + +/* Define if your target OS is Linux */ +#undef LINUX + +/* Define if your target OS is SunOS 5.x */ +#undef SOLARIS + +/* Define if your target OS is BSD/OS 4.x */ +#undef BSDI + + +/* + * What are these??? + */ + +/* ? */ +#undef USE_IFALIAS + +/* Define if you wish to use the bpf interface */ +#undef USE_BPF + +/* Define if you wish to use the dlpi interface */ +#undef USE_DLPI + +/* ? */ +#undef USE_SOCKET + + +/* Define if prototypes for malloc can be found in stdlib.h */ +#undef STDLIB_MALLOC + +/* Define if your include files defines a prototype for sys_errlist[] */ +#undef HAVE_SYS_ERRLIST + +/* This isn't used anywhere (that I could find) so I don't know what it means*/ +#undef IFCONFIG_REQUIRES_DOWN_ADDRESS + +/* Define if your OS have broken cmsg fields in the msghdr struct (Linux) */ +#undef BROKEN_CMSG_FIELDS + +/* Define if sockaddr structure has sa_len member */ +#undef SA_LEN_IN_SOCKADDR + + + +/* + * Provide a common way to refer to ints with specific size. (Is this + * used everywhere?) The names used are {u_,}int{8,16,32,64}_t unless + * they are defined in sys/types.h they are defined in ints.h using + * the BIT macros. e.g. if int8_t isn't defined in sys/types.h int8_t + * is typedef:ed to BIT8. + * + */ + +/* Define to a basic signed type that is 8 bits in size */ +#undef BIT8 + +/* Define to a basic signed type that is 16 bits in size */ +#undef BIT16 + +/* Define to a basic signed type that is 32 bits in size */ +#undef BIT32 + +/* Define to a basic signed type that is 64 bits in size */ +#undef BIT64 + +/* Define if sys/types.h defines this type */ +#undef HAVE_int8_t + +/* Define if sys/types.h defines this type */ +#undef HAVE_u_int8_t + +/* Define if sys/types.h defines this type */ +#undef HAVE_int16_t + +/* Define if sys/types.h defines this type */ +#undef HAVE_u_int16_t + +/* Define if sys/types.h defines this type */ +#undef HAVE_int32_t + +/* Define if sys/types.h defines this type */ +#undef HAVE_u_int32_t + +/* Define if sys/types.h defines this type */ +#undef HAVE_int64_t + +/* Define if sys/types.h defines this type */ +#undef HAVE_u_int64_t + + + + + + +/* */ +#undef ETHER_HEADER_USES_ETHER_ADDR + +/* */ +#undef HAVE_DLIOCRAW + +/* */ +#undef HAVE_ETHERADDRL + +/* */ +#undef HAVE_ETHER_ADDR_LEN + +/* */ +#undef HAVE_MSGHDR_MSG_CONTROL + +/* */ +#undef HAVE_SIOCGARP + +/* */ +#undef HAVE_SIOCGIFCONF + +/* */ +#undef HAVE_SIOCGIFHWADDR + +/* */ +#undef HAVE_arpreq + +/* */ +#undef HAVE_caddr_t + +/* */ +#undef HAVE_ether_header + +/* */ +#undef HAVE_ethhdr + +/* */ +#undef HAVE_ifnet + +/* */ +#undef HAVE_in_addr + +/* */ +#undef HAVE_sockaddr + +/* */ +#undef HAVE_sockaddr_dl + +/* */ +#undef HAVE_sockaddr_in + +/* */ +#undef NEED_HAVE_sockaddr_dl + + + +/* + * I have yet to figure out what all this need_* stuff is for, shouldn't + * it suffice with using have_* ??? + */ + +/* */ +#undef NEED_LINUX_SOCKIOS_H + +/* */ +#undef NEED_NETINET_IF_ETHER_H + +/* */ +#undef NEED_NETINET_IN_H + +/* */ +#undef NEED_NET_ETHERNET_H + +/* */ +#undef NEED_NET_IF_ARP_H + +/* */ +#undef NEED_NET_IF_DL_H + +/* */ +#undef NEED_NET_IF_H + +/* */ +#undef NEED_SYS_BITYPES_H + +/* */ +#undef NEED_SYS_DLPI_H + +/* */ +#undef NEED_SYS_ETHERNET_H + +/* */ +#undef NEED_SYS_SOCKETIO_H + +/* */ +#undef NEED_SYS_SOCKET_H + +/* */ +#undef NEED_SYS_SOCKIO_H + +/* */ +#undef NEED_SYS_TYPES_H + +/* if we have the openssl with engine support */ +#undef HAVE_SSL_ENGINE +/* ... with Rainbow patches */ +#undef HAVE_RAINBOW_PATCHES +/* ... with Rainbow's libswift */ +#undef HAVE_SWIFT +/* ... with one of our HW checks */ +#undef HAVE_LOCAL_ENGINE_SETUP +#undef HAVE_SSL_HW_CHECK +/* ... with patch to get cfg password from card */ +#undef HAVE_ENGINE_GET_PASSWORD +/* ... with our buffer patches */ +#undef HAVE_SSL_BUFFER_CB + +/* */ +#undef ISD_SYSTEM_VSN + +/* eventpoll */ +#undef HAVE_KPOLL + +/* Have/use netfilter (a.k.a. iptables) */ +#undef HAVE_NETFILTER + +/* Non-standard support for "non-local connect()" in Linux 2.4 */ +#undef HAVE_NONLOCAL_CONNECT + +/* atomic asmebler op in asm/atomic.h ? */ +#undef HAVE_ATOMIC_OPS + + +@BOTTOM@ + +#endif /* WIN32 */ +#endif /* _CONFIG_H_ */ + diff --git a/bfile/config/aclocal.m4 b/bfile/config/aclocal.m4 new file mode 100644 index 0000000..e80972c --- /dev/null +++ b/bfile/config/aclocal.m4 @@ -0,0 +1,32 @@ +dnl ---------------------------------------------------------------------- +dnl +dnl BT_MSG_CONTROL checks for msg_control member in msghdr and that +dnl the cmsg fields aren't broken... +dnl + +AC_DEFUN(BT_MSG_CONTROL, +[ +AC_CACHE_CHECK([for msg_control member in msghdr], + bt_cv_have_msghdr_msg_control, +[AC_TRY_COMPILE([#include +#include ], + [struct msghdr msg; + msg.msg_control;], + bt_cv_have_msghdr_msg_control=yes, bt_cv_have_msghdr_msg_control=no)]) +if test $bt_cv_have_msghdr_msg_control = yes; then + AC_DEFINE(HAVE_MSGHDR_MSG_CONTROL) +fi + +if test $bt_cv_have_msghdr_msg_control = yes; then + AC_MSG_CHECKING(for broken CMSG_FIELDS) + case "$target_os" in + linux*) + AC_DEFINE(BROKEN_CMSG_FIELDS) + AC_MSG_RESULT(yes) + ;; + *) + AC_MSG_RESULT(no) + ;; + esac +fi +]) diff --git a/bfile/config/config.guess b/bfile/config/config.guess new file mode 100755 index 0000000..fd30ab0 --- /dev/null +++ b/bfile/config/config.guess @@ -0,0 +1,1354 @@ +#! /bin/sh +# Attempt to guess a canonical system name. +# Copyright (C) 1992, 1993, 1994, 1995, 1996, 1997, 1998, 1999, +# 2000, 2001, 2002 Free Software Foundation, Inc. + +timestamp='2002-07-23' + +# This file 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# +# As a special exception to the GNU General Public License, if you +# distribute this file as part of a program that contains a +# configuration script generated by Autoconf, you may include it under +# the same distribution terms that you use for the rest of that program. + +# Originally written by Per Bothner . +# Please send patches to . Submit a context +# diff and a properly formatted ChangeLog entry. +# +# This script attempts to guess a canonical system name similar to +# config.sub. If it succeeds, it prints the system name on stdout, and +# exits with 0. Otherwise, it exits with 1. +# +# The plan is that this can be called by configure scripts if you +# don't specify an explicit build system type. + +me=`echo "$0" | sed -e 's,.*/,,'` + +usage="\ +Usage: $0 [OPTION] + +Output the configuration name of the system \`$me' is run on. + +Operation modes: + -h, --help print this help, then exit + -t, --time-stamp print date of last modification, then exit + -v, --version print version number, then exit + +Report bugs and patches to ." + +version="\ +GNU config.guess ($timestamp) + +Originally written by Per Bothner. +Copyright (C) 1992, 1993, 1994, 1995, 1996, 1997, 1998, 1999, 2000, 2001 +Free Software Foundation, Inc. + +This is free software; see the source for copying conditions. There is NO +warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE." + +help=" +Try \`$me --help' for more information." + +# Parse command line +while test $# -gt 0 ; do + case $1 in + --time-stamp | --time* | -t ) + echo "$timestamp" ; exit 0 ;; + --version | -v ) + echo "$version" ; exit 0 ;; + --help | --h* | -h ) + echo "$usage"; exit 0 ;; + -- ) # Stop option processing + shift; break ;; + - ) # Use stdin as input. + break ;; + -* ) + echo "$me: invalid option $1$help" >&2 + exit 1 ;; + * ) + break ;; + esac +done + +if test $# != 0; then + echo "$me: too many arguments$help" >&2 + exit 1 +fi + +trap 'exit 1' 1 2 15 + +# CC_FOR_BUILD -- compiler used by this script. Note that the use of a +# compiler to aid in system detection is discouraged as it requires +# temporary files to be created and, as you can see below, it is a +# headache to deal with in a portable fashion. + +# Historically, `CC_FOR_BUILD' used to be named `HOST_CC'. We still +# use `HOST_CC' if defined, but it is deprecated. + +# This shell variable is my proudest work .. or something. --bje + +set_cc_for_build='tmpdir=${TMPDIR-/tmp}/config-guess-$$ ; +(old=`umask` && umask 077 && mkdir $tmpdir && umask $old && unset old) + || (echo "$me: cannot create $tmpdir" >&2 && exit 1) ; +dummy=$tmpdir/dummy ; +files="$dummy.c $dummy.o $dummy.rel $dummy" ; +trap '"'"'rm -f $files; rmdir $tmpdir; exit 1'"'"' 1 2 15 ; +case $CC_FOR_BUILD,$HOST_CC,$CC in + ,,) echo "int x;" > $dummy.c ; + for c in cc gcc c89 c99 ; do + if ($c $dummy.c -c -o $dummy.o) >/dev/null 2>&1 ; then + CC_FOR_BUILD="$c"; break ; + fi ; + done ; + rm -f $files ; + if test x"$CC_FOR_BUILD" = x ; then + CC_FOR_BUILD=no_compiler_found ; + fi + ;; + ,,*) CC_FOR_BUILD=$CC ;; + ,*,*) CC_FOR_BUILD=$HOST_CC ;; +esac ; +unset files' + +# This is needed to find uname on a Pyramid OSx when run in the BSD universe. +# (ghazi@noc.rutgers.edu 1994-08-24) +if (test -f /.attbin/uname) >/dev/null 2>&1 ; then + PATH=$PATH:/.attbin ; export PATH +fi + +UNAME_MACHINE=`(uname -m) 2>/dev/null` || UNAME_MACHINE=unknown +UNAME_RELEASE=`(uname -r) 2>/dev/null` || UNAME_RELEASE=unknown +UNAME_SYSTEM=`(uname -s) 2>/dev/null` || UNAME_SYSTEM=unknown +UNAME_VERSION=`(uname -v) 2>/dev/null` || UNAME_VERSION=unknown + +# Note: order is significant - the case branches are not exclusive. + +case "${UNAME_MACHINE}:${UNAME_SYSTEM}:${UNAME_RELEASE}:${UNAME_VERSION}" in + *:NetBSD:*:*) + # NetBSD (nbsd) targets should (where applicable) match one or + # more of the tupples: *-*-netbsdelf*, *-*-netbsdaout*, + # *-*-netbsdecoff* and *-*-netbsd*. For targets that recently + # switched to ELF, *-*-netbsd* would select the old + # object file format. This provides both forward + # compatibility and a consistent mechanism for selecting the + # object file format. + # + # Note: NetBSD doesn't particularly care about the vendor + # portion of the name. We always set it to "unknown". + sysctl="sysctl -n hw.machine_arch" + UNAME_MACHINE_ARCH=`(/sbin/$sysctl 2>/dev/null || \ + /usr/sbin/$sysctl 2>/dev/null || echo unknown)` + case "${UNAME_MACHINE_ARCH}" in + armeb) machine=armeb-unknown ;; + arm*) machine=arm-unknown ;; + sh3el) machine=shl-unknown ;; + sh3eb) machine=sh-unknown ;; + *) machine=${UNAME_MACHINE_ARCH}-unknown ;; + esac + # The Operating System including object format, if it has switched + # to ELF recently, or will in the future. + case "${UNAME_MACHINE_ARCH}" in + arm*|i386|m68k|ns32k|sh3*|sparc|vax) + eval $set_cc_for_build + if echo __ELF__ | $CC_FOR_BUILD -E - 2>/dev/null \ + | grep __ELF__ >/dev/null + then + # Once all utilities can be ECOFF (netbsdecoff) or a.out (netbsdaout). + # Return netbsd for either. FIX? + os=netbsd + else + os=netbsdelf + fi + ;; + *) + os=netbsd + ;; + esac + # The OS release + release=`echo ${UNAME_RELEASE}|sed -e 's/[-_].*/\./'` + # Since CPU_TYPE-MANUFACTURER-KERNEL-OPERATING_SYSTEM: + # contains redundant information, the shorter form: + # CPU_TYPE-MANUFACTURER-OPERATING_SYSTEM is used. + echo "${machine}-${os}${release}" + exit 0 ;; + amiga:OpenBSD:*:*) + echo m68k-unknown-openbsd${UNAME_RELEASE} + exit 0 ;; + arc:OpenBSD:*:*) + echo mipsel-unknown-openbsd${UNAME_RELEASE} + exit 0 ;; + hp300:OpenBSD:*:*) + echo m68k-unknown-openbsd${UNAME_RELEASE} + exit 0 ;; + mac68k:OpenBSD:*:*) + echo m68k-unknown-openbsd${UNAME_RELEASE} + exit 0 ;; + macppc:OpenBSD:*:*) + echo powerpc-unknown-openbsd${UNAME_RELEASE} + exit 0 ;; + mvme68k:OpenBSD:*:*) + echo m68k-unknown-openbsd${UNAME_RELEASE} + exit 0 ;; + mvme88k:OpenBSD:*:*) + echo m88k-unknown-openbsd${UNAME_RELEASE} + exit 0 ;; + mvmeppc:OpenBSD:*:*) + echo powerpc-unknown-openbsd${UNAME_RELEASE} + exit 0 ;; + pmax:OpenBSD:*:*) + echo mipsel-unknown-openbsd${UNAME_RELEASE} + exit 0 ;; + sgi:OpenBSD:*:*) + echo mipseb-unknown-openbsd${UNAME_RELEASE} + exit 0 ;; + sun3:OpenBSD:*:*) + echo m68k-unknown-openbsd${UNAME_RELEASE} + exit 0 ;; + wgrisc:OpenBSD:*:*) + echo mipsel-unknown-openbsd${UNAME_RELEASE} + exit 0 ;; + *:OpenBSD:*:*) + echo ${UNAME_MACHINE}-unknown-openbsd${UNAME_RELEASE} + exit 0 ;; + alpha:OSF1:*:*) + if test $UNAME_RELEASE = "V4.0"; then + UNAME_RELEASE=`/usr/sbin/sizer -v | awk '{print $3}'` + fi + # A Vn.n version is a released version. + # A Tn.n version is a released field test version. + # A Xn.n version is an unreleased experimental baselevel. + # 1.2 uses "1.2" for uname -r. + eval $set_cc_for_build + cat <$dummy.s + .data +\$Lformat: + .byte 37,100,45,37,120,10,0 # "%d-%x\n" + + .text + .globl main + .align 4 + .ent main +main: + .frame \$30,16,\$26,0 + ldgp \$29,0(\$27) + .prologue 1 + .long 0x47e03d80 # implver \$0 + lda \$2,-1 + .long 0x47e20c21 # amask \$2,\$1 + lda \$16,\$Lformat + mov \$0,\$17 + not \$1,\$18 + jsr \$26,printf + ldgp \$29,0(\$26) + mov 0,\$16 + jsr \$26,exit + .end main +EOF + $CC_FOR_BUILD $dummy.s -o $dummy 2>/dev/null + if test "$?" = 0 ; then + case `$dummy` in + 0-0) + UNAME_MACHINE="alpha" + ;; + 1-0) + UNAME_MACHINE="alphaev5" + ;; + 1-1) + UNAME_MACHINE="alphaev56" + ;; + 1-101) + UNAME_MACHINE="alphapca56" + ;; + 2-303) + UNAME_MACHINE="alphaev6" + ;; + 2-307) + UNAME_MACHINE="alphaev67" + ;; + 2-1307) + UNAME_MACHINE="alphaev68" + ;; + 3-1307) + UNAME_MACHINE="alphaev7" + ;; + esac + fi + rm -f $dummy.s $dummy && rmdir $tmpdir + echo ${UNAME_MACHINE}-dec-osf`echo ${UNAME_RELEASE} | sed -e 's/^[VTX]//' | tr 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 'abcdefghijklmnopqrstuvwxyz'` + exit 0 ;; + Alpha\ *:Windows_NT*:*) + # How do we know it's Interix rather than the generic POSIX subsystem? + # Should we change UNAME_MACHINE based on the output of uname instead + # of the specific Alpha model? + echo alpha-pc-interix + exit 0 ;; + 21064:Windows_NT:50:3) + echo alpha-dec-winnt3.5 + exit 0 ;; + Amiga*:UNIX_System_V:4.0:*) + echo m68k-unknown-sysv4 + exit 0;; + *:[Aa]miga[Oo][Ss]:*:*) + echo ${UNAME_MACHINE}-unknown-amigaos + exit 0 ;; + *:[Mm]orph[Oo][Ss]:*:*) + echo ${UNAME_MACHINE}-unknown-morphos + exit 0 ;; + *:OS/390:*:*) + echo i370-ibm-openedition + exit 0 ;; + arm:RISC*:1.[012]*:*|arm:riscix:1.[012]*:*) + echo arm-acorn-riscix${UNAME_RELEASE} + exit 0;; + SR2?01:HI-UX/MPP:*:* | SR8000:HI-UX/MPP:*:*) + echo hppa1.1-hitachi-hiuxmpp + exit 0;; + Pyramid*:OSx*:*:* | MIS*:OSx*:*:* | MIS*:SMP_DC-OSx*:*:*) + # akee@wpdis03.wpafb.af.mil (Earle F. Ake) contributed MIS and NILE. + if test "`(/bin/universe) 2>/dev/null`" = att ; then + echo pyramid-pyramid-sysv3 + else + echo pyramid-pyramid-bsd + fi + exit 0 ;; + NILE*:*:*:dcosx) + echo pyramid-pyramid-svr4 + exit 0 ;; + DRS?6000:UNIX_SV:4.2*:7*) + case `/usr/bin/uname -p` in + sparc) echo sparc-icl-nx7 && exit 0 ;; + esac ;; + sun4H:SunOS:5.*:*) + echo sparc-hal-solaris2`echo ${UNAME_RELEASE}|sed -e 's/[^.]*//'` + exit 0 ;; + sun4*:SunOS:5.*:* | tadpole*:SunOS:5.*:*) + echo sparc-sun-solaris2`echo ${UNAME_RELEASE}|sed -e 's/[^.]*//'` + exit 0 ;; + i86pc:SunOS:5.*:*) + echo i386-pc-solaris2`echo ${UNAME_RELEASE}|sed -e 's/[^.]*//'` + exit 0 ;; + sun4*:SunOS:6*:*) + # According to config.sub, this is the proper way to canonicalize + # SunOS6. Hard to guess exactly what SunOS6 will be like, but + # it's likely to be more like Solaris than SunOS4. + echo sparc-sun-solaris3`echo ${UNAME_RELEASE}|sed -e 's/[^.]*//'` + exit 0 ;; + sun4*:SunOS:*:*) + case "`/usr/bin/arch -k`" in + Series*|S4*) + UNAME_RELEASE=`uname -v` + ;; + esac + # Japanese Language versions have a version number like `4.1.3-JL'. + echo sparc-sun-sunos`echo ${UNAME_RELEASE}|sed -e 's/-/_/'` + exit 0 ;; + sun3*:SunOS:*:*) + echo m68k-sun-sunos${UNAME_RELEASE} + exit 0 ;; + sun*:*:4.2BSD:*) + UNAME_RELEASE=`(sed 1q /etc/motd | awk '{print substr($5,1,3)}') 2>/dev/null` + test "x${UNAME_RELEASE}" = "x" && UNAME_RELEASE=3 + case "`/bin/arch`" in + sun3) + echo m68k-sun-sunos${UNAME_RELEASE} + ;; + sun4) + echo sparc-sun-sunos${UNAME_RELEASE} + ;; + esac + exit 0 ;; + aushp:SunOS:*:*) + echo sparc-auspex-sunos${UNAME_RELEASE} + exit 0 ;; + # The situation for MiNT is a little confusing. The machine name + # can be virtually everything (everything which is not + # "atarist" or "atariste" at least should have a processor + # > m68000). The system name ranges from "MiNT" over "FreeMiNT" + # to the lowercase version "mint" (or "freemint"). Finally + # the system name "TOS" denotes a system which is actually not + # MiNT. But MiNT is downward compatible to TOS, so this should + # be no problem. + atarist[e]:*MiNT:*:* | atarist[e]:*mint:*:* | atarist[e]:*TOS:*:*) + echo m68k-atari-mint${UNAME_RELEASE} + exit 0 ;; + atari*:*MiNT:*:* | atari*:*mint:*:* | atarist[e]:*TOS:*:*) + echo m68k-atari-mint${UNAME_RELEASE} + exit 0 ;; + *falcon*:*MiNT:*:* | *falcon*:*mint:*:* | *falcon*:*TOS:*:*) + echo m68k-atari-mint${UNAME_RELEASE} + exit 0 ;; + milan*:*MiNT:*:* | milan*:*mint:*:* | *milan*:*TOS:*:*) + echo m68k-milan-mint${UNAME_RELEASE} + exit 0 ;; + hades*:*MiNT:*:* | hades*:*mint:*:* | *hades*:*TOS:*:*) + echo m68k-hades-mint${UNAME_RELEASE} + exit 0 ;; + *:*MiNT:*:* | *:*mint:*:* | *:*TOS:*:*) + echo m68k-unknown-mint${UNAME_RELEASE} + exit 0 ;; + powerpc:machten:*:*) + echo powerpc-apple-machten${UNAME_RELEASE} + exit 0 ;; + RISC*:Mach:*:*) + echo mips-dec-mach_bsd4.3 + exit 0 ;; + RISC*:ULTRIX:*:*) + echo mips-dec-ultrix${UNAME_RELEASE} + exit 0 ;; + VAX*:ULTRIX*:*:*) + echo vax-dec-ultrix${UNAME_RELEASE} + exit 0 ;; + 2020:CLIX:*:* | 2430:CLIX:*:*) + echo clipper-intergraph-clix${UNAME_RELEASE} + exit 0 ;; + mips:*:*:UMIPS | mips:*:*:RISCos) + eval $set_cc_for_build + sed 's/^ //' << EOF >$dummy.c +#ifdef __cplusplus +#include /* for printf() prototype */ + int main (int argc, char *argv[]) { +#else + int main (argc, argv) int argc; char *argv[]; { +#endif + #if defined (host_mips) && defined (MIPSEB) + #if defined (SYSTYPE_SYSV) + printf ("mips-mips-riscos%ssysv\n", argv[1]); exit (0); + #endif + #if defined (SYSTYPE_SVR4) + printf ("mips-mips-riscos%ssvr4\n", argv[1]); exit (0); + #endif + #if defined (SYSTYPE_BSD43) || defined(SYSTYPE_BSD) + printf ("mips-mips-riscos%sbsd\n", argv[1]); exit (0); + #endif + #endif + exit (-1); + } +EOF + $CC_FOR_BUILD $dummy.c -o $dummy \ + && $dummy `echo "${UNAME_RELEASE}" | sed -n 's/\([0-9]*\).*/\1/p'` \ + && rm -f $dummy.c $dummy && rmdir $tmpdir && exit 0 + rm -f $dummy.c $dummy && rmdir $tmpdir + echo mips-mips-riscos${UNAME_RELEASE} + exit 0 ;; + Motorola:PowerMAX_OS:*:*) + echo powerpc-motorola-powermax + exit 0 ;; + Night_Hawk:*:*:PowerMAX_OS) + echo powerpc-harris-powermax + exit 0 ;; + Night_Hawk:Power_UNIX:*:*) + echo powerpc-harris-powerunix + exit 0 ;; + m88k:CX/UX:7*:*) + echo m88k-harris-cxux7 + exit 0 ;; + m88k:*:4*:R4*) + echo m88k-motorola-sysv4 + exit 0 ;; + m88k:*:3*:R3*) + echo m88k-motorola-sysv3 + exit 0 ;; + AViiON:dgux:*:*) + # DG/UX returns AViiON for all architectures + UNAME_PROCESSOR=`/usr/bin/uname -p` + if [ $UNAME_PROCESSOR = mc88100 ] || [ $UNAME_PROCESSOR = mc88110 ] + then + if [ ${TARGET_BINARY_INTERFACE}x = m88kdguxelfx ] || \ + [ ${TARGET_BINARY_INTERFACE}x = x ] + then + echo m88k-dg-dgux${UNAME_RELEASE} + else + echo m88k-dg-dguxbcs${UNAME_RELEASE} + fi + else + echo i586-dg-dgux${UNAME_RELEASE} + fi + exit 0 ;; + M88*:DolphinOS:*:*) # DolphinOS (SVR3) + echo m88k-dolphin-sysv3 + exit 0 ;; + M88*:*:R3*:*) + # Delta 88k system running SVR3 + echo m88k-motorola-sysv3 + exit 0 ;; + XD88*:*:*:*) # Tektronix XD88 system running UTekV (SVR3) + echo m88k-tektronix-sysv3 + exit 0 ;; + Tek43[0-9][0-9]:UTek:*:*) # Tektronix 4300 system running UTek (BSD) + echo m68k-tektronix-bsd + exit 0 ;; + *:IRIX*:*:*) + echo mips-sgi-irix`echo ${UNAME_RELEASE}|sed -e 's/-/_/g'` + exit 0 ;; + ????????:AIX?:[12].1:2) # AIX 2.2.1 or AIX 2.1.1 is RT/PC AIX. + echo romp-ibm-aix # uname -m gives an 8 hex-code CPU id + exit 0 ;; # Note that: echo "'`uname -s`'" gives 'AIX ' + i*86:AIX:*:*) + echo i386-ibm-aix + exit 0 ;; + ia64:AIX:*:*) + if [ -x /usr/bin/oslevel ] ; then + IBM_REV=`/usr/bin/oslevel` + else + IBM_REV=${UNAME_VERSION}.${UNAME_RELEASE} + fi + echo ${UNAME_MACHINE}-ibm-aix${IBM_REV} + exit 0 ;; + *:AIX:2:3) + if grep bos325 /usr/include/stdio.h >/dev/null 2>&1; then + eval $set_cc_for_build + sed 's/^ //' << EOF >$dummy.c + #include + + main() + { + if (!__power_pc()) + exit(1); + puts("powerpc-ibm-aix3.2.5"); + exit(0); + } +EOF + $CC_FOR_BUILD $dummy.c -o $dummy && $dummy && rm -f $dummy.c $dummy && rmdir $tmpdir && exit 0 + rm -f $dummy.c $dummy && rmdir $tmpdir + echo rs6000-ibm-aix3.2.5 + elif grep bos324 /usr/include/stdio.h >/dev/null 2>&1; then + echo rs6000-ibm-aix3.2.4 + else + echo rs6000-ibm-aix3.2 + fi + exit 0 ;; + *:AIX:*:[45]) + IBM_CPU_ID=`/usr/sbin/lsdev -C -c processor -S available | sed 1q | awk '{ print $1 }'` + if /usr/sbin/lsattr -El ${IBM_CPU_ID} | grep ' POWER' >/dev/null 2>&1; then + IBM_ARCH=rs6000 + else + IBM_ARCH=powerpc + fi + if [ -x /usr/bin/oslevel ] ; then + IBM_REV=`/usr/bin/oslevel` + else + IBM_REV=${UNAME_VERSION}.${UNAME_RELEASE} + fi + echo ${IBM_ARCH}-ibm-aix${IBM_REV} + exit 0 ;; + *:AIX:*:*) + echo rs6000-ibm-aix + exit 0 ;; + ibmrt:4.4BSD:*|romp-ibm:BSD:*) + echo romp-ibm-bsd4.4 + exit 0 ;; + ibmrt:*BSD:*|romp-ibm:BSD:*) # covers RT/PC BSD and + echo romp-ibm-bsd${UNAME_RELEASE} # 4.3 with uname added to + exit 0 ;; # report: romp-ibm BSD 4.3 + *:BOSX:*:*) + echo rs6000-bull-bosx + exit 0 ;; + DPX/2?00:B.O.S.:*:*) + echo m68k-bull-sysv3 + exit 0 ;; + 9000/[34]??:4.3bsd:1.*:*) + echo m68k-hp-bsd + exit 0 ;; + hp300:4.4BSD:*:* | 9000/[34]??:4.3bsd:2.*:*) + echo m68k-hp-bsd4.4 + exit 0 ;; + 9000/[34678]??:HP-UX:*:*) + HPUX_REV=`echo ${UNAME_RELEASE}|sed -e 's/[^.]*.[0B]*//'` + case "${UNAME_MACHINE}" in + 9000/31? ) HP_ARCH=m68000 ;; + 9000/[34]?? ) HP_ARCH=m68k ;; + 9000/[678][0-9][0-9]) + if [ -x /usr/bin/getconf ]; then + sc_cpu_version=`/usr/bin/getconf SC_CPU_VERSION 2>/dev/null` + sc_kernel_bits=`/usr/bin/getconf SC_KERNEL_BITS 2>/dev/null` + case "${sc_cpu_version}" in + 523) HP_ARCH="hppa1.0" ;; # CPU_PA_RISC1_0 + 528) HP_ARCH="hppa1.1" ;; # CPU_PA_RISC1_1 + 532) # CPU_PA_RISC2_0 + case "${sc_kernel_bits}" in + 32) HP_ARCH="hppa2.0n" ;; + 64) HP_ARCH="hppa2.0w" ;; + '') HP_ARCH="hppa2.0" ;; # HP-UX 10.20 + esac ;; + esac + fi + if [ "${HP_ARCH}" = "" ]; then + eval $set_cc_for_build + sed 's/^ //' << EOF >$dummy.c + + #define _HPUX_SOURCE + #include + #include + + int main () + { + #if defined(_SC_KERNEL_BITS) + long bits = sysconf(_SC_KERNEL_BITS); + #endif + long cpu = sysconf (_SC_CPU_VERSION); + + switch (cpu) + { + case CPU_PA_RISC1_0: puts ("hppa1.0"); break; + case CPU_PA_RISC1_1: puts ("hppa1.1"); break; + case CPU_PA_RISC2_0: + #if defined(_SC_KERNEL_BITS) + switch (bits) + { + case 64: puts ("hppa2.0w"); break; + case 32: puts ("hppa2.0n"); break; + default: puts ("hppa2.0"); break; + } break; + #else /* !defined(_SC_KERNEL_BITS) */ + puts ("hppa2.0"); break; + #endif + default: puts ("hppa1.0"); break; + } + exit (0); + } +EOF + (CCOPTS= $CC_FOR_BUILD $dummy.c -o $dummy 2>/dev/null) && HP_ARCH=`$dummy` + if test -z "$HP_ARCH"; then HP_ARCH=hppa; fi + rm -f $dummy.c $dummy && rmdir $tmpdir + fi ;; + esac + echo ${HP_ARCH}-hp-hpux${HPUX_REV} + exit 0 ;; + ia64:HP-UX:*:*) + HPUX_REV=`echo ${UNAME_RELEASE}|sed -e 's/[^.]*.[0B]*//'` + echo ia64-hp-hpux${HPUX_REV} + exit 0 ;; + 3050*:HI-UX:*:*) + eval $set_cc_for_build + sed 's/^ //' << EOF >$dummy.c + #include + int + main () + { + long cpu = sysconf (_SC_CPU_VERSION); + /* The order matters, because CPU_IS_HP_MC68K erroneously returns + true for CPU_PA_RISC1_0. CPU_IS_PA_RISC returns correct + results, however. */ + if (CPU_IS_PA_RISC (cpu)) + { + switch (cpu) + { + case CPU_PA_RISC1_0: puts ("hppa1.0-hitachi-hiuxwe2"); break; + case CPU_PA_RISC1_1: puts ("hppa1.1-hitachi-hiuxwe2"); break; + case CPU_PA_RISC2_0: puts ("hppa2.0-hitachi-hiuxwe2"); break; + default: puts ("hppa-hitachi-hiuxwe2"); break; + } + } + else if (CPU_IS_HP_MC68K (cpu)) + puts ("m68k-hitachi-hiuxwe2"); + else puts ("unknown-hitachi-hiuxwe2"); + exit (0); + } +EOF + $CC_FOR_BUILD $dummy.c -o $dummy && $dummy && rm -f $dummy.c $dummy && rmdir $tmpdir && exit 0 + rm -f $dummy.c $dummy && rmdir $tmpdir + echo unknown-hitachi-hiuxwe2 + exit 0 ;; + 9000/7??:4.3bsd:*:* | 9000/8?[79]:4.3bsd:*:* ) + echo hppa1.1-hp-bsd + exit 0 ;; + 9000/8??:4.3bsd:*:*) + echo hppa1.0-hp-bsd + exit 0 ;; + *9??*:MPE/iX:*:* | *3000*:MPE/iX:*:*) + echo hppa1.0-hp-mpeix + exit 0 ;; + hp7??:OSF1:*:* | hp8?[79]:OSF1:*:* ) + echo hppa1.1-hp-osf + exit 0 ;; + hp8??:OSF1:*:*) + echo hppa1.0-hp-osf + exit 0 ;; + i*86:OSF1:*:*) + if [ -x /usr/sbin/sysversion ] ; then + echo ${UNAME_MACHINE}-unknown-osf1mk + else + echo ${UNAME_MACHINE}-unknown-osf1 + fi + exit 0 ;; + parisc*:Lites*:*:*) + echo hppa1.1-hp-lites + exit 0 ;; + C1*:ConvexOS:*:* | convex:ConvexOS:C1*:*) + echo c1-convex-bsd + exit 0 ;; + C2*:ConvexOS:*:* | convex:ConvexOS:C2*:*) + if getsysinfo -f scalar_acc + then echo c32-convex-bsd + else echo c2-convex-bsd + fi + exit 0 ;; + C34*:ConvexOS:*:* | convex:ConvexOS:C34*:*) + echo c34-convex-bsd + exit 0 ;; + C38*:ConvexOS:*:* | convex:ConvexOS:C38*:*) + echo c38-convex-bsd + exit 0 ;; + C4*:ConvexOS:*:* | convex:ConvexOS:C4*:*) + echo c4-convex-bsd + exit 0 ;; + CRAY*Y-MP:*:*:*) + echo ymp-cray-unicos${UNAME_RELEASE} | sed -e 's/\.[^.]*$/.X/' + exit 0 ;; + CRAY*[A-Z]90:*:*:*) + echo ${UNAME_MACHINE}-cray-unicos${UNAME_RELEASE} \ + | sed -e 's/CRAY.*\([A-Z]90\)/\1/' \ + -e y/ABCDEFGHIJKLMNOPQRSTUVWXYZ/abcdefghijklmnopqrstuvwxyz/ \ + -e 's/\.[^.]*$/.X/' + exit 0 ;; + CRAY*TS:*:*:*) + echo t90-cray-unicos${UNAME_RELEASE} | sed -e 's/\.[^.]*$/.X/' + exit 0 ;; + CRAY*T3D:*:*:*) + echo alpha-cray-unicosmk${UNAME_RELEASE} | sed -e 's/\.[^.]*$/.X/' + exit 0 ;; + CRAY*T3E:*:*:*) + echo alphaev5-cray-unicosmk${UNAME_RELEASE} | sed -e 's/\.[^.]*$/.X/' + exit 0 ;; + CRAY*SV1:*:*:*) + echo sv1-cray-unicos${UNAME_RELEASE} | sed -e 's/\.[^.]*$/.X/' + exit 0 ;; + F30[01]:UNIX_System_V:*:* | F700:UNIX_System_V:*:*) + FUJITSU_PROC=`uname -m | tr 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 'abcdefghijklmnopqrstuvwxyz'` + FUJITSU_SYS=`uname -p | tr 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 'abcdefghijklmnopqrstuvwxyz' | sed -e 's/\///'` + FUJITSU_REL=`echo ${UNAME_RELEASE} | sed -e 's/ /_/'` + echo "${FUJITSU_PROC}-fujitsu-${FUJITSU_SYS}${FUJITSU_REL}" + exit 0 ;; + i*86:BSD/386:*:* | i*86:BSD/OS:*:* | *:Ascend\ Embedded/OS:*:*) + echo ${UNAME_MACHINE}-pc-bsdi${UNAME_RELEASE} + exit 0 ;; + sparc*:BSD/OS:*:*) + echo sparc-unknown-bsdi${UNAME_RELEASE} + exit 0 ;; + *:BSD/OS:*:*) + echo ${UNAME_MACHINE}-unknown-bsdi${UNAME_RELEASE} + exit 0 ;; + *:FreeBSD:*:*) + # Determine whether the default compiler uses glibc. + eval $set_cc_for_build + sed 's/^ //' << EOF >$dummy.c + #include + #if __GLIBC__ >= 2 + LIBC=gnu + #else + LIBC= + #endif +EOF + eval `$CC_FOR_BUILD -E $dummy.c 2>/dev/null | grep ^LIBC=` + rm -f $dummy.c && rmdir $tmpdir + echo ${UNAME_MACHINE}-unknown-freebsd`echo ${UNAME_RELEASE}|sed -e 's/[-(].*//'`${LIBC:+-$LIBC} + exit 0 ;; + i*:CYGWIN*:*) + echo ${UNAME_MACHINE}-pc-cygwin + exit 0 ;; + i*:MINGW*:*) + echo ${UNAME_MACHINE}-pc-mingw32 + exit 0 ;; + i*:PW*:*) + echo ${UNAME_MACHINE}-pc-pw32 + exit 0 ;; + x86:Interix*:3*) + echo i386-pc-interix3 + exit 0 ;; + i*:Windows_NT*:* | Pentium*:Windows_NT*:*) + # How do we know it's Interix rather than the generic POSIX subsystem? + # It also conflicts with pre-2.0 versions of AT&T UWIN. Should we + # UNAME_MACHINE based on the output of uname instead of i386? + echo i386-pc-interix + exit 0 ;; + i*:UWIN*:*) + echo ${UNAME_MACHINE}-pc-uwin + exit 0 ;; + p*:CYGWIN*:*) + echo powerpcle-unknown-cygwin + exit 0 ;; + prep*:SunOS:5.*:*) + echo powerpcle-unknown-solaris2`echo ${UNAME_RELEASE}|sed -e 's/[^.]*//'` + exit 0 ;; + *:GNU:*:*) + echo `echo ${UNAME_MACHINE}|sed -e 's,[-/].*$,,'`-unknown-gnu`echo ${UNAME_RELEASE}|sed -e 's,/.*$,,'` + exit 0 ;; + i*86:Minix:*:*) + echo ${UNAME_MACHINE}-pc-minix + exit 0 ;; + arm*:Linux:*:*) + echo ${UNAME_MACHINE}-unknown-linux-gnu + exit 0 ;; + ia64:Linux:*:*) + echo ${UNAME_MACHINE}-unknown-linux-gnu + exit 0 ;; + m68*:Linux:*:*) + echo ${UNAME_MACHINE}-unknown-linux-gnu + exit 0 ;; + mips:Linux:*:*) + eval $set_cc_for_build + sed 's/^ //' << EOF >$dummy.c + #undef CPU + #undef mips + #undef mipsel + #if defined(__MIPSEL__) || defined(__MIPSEL) || defined(_MIPSEL) || defined(MIPSEL) + CPU=mipsel + #else + #if defined(__MIPSEB__) || defined(__MIPSEB) || defined(_MIPSEB) || defined(MIPSEB) + CPU=mips + #else + CPU= + #endif + #endif +EOF + eval `$CC_FOR_BUILD -E $dummy.c 2>/dev/null | grep ^CPU=` + rm -f $dummy.c && rmdir $tmpdir + test x"${CPU}" != x && echo "${CPU}-pc-linux-gnu" && exit 0 + ;; + ppc:Linux:*:*) + echo powerpc-unknown-linux-gnu + exit 0 ;; + ppc64:Linux:*:*) + echo powerpc64-unknown-linux-gnu + exit 0 ;; + alpha:Linux:*:*) + case `sed -n '/^cpu model/s/^.*: \(.*\)/\1/p' < /proc/cpuinfo` in + EV5) UNAME_MACHINE=alphaev5 ;; + EV56) UNAME_MACHINE=alphaev56 ;; + PCA56) UNAME_MACHINE=alphapca56 ;; + PCA57) UNAME_MACHINE=alphapca56 ;; + EV6) UNAME_MACHINE=alphaev6 ;; + EV67) UNAME_MACHINE=alphaev67 ;; + EV68*) UNAME_MACHINE=alphaev68 ;; + esac + objdump --private-headers /bin/sh | grep ld.so.1 >/dev/null + if test "$?" = 0 ; then LIBC="libc1" ; else LIBC="" ; fi + echo ${UNAME_MACHINE}-unknown-linux-gnu${LIBC} + exit 0 ;; + parisc:Linux:*:* | hppa:Linux:*:*) + # Look for CPU level + case `grep '^cpu[^a-z]*:' /proc/cpuinfo 2>/dev/null | cut -d' ' -f2` in + PA7*) echo hppa1.1-unknown-linux-gnu ;; + PA8*) echo hppa2.0-unknown-linux-gnu ;; + *) echo hppa-unknown-linux-gnu ;; + esac + exit 0 ;; + parisc64:Linux:*:* | hppa64:Linux:*:*) + echo hppa64-unknown-linux-gnu + exit 0 ;; + s390:Linux:*:* | s390x:Linux:*:*) + echo ${UNAME_MACHINE}-ibm-linux + exit 0 ;; + sh*:Linux:*:*) + echo ${UNAME_MACHINE}-unknown-linux-gnu + exit 0 ;; + sparc:Linux:*:* | sparc64:Linux:*:*) + echo ${UNAME_MACHINE}-unknown-linux-gnu + exit 0 ;; + x86_64:Linux:*:*) + echo x86_64-unknown-linux-gnu + exit 0 ;; + i*86:Linux:*:*) + # The BFD linker knows what the default object file format is, so + # first see if it will tell us. cd to the root directory to prevent + # problems with other programs or directories called `ld' in the path. + # Set LC_ALL=C to ensure ld outputs messages in English. + ld_supported_targets=`cd /; LC_ALL=C ld --help 2>&1 \ + | sed -ne '/supported targets:/!d + s/[ ][ ]*/ /g + s/.*supported targets: *// + s/ .*// + p'` + case "$ld_supported_targets" in + elf32-i386) + TENTATIVE="${UNAME_MACHINE}-pc-linux-gnu" + ;; + a.out-i386-linux) + echo "${UNAME_MACHINE}-pc-linux-gnuaout" + exit 0 ;; + coff-i386) + echo "${UNAME_MACHINE}-pc-linux-gnucoff" + exit 0 ;; + "") + # Either a pre-BFD a.out linker (linux-gnuoldld) or + # one that does not give us useful --help. + echo "${UNAME_MACHINE}-pc-linux-gnuoldld" + exit 0 ;; + esac + # Determine whether the default compiler is a.out or elf + eval $set_cc_for_build + sed 's/^ //' << EOF >$dummy.c + #include + #ifdef __ELF__ + # ifdef __GLIBC__ + # if __GLIBC__ >= 2 + LIBC=gnu + # else + LIBC=gnulibc1 + # endif + # else + LIBC=gnulibc1 + # endif + #else + #ifdef __INTEL_COMPILER + LIBC=gnu + #else + LIBC=gnuaout + #endif + #endif +EOF + eval `$CC_FOR_BUILD -E $dummy.c 2>/dev/null | grep ^LIBC=` + rm -f $dummy.c && rmdir $tmpdir + test x"${LIBC}" != x && echo "${UNAME_MACHINE}-pc-linux-${LIBC}" && exit 0 + test x"${TENTATIVE}" != x && echo "${TENTATIVE}" && exit 0 + ;; + i*86:DYNIX/ptx:4*:*) + # ptx 4.0 does uname -s correctly, with DYNIX/ptx in there. + # earlier versions are messed up and put the nodename in both + # sysname and nodename. + echo i386-sequent-sysv4 + exit 0 ;; + i*86:UNIX_SV:4.2MP:2.*) + # Unixware is an offshoot of SVR4, but it has its own version + # number series starting with 2... + # I am not positive that other SVR4 systems won't match this, + # I just have to hope. -- rms. + # Use sysv4.2uw... so that sysv4* matches it. + echo ${UNAME_MACHINE}-pc-sysv4.2uw${UNAME_VERSION} + exit 0 ;; + i*86:*:4.*:* | i*86:SYSTEM_V:4.*:*) + UNAME_REL=`echo ${UNAME_RELEASE} | sed 's/\/MP$//'` + if grep Novell /usr/include/link.h >/dev/null 2>/dev/null; then + echo ${UNAME_MACHINE}-univel-sysv${UNAME_REL} + else + echo ${UNAME_MACHINE}-pc-sysv${UNAME_REL} + fi + exit 0 ;; + i*86:*:5:[78]*) + case `/bin/uname -X | grep "^Machine"` in + *486*) UNAME_MACHINE=i486 ;; + *Pentium) UNAME_MACHINE=i586 ;; + *Pent*|*Celeron) UNAME_MACHINE=i686 ;; + esac + echo ${UNAME_MACHINE}-unknown-sysv${UNAME_RELEASE}${UNAME_SYSTEM}${UNAME_VERSION} + exit 0 ;; + i*86:*:3.2:*) + if test -f /usr/options/cb.name; then + UNAME_REL=`sed -n 's/.*Version //p' /dev/null >/dev/null ; then + UNAME_REL=`(/bin/uname -X|grep Release|sed -e 's/.*= //')` + (/bin/uname -X|grep i80486 >/dev/null) && UNAME_MACHINE=i486 + (/bin/uname -X|grep '^Machine.*Pentium' >/dev/null) \ + && UNAME_MACHINE=i586 + (/bin/uname -X|grep '^Machine.*Pent *II' >/dev/null) \ + && UNAME_MACHINE=i686 + (/bin/uname -X|grep '^Machine.*Pentium Pro' >/dev/null) \ + && UNAME_MACHINE=i686 + echo ${UNAME_MACHINE}-pc-sco$UNAME_REL + else + echo ${UNAME_MACHINE}-pc-sysv32 + fi + exit 0 ;; + i*86:*DOS:*:*) + echo ${UNAME_MACHINE}-pc-msdosdjgpp + exit 0 ;; + pc:*:*:*) + # Left here for compatibility: + # uname -m prints for DJGPP always 'pc', but it prints nothing about + # the processor, so we play safe by assuming i386. + echo i386-pc-msdosdjgpp + exit 0 ;; + Intel:Mach:3*:*) + echo i386-pc-mach3 + exit 0 ;; + paragon:*:*:*) + echo i860-intel-osf1 + exit 0 ;; + i860:*:4.*:*) # i860-SVR4 + if grep Stardent /usr/include/sys/uadmin.h >/dev/null 2>&1 ; then + echo i860-stardent-sysv${UNAME_RELEASE} # Stardent Vistra i860-SVR4 + else # Add other i860-SVR4 vendors below as they are discovered. + echo i860-unknown-sysv${UNAME_RELEASE} # Unknown i860-SVR4 + fi + exit 0 ;; + mini*:CTIX:SYS*5:*) + # "miniframe" + echo m68010-convergent-sysv + exit 0 ;; + M68*:*:R3V[567]*:*) + test -r /sysV68 && echo 'm68k-motorola-sysv' && exit 0 ;; + 3[34]??:*:4.0:3.0 | 3[34]??A:*:4.0:3.0 | 3[34]??,*:*:4.0:3.0 | 3[34]??/*:*:4.0:3.0 | 4400:*:4.0:3.0 | 4850:*:4.0:3.0 | SKA40:*:4.0:3.0) + OS_REL='' + test -r /etc/.relid \ + && OS_REL=.`sed -n 's/[^ ]* [^ ]* \([0-9][0-9]\).*/\1/p' < /etc/.relid` + /bin/uname -p 2>/dev/null | grep 86 >/dev/null \ + && echo i486-ncr-sysv4.3${OS_REL} && exit 0 + /bin/uname -p 2>/dev/null | /bin/grep entium >/dev/null \ + && echo i586-ncr-sysv4.3${OS_REL} && exit 0 ;; + 3[34]??:*:4.0:* | 3[34]??,*:*:4.0:*) + /bin/uname -p 2>/dev/null | grep 86 >/dev/null \ + && echo i486-ncr-sysv4 && exit 0 ;; + m68*:LynxOS:2.*:* | m68*:LynxOS:3.0*:*) + echo m68k-unknown-lynxos${UNAME_RELEASE} + exit 0 ;; + mc68030:UNIX_System_V:4.*:*) + echo m68k-atari-sysv4 + exit 0 ;; + i*86:LynxOS:2.*:* | i*86:LynxOS:3.[01]*:* | i*86:LynxOS:4.0*:*) + echo i386-unknown-lynxos${UNAME_RELEASE} + exit 0 ;; + TSUNAMI:LynxOS:2.*:*) + echo sparc-unknown-lynxos${UNAME_RELEASE} + exit 0 ;; + rs6000:LynxOS:2.*:*) + echo rs6000-unknown-lynxos${UNAME_RELEASE} + exit 0 ;; + PowerPC:LynxOS:2.*:* | PowerPC:LynxOS:3.[01]*:* | PowerPC:LynxOS:4.0*:*) + echo powerpc-unknown-lynxos${UNAME_RELEASE} + exit 0 ;; + SM[BE]S:UNIX_SV:*:*) + echo mips-dde-sysv${UNAME_RELEASE} + exit 0 ;; + RM*:ReliantUNIX-*:*:*) + echo mips-sni-sysv4 + exit 0 ;; + RM*:SINIX-*:*:*) + echo mips-sni-sysv4 + exit 0 ;; + *:SINIX-*:*:*) + if uname -p 2>/dev/null >/dev/null ; then + UNAME_MACHINE=`(uname -p) 2>/dev/null` + echo ${UNAME_MACHINE}-sni-sysv4 + else + echo ns32k-sni-sysv + fi + exit 0 ;; + PENTIUM:*:4.0*:*) # Unisys `ClearPath HMP IX 4000' SVR4/MP effort + # says + echo i586-unisys-sysv4 + exit 0 ;; + *:UNIX_System_V:4*:FTX*) + # From Gerald Hewes . + # How about differentiating between stratus architectures? -djm + echo hppa1.1-stratus-sysv4 + exit 0 ;; + *:*:*:FTX*) + # From seanf@swdc.stratus.com. + echo i860-stratus-sysv4 + exit 0 ;; + *:VOS:*:*) + # From Paul.Green@stratus.com. + echo hppa1.1-stratus-vos + exit 0 ;; + mc68*:A/UX:*:*) + echo m68k-apple-aux${UNAME_RELEASE} + exit 0 ;; + news*:NEWS-OS:6*:*) + echo mips-sony-newsos6 + exit 0 ;; + R[34]000:*System_V*:*:* | R4000:UNIX_SYSV:*:* | R*000:UNIX_SV:*:*) + if [ -d /usr/nec ]; then + echo mips-nec-sysv${UNAME_RELEASE} + else + echo mips-unknown-sysv${UNAME_RELEASE} + fi + exit 0 ;; + BeBox:BeOS:*:*) # BeOS running on hardware made by Be, PPC only. + echo powerpc-be-beos + exit 0 ;; + BeMac:BeOS:*:*) # BeOS running on Mac or Mac clone, PPC only. + echo powerpc-apple-beos + exit 0 ;; + BePC:BeOS:*:*) # BeOS running on Intel PC compatible. + echo i586-pc-beos + exit 0 ;; + SX-4:SUPER-UX:*:*) + echo sx4-nec-superux${UNAME_RELEASE} + exit 0 ;; + SX-5:SUPER-UX:*:*) + echo sx5-nec-superux${UNAME_RELEASE} + exit 0 ;; + Power*:Rhapsody:*:*) + echo powerpc-apple-rhapsody${UNAME_RELEASE} + exit 0 ;; + *:Rhapsody:*:*) + echo ${UNAME_MACHINE}-apple-rhapsody${UNAME_RELEASE} + exit 0 ;; + *:Darwin:*:*) + echo `uname -p`-apple-darwin${UNAME_RELEASE} + exit 0 ;; + *:procnto*:*:* | *:QNX:[0123456789]*:*) + UNAME_PROCESSOR=`uname -p` + if test "$UNAME_PROCESSOR" = "x86"; then + UNAME_PROCESSOR=i386 + UNAME_MACHINE=pc + fi + echo ${UNAME_PROCESSOR}-${UNAME_MACHINE}-nto-qnx${UNAME_RELEASE} + exit 0 ;; + *:QNX:*:4*) + echo i386-pc-qnx + exit 0 ;; + NSR-[GKLNPTVW]:NONSTOP_KERNEL:*:*) + echo nsr-tandem-nsk${UNAME_RELEASE} + exit 0 ;; + *:NonStop-UX:*:*) + echo mips-compaq-nonstopux + exit 0 ;; + BS2000:POSIX*:*:*) + echo bs2000-siemens-sysv + exit 0 ;; + DS/*:UNIX_System_V:*:*) + echo ${UNAME_MACHINE}-${UNAME_SYSTEM}-${UNAME_RELEASE} + exit 0 ;; + *:Plan9:*:*) + # "uname -m" is not consistent, so use $cputype instead. 386 + # is converted to i386 for consistency with other x86 + # operating systems. + if test "$cputype" = "386"; then + UNAME_MACHINE=i386 + else + UNAME_MACHINE="$cputype" + fi + echo ${UNAME_MACHINE}-unknown-plan9 + exit 0 ;; + i*86:OS/2:*:*) + # If we were able to find `uname', then EMX Unix compatibility + # is probably installed. + echo ${UNAME_MACHINE}-pc-os2-emx + exit 0 ;; + *:TOPS-10:*:*) + echo pdp10-unknown-tops10 + exit 0 ;; + *:TENEX:*:*) + echo pdp10-unknown-tenex + exit 0 ;; + KS10:TOPS-20:*:* | KL10:TOPS-20:*:* | TYPE4:TOPS-20:*:*) + echo pdp10-dec-tops20 + exit 0 ;; + XKL-1:TOPS-20:*:* | TYPE5:TOPS-20:*:*) + echo pdp10-xkl-tops20 + exit 0 ;; + *:TOPS-20:*:*) + echo pdp10-unknown-tops20 + exit 0 ;; + *:ITS:*:*) + echo pdp10-unknown-its + exit 0 ;; + i*86:XTS-300:*:STOP) + echo ${UNAME_MACHINE}-unknown-stop + exit 0 ;; + i*86:atheos:*:*) + echo ${UNAME_MACHINE}-unknown-atheos + exit 0 ;; +esac + +#echo '(No uname command or uname output not recognized.)' 1>&2 +#echo "${UNAME_MACHINE}:${UNAME_SYSTEM}:${UNAME_RELEASE}:${UNAME_VERSION}" 1>&2 + +eval $set_cc_for_build +cat >$dummy.c < +# include +#endif +main () +{ +#if defined (sony) +#if defined (MIPSEB) + /* BFD wants "bsd" instead of "newsos". Perhaps BFD should be changed, + I don't know.... */ + printf ("mips-sony-bsd\n"); exit (0); +#else +#include + printf ("m68k-sony-newsos%s\n", +#ifdef NEWSOS4 + "4" +#else + "" +#endif + ); exit (0); +#endif +#endif + +#if defined (__arm) && defined (__acorn) && defined (__unix) + printf ("arm-acorn-riscix"); exit (0); +#endif + +#if defined (hp300) && !defined (hpux) + printf ("m68k-hp-bsd\n"); exit (0); +#endif + +#if defined (NeXT) +#if !defined (__ARCHITECTURE__) +#define __ARCHITECTURE__ "m68k" +#endif + int version; + version=`(hostinfo | sed -n 's/.*NeXT Mach \([0-9]*\).*/\1/p') 2>/dev/null`; + if (version < 4) + printf ("%s-next-nextstep%d\n", __ARCHITECTURE__, version); + else + printf ("%s-next-openstep%d\n", __ARCHITECTURE__, version); + exit (0); +#endif + +#if defined (MULTIMAX) || defined (n16) +#if defined (UMAXV) + printf ("ns32k-encore-sysv\n"); exit (0); +#else +#if defined (CMU) + printf ("ns32k-encore-mach\n"); exit (0); +#else + printf ("ns32k-encore-bsd\n"); exit (0); +#endif +#endif +#endif + +#if defined (__386BSD__) + printf ("i386-pc-bsd\n"); exit (0); +#endif + +#if defined (sequent) +#if defined (i386) + printf ("i386-sequent-dynix\n"); exit (0); +#endif +#if defined (ns32000) + printf ("ns32k-sequent-dynix\n"); exit (0); +#endif +#endif + +#if defined (_SEQUENT_) + struct utsname un; + + uname(&un); + + if (strncmp(un.version, "V2", 2) == 0) { + printf ("i386-sequent-ptx2\n"); exit (0); + } + if (strncmp(un.version, "V1", 2) == 0) { /* XXX is V1 correct? */ + printf ("i386-sequent-ptx1\n"); exit (0); + } + printf ("i386-sequent-ptx\n"); exit (0); + +#endif + +#if defined (vax) +# if !defined (ultrix) +# include +# if defined (BSD) +# if BSD == 43 + printf ("vax-dec-bsd4.3\n"); exit (0); +# else +# if BSD == 199006 + printf ("vax-dec-bsd4.3reno\n"); exit (0); +# else + printf ("vax-dec-bsd\n"); exit (0); +# endif +# endif +# else + printf ("vax-dec-bsd\n"); exit (0); +# endif +# else + printf ("vax-dec-ultrix\n"); exit (0); +# endif +#endif + +#if defined (alliant) && defined (i860) + printf ("i860-alliant-bsd\n"); exit (0); +#endif + + exit (1); +} +EOF + +$CC_FOR_BUILD $dummy.c -o $dummy 2>/dev/null && $dummy && rm -f $dummy.c $dummy && rmdir $tmpdir && exit 0 +rm -f $dummy.c $dummy && rmdir $tmpdir + +# Apollos put the system type in the environment. + +test -d /usr/apollo && { echo ${ISP}-apollo-${SYSTYPE}; exit 0; } + +# Convex versions that predate uname can use getsysinfo(1) + +if [ -x /usr/convex/getsysinfo ] +then + case `getsysinfo -f cpu_type` in + c1*) + echo c1-convex-bsd + exit 0 ;; + c2*) + if getsysinfo -f scalar_acc + then echo c32-convex-bsd + else echo c2-convex-bsd + fi + exit 0 ;; + c34*) + echo c34-convex-bsd + exit 0 ;; + c38*) + echo c38-convex-bsd + exit 0 ;; + c4*) + echo c4-convex-bsd + exit 0 ;; + esac +fi + +cat >&2 < in order to provide the needed +information to handle your system. + +config.guess timestamp = $timestamp + +uname -m = `(uname -m) 2>/dev/null || echo unknown` +uname -r = `(uname -r) 2>/dev/null || echo unknown` +uname -s = `(uname -s) 2>/dev/null || echo unknown` +uname -v = `(uname -v) 2>/dev/null || echo unknown` + +/usr/bin/uname -p = `(/usr/bin/uname -p) 2>/dev/null` +/bin/uname -X = `(/bin/uname -X) 2>/dev/null` + +hostinfo = `(hostinfo) 2>/dev/null` +/bin/universe = `(/bin/universe) 2>/dev/null` +/usr/bin/arch -k = `(/usr/bin/arch -k) 2>/dev/null` +/bin/arch = `(/bin/arch) 2>/dev/null` +/usr/bin/oslevel = `(/usr/bin/oslevel) 2>/dev/null` +/usr/convex/getsysinfo = `(/usr/convex/getsysinfo) 2>/dev/null` + +UNAME_MACHINE = ${UNAME_MACHINE} +UNAME_RELEASE = ${UNAME_RELEASE} +UNAME_SYSTEM = ${UNAME_SYSTEM} +UNAME_VERSION = ${UNAME_VERSION} +EOF + +exit 1 + +# Local variables: +# eval: (add-hook 'write-file-hooks 'time-stamp) +# time-stamp-start: "timestamp='" +# time-stamp-format: "%:y-%02m-%02d" +# time-stamp-end: "'" +# End: diff --git a/bfile/config/config.h.in b/bfile/config/config.h.in new file mode 100644 index 0000000..097fbca --- /dev/null +++ b/bfile/config/config.h.in @@ -0,0 +1,319 @@ +/* config.h.in. Generated from configure.in by autoheader. */ +#ifndef _CONFIG_H_ +#define _CONFIG_H_ + +#ifndef WIN32 + + +/* + * This file contains prototypes for config.h.in which autoheader + * could not figure by itself. I.e. if you write your own test which + * defines a macro you will probably have to put it here, but if you + * use any standard test AC_* it will be detected by autoheader. + */ + +#undef WIN32 + +/* Define to a string defining your target. ($target in configure.in) */ +#undef CPU_VENDOR_OS + + +/* Define to the full path of the ifconfig program */ +#undef IFCONFIG + +/* Define to the full path of the route program */ +#undef ROUTE + +/* Define to the full path of the arp program */ +#undef ARP + + +/* + * These four below should definately be done away with!! + */ + +/* Define if your target OS is a BSD derivative */ +#undef BSD + +/* Define if your target OS is Linux */ +#undef LINUX + +/* Define if your target OS is SunOS 5.x */ +#undef SOLARIS + +/* Define if your target OS is BSD/OS 4.x */ +#undef BSDI + + +/* + * What are these??? + */ + +/* ? */ +#undef USE_IFALIAS + +/* Define if you wish to use the bpf interface */ +#undef USE_BPF + +/* Define if you wish to use the dlpi interface */ +#undef USE_DLPI + +/* ? */ +#undef USE_SOCKET + + +/* Define if prototypes for malloc can be found in stdlib.h */ +#undef STDLIB_MALLOC + +/* Define if your include files defines a prototype for sys_errlist[] */ +#undef HAVE_SYS_ERRLIST + +/* This isn't used anywhere (that I could find) so I don't know what it means*/ +#undef IFCONFIG_REQUIRES_DOWN_ADDRESS + +/* Define if your OS have broken cmsg fields in the msghdr struct (Linux) */ +#undef BROKEN_CMSG_FIELDS + +/* Define if sockaddr structure has sa_len member */ +#undef SA_LEN_IN_SOCKADDR + + + +/* + * Provide a common way to refer to ints with specific size. (Is this + * used everywhere?) The names used are {u_,}int{8,16,32,64}_t unless + * they are defined in sys/types.h they are defined in ints.h using + * the BIT macros. e.g. if int8_t isn't defined in sys/types.h int8_t + * is typedef:ed to BIT8. + * + */ + +/* Define to a basic signed type that is 8 bits in size */ +#undef BIT8 + +/* Define to a basic signed type that is 16 bits in size */ +#undef BIT16 + +/* Define to a basic signed type that is 32 bits in size */ +#undef BIT32 + +/* Define to a basic signed type that is 64 bits in size */ +#undef BIT64 + +/* Define if sys/types.h defines this type */ +#undef HAVE_int8_t + +/* Define if sys/types.h defines this type */ +#undef HAVE_u_int8_t + +/* Define if sys/types.h defines this type */ +#undef HAVE_int16_t + +/* Define if sys/types.h defines this type */ +#undef HAVE_u_int16_t + +/* Define if sys/types.h defines this type */ +#undef HAVE_int32_t + +/* Define if sys/types.h defines this type */ +#undef HAVE_u_int32_t + +/* Define if sys/types.h defines this type */ +#undef HAVE_int64_t + +/* Define if sys/types.h defines this type */ +#undef HAVE_u_int64_t + + + + + + +/* */ +#undef ETHER_HEADER_USES_ETHER_ADDR + +/* */ +#undef HAVE_DLIOCRAW + +/* */ +#undef HAVE_ETHERADDRL + +/* */ +#undef HAVE_ETHER_ADDR_LEN + +/* */ +#undef HAVE_MSGHDR_MSG_CONTROL + +/* */ +#undef HAVE_SIOCGARP + +/* */ +#undef HAVE_SIOCGIFCONF + +/* */ +#undef HAVE_SIOCGIFHWADDR + +/* */ +#undef HAVE_arpreq + +/* */ +#undef HAVE_caddr_t + +/* */ +#undef HAVE_ether_header + +/* */ +#undef HAVE_ethhdr + +/* */ +#undef HAVE_ifnet + +/* */ +#undef HAVE_in_addr + +/* */ +#undef HAVE_sockaddr + +/* */ +#undef HAVE_sockaddr_dl + +/* */ +#undef HAVE_sockaddr_in + +/* */ +#undef NEED_HAVE_sockaddr_dl + + + +/* + * I have yet to figure out what all this need_* stuff is for, shouldn't + * it suffice with using have_* ??? + */ + +/* */ +#undef NEED_LINUX_SOCKIOS_H + +/* */ +#undef NEED_NETINET_IF_ETHER_H + +/* */ +#undef NEED_NETINET_IN_H + +/* */ +#undef NEED_NET_ETHERNET_H + +/* */ +#undef NEED_NET_IF_ARP_H + +/* */ +#undef NEED_NET_IF_DL_H + +/* */ +#undef NEED_NET_IF_H + +/* */ +#undef NEED_SYS_BITYPES_H + +/* */ +#undef NEED_SYS_DLPI_H + +/* */ +#undef NEED_SYS_ETHERNET_H + +/* */ +#undef NEED_SYS_SOCKETIO_H + +/* */ +#undef NEED_SYS_SOCKET_H + +/* */ +#undef NEED_SYS_SOCKIO_H + +/* */ +#undef NEED_SYS_TYPES_H + +/* if we have the openssl with engine support */ +#undef HAVE_SSL_ENGINE +/* ... with Rainbow patches */ +#undef HAVE_RAINBOW_PATCHES +/* ... with Rainbow's libswift */ +#undef HAVE_SWIFT +/* ... with one of our HW checks */ +#undef HAVE_LOCAL_ENGINE_SETUP +#undef HAVE_SSL_HW_CHECK +/* ... with patch to get cfg password from card */ +#undef HAVE_ENGINE_GET_PASSWORD +/* ... with our buffer patches */ +#undef HAVE_SSL_BUFFER_CB + +/* */ +#undef ISD_SYSTEM_VSN + +/* eventpoll */ +#undef HAVE_KPOLL + +/* Have/use netfilter (a.k.a. iptables) */ +#undef HAVE_NETFILTER + +/* Non-standard support for "non-local connect()" in Linux 2.4 */ +#undef HAVE_NONLOCAL_CONNECT + +/* atomic asmebler op in asm/atomic.h ? */ +#undef HAVE_ATOMIC_OPS + + + +/* Description */ +#undef DARWIN + +/* Define to 1 if you have the header file. */ +#undef HAVE_INTTYPES_H + +/* Define to 1 if you have the header file. */ +#undef HAVE_MALLOC_H + +/* Define to 1 if you have the header file. */ +#undef HAVE_MEMORY_H + +/* Define to 1 if you have the header file. */ +#undef HAVE_STDINT_H + +/* Define to 1 if you have the header file. */ +#undef HAVE_STDLIB_H + +/* Define to 1 if you have the header file. */ +#undef HAVE_STRINGS_H + +/* Define to 1 if you have the header file. */ +#undef HAVE_STRING_H + +/* Define to 1 if you have the header file. */ +#undef HAVE_SYS_STAT_H + +/* Define to 1 if you have the header file. */ +#undef HAVE_SYS_TYPES_H + +/* Define to 1 if you have the header file. */ +#undef HAVE_UNISTD_H + +/* Define to the address where bug reports for this package should be sent. */ +#undef PACKAGE_BUGREPORT + +/* Define to the full name of this package. */ +#undef PACKAGE_NAME + +/* Define to the full name and version of this package. */ +#undef PACKAGE_STRING + +/* Define to the one symbol short name of this package. */ +#undef PACKAGE_TARNAME + +/* Define to the version of this package. */ +#undef PACKAGE_VERSION + +/* Define to 1 if you have the ANSI C header files. */ +#undef STDC_HEADERS + +#endif /* WIN32 */ +#endif /* _CONFIG_H_ */ + diff --git a/bfile/config/config.sub b/bfile/config/config.sub new file mode 100755 index 0000000..9ff085e --- /dev/null +++ b/bfile/config/config.sub @@ -0,0 +1,1460 @@ +#! /bin/sh +# Configuration validation subroutine script. +# Copyright (C) 1992, 1993, 1994, 1995, 1996, 1997, 1998, 1999, +# 2000, 2001, 2002 Free Software Foundation, Inc. + +timestamp='2002-07-03' + +# This file is (in principle) common to ALL GNU software. +# The presence of a machine in this file suggests that SOME GNU software +# can handle that machine. It does not imply ALL GNU software can. +# +# This file 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., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +# As a special exception to the GNU General Public License, if you +# distribute this file as part of a program that contains a +# configuration script generated by Autoconf, you may include it under +# the same distribution terms that you use for the rest of that program. + +# Please send patches to . Submit a context +# diff and a properly formatted ChangeLog entry. +# +# Configuration subroutine to validate and canonicalize a configuration type. +# Supply the specified configuration type as an argument. +# If it is invalid, we print an error message on stderr and exit with code 1. +# Otherwise, we print the canonical config type on stdout and succeed. + +# This file is supposed to be the same for all GNU packages +# and recognize all the CPU types, system types and aliases +# that are meaningful with *any* GNU software. +# Each package is responsible for reporting which valid configurations +# it does not support. The user should be able to distinguish +# a failure to support a valid configuration from a meaningless +# configuration. + +# The goal of this file is to map all the various variations of a given +# machine specification into a single specification in the form: +# CPU_TYPE-MANUFACTURER-OPERATING_SYSTEM +# or in some cases, the newer four-part form: +# CPU_TYPE-MANUFACTURER-KERNEL-OPERATING_SYSTEM +# It is wrong to echo any other type of specification. + +me=`echo "$0" | sed -e 's,.*/,,'` + +usage="\ +Usage: $0 [OPTION] CPU-MFR-OPSYS + $0 [OPTION] ALIAS + +Canonicalize a configuration name. + +Operation modes: + -h, --help print this help, then exit + -t, --time-stamp print date of last modification, then exit + -v, --version print version number, then exit + +Report bugs and patches to ." + +version="\ +GNU config.sub ($timestamp) + +Copyright (C) 1992, 1993, 1994, 1995, 1996, 1997, 1998, 1999, 2000, 2001 +Free Software Foundation, Inc. + +This is free software; see the source for copying conditions. There is NO +warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE." + +help=" +Try \`$me --help' for more information." + +# Parse command line +while test $# -gt 0 ; do + case $1 in + --time-stamp | --time* | -t ) + echo "$timestamp" ; exit 0 ;; + --version | -v ) + echo "$version" ; exit 0 ;; + --help | --h* | -h ) + echo "$usage"; exit 0 ;; + -- ) # Stop option processing + shift; break ;; + - ) # Use stdin as input. + break ;; + -* ) + echo "$me: invalid option $1$help" + exit 1 ;; + + *local*) + # First pass through any local machine types. + echo $1 + exit 0;; + + * ) + break ;; + esac +done + +case $# in + 0) echo "$me: missing argument$help" >&2 + exit 1;; + 1) ;; + *) echo "$me: too many arguments$help" >&2 + exit 1;; +esac + +# Separate what the user gave into CPU-COMPANY and OS or KERNEL-OS (if any). +# Here we must recognize all the valid KERNEL-OS combinations. +maybe_os=`echo $1 | sed 's/^\(.*\)-\([^-]*-[^-]*\)$/\2/'` +case $maybe_os in + nto-qnx* | linux-gnu* | freebsd*-gnu* | storm-chaos* | os2-emx* | windows32-* | rtmk-nova*) + os=-$maybe_os + basic_machine=`echo $1 | sed 's/^\(.*\)-\([^-]*-[^-]*\)$/\1/'` + ;; + *) + basic_machine=`echo $1 | sed 's/-[^-]*$//'` + if [ $basic_machine != $1 ] + then os=`echo $1 | sed 's/.*-/-/'` + else os=; fi + ;; +esac + +### Let's recognize common machines as not being operating systems so +### that things like config.sub decstation-3100 work. We also +### recognize some manufacturers as not being operating systems, so we +### can provide default operating systems below. +case $os in + -sun*os*) + # Prevent following clause from handling this invalid input. + ;; + -dec* | -mips* | -sequent* | -encore* | -pc532* | -sgi* | -sony* | \ + -att* | -7300* | -3300* | -delta* | -motorola* | -sun[234]* | \ + -unicom* | -ibm* | -next | -hp | -isi* | -apollo | -altos* | \ + -convergent* | -ncr* | -news | -32* | -3600* | -3100* | -hitachi* |\ + -c[123]* | -convex* | -sun | -crds | -omron* | -dg | -ultra | -tti* | \ + -harris | -dolphin | -highlevel | -gould | -cbm | -ns | -masscomp | \ + -apple | -axis) + os= + basic_machine=$1 + ;; + -sim | -cisco | -oki | -wec | -winbond) + os= + basic_machine=$1 + ;; + -scout) + ;; + -wrs) + os=-vxworks + basic_machine=$1 + ;; + -chorusos*) + os=-chorusos + basic_machine=$1 + ;; + -chorusrdb) + os=-chorusrdb + basic_machine=$1 + ;; + -hiux*) + os=-hiuxwe2 + ;; + -sco5) + os=-sco3.2v5 + basic_machine=`echo $1 | sed -e 's/86-.*/86-pc/'` + ;; + -sco4) + os=-sco3.2v4 + basic_machine=`echo $1 | sed -e 's/86-.*/86-pc/'` + ;; + -sco3.2.[4-9]*) + os=`echo $os | sed -e 's/sco3.2./sco3.2v/'` + basic_machine=`echo $1 | sed -e 's/86-.*/86-pc/'` + ;; + -sco3.2v[4-9]*) + # Don't forget version if it is 3.2v4 or newer. + basic_machine=`echo $1 | sed -e 's/86-.*/86-pc/'` + ;; + -sco*) + os=-sco3.2v2 + basic_machine=`echo $1 | sed -e 's/86-.*/86-pc/'` + ;; + -udk*) + basic_machine=`echo $1 | sed -e 's/86-.*/86-pc/'` + ;; + -isc) + os=-isc2.2 + basic_machine=`echo $1 | sed -e 's/86-.*/86-pc/'` + ;; + -clix*) + basic_machine=clipper-intergraph + ;; + -isc*) + basic_machine=`echo $1 | sed -e 's/86-.*/86-pc/'` + ;; + -lynx*) + os=-lynxos + ;; + -ptx*) + basic_machine=`echo $1 | sed -e 's/86-.*/86-sequent/'` + ;; + -windowsnt*) + os=`echo $os | sed -e 's/windowsnt/winnt/'` + ;; + -psos*) + os=-psos + ;; + -mint | -mint[0-9]*) + basic_machine=m68k-atari + os=-mint + ;; +esac + +# Decode aliases for certain CPU-COMPANY combinations. +case $basic_machine in + # Recognize the basic CPU types without company name. + # Some are omitted here because they have special meanings below. + 1750a | 580 \ + | a29k \ + | alpha | alphaev[4-8] | alphaev56 | alphaev6[78] | alphapca5[67] \ + | alpha64 | alpha64ev[4-8] | alpha64ev56 | alpha64ev6[78] | alpha64pca5[67] \ + | arc | arm | arm[bl]e | arme[lb] | armv[2345] | armv[345][lb] | avr \ + | c4x | clipper \ + | d10v | d30v | dlx | dsp16xx \ + | fr30 | frv \ + | h8300 | h8500 | hppa | hppa1.[01] | hppa2.0 | hppa2.0[nw] | hppa64 \ + | i370 | i860 | i960 | ia64 \ + | ip2k \ + | m32r | m68000 | m68k | m88k | mcore \ + | mips | mipsbe | mipseb | mipsel | mipsle \ + | mips16 \ + | mips64 | mips64el \ + | mips64orion | mips64orionel \ + | mips64vr4100 | mips64vr4100el \ + | mips64vr4300 | mips64vr4300el \ + | mips64vr5000 | mips64vr5000el \ + | mipsisa32 | mipsisa32el \ + | mipsisa64 | mipsisa64el \ + | mipsisa64sb1 | mipsisa64sb1el \ + | mipstx39 | mipstx39el \ + | mn10200 | mn10300 \ + | ns16k | ns32k \ + | openrisc | or32 \ + | pdp10 | pdp11 | pj | pjl \ + | powerpc | powerpc64 | powerpc64le | powerpcle | ppcbe \ + | pyramid \ + | sh | sh[1234] | sh3e | sh[34]eb | shbe | shle | sh[1234]le | sh3ele \ + | sh64 | sh64le \ + | sparc | sparc64 | sparc86x | sparclet | sparclite | sparcv9 | sparcv9b \ + | strongarm \ + | tahoe | thumb | tic80 | tron \ + | v850 | v850e \ + | we32k \ + | x86 | xscale | xstormy16 | xtensa \ + | z8k) + basic_machine=$basic_machine-unknown + ;; + m6811 | m68hc11 | m6812 | m68hc12) + # Motorola 68HC11/12. + basic_machine=$basic_machine-unknown + os=-none + ;; + m88110 | m680[12346]0 | m683?2 | m68360 | m5200 | v70 | w65 | z8k) + ;; + + # We use `pc' rather than `unknown' + # because (1) that's what they normally are, and + # (2) the word "unknown" tends to confuse beginning users. + i*86 | x86_64) + basic_machine=$basic_machine-pc + ;; + # Object if more than one company name word. + *-*-*) + echo Invalid configuration \`$1\': machine \`$basic_machine\' not recognized 1>&2 + exit 1 + ;; + # Recognize the basic CPU types with company name. + 580-* \ + | a29k-* \ + | alpha-* | alphaev[4-8]-* | alphaev56-* | alphaev6[78]-* \ + | alpha64-* | alpha64ev[4-8]-* | alpha64ev56-* | alpha64ev6[78]-* \ + | alphapca5[67]-* | alpha64pca5[67]-* | arc-* \ + | arm-* | armbe-* | armle-* | armeb-* | armv*-* \ + | avr-* \ + | bs2000-* \ + | c[123]* | c30-* | [cjt]90-* | c54x-* \ + | clipper-* | cydra-* \ + | d10v-* | d30v-* | dlx-* \ + | elxsi-* \ + | f30[01]-* | f700-* | fr30-* | frv-* | fx80-* \ + | h8300-* | h8500-* \ + | hppa-* | hppa1.[01]-* | hppa2.0-* | hppa2.0[nw]-* | hppa64-* \ + | i*86-* | i860-* | i960-* | ia64-* \ + | ip2k-* \ + | m32r-* \ + | m68000-* | m680[012346]0-* | m68360-* | m683?2-* | m68k-* \ + | m88110-* | m88k-* | mcore-* \ + | mips-* | mipsbe-* | mipseb-* | mipsel-* | mipsle-* \ + | mips16-* \ + | mips64-* | mips64el-* \ + | mips64orion-* | mips64orionel-* \ + | mips64vr4100-* | mips64vr4100el-* \ + | mips64vr4300-* | mips64vr4300el-* \ + | mips64vr5000-* | mips64vr5000el-* \ + | mipsisa32-* | mipsisa32el-* \ + | mipsisa64-* | mipsisa64el-* \ + | mipsisa64sb1-* | mipsisa64sb1el-* \ + | mipstx39 | mipstx39el \ + | none-* | np1-* | ns16k-* | ns32k-* \ + | orion-* \ + | pdp10-* | pdp11-* | pj-* | pjl-* | pn-* | power-* \ + | powerpc-* | powerpc64-* | powerpc64le-* | powerpcle-* | ppcbe-* \ + | pyramid-* \ + | romp-* | rs6000-* \ + | sh-* | sh[1234]-* | sh3e-* | sh[34]eb-* | shbe-* \ + | shle-* | sh[1234]le-* | sh3ele-* | sh64-* | sh64le-* \ + | sparc-* | sparc64-* | sparc86x-* | sparclet-* | sparclite-* \ + | sparcv9-* | sparcv9b-* | strongarm-* | sv1-* | sx?-* \ + | tahoe-* | thumb-* | tic30-* | tic54x-* | tic80-* | tron-* \ + | v850-* | v850e-* | vax-* \ + | we32k-* \ + | x86-* | x86_64-* | xps100-* | xscale-* | xstormy16-* \ + | xtensa-* \ + | ymp-* \ + | z8k-*) + ;; + # Recognize the various machine names and aliases which stand + # for a CPU type and a company and sometimes even an OS. + 386bsd) + basic_machine=i386-unknown + os=-bsd + ;; + 3b1 | 7300 | 7300-att | att-7300 | pc7300 | safari | unixpc) + basic_machine=m68000-att + ;; + 3b*) + basic_machine=we32k-att + ;; + a29khif) + basic_machine=a29k-amd + os=-udi + ;; + adobe68k) + basic_machine=m68010-adobe + os=-scout + ;; + alliant | fx80) + basic_machine=fx80-alliant + ;; + altos | altos3068) + basic_machine=m68k-altos + ;; + am29k) + basic_machine=a29k-none + os=-bsd + ;; + amdahl) + basic_machine=580-amdahl + os=-sysv + ;; + amiga | amiga-*) + basic_machine=m68k-unknown + ;; + amigaos | amigados) + basic_machine=m68k-unknown + os=-amigaos + ;; + amigaunix | amix) + basic_machine=m68k-unknown + os=-sysv4 + ;; + apollo68) + basic_machine=m68k-apollo + os=-sysv + ;; + apollo68bsd) + basic_machine=m68k-apollo + os=-bsd + ;; + aux) + basic_machine=m68k-apple + os=-aux + ;; + balance) + basic_machine=ns32k-sequent + os=-dynix + ;; + c90) + basic_machine=c90-cray + os=-unicos + ;; + convex-c1) + basic_machine=c1-convex + os=-bsd + ;; + convex-c2) + basic_machine=c2-convex + os=-bsd + ;; + convex-c32) + basic_machine=c32-convex + os=-bsd + ;; + convex-c34) + basic_machine=c34-convex + os=-bsd + ;; + convex-c38) + basic_machine=c38-convex + os=-bsd + ;; + cray | j90) + basic_machine=j90-cray + os=-unicos + ;; + crds | unos) + basic_machine=m68k-crds + ;; + cris | cris-* | etrax*) + basic_machine=cris-axis + ;; + da30 | da30-*) + basic_machine=m68k-da30 + ;; + decstation | decstation-3100 | pmax | pmax-* | pmin | dec3100 | decstatn) + basic_machine=mips-dec + ;; + decsystem10* | dec10*) + basic_machine=pdp10-dec + os=-tops10 + ;; + decsystem20* | dec20*) + basic_machine=pdp10-dec + os=-tops20 + ;; + delta | 3300 | motorola-3300 | motorola-delta \ + | 3300-motorola | delta-motorola) + basic_machine=m68k-motorola + ;; + delta88) + basic_machine=m88k-motorola + os=-sysv3 + ;; + dpx20 | dpx20-*) + basic_machine=rs6000-bull + os=-bosx + ;; + dpx2* | dpx2*-bull) + basic_machine=m68k-bull + os=-sysv3 + ;; + ebmon29k) + basic_machine=a29k-amd + os=-ebmon + ;; + elxsi) + basic_machine=elxsi-elxsi + os=-bsd + ;; + encore | umax | mmax) + basic_machine=ns32k-encore + ;; + es1800 | OSE68k | ose68k | ose | OSE) + basic_machine=m68k-ericsson + os=-ose + ;; + fx2800) + basic_machine=i860-alliant + ;; + genix) + basic_machine=ns32k-ns + ;; + gmicro) + basic_machine=tron-gmicro + os=-sysv + ;; + go32) + basic_machine=i386-pc + os=-go32 + ;; + h3050r* | hiux*) + basic_machine=hppa1.1-hitachi + os=-hiuxwe2 + ;; + h8300hms) + basic_machine=h8300-hitachi + os=-hms + ;; + h8300xray) + basic_machine=h8300-hitachi + os=-xray + ;; + h8500hms) + basic_machine=h8500-hitachi + os=-hms + ;; + harris) + basic_machine=m88k-harris + os=-sysv3 + ;; + hp300-*) + basic_machine=m68k-hp + ;; + hp300bsd) + basic_machine=m68k-hp + os=-bsd + ;; + hp300hpux) + basic_machine=m68k-hp + os=-hpux + ;; + hp3k9[0-9][0-9] | hp9[0-9][0-9]) + basic_machine=hppa1.0-hp + ;; + hp9k2[0-9][0-9] | hp9k31[0-9]) + basic_machine=m68000-hp + ;; + hp9k3[2-9][0-9]) + basic_machine=m68k-hp + ;; + hp9k6[0-9][0-9] | hp6[0-9][0-9]) + basic_machine=hppa1.0-hp + ;; + hp9k7[0-79][0-9] | hp7[0-79][0-9]) + basic_machine=hppa1.1-hp + ;; + hp9k78[0-9] | hp78[0-9]) + # FIXME: really hppa2.0-hp + basic_machine=hppa1.1-hp + ;; + hp9k8[67]1 | hp8[67]1 | hp9k80[24] | hp80[24] | hp9k8[78]9 | hp8[78]9 | hp9k893 | hp893) + # FIXME: really hppa2.0-hp + basic_machine=hppa1.1-hp + ;; + hp9k8[0-9][13679] | hp8[0-9][13679]) + basic_machine=hppa1.1-hp + ;; + hp9k8[0-9][0-9] | hp8[0-9][0-9]) + basic_machine=hppa1.0-hp + ;; + hppa-next) + os=-nextstep3 + ;; + hppaosf) + basic_machine=hppa1.1-hp + os=-osf + ;; + hppro) + basic_machine=hppa1.1-hp + os=-proelf + ;; + i370-ibm* | ibm*) + basic_machine=i370-ibm + ;; +# I'm not sure what "Sysv32" means. Should this be sysv3.2? + i*86v32) + basic_machine=`echo $1 | sed -e 's/86.*/86-pc/'` + os=-sysv32 + ;; + i*86v4*) + basic_machine=`echo $1 | sed -e 's/86.*/86-pc/'` + os=-sysv4 + ;; + i*86v) + basic_machine=`echo $1 | sed -e 's/86.*/86-pc/'` + os=-sysv + ;; + i*86sol2) + basic_machine=`echo $1 | sed -e 's/86.*/86-pc/'` + os=-solaris2 + ;; + i386mach) + basic_machine=i386-mach + os=-mach + ;; + i386-vsta | vsta) + basic_machine=i386-unknown + os=-vsta + ;; + iris | iris4d) + basic_machine=mips-sgi + case $os in + -irix*) + ;; + *) + os=-irix4 + ;; + esac + ;; + isi68 | isi) + basic_machine=m68k-isi + os=-sysv + ;; + m88k-omron*) + basic_machine=m88k-omron + ;; + magnum | m3230) + basic_machine=mips-mips + os=-sysv + ;; + merlin) + basic_machine=ns32k-utek + os=-sysv + ;; + mingw32) + basic_machine=i386-pc + os=-mingw32 + ;; + miniframe) + basic_machine=m68000-convergent + ;; + *mint | -mint[0-9]* | *MiNT | *MiNT[0-9]*) + basic_machine=m68k-atari + os=-mint + ;; + mips3*-*) + basic_machine=`echo $basic_machine | sed -e 's/mips3/mips64/'` + ;; + mips3*) + basic_machine=`echo $basic_machine | sed -e 's/mips3/mips64/'`-unknown + ;; + mmix*) + basic_machine=mmix-knuth + os=-mmixware + ;; + monitor) + basic_machine=m68k-rom68k + os=-coff + ;; + morphos) + basic_machine=powerpc-unknown + os=-morphos + ;; + msdos) + basic_machine=i386-pc + os=-msdos + ;; + mvs) + basic_machine=i370-ibm + os=-mvs + ;; + ncr3000) + basic_machine=i486-ncr + os=-sysv4 + ;; + netbsd386) + basic_machine=i386-unknown + os=-netbsd + ;; + netwinder) + basic_machine=armv4l-rebel + os=-linux + ;; + news | news700 | news800 | news900) + basic_machine=m68k-sony + os=-newsos + ;; + news1000) + basic_machine=m68030-sony + os=-newsos + ;; + news-3600 | risc-news) + basic_machine=mips-sony + os=-newsos + ;; + necv70) + basic_machine=v70-nec + os=-sysv + ;; + next | m*-next ) + basic_machine=m68k-next + case $os in + -nextstep* ) + ;; + -ns2*) + os=-nextstep2 + ;; + *) + os=-nextstep3 + ;; + esac + ;; + nh3000) + basic_machine=m68k-harris + os=-cxux + ;; + nh[45]000) + basic_machine=m88k-harris + os=-cxux + ;; + nindy960) + basic_machine=i960-intel + os=-nindy + ;; + mon960) + basic_machine=i960-intel + os=-mon960 + ;; + nonstopux) + basic_machine=mips-compaq + os=-nonstopux + ;; + np1) + basic_machine=np1-gould + ;; + nsr-tandem) + basic_machine=nsr-tandem + ;; + op50n-* | op60c-*) + basic_machine=hppa1.1-oki + os=-proelf + ;; + or32 | or32-*) + basic_machine=or32-unknown + os=-coff + ;; + OSE68000 | ose68000) + basic_machine=m68000-ericsson + os=-ose + ;; + os68k) + basic_machine=m68k-none + os=-os68k + ;; + pa-hitachi) + basic_machine=hppa1.1-hitachi + os=-hiuxwe2 + ;; + paragon) + basic_machine=i860-intel + os=-osf + ;; + pbd) + basic_machine=sparc-tti + ;; + pbb) + basic_machine=m68k-tti + ;; + pc532 | pc532-*) + basic_machine=ns32k-pc532 + ;; + pentium | p5 | k5 | k6 | nexgen | viac3) + basic_machine=i586-pc + ;; + pentiumpro | p6 | 6x86 | athlon) + basic_machine=i686-pc + ;; + pentiumii | pentium2) + basic_machine=i686-pc + ;; + pentium-* | p5-* | k5-* | k6-* | nexgen-* | viac3-*) + basic_machine=i586-`echo $basic_machine | sed 's/^[^-]*-//'` + ;; + pentiumpro-* | p6-* | 6x86-* | athlon-*) + basic_machine=i686-`echo $basic_machine | sed 's/^[^-]*-//'` + ;; + pentiumii-* | pentium2-*) + basic_machine=i686-`echo $basic_machine | sed 's/^[^-]*-//'` + ;; + pn) + basic_machine=pn-gould + ;; + power) basic_machine=power-ibm + ;; + ppc) basic_machine=powerpc-unknown + ;; + ppc-*) basic_machine=powerpc-`echo $basic_machine | sed 's/^[^-]*-//'` + ;; + ppcle | powerpclittle | ppc-le | powerpc-little) + basic_machine=powerpcle-unknown + ;; + ppcle-* | powerpclittle-*) + basic_machine=powerpcle-`echo $basic_machine | sed 's/^[^-]*-//'` + ;; + ppc64) basic_machine=powerpc64-unknown + ;; + ppc64-*) basic_machine=powerpc64-`echo $basic_machine | sed 's/^[^-]*-//'` + ;; + ppc64le | powerpc64little | ppc64-le | powerpc64-little) + basic_machine=powerpc64le-unknown + ;; + ppc64le-* | powerpc64little-*) + basic_machine=powerpc64le-`echo $basic_machine | sed 's/^[^-]*-//'` + ;; + ps2) + basic_machine=i386-ibm + ;; + pw32) + basic_machine=i586-unknown + os=-pw32 + ;; + rom68k) + basic_machine=m68k-rom68k + os=-coff + ;; + rm[46]00) + basic_machine=mips-siemens + ;; + rtpc | rtpc-*) + basic_machine=romp-ibm + ;; + s390 | s390-*) + basic_machine=s390-ibm + ;; + s390x | s390x-*) + basic_machine=s390x-ibm + ;; + sa29200) + basic_machine=a29k-amd + os=-udi + ;; + sequent) + basic_machine=i386-sequent + ;; + sh) + basic_machine=sh-hitachi + os=-hms + ;; + sparclite-wrs | simso-wrs) + basic_machine=sparclite-wrs + os=-vxworks + ;; + sps7) + basic_machine=m68k-bull + os=-sysv2 + ;; + spur) + basic_machine=spur-unknown + ;; + st2000) + basic_machine=m68k-tandem + ;; + stratus) + basic_machine=i860-stratus + os=-sysv4 + ;; + sun2) + basic_machine=m68000-sun + ;; + sun2os3) + basic_machine=m68000-sun + os=-sunos3 + ;; + sun2os4) + basic_machine=m68000-sun + os=-sunos4 + ;; + sun3os3) + basic_machine=m68k-sun + os=-sunos3 + ;; + sun3os4) + basic_machine=m68k-sun + os=-sunos4 + ;; + sun4os3) + basic_machine=sparc-sun + os=-sunos3 + ;; + sun4os4) + basic_machine=sparc-sun + os=-sunos4 + ;; + sun4sol2) + basic_machine=sparc-sun + os=-solaris2 + ;; + sun3 | sun3-*) + basic_machine=m68k-sun + ;; + sun4) + basic_machine=sparc-sun + ;; + sun386 | sun386i | roadrunner) + basic_machine=i386-sun + ;; + sv1) + basic_machine=sv1-cray + os=-unicos + ;; + symmetry) + basic_machine=i386-sequent + os=-dynix + ;; + t3d) + basic_machine=alpha-cray + os=-unicos + ;; + t3e) + basic_machine=alphaev5-cray + os=-unicos + ;; + t90) + basic_machine=t90-cray + os=-unicos + ;; + tic54x | c54x*) + basic_machine=tic54x-unknown + os=-coff + ;; + tx39) + basic_machine=mipstx39-unknown + ;; + tx39el) + basic_machine=mipstx39el-unknown + ;; + toad1) + basic_machine=pdp10-xkl + os=-tops20 + ;; + tower | tower-32) + basic_machine=m68k-ncr + ;; + udi29k) + basic_machine=a29k-amd + os=-udi + ;; + ultra3) + basic_machine=a29k-nyu + os=-sym1 + ;; + v810 | necv810) + basic_machine=v810-nec + os=-none + ;; + vaxv) + basic_machine=vax-dec + os=-sysv + ;; + vms) + basic_machine=vax-dec + os=-vms + ;; + vpp*|vx|vx-*) + basic_machine=f301-fujitsu + ;; + vxworks960) + basic_machine=i960-wrs + os=-vxworks + ;; + vxworks68) + basic_machine=m68k-wrs + os=-vxworks + ;; + vxworks29k) + basic_machine=a29k-wrs + os=-vxworks + ;; + w65*) + basic_machine=w65-wdc + os=-none + ;; + w89k-*) + basic_machine=hppa1.1-winbond + os=-proelf + ;; + windows32) + basic_machine=i386-pc + os=-windows32-msvcrt + ;; + xps | xps100) + basic_machine=xps100-honeywell + ;; + ymp) + basic_machine=ymp-cray + os=-unicos + ;; + z8k-*-coff) + basic_machine=z8k-unknown + os=-sim + ;; + none) + basic_machine=none-none + os=-none + ;; + +# Here we handle the default manufacturer of certain CPU types. It is in +# some cases the only manufacturer, in others, it is the most popular. + w89k) + basic_machine=hppa1.1-winbond + ;; + op50n) + basic_machine=hppa1.1-oki + ;; + op60c) + basic_machine=hppa1.1-oki + ;; + romp) + basic_machine=romp-ibm + ;; + rs6000) + basic_machine=rs6000-ibm + ;; + vax) + basic_machine=vax-dec + ;; + pdp10) + # there are many clones, so DEC is not a safe bet + basic_machine=pdp10-unknown + ;; + pdp11) + basic_machine=pdp11-dec + ;; + we32k) + basic_machine=we32k-att + ;; + sh3 | sh4 | sh3eb | sh4eb | sh[1234]le | sh3ele) + basic_machine=sh-unknown + ;; + sh64) + basic_machine=sh64-unknown + ;; + sparc | sparcv9 | sparcv9b) + basic_machine=sparc-sun + ;; + cydra) + basic_machine=cydra-cydrome + ;; + orion) + basic_machine=orion-highlevel + ;; + orion105) + basic_machine=clipper-highlevel + ;; + mac | mpw | mac-mpw) + basic_machine=m68k-apple + ;; + pmac | pmac-mpw) + basic_machine=powerpc-apple + ;; + c4x*) + basic_machine=c4x-none + os=-coff + ;; + *-unknown) + # Make sure to match an already-canonicalized machine name. + ;; + *) + echo Invalid configuration \`$1\': machine \`$basic_machine\' not recognized 1>&2 + exit 1 + ;; +esac + +# Here we canonicalize certain aliases for manufacturers. +case $basic_machine in + *-digital*) + basic_machine=`echo $basic_machine | sed 's/digital.*/dec/'` + ;; + *-commodore*) + basic_machine=`echo $basic_machine | sed 's/commodore.*/cbm/'` + ;; + *) + ;; +esac + +# Decode manufacturer-specific aliases for certain operating systems. + +if [ x"$os" != x"" ] +then +case $os in + # First match some system type aliases + # that might get confused with valid system types. + # -solaris* is a basic system type, with this one exception. + -solaris1 | -solaris1.*) + os=`echo $os | sed -e 's|solaris1|sunos4|'` + ;; + -solaris) + os=-solaris2 + ;; + -svr4*) + os=-sysv4 + ;; + -unixware*) + os=-sysv4.2uw + ;; + -gnu/linux*) + os=`echo $os | sed -e 's|gnu/linux|linux-gnu|'` + ;; + # First accept the basic system types. + # The portable systems comes first. + # Each alternative MUST END IN A *, to match a version number. + # -sysv* is not here because it comes later, after sysvr4. + -gnu* | -bsd* | -mach* | -minix* | -genix* | -ultrix* | -irix* \ + | -*vms* | -sco* | -esix* | -isc* | -aix* | -sunos | -sunos[34]*\ + | -hpux* | -unos* | -osf* | -luna* | -dgux* | -solaris* | -sym* \ + | -amigaos* | -amigados* | -msdos* | -newsos* | -unicos* | -aof* \ + | -aos* \ + | -nindy* | -vxsim* | -vxworks* | -ebmon* | -hms* | -mvs* \ + | -clix* | -riscos* | -uniplus* | -iris* | -rtu* | -xenix* \ + | -hiux* | -386bsd* | -netbsd* | -openbsd* | -freebsd* | -riscix* \ + | -lynxos* | -bosx* | -nextstep* | -cxux* | -aout* | -elf* | -oabi* \ + | -ptx* | -coff* | -ecoff* | -winnt* | -domain* | -vsta* \ + | -udi* | -eabi* | -lites* | -ieee* | -go32* | -aux* \ + | -chorusos* | -chorusrdb* \ + | -cygwin* | -pe* | -psos* | -moss* | -proelf* | -rtems* \ + | -mingw32* | -linux-gnu* | -uxpv* | -beos* | -mpeix* | -udk* \ + | -interix* | -uwin* | -rhapsody* | -darwin* | -opened* \ + | -openstep* | -oskit* | -conix* | -pw32* | -nonstopux* \ + | -storm-chaos* | -tops10* | -tenex* | -tops20* | -its* \ + | -os2* | -vos* | -palmos* | -uclinux* | -nucleus* \ + | -morphos* | -superux* | -rtmk* | -rtmk-nova* | -windiss* | -powermax*) + # Remember, each alternative MUST END IN *, to match a version number. + ;; + -qnx*) + case $basic_machine in + x86-* | i*86-*) + ;; + *) + os=-nto$os + ;; + esac + ;; + -nto*) + os=-nto-qnx + ;; + -sim | -es1800* | -hms* | -xray | -os68k* | -none* | -v88r* \ + | -windows* | -osx | -abug | -netware* | -os9* | -beos* \ + | -macos* | -mpw* | -magic* | -mmixware* | -mon960* | -lnews*) + ;; + -mac*) + os=`echo $os | sed -e 's|mac|macos|'` + ;; + -linux*) + os=`echo $os | sed -e 's|linux|linux-gnu|'` + ;; + -sunos5*) + os=`echo $os | sed -e 's|sunos5|solaris2|'` + ;; + -sunos6*) + os=`echo $os | sed -e 's|sunos6|solaris3|'` + ;; + -opened*) + os=-openedition + ;; + -wince*) + os=-wince + ;; + -osfrose*) + os=-osfrose + ;; + -osf*) + os=-osf + ;; + -utek*) + os=-bsd + ;; + -dynix*) + os=-bsd + ;; + -acis*) + os=-aos + ;; + -atheos*) + os=-atheos + ;; + -386bsd) + os=-bsd + ;; + -ctix* | -uts*) + os=-sysv + ;; + -nova*) + os=-rtmk-nova + ;; + -ns2 ) + os=-nextstep2 + ;; + -nsk*) + os=-nsk + ;; + # Preserve the version number of sinix5. + -sinix5.*) + os=`echo $os | sed -e 's|sinix|sysv|'` + ;; + -sinix*) + os=-sysv4 + ;; + -triton*) + os=-sysv3 + ;; + -oss*) + os=-sysv3 + ;; + -svr4) + os=-sysv4 + ;; + -svr3) + os=-sysv3 + ;; + -sysvr4) + os=-sysv4 + ;; + # This must come after -sysvr4. + -sysv*) + ;; + -ose*) + os=-ose + ;; + -es1800*) + os=-ose + ;; + -xenix) + os=-xenix + ;; + -*mint | -mint[0-9]* | -*MiNT | -MiNT[0-9]*) + os=-mint + ;; + -none) + ;; + *) + # Get rid of the `-' at the beginning of $os. + os=`echo $os | sed 's/[^-]*-//'` + echo Invalid configuration \`$1\': system \`$os\' not recognized 1>&2 + exit 1 + ;; +esac +else + +# Here we handle the default operating systems that come with various machines. +# The value should be what the vendor currently ships out the door with their +# machine or put another way, the most popular os provided with the machine. + +# Note that if you're going to try to match "-MANUFACTURER" here (say, +# "-sun"), then you have to tell the case statement up towards the top +# that MANUFACTURER isn't an operating system. Otherwise, code above +# will signal an error saying that MANUFACTURER isn't an operating +# system, and we'll never get to this point. + +case $basic_machine in + *-acorn) + os=-riscix1.2 + ;; + arm*-rebel) + os=-linux + ;; + arm*-semi) + os=-aout + ;; + # This must come before the *-dec entry. + pdp10-*) + os=-tops20 + ;; + pdp11-*) + os=-none + ;; + *-dec | vax-*) + os=-ultrix4.2 + ;; + m68*-apollo) + os=-domain + ;; + i386-sun) + os=-sunos4.0.2 + ;; + m68000-sun) + os=-sunos3 + # This also exists in the configure program, but was not the + # default. + # os=-sunos4 + ;; + m68*-cisco) + os=-aout + ;; + mips*-cisco) + os=-elf + ;; + mips*-*) + os=-elf + ;; + or32-*) + os=-coff + ;; + *-tti) # must be before sparc entry or we get the wrong os. + os=-sysv3 + ;; + sparc-* | *-sun) + os=-sunos4.1.1 + ;; + *-be) + os=-beos + ;; + *-ibm) + os=-aix + ;; + *-wec) + os=-proelf + ;; + *-winbond) + os=-proelf + ;; + *-oki) + os=-proelf + ;; + *-hp) + os=-hpux + ;; + *-hitachi) + os=-hiux + ;; + i860-* | *-att | *-ncr | *-altos | *-motorola | *-convergent) + os=-sysv + ;; + *-cbm) + os=-amigaos + ;; + *-dg) + os=-dgux + ;; + *-dolphin) + os=-sysv3 + ;; + m68k-ccur) + os=-rtu + ;; + m88k-omron*) + os=-luna + ;; + *-next ) + os=-nextstep + ;; + *-sequent) + os=-ptx + ;; + *-crds) + os=-unos + ;; + *-ns) + os=-genix + ;; + i370-*) + os=-mvs + ;; + *-next) + os=-nextstep3 + ;; + *-gould) + os=-sysv + ;; + *-highlevel) + os=-bsd + ;; + *-encore) + os=-bsd + ;; + *-sgi) + os=-irix + ;; + *-siemens) + os=-sysv4 + ;; + *-masscomp) + os=-rtu + ;; + f30[01]-fujitsu | f700-fujitsu) + os=-uxpv + ;; + *-rom68k) + os=-coff + ;; + *-*bug) + os=-coff + ;; + *-apple) + os=-macos + ;; + *-atari*) + os=-mint + ;; + *) + os=-none + ;; +esac +fi + +# Here we handle the case where we know the os, and the CPU type, but not the +# manufacturer. We pick the logical manufacturer. +vendor=unknown +case $basic_machine in + *-unknown) + case $os in + -riscix*) + vendor=acorn + ;; + -sunos*) + vendor=sun + ;; + -aix*) + vendor=ibm + ;; + -beos*) + vendor=be + ;; + -hpux*) + vendor=hp + ;; + -mpeix*) + vendor=hp + ;; + -hiux*) + vendor=hitachi + ;; + -unos*) + vendor=crds + ;; + -dgux*) + vendor=dg + ;; + -luna*) + vendor=omron + ;; + -genix*) + vendor=ns + ;; + -mvs* | -opened*) + vendor=ibm + ;; + -ptx*) + vendor=sequent + ;; + -vxsim* | -vxworks* | -windiss*) + vendor=wrs + ;; + -aux*) + vendor=apple + ;; + -hms*) + vendor=hitachi + ;; + -mpw* | -macos*) + vendor=apple + ;; + -*mint | -mint[0-9]* | -*MiNT | -MiNT[0-9]*) + vendor=atari + ;; + -vos*) + vendor=stratus + ;; + esac + basic_machine=`echo $basic_machine | sed "s/unknown/$vendor/"` + ;; +esac + +echo $basic_machine$os +exit 0 + +# Local variables: +# eval: (add-hook 'write-file-hooks 'time-stamp) +# time-stamp-start: "timestamp='" +# time-stamp-format: "%:y-%02m-%02d" +# time-stamp-end: "'" +# End: diff --git a/bfile/config/configure.in b/bfile/config/configure.in new file mode 100644 index 0000000..9d33427 --- /dev/null +++ b/bfile/config/configure.in @@ -0,0 +1,88 @@ +AC_INIT + +dnl work out who the cpu, vendor and OS are +AC_CANONICAL_SYSTEM +AC_DEFINE_UNQUOTED(CPU_VENDOR_OS, "$target") + +dnl Programs + +AC_PROG_CC +AC_PATH_PROG(ERL, erl) +AC_PATH_PROG(ERLC, erlc) +ERLBINDIR=`dirname $ERL` ; ERLBINDIR=`dirname $ERLBINDIR`/lib/erlang/bin + +ERLDIR=`awk -F= '/ROOTDIR=/ { print [$]2; exit; }' $ERL` +AC_SUBST(ERL) +AC_SUBST(ERLC) +AC_SUBST(ERLBINDIR) +AC_SUBST(ERLDIR) + +if test ! -d "$ERLDIR" ; then + AC_MSG_ERROR([Broken Erlang installation, $ERLDIR does not exist!]) +fi + +dnl C header files + +AC_CONFIG_HEADER(config.h:config.h.in) + +AC_CHECK_HEADERS(malloc.h) + +BT_MSG_CONTROL + +case "$target_os" in + *cygwin*) + : + dnl fix this later + ;; + linux*) + AC_DEFINE(LINUX) + LD_SHARED="ld -shared" + ;; + *bsd*) + AC_DEFINE(BSD) + LD_SHARED="ld -Bshareable" + ;; + *solaris*) + AC_DEFINE(SOLARIS) + LD_SHARED="ld -G" + ;; + *darwin*) + AC_DEFINE([DARWIN], [], [Description]) + LD_SHARED="cc -bundle -flat_namespace -undefined suppress" + ;; + *) + LD_SHARED="ld -shared" + ;; +esac + +AC_SUBST(LD_SHARED) + + +dnl libnsl and libsocket tests borrowed from ethereal's autoconf scheme. +dnl # msh@cis.ufl.edu says -lnsl (and -lsocket) are needed for his 386/AT, +dnl # to get the SysV transport functions. +dnl # chad@anasazi.com says the Pyramid MIS-ES running DC/OSx (SVR4) +dnl # needs -lnsl. +dnl # The nsl library prevents programs from opening the X display +dnl # on Irix 5.2, according to dickey@clark.net. +AC_CHECK_FUNC(gethostbyname, , + AC_CHECK_LIB(nsl, gethostbyname, NSL_LIBS="-lnsl")) +AC_SUBST(NSL_LIBS) +dnl # lieder@skyler.mavd.honeywell.com says without -lsocket, +dnl # socket/setsockopt and other routines are undefined under SCO ODT +dnl # 2.0. But -lsocket is broken on IRIX 5.2 (and is not necessary +dnl # on later versions), says simon@lia.di.epfl.ch: it contains +dnl # gethostby* variants that don't use the nameserver (or something). +dnl # -lsocket must be given before -lnsl if both are needed. +dnl # We assume that if connect needs -lnsl, so does gethostbyname. +AC_CHECK_FUNC(connect, , + AC_CHECK_LIB(socket, connect, SOCKET_LIBS="-lsocket", + AC_MSG_ERROR(Function 'socket' not found.), $NSL_LIBS)) +AC_SUBST(SOCKET_LIBS) + +dnl +dnl End. + +AC_OUTPUT(include.mk) + + diff --git a/bfile/config/include.mk.in b/bfile/config/include.mk.in new file mode 100644 index 0000000..e81232b --- /dev/null +++ b/bfile/config/include.mk.in @@ -0,0 +1,75 @@ +## -*- makefile -*- + +###################################################################### +## C + +CC := @CC@ +CFLAGS := @CFLAGS@ @DEFS@ + +LD_SHARED := @LD_SHARED@ + +###################################################################### +## Erlang + +ERL = @ERL@ +ERLC = @ERLC@ +ERLDIR = @ERLDIR@ + +ERL_C_INCLUDE_DIR := $(ERLDIR)/usr/include + + +ERLC_FLAGS := -W + +ifndef no_debug_info + ERLC_FLAGS += +debug_info +endif + +ifdef debug + ERLC_FLAGS += -Ddebug +endif + +EBIN_DIR := ../ebin +DOC_DIR := ../doc +EMULATOR := beam + +ERL_SOURCES := $(wildcard *.erl) +ERL_HEADERS := $(wildcard *.hrl) $(wildcard ../include/*.hrl) +ERL_OBJECTS := $(ERL_SOURCES:%.erl=$(EBIN_DIR)/%.$(EMULATOR)) +ERL_DOCUMENTS := $(ERL_SOURCES:%.erl=$(DOC_DIR)/%.html) + +# Hmm, don't know if you are supposed to like this better... ;-) +APPSCRIPT = '$$vsn=shift; $$mods=""; while(@ARGV){ $$_=shift; s/^([A-Z].*)$$/\'\''$$1\'\''/; $$mods.=", " if $$mods; $$mods .= $$_; } while(<>) { s/%VSN%/$$vsn/; s/%MODULES%/$$mods/; print; }' + + +../ebin/%.app: %.app.src ../vsn.mk Makefile + perl -e $(APPSCRIPT) "$(VSN)" $(MODULES) < $< > $@ + +../ebin/%.appup: %.appup + cp $< $@ + + +$(EBIN_DIR)/%.$(EMULATOR): %.erl + $(ERLC) $(ERLC_FLAGS) -o $(EBIN_DIR) $< + +# generate documentation with edoc: +# this is still not the proper way to do it, but it works +# (see the wumpus application for an example) + +$(DOC_DIR)/%.html: %.erl + ${ERL} -noshell \ + -pa ../../syntax_tools/ebin \ + -pa ../../edoc/ebin \ + -pa ../../xmerl/ebin \ + -pa ../../ucs/ebin \ + -run edoc file $< -run init stop + mv *.html $(DOC_DIR) + +# C linking items + +NSL_LIBS = @NSL_LIBS@ +SOCKET_LIBS = @SOCKET_LIBS@ + +# Miscellaneous + +GROFF = @GROFF@ +PS2PDF = @PS2PDF@ diff --git a/bfile/config/install-sh b/bfile/config/install-sh new file mode 100755 index 0000000..e843669 --- /dev/null +++ b/bfile/config/install-sh @@ -0,0 +1,250 @@ +#!/bin/sh +# +# install - install a program, script, or datafile +# This comes from X11R5 (mit/util/scripts/install.sh). +# +# Copyright 1991 by the Massachusetts Institute of Technology +# +# Permission to use, copy, modify, distribute, and sell this software and its +# documentation for any purpose is hereby granted without fee, provided that +# the above copyright notice appear in all copies and that both that +# copyright notice and this permission notice appear in supporting +# documentation, and that the name of M.I.T. not be used in advertising or +# publicity pertaining to distribution of the software without specific, +# written prior permission. M.I.T. makes no representations about the +# suitability of this software for any purpose. It is provided "as is" +# without express or implied warranty. +# +# Calling this script install-sh is preferred over install.sh, to prevent +# `make' implicit rules from creating a file called install from it +# when there is no Makefile. +# +# This script is compatible with the BSD install script, but was written +# from scratch. It can only install one file at a time, a restriction +# shared with many OS's install programs. + + +# set DOITPROG to echo to test this script + +# Don't use :- since 4.3BSD and earlier shells don't like it. +doit="${DOITPROG-}" + + +# put in absolute paths if you don't have them in your path; or use env. vars. + +mvprog="${MVPROG-mv}" +cpprog="${CPPROG-cp}" +chmodprog="${CHMODPROG-chmod}" +chownprog="${CHOWNPROG-chown}" +chgrpprog="${CHGRPPROG-chgrp}" +stripprog="${STRIPPROG-strip}" +rmprog="${RMPROG-rm}" +mkdirprog="${MKDIRPROG-mkdir}" + +transformbasename="" +transform_arg="" +instcmd="$mvprog" +chmodcmd="$chmodprog 0755" +chowncmd="" +chgrpcmd="" +stripcmd="" +rmcmd="$rmprog -f" +mvcmd="$mvprog" +src="" +dst="" +dir_arg="" + +while [ x"$1" != x ]; do + case $1 in + -c) instcmd="$cpprog" + shift + continue;; + + -d) dir_arg=true + shift + continue;; + + -m) chmodcmd="$chmodprog $2" + shift + shift + continue;; + + -o) chowncmd="$chownprog $2" + shift + shift + continue;; + + -g) chgrpcmd="$chgrpprog $2" + shift + shift + continue;; + + -s) stripcmd="$stripprog" + shift + continue;; + + -t=*) transformarg=`echo $1 | sed 's/-t=//'` + shift + continue;; + + -b=*) transformbasename=`echo $1 | sed 's/-b=//'` + shift + continue;; + + *) if [ x"$src" = x ] + then + src=$1 + else + # this colon is to work around a 386BSD /bin/sh bug + : + dst=$1 + fi + shift + continue;; + esac +done + +if [ x"$src" = x ] +then + echo "install: no input file specified" + exit 1 +else + true +fi + +if [ x"$dir_arg" != x ]; then + dst=$src + src="" + + if [ -d $dst ]; then + instcmd=: + else + instcmd=mkdir + fi +else + +# Waiting for this to be detected by the "$instcmd $src $dsttmp" command +# might cause directories to be created, which would be especially bad +# if $src (and thus $dsttmp) contains '*'. + + if [ -f $src -o -d $src ] + then + true + else + echo "install: $src does not exist" + exit 1 + fi + + if [ x"$dst" = x ] + then + echo "install: no destination specified" + exit 1 + else + true + fi + +# If destination is a directory, append the input filename; if your system +# does not like double slashes in filenames, you may need to add some logic + + if [ -d $dst ] + then + dst="$dst"/`basename $src` + else + true + fi +fi + +## this sed command emulates the dirname command +dstdir=`echo $dst | sed -e 's,[^/]*$,,;s,/$,,;s,^$,.,'` + +# Make sure that the destination directory exists. +# this part is taken from Noah Friedman's mkinstalldirs script + +# Skip lots of stat calls in the usual case. +if [ ! -d "$dstdir" ]; then +defaultIFS=' +' +IFS="${IFS-${defaultIFS}}" + +oIFS="${IFS}" +# Some sh's can't handle IFS=/ for some reason. +IFS='%' +set - `echo ${dstdir} | sed -e 's@/@%@g' -e 's@^%@/@'` +IFS="${oIFS}" + +pathcomp='' + +while [ $# -ne 0 ] ; do + pathcomp="${pathcomp}${1}" + shift + + if [ ! -d "${pathcomp}" ] ; + then + $mkdirprog "${pathcomp}" + else + true + fi + + pathcomp="${pathcomp}/" +done +fi + +if [ x"$dir_arg" != x ] +then + $doit $instcmd $dst && + + if [ x"$chowncmd" != x ]; then $doit $chowncmd $dst; else true ; fi && + if [ x"$chgrpcmd" != x ]; then $doit $chgrpcmd $dst; else true ; fi && + if [ x"$stripcmd" != x ]; then $doit $stripcmd $dst; else true ; fi && + if [ x"$chmodcmd" != x ]; then $doit $chmodcmd $dst; else true ; fi +else + +# If we're going to rename the final executable, determine the name now. + + if [ x"$transformarg" = x ] + then + dstfile=`basename $dst` + else + dstfile=`basename $dst $transformbasename | + sed $transformarg`$transformbasename + fi + +# don't allow the sed command to completely eliminate the filename + + if [ x"$dstfile" = x ] + then + dstfile=`basename $dst` + else + true + fi + +# Make a temp file name in the proper directory. + + dsttmp=$dstdir/#inst.$$# + +# Move or copy the file name to the temp name + + $doit $instcmd $src $dsttmp && + + trap "rm -f ${dsttmp}" 0 && + +# and set any options; do chmod last to preserve setuid bits + +# If any of these fail, we abort the whole thing. If we want to +# ignore errors from any of these, just make sure not to ignore +# errors from the above "$doit $instcmd $src $dsttmp" command. + + if [ x"$chowncmd" != x ]; then $doit $chowncmd $dsttmp; else true;fi && + if [ x"$chgrpcmd" != x ]; then $doit $chgrpcmd $dsttmp; else true;fi && + if [ x"$stripcmd" != x ]; then $doit $stripcmd $dsttmp; else true;fi && + if [ x"$chmodcmd" != x ]; then $doit $chmodcmd $dsttmp; else true;fi && + +# Now rename the file to the real destination. + + $doit $rmcmd -f $dstdir/$dstfile && + $doit $mvcmd $dsttmp $dstdir/$dstfile + +fi && + + +exit 0 diff --git a/bfile/src/Makefile b/bfile/src/Makefile new file mode 100644 index 0000000..069f8b4 --- /dev/null +++ b/bfile/src/Makefile @@ -0,0 +1,25 @@ + +include ../config/include.mk + +include ../vsn.mk +VSN=$(FD_SERVER_VSN) + +ifeq ($(TYPE),debug) +DEBUG_FLAGS = -Ddebug +else +DEBUG_FLAGS = +endif + +MODULES=bfile + +EBIN = ../ebin +EBIN_FILES = $(MODULES:%=$(EBIN)/%.$(EMULATOR)) $(EBIN)/bfile.app + + +all: $(EBIN_FILES) + +debug: + $(MAKE) DEBUG=-DDEBUG +clean: + rm -rf $(EBIN_FILES) + diff --git a/bfile/src/bfile.app.src b/bfile/src/bfile.app.src new file mode 100644 index 0000000..e806ef2 --- /dev/null +++ b/bfile/src/bfile.app.src @@ -0,0 +1,7 @@ +{application,bfile, + [{description,"Fast FILE interface"}, + {vsn,"%VSN%"}, + {modules,[%MODULES%]}, + {applications,[kernel,stdlib]}, + {env, []}, + {mod,{bfile,[]}}]}. diff --git a/bfile/src/bfile.erl b/bfile/src/bfile.erl new file mode 100644 index 0000000..6cc8efe --- /dev/null +++ b/bfile/src/bfile.erl @@ -0,0 +1,224 @@ +%%% +%%% File : bfile.erl +%%% Author : Claes Wikstrom +%%% Purpose : Interface to stdio buffered FILE io +%%% Created : 22 Nov 1999 by Claes Wikstrom +%%%---------------------------------------------------------------------- + +-module(bfile). +-vsn("$Revision: 1.1 $ "). +-author('klacke@kaja.klacke.net'). + +-export([ + load_driver/0, + fopen/2, + fclose/1, + fread/2, + fwrite/2, + feof/1, + ferror/1, + set_linebuf_size/2, + fseek/3, + ftell/1, + ftruncate/1, + fflush/1, + frewind/1, + fgetc/1, + fungetc/2, + fgets/1, + gets/1, + pwrite/3, + pread/3 + ]). + + +%% Opcodes +-define(OPEN, $o). +-define(CLOSE, $c). +-define(READ, $r). +-define(WRITE, $w). +-define(SEEK, $s). +-define(TELL, $t). +-define(TRUNCATE, $T). +-define(FLUSH, $f). +-define(OEOF, $e). +-define(ERROR, $E). +-define(GETC, $g). +-define(GETS, $G). +-define(GETS2, $2). +-define(SET_LINEBUF_SIZE, $S). +-define(UNGETC, $u). + + +%% ret codes +-define(VALUE, $v). +-define(FLINE, $L). +-define(OK, $o). +-define(I32, $O). +-define(NOLINE,$N). +-define(FERROR, $E). +-define(REOF, $x). + +-define(int32(X), + [((X) bsr 24) band 16#ff, ((X) bsr 16) band 16#ff, + ((X) bsr 8) band 16#ff, (X) band 16#ff]). +%% Bytes to unsigned +-define(u32(X3,X2,X1,X0), + (((X3) bsl 24) bor ((X2) bsl 16) bor ((X1) bsl 8) bor (X0))). +%% Bytes to signed +-define(i32(X3,X2,X1,X0), + (?u32(X3,X2,X1,X0) - + (if (X3) > 127 -> 16#100000000; true -> 0 end))). + +load_driver() -> + Dir = filename:join([filename:dirname(code:which(bfile)),"..", "priv"]), + erl_ddll:load_driver(Dir, "FILE_drv"). + + +%% Flags = "r" | "w" | ... etc, see fopen(3) +%% Ret: {ok, Fd} | {error, Reason} + +fopen(Fname, Flags) -> + P = open_port({spawn, 'FILE_drv'}, [binary]), + Res = erlang_port_control(P, ?OPEN, [Fname, 0, Flags, 0]), + case decode(Res) of + ok -> + {ok, {bfile, P}}; + Err -> + unlink(P), + exit(P, die), + Err + end. + +%% void() +fclose({bfile, Fd}) -> + unlink(Fd), + catch erlang:port_close(Fd). + +%% {ok, #Bin} | {error, Reason} | eof +fread({bfile, Fd}, Sz) -> + Res = erlang_port_control(Fd, ?READ, ?int32(Sz)), + decode(Res). + +%% ok | {error, Reason} +fwrite({bfile, Fd}, IoList) -> + Res = erlang_port_control(Fd, ?WRITE, IoList), + decode(Res). + +%% ok | {error, Reason} +pwrite(BFd, Pos, IoList) -> + case fseek(BFd, Pos, seek_set) of + ok -> + fwrite(BFd, IoList); + Error -> + Error + end. + +%% {ok, #Bin} | {error, Reason} | eof +pread(BFd, Pos, Sz) -> + case fseek(BFd, Pos, seek_set) of + ok -> + fread(BFd, Sz); + Error -> + Error + end. + +%% bool +feof({bfile, Fd}) -> + Res = erlang_port_control(Fd, ?OEOF, []), + bool(decode(Res)). + +%% bool +ferror({bfile, Fd}) -> + Res = erlang_port_control(Fd, ?ERROR, []), + bool(decode(Res)). + + +%% void() +set_linebuf_size({bfile, Fd}, Sz) -> + Res = erlang_port_control(Fd, ?SET_LINEBUF_SIZE, ?int32(Sz)), + decode(Res). + +%% Whence == seek_set | seek_cur || seek_end +%% ok | {error, Reason} +fseek({bfile, Fd}, Offs, Whence) -> + Res = erlang_port_control(Fd, ?SEEK, [?int32(Offs), whence_enc(Whence)]), + decode(Res). + +%% {ok, Int} | {error, Reason} +ftell({bfile, Fd}) -> + Res = erlang_port_control(Fd, ?TELL, []), + decode(Res). + +%% ok | {error, Reason} +ftruncate({bfile, Fd}) -> + Res = erlang_port_control(Fd, ?TRUNCATE, []), + decode(Res). + +%% ok | {error, Reason} +fflush({bfile, Fd}) -> + Res = erlang_port_control(Fd, ?FLUSH, []), + decode(Res). + +%% ok | {error, Reason} +frewind(BFd) -> + fseek(BFd, 0, seek_set). + +%% {ok, Char} | {error, Reason} | eof +fgetc({bfile, Fd}) -> + Res = erlang_port_control(Fd, ?GETC, []), + decode(Res). + +%% ok | {error, Reason} +fungetc({bfile, Fd}, Char) -> + Res = erlang_port_control(Fd, ?UNGETC, [Char]), + decode(Res). + +%% {line, #Bin} | {noline, #Bin} | {error, Reason} | eof +%% including newline +fgets({bfile, Fd}) -> + Res = erlang_port_control(Fd, ?GETS, []), + decode(Res). + +%% {line, #Bin} | {noline, #Bin} | {error, Reason} | eof +%% not including newline +gets({bfile, Fd}) -> + Res = erlang_port_control(Fd, ?GETS2, []), + decode(Res). + + +whence_enc(seek_set) -> + 1; +whence_enc(seek_cur) -> + 2; +whence_enc(seek_end) -> + 3. + + +bool({ok, 1}) -> + true; +bool({ok, 0}) -> + false. + + +decode(Res) -> + case Res of + <> -> + {ok, Bin}; + <> -> + {line, Bin}; + <> -> + ok; + <> -> + {ok, ?i32(X1, X2, X3, X4)}; + <> -> + {noline, Bin}; + <> -> + {error, list_to_atom(binary_to_list(Err))}; + <> -> + eof + end. + +erlang_port_control(P, C, Data) -> + erlang:port_control(P, C, Data). + diff --git a/bfile/tests/read.erl b/bfile/tests/read.erl new file mode 100644 index 0000000..d03f399 --- /dev/null +++ b/bfile/tests/read.erl @@ -0,0 +1,19 @@ +-module(read). +-export([start/1, start/2]). + +scan_file(F, Readsize, Total) -> + Rd = bfile:fread(F, Readsize), + case Rd of + {ok, Bin} -> scan_file(F, Readsize, size(Bin)+Total); + eof -> Total + end. +scan_file(F, Readsize) -> scan_file(F, Readsize, 0). + +start(File, Readsize) -> + bfile:load_driver(), + {ok, F} = bfile:fopen(File, "r"), + T = scan_file(F, Readsize), + io:format("read ~p bytes~n", [T]), + bfile:fclose(F). +start(File) -> + start(File, 512*1024). diff --git a/bfile/tests/readold.erl b/bfile/tests/readold.erl new file mode 100644 index 0000000..9e5ef57 --- /dev/null +++ b/bfile/tests/readold.erl @@ -0,0 +1,18 @@ +-module(readold). +-export([start/1, start/2]). + +scan_file(F, Readsize, Total) -> + Rd = file:read(F, Readsize), + case Rd of + {ok, Bin} -> scan_file(F, Readsize, size(Bin)+Total); + eof -> Total + end. +scan_file(F, Readsize) -> scan_file(F, Readsize, 0). + +start(File, Readsize) -> + {ok, F} = file:open(File, [raw, binary, read]), + T = scan_file(F, Readsize), + io:format("read ~p bytes~n", [T]), + file:close(F). +start(File) -> + start(File, 512*1024). diff --git a/bfile/tests/write.erl b/bfile/tests/write.erl new file mode 100644 index 0000000..87ea217 --- /dev/null +++ b/bfile/tests/write.erl @@ -0,0 +1,17 @@ +-module(write). +-export([start/2, start/3]). + +dump_file(F, Data, 0) -> + ok; +dump_file(F, Data, N) -> + bfile:fwrite(F, Data), + dump_file(F, Data, N - 1). + +start(File, Data, N) -> + bfile:load_driver(), + {ok, F} = bfile:fopen(File, "w"), + dump_file(F, Data, N), + bfile:fclose(F). +start(File, N) -> + Data = list_to_binary(lists:duplicate(1000000, 10)), + start(File, Data, N). diff --git a/bfile/tests/writeold.erl b/bfile/tests/writeold.erl new file mode 100644 index 0000000..79643e2 --- /dev/null +++ b/bfile/tests/writeold.erl @@ -0,0 +1,16 @@ +-module(writeold). +-export([start/2, start/3]). + +dump_file(F, Data, 0) -> + ok; +dump_file(F, Data, N) -> + file:write(F, Data), + dump_file(F, Data, N - 1). + +start(File, Data, N) -> + {ok, F} = file:open(File, [raw, binary, write]), + dump_file(F, Data, N), + file:close(F). +start(File, N) -> + Data = list_to_binary(lists:duplicate(1000000, 10)), + start(File, Data, N). diff --git a/bfile/vsn.mk b/bfile/vsn.mk new file mode 100644 index 0000000..b09fb24 --- /dev/null +++ b/bfile/vsn.mk @@ -0,0 +1 @@ +BFILE_VSN=1.0 diff --git a/dns/src/dns.erl b/dns/src/dns.erl new file mode 100644 index 0000000..5712b92 --- /dev/null +++ b/dns/src/dns.erl @@ -0,0 +1,381 @@ +%%%------------------------------------------------------------------- +%% @copyright Process One 2008-2009 +%% @author Geoff Cant +%% @author Geoff Cant +%% @version {@vsn}, {@date} {@time} +%% @doc DNS Lookup and utility functions. +%% +%% Provides a programmer-friendly API for a number of undocumented OTP +%% dns lookup, resolution, caching and configuration functions. +%% +%% Also provides utility functions for performing lookups while +%% bypassing local resolver caching, finding closest parent zones, +%% parsing `resolv.conf' files, simplifying #dns_rec{} answers and more. +%% @end +%%%------------------------------------------------------------------- +-module(dns). + +-include_lib("eunit/include/eunit.hrl"). + +%% API +-export([lookup/2 + ,lookup/3 + ,lookup/4 + ,lookup/5 + ,lookup/7 + ,lookup_cache/3 + ,cache_lookup/1 + ,simplify/1]). + +-export([find_soa/1 + ,nameservers/1 + ,nameservers/2 + ,nameserver_address/1 + ,direct_lookup/3 + ,info/2 + ,to_proplist/1 + ,parse_resolv/1 + ,domain_exists/1 + ,expand_options/1 + ,resolvers/0 + ]). + +-include_lib("kernel/src/inet_dns.hrl"). +-include_lib("kernel/include/inet.hrl"). + +%% @type query_class() = in. +%% An atom with the name of a DNS query class. `in' is the only class +%% used (and probably tested) in the OTP lookup code. + +%% @type query_type() = a | cname | soa | mx | ns | srv | any. +%% An atom with the name of a DNS query type. This list is not +%% exhaustive. + +%% @type simple_rr() = {Name::string(), query_type(), RRdata::term(), [simple_rr_info()]}. +%% A simplified version of #dns_rr{} resource records. + +%% @type simple_rr_info() = {ttl, TimeToLive::integer()} | {class, query_class()}. + +%% @type ip_address() = {integer(),integer(),integer(),integer()}. + +%% @type address() = ip_address() | {ip_address(), port_no()} | +%% string() | #dns_rr{}. + +%% @type port_no() = integer(). + +%% @type resolver() = {ip_address(), port_no()}. + +%% @type query_options() = [query_option()]. +%% A proplist of configuration options for lookup behaviour. + +%% @type query_option() = {read_cache, bool()} | +%% {write_cache, bool()} | +%% {timeout, TimeOut::integer()} | +%% {servers, [address()]} | +%% {class, query_class()} | +%% defaults | read_cached. +%%
    +%%
  • `read_cache' - if `true' (default: `false'), return results from the `inet_db' cache +%% before making a query over the network.
  • +%%
  • `write_cache' - if `true' (default: `false'), write successful query result RRs to +%% the `inet_db' cache
  • +%%
  • `timeout' - number of milliseconds (default: 5000) to wait for a response from a +%% nameserver.
  • +%%
  • `servers' - a list of nameservers to query for the RRs. Defaults +%% to `inet_db:res_option(nameserver)' which is usually the nameservers +%% specified in '/etc/resolv.conf'
  • +%%
  • `class' - record class to query for (default: `in').
  • +%%
  • `defaults' - shorthand for +%% +%% [{servers, dns:resolvers()}, +%% {read_cache, false}, {write_cache, false}, +%% {timeout, timer:seconds(5)}, {class, in}] +%% +%%
  • +%%
  • `read_cached' - shorthand for +%% +%% [{servers, dns:resolvers()}, +%% {read_cache, true}, {write_cache, false}, +%% {timeout, timer:seconds(5)}, {class, in}] +%% +%%
  • +%%
+ +%%==================================================================== +%% RR lookup API +%%==================================================================== + +%% @spec lookup(Name::string(), query_type()) -> #dns_res{} +%% @doc Query for a DNS record of Type for Name. Uses reasonable default +%% options for class (`in') timeouts (5s), caching (no caching) and +%% nameservers (`inet_db' defaults). +%% @end +%% @equiv lookup(Name, Type, [defaults]) +lookup(Name, Type) + when is_list(Name), is_atom(Type) -> + lookup(Name, Type, [defaults]). + +%% @spec lookup(Name::string(), query_type(), query_options()) -> #dns_res{} +%% @doc Query for a DNS record of Type for Name. Takes a variety of +%% query options. +%% @end +lookup(Name, Type, Options) + when is_list(Name), is_atom(Type), + is_list(Options) -> + lookup2(Name, Type, expand_options(Options)). + +%% @private +lookup2(Name, Type, Options) -> + Timeout = proplists:get_value(timeout, Options, timer:seconds(5)), + Servers = [nameserver_address(R) + || R <- proplists:get_value(servers, Options, resolvers())], + Class = proplists:get_value(class, Options, in), + ReadCache = proplists:get_value(read_cache, Options, false), + WriteCache = proplists:get_value(write_cache, Options, false), + lookup(Name, Class, Type, Servers, Timeout, ReadCache, WriteCache). + +%% @spec lookup(Name::string(), query_type(), +%% NameServers, Timeout::integer()) -> #dns_res{} +%% where NameServers = [resolver()] +%% @doc Query the given Nameservers for a DNS record of Type (class `in') for +%% Name. Takes a Timeout in milliseconds. Doesn't read or write the +%% `inet_db' RR cache. +%% @equiv lookup(Name, in, Type, Servers, Timeout, false, false) +lookup(Name, Type, Servers, Timeout) + when is_list(Name), is_atom(Type), + is_list(Servers), is_integer(Timeout) -> + lookup(Name, in, Type, Servers, Timeout). + +%% @spec lookup(Name::string(), query_class(), query_type(), +%% [resolver()], Timeout::integer()) -> #dns_res{} +%% @doc Query the given Nameservers for a DNS record of Class, Type for +%% Name. Takes a Timeout in milliseconds. Doesn't read or write the +%% `inet_db' RR cache. +%% @equiv lookup(Name, Class, Type, Servers, Timeout, false, false) +lookup(Name, Class, Type, Servers, Timeout) + when is_list(Name), is_atom(Class), is_atom(Type), + is_list(Servers), is_integer(Timeout) -> + lookup(Name, Class, Type, Servers, Timeout, false, false). + +%% @spec lookup(Name::string(), class(), query_type(), +%% Servers::[address()], Timeout::integer(), +%% ReadCache::bool(), WriteCache::bool()) -> #dns_res{} +%% @doc Query the given Nameservers for a DNS record of Class, Type for +%% Name. Takes a Timeout in milliseconds. Returns results from the +%% `inet_db' RR cache if available and ReadCache is `true'. Writes +%% successful query answers to the cache if WriteCache is `true'. +%% @end +%% Query inet_db cache. +lookup(Name, Class = in, Type, Servers, Timeout, + _ReadCache = true, WriteCache) -> + case lookup_cache(Class, Name, Type) of + {ok, Answer} -> {ok, Answer}; + {error, nxdomain} -> + lookup(Name, Class, Type, Servers, Timeout, false, WriteCache) + end; +%% Perform DNS lookup and write results to inet_db cache +lookup(Name, Class, Type, Servers, Timeout, + _ReadCache, _WriteCache = true) -> + case lookup(Name, Class, Type, Servers, Timeout, false, false) of + {ok, Rec} -> cache_lookup(Rec), Rec; + Else -> Else + end; +%% Lookup via DNS +lookup(Name, Class, Type, Servers, Timeout, + _ReadCache = false, _WriteCache = false) -> + inet_res:nnslookup(Name, Class, Type, Servers, Timeout). + +%% @private +%% Process options list +expand_options(Opts) -> + proplists:expand(option_expansions(), Opts). +%% @private +option_expansions() -> + [{defaults, [{servers, resolvers()}, + {timeout, timer:seconds(5)}, + {read_cache, false}, + {write_cache, false}]} + ,{read_cached, [{servers, resolvers()}, + {timeout, timer:seconds(5)}, + {read_cache, true}, + {write_cache, false}]} + ]. + +%% @spec simplify(Result) -> {error, Reason::term()} | [simple_rr()] +%% where Result = {ok, #dns_rec{}} | {error, Reason::term()} +%% @doc Simplify the records returned from lookup to fixed-format +%% tuples instead of records. +%% @end +simplify({ok, #dns_rec{anlist=A}}) -> + {ok, [{Name, Type, Data, [{ttl, TTL},{class,Class}]} + || #dns_rr{domain=Name,class=Class, + type=Type,data=Data, + ttl=TTL} <- A]}; +simplify(E) -> E. + +%%==================================================================== +%% Cache API +%%==================================================================== + +%% @spec lookup_cache(query_class(), Name::string(), query_type()) -> +%% {ok, #dns_rec{}} | {error, Reason::term()} +%% @doc Query `inet_db' RR cache. Converts the `inet_db' `#hostent{}' respone +%% into a fake `#dns_rec{}'. +lookup_cache(Class, Name, Type) -> + case inet_db:getbyname(Name, Type) of + {ok, #hostent{h_addr_list=Answers}} -> + %% Convert hostents to fake dns records + RRs = [#dns_rr{domain=Name, class=Class, + type=Type, ttl=cached, + data=D} + || D <- Answers], + {ok, #dns_rec{header=cached, anlist=RRs}}; + Else -> Else + end. + +%% @spec cache_lookup(#dns_rec{}) -> ok +%% @doc Store dns answer resource records in inet_db cache +cache_lookup(#dns_rec{anlist=RRs}) -> + lists:foreach(fun inet_db:add_rr/1, RRs), + ok. + +%%==================================================================== +%% Utility API +%%==================================================================== + +%% @spec resolvers() -> [resolver()] +%% @doc Returns a list of name servers that can be used as recursive +%% resolvers. Resolvers taken from the `inet_db' `namserver' setting. +resolvers() -> + inet_db:res_option(nameserver). + +%% @spec find_soa(Name::string()) -> {ok, {SoaName::string(), #dns_rr{}}} | +%% {error, Reason::term()} +%% @doc Recursively climb the ancestry of a domain name to find the +%% most specific SOA. (Closest domain delegation/authority) +%% Given `foo.bar.baz.com' would try `foo.bar.baz.com' then `bar.baz.com' +%% then `baz.com' and so on. +find_soa(Name) -> + Components = string:tokens(Name, "."), + find_soa2(Components). + +%% @private +find_soa2([]) -> + {error, nxdomain}; +find_soa2(Components) -> + Name = string:join(Components,"."), + case lookup(Name, soa) of + {ok, #dns_rec{anlist=[RR = #dns_rr{domain=Name,type=soa}]}} -> + {ok, {Name, RR}}; + {error, nxdomain} -> + find_soa2(tl(Components)); + Else -> Else + end. + +%% @spec domain_exists(DomainName::string()) -> bool() +%% @doc Returns `true' if a name exists and has an SOA record. +domain_exists(Name) -> + case lookup(Name, soa) of + {ok, #dns_rec{anlist=[#dns_rr{domain=Name,type=soa}]}} -> + true; + {error, nxdomain} -> + false; + Else -> erlang:error(Else) + end. + +%% @spec parse_resolv(FileName::string()) -> [ip_address()] +%% @doc Returns a list of nameservers from a POSIX style `resolv.conf' file. +parse_resolv(File) -> + {ok, Rs} = inet_parse:resolv(File), + [NS || {nameserver,NS} <- Rs]. + +%% @spec nameservers(Domain::string()) -> [resolver()] +%% @equiv nameservers(Domain, [read_cached]) +nameservers(Domain) -> nameservers(Domain, [read_cached]). + +%% @spec nameservers(Domain::string(), query_options()) -> [resolver()] +%% @doc Retrieve the names and addresses of the authoritative +%% nameservers for Domain. Will perform one NS lookup if the +%% resolving nameserver returns the address records for the queried +%% records, otherwise will perform 1 + N lookups (N = number of NS +%% records for Domain). Takes a list of query option that will be used +%% for NS lookups. +nameservers(Domain, Options) -> + case lookup(Domain, ns, Options) of + E = {error, _} -> E; + {ok, #dns_rec{anlist=RRs, arlist=AR}} -> + Names = [Name + || #dns_rr{data=Name,type=ns,domain=D} <- RRs, + D =:= Domain], + Additional = [Addr || #dns_rr{data=Addr,type=a,domain=Name} <- AR, + lists:member(Name, Names)], + if length(Additional) > 0 -> + [nameserver_address(Addr) || Addr <- Additional]; + true -> + [nameserver_address(RR) || RR <- RRs] + end + end. + +%% @spec nameserver_address(address()) -> resolver() | error +%% @doc +%% Convert a variety of nameserver address specifications into a +%% host-port tuple for use with `gen_udp' and `inet_res'. +%% @end +%% @todo Should take/respect query options instead of calling inet:getaddr/2. +nameserver_address(IP = {_, _, _, _}) -> {IP,53}; +nameserver_address(Addr = {{_, _, _, _}, _}) -> Addr; +nameserver_address(Name) when is_list(Name) -> + case inet:getaddr(Name, inet) of + {ok, IP} -> {IP, 53}; + _ -> error + end; +nameserver_address(#dns_rr{data=Name,type=ns}) -> + nameserver_address(Name). + +%% @spec direct_lookup(Name::string(), query_type(), Domain::string()) +%% -> #dns_res{} +%% @doc Lookup the given Name/Type against the authoritative NSs for +%% Domain. This will bypass nxdomain caching and should result in +%% quicker recognition of changed/added records. +%% This is mainly intended for checking if people have setup a set of +%% DNS records correctly (say for a white-label hosting service). +direct_lookup(Name, Type, Domain) -> + Servers = [{A,53} || {_NS, A} <- nameservers(Domain)], + lookup(Name, Type, [{servers, Servers}]). + +%%==================================================================== +%% inet record manipulation and access API +%%==================================================================== + +%% @private +info(Field, Rec) -> + Fields = fields(Rec), + FieldIdx = lists:zip(Fields, lists:seq(2,length(Fields)+1)), + element(proplists:get_value(Field,FieldIdx), Rec). + +%% @private +fields(#dns_rec{}) -> fields(dns_rec); +fields(dns_rec) -> record_info(fields, dns_rec); +fields(#dns_rr{}) -> fields(dns_rr); +fields(dns_rr) -> record_info(fields, dns_rr). + +%% @private +to_proplist(R) -> + Keys = fields(R), + Values = tl(tuple_to_list(R)), + lists:zip(Keys,Values). + +%%==================================================================== +%% Unit Tests +%%==================================================================== +%% @todo Add more unit tests. + +%% @private +nameserver_address_test_() -> + T = [{{1,2,3,4}, {{1,2,3,4},53}}, + {{{1,2,3,4},53}, {{1,2,3,4},53}}, + {#dns_rr{data={1,2,3,4},type=ns}, {{1,2,3,4},53}}, + {"localhost", {{127,0,0,1},53}}, + {#dns_rr{data="localhost",type=ns}, {{127,0,0,1},53}}], + [ ?_assertMatch(C when C =:= B, nameserver_address(A)) + || {A,B} <- T]. diff --git a/ejabberd-dev/README.txt b/ejabberd-dev/README.txt new file mode 100644 index 0000000..f461fa1 --- /dev/null +++ b/ejabberd-dev/README.txt @@ -0,0 +1,5 @@ +ejabberd-dev is an helper module containing include files from the ejabberd +project. + +This should allow to develop or build ejabberd modules without a complete +development version of ejabberd (for example with a binary version of ejabberd). \ No newline at end of file diff --git a/ejabberd-dev/include/adhoc.hrl b/ejabberd-dev/include/adhoc.hrl new file mode 100644 index 0000000..45c74ca --- /dev/null +++ b/ejabberd-dev/include/adhoc.hrl @@ -0,0 +1,36 @@ +%%%---------------------------------------------------------------------- +%%% +%%% ejabberd, Copyright (C) 2002-2012 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., 59 Temple Place, Suite 330, Boston, MA +%%% 02111-1307 USA +%%% +%%%---------------------------------------------------------------------- + +-record(adhoc_request, {lang, + node, + sessionid, + action, + xdata, + others}). + +-record(adhoc_response, {lang, + node, + sessionid, + status, + defaultaction = "", + actions = [], + notes = [], + elements = []}). diff --git a/ejabberd-dev/include/ejabberd.hrl b/ejabberd-dev/include/ejabberd.hrl new file mode 100644 index 0000000..7d1f554 --- /dev/null +++ b/ejabberd-dev/include/ejabberd.hrl @@ -0,0 +1,64 @@ +%%%---------------------------------------------------------------------- +%%% +%%% ejabberd, Copyright (C) 2002-2012 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., 59 Temple Place, Suite 330, Boston, MA +%%% 02111-1307 USA +%%% +%%%---------------------------------------------------------------------- + +%% This macro returns a string of the ejabberd version running, e.g. "2.3.4" +%% If the ejabberd application description isn't loaded, returns atom: undefined +-define(VERSION, element(2, application:get_key(ejabberd,vsn))). + +-define(MYHOSTS, ejabberd_config:get_global_option(hosts)). +-define(MYNAME, hd(ejabberd_config:get_global_option(hosts))). +-define(MYLANG, ejabberd_config:get_global_option(language)). + +-define(MSGS_DIR, "msgs"). +-define(CONFIG_PATH, "ejabberd.cfg"). +-define(LOG_PATH, "ejabberd.log"). + +-define(EJABBERD_URI, "http://www.process-one.net/en/ejabberd/"). + +-define(S2STIMEOUT, 600000). + +%%-define(DBGFSM, true). + +-record(scram, {storedkey, serverkey, salt, iterationcount}). +-define(SCRAM_DEFAULT_ITERATION_COUNT, 4096). + +%% --------------------------------- +%% Logging mechanism + +%% Print in standard output +-define(PRINT(Format, Args), + io:format(Format, Args)). + +-define(DEBUG(Format, Args), + ejabberd_logger:debug_msg(?MODULE,?LINE,Format, Args)). + +-define(INFO_MSG(Format, Args), + ejabberd_logger:info_msg(?MODULE,?LINE,Format, Args)). + +-define(WARNING_MSG(Format, Args), + ejabberd_logger:warning_msg(?MODULE,?LINE,Format, Args)). + +-define(ERROR_MSG(Format, Args), + ejabberd_logger:error_msg(?MODULE,?LINE,Format, Args)). + +-define(CRITICAL_MSG(Format, Args), + ejabberd_logger:critical_msg(?MODULE,?LINE,Format, Args)). + diff --git a/ejabberd-dev/include/ejabberd_commands.hrl b/ejabberd-dev/include/ejabberd_commands.hrl new file mode 100644 index 0000000..5798a6b --- /dev/null +++ b/ejabberd-dev/include/ejabberd_commands.hrl @@ -0,0 +1,52 @@ +%%%---------------------------------------------------------------------- +%%% +%%% ejabberd, Copyright (C) 2002-2012 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., 59 Temple Place, Suite 330, Boston, MA +%%% 02111-1307 USA +%%% +%%%---------------------------------------------------------------------- + +-record(ejabberd_commands, {name, tags = [], + desc = "", longdesc = "", + module, function, + args = [], result = rescode}). + +%% @type ejabberd_commands() = #ejabberd_commands{ +%% name = atom(), +%% tags = [atom()], +%% desc = string(), +%% longdesc = string(), +%% module = atom(), +%% function = atom(), +%% args = [aterm()], +%% result = rterm() +%% }. +%% desc: Description of the command +%% args: Describe the accepted arguments. +%% This way the function that calls the command can format the +%% arguments before calling. + +%% @type atype() = integer | string | {tuple, [aterm()]} | {list, aterm()}. +%% Allowed types for arguments are integer, string, tuple and list. + +%% @type rtype() = integer | string | atom | {tuple, [rterm()]} | {list, rterm()} | rescode | restuple. +%% A rtype is either an atom or a tuple with two elements. + +%% @type aterm() = {Name::atom(), Type::atype()}. +%% An argument term is a tuple with the term name and the term type. + +%% @type rterm() = {Name::atom(), Type::rtype()}. +%% A result term is a tuple with the term name and the term type. diff --git a/ejabberd-dev/include/ejabberd_config.hrl b/ejabberd-dev/include/ejabberd_config.hrl new file mode 100644 index 0000000..4cacd36 --- /dev/null +++ b/ejabberd-dev/include/ejabberd_config.hrl @@ -0,0 +1,28 @@ +%%%---------------------------------------------------------------------- +%%% +%%% ejabberd, Copyright (C) 2002-2012 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., 59 Temple Place, Suite 330, Boston, MA +%%% 02111-1307 USA +%%% +%%%---------------------------------------------------------------------- + +-record(config, {key, value}). +-record(local_config, {key, value}). +-record(state, {opts = [], + hosts = [], + override_local = false, + override_global = false, + override_acls = false}). diff --git a/ejabberd-dev/include/ejabberd_ctl.hrl b/ejabberd-dev/include/ejabberd_ctl.hrl new file mode 100644 index 0000000..d715134 --- /dev/null +++ b/ejabberd-dev/include/ejabberd_ctl.hrl @@ -0,0 +1,25 @@ +%%%---------------------------------------------------------------------- +%%% +%%% ejabberd, Copyright (C) 2002-2012 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., 59 Temple Place, Suite 330, Boston, MA +%%% 02111-1307 USA +%%% +%%%---------------------------------------------------------------------- + +-define(STATUS_SUCCESS, 0). +-define(STATUS_ERROR, 1). +-define(STATUS_USAGE, 2). +-define(STATUS_BADRPC, 3). diff --git a/ejabberd-dev/include/jlib.hrl b/ejabberd-dev/include/jlib.hrl new file mode 100644 index 0000000..ae5862d --- /dev/null +++ b/ejabberd-dev/include/jlib.hrl @@ -0,0 +1,336 @@ +%%%---------------------------------------------------------------------- +%%% +%%% ejabberd, Copyright (C) 2002-2012 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., 59 Temple Place, Suite 330, Boston, MA +%%% 02111-1307 USA +%%% +%%%---------------------------------------------------------------------- + +-define(NS_DISCO_ITEMS, "http://jabber.org/protocol/disco#items"). +-define(NS_DISCO_INFO, "http://jabber.org/protocol/disco#info"). +-define(NS_VCARD, "vcard-temp"). +-define(NS_VCARD_UPDATE, "vcard-temp:x:update"). +-define(NS_AUTH, "jabber:iq:auth"). +-define(NS_AUTH_ERROR, "jabber:iq:auth:error"). +-define(NS_REGISTER, "jabber:iq:register"). +-define(NS_SEARCH, "jabber:iq:search"). +-define(NS_ROSTER, "jabber:iq:roster"). +-define(NS_ROSTER_VER, "urn:xmpp:features:rosterver"). +-define(NS_PRIVACY, "jabber:iq:privacy"). +-define(NS_BLOCKING, "urn:xmpp:blocking"). +-define(NS_PRIVATE, "jabber:iq:private"). +-define(NS_VERSION, "jabber:iq:version"). +-define(NS_TIME90, "jabber:iq:time"). % TODO: Remove once XEP-0090 is Obsolete +-define(NS_TIME, "urn:xmpp:time"). +-define(NS_LAST, "jabber:iq:last"). +-define(NS_XDATA, "jabber:x:data"). +-define(NS_IQDATA, "jabber:iq:data"). +-define(NS_DELAY91, "jabber:x:delay"). % TODO: Remove once XEP-0091 is Obsolete +-define(NS_DELAY, "urn:xmpp:delay"). +-define(NS_EXPIRE, "jabber:x:expire"). +-define(NS_EVENT, "jabber:x:event"). +-define(NS_CHATSTATES, "http://jabber.org/protocol/chatstates"). +-define(NS_XCONFERENCE, "jabber:x:conference"). +-define(NS_STATS, "http://jabber.org/protocol/stats"). +-define(NS_MUC, "http://jabber.org/protocol/muc"). +-define(NS_MUC_USER, "http://jabber.org/protocol/muc#user"). +-define(NS_MUC_ADMIN, "http://jabber.org/protocol/muc#admin"). +-define(NS_MUC_OWNER, "http://jabber.org/protocol/muc#owner"). +-define(NS_MUC_UNIQUE, "http://jabber.org/protocol/muc#unique"). +-define(NS_PUBSUB, "http://jabber.org/protocol/pubsub"). +-define(NS_PUBSUB_EVENT, "http://jabber.org/protocol/pubsub#event"). +-define(NS_PUBSUB_OWNER, "http://jabber.org/protocol/pubsub#owner"). +-define(NS_PUBSUB_NMI, "http://jabber.org/protocol/pubsub#node-meta-info"). +-define(NS_PUBSUB_ERRORS,"http://jabber.org/protocol/pubsub#errors"). +-define(NS_PUBSUB_NODE_CONFIG, "http://jabber.org/protocol/pubsub#node_config"). +-define(NS_PUBSUB_SUB_OPTIONS, "http://jabber.org/protocol/pubsub#subscribe_options"). +-define(NS_PUBSUB_SUB_AUTH, "http://jabber.org/protocol/pubsub#subscribe_authorization"). +-define(NS_PUBSUB_GET_PENDING, "http://jabber.org/protocol/pubsub#get-pending"). +-define(NS_COMMANDS, "http://jabber.org/protocol/commands"). +-define(NS_BYTESTREAMS, "http://jabber.org/protocol/bytestreams"). +-define(NS_ADMIN, "http://jabber.org/protocol/admin"). +-define(NS_SERVERINFO, "http://jabber.org/network/serverinfo"). + +-define(NS_RSM, "http://jabber.org/protocol/rsm"). +-define(NS_EJABBERD_CONFIG, "ejabberd:config"). + +-define(NS_STREAM, "http://etherx.jabber.org/streams"). + +-define(NS_STANZAS, "urn:ietf:params:xml:ns:xmpp-stanzas"). +-define(NS_STREAMS, "urn:ietf:params:xml:ns:xmpp-streams"). + +-define(NS_TLS, "urn:ietf:params:xml:ns:xmpp-tls"). +-define(NS_SASL, "urn:ietf:params:xml:ns:xmpp-sasl"). +-define(NS_SESSION, "urn:ietf:params:xml:ns:xmpp-session"). +-define(NS_BIND, "urn:ietf:params:xml:ns:xmpp-bind"). + +-define(NS_FEATURE_IQAUTH, "http://jabber.org/features/iq-auth"). +-define(NS_FEATURE_IQREGISTER, "http://jabber.org/features/iq-register"). +-define(NS_FEATURE_COMPRESS, "http://jabber.org/features/compress"). +-define(NS_FEATURE_MSGOFFLINE, "msgoffline"). + +-define(NS_COMPRESS, "http://jabber.org/protocol/compress"). + +-define(NS_CAPS, "http://jabber.org/protocol/caps"). +-define(NS_SHIM, "http://jabber.org/protocol/shim"). +-define(NS_ADDRESS, "http://jabber.org/protocol/address"). + +%% CAPTCHA related NSes. +-define(NS_OOB, "jabber:x:oob"). +-define(NS_CAPTCHA, "urn:xmpp:captcha"). +-define(NS_MEDIA, "urn:xmpp:media-element"). +-define(NS_BOB, "urn:xmpp:bob"). + +% TODO: remove "code" attribute (currently it used for backward-compatibility) +-define(STANZA_ERROR(Code, Type, Condition), + {xmlelement, "error", + [{"code", Code}, {"type", Type}], + [{xmlelement, Condition, [{"xmlns", ?NS_STANZAS}], []}]}). + +-define(ERR_BAD_FORMAT, + ?STANZA_ERROR("406", "modify", "bad-format")). +-define(ERR_BAD_REQUEST, + ?STANZA_ERROR("400", "modify", "bad-request")). +-define(ERR_CONFLICT, + ?STANZA_ERROR("409", "cancel", "conflict")). +-define(ERR_FEATURE_NOT_IMPLEMENTED, + ?STANZA_ERROR("501", "cancel", "feature-not-implemented")). +-define(ERR_FORBIDDEN, + ?STANZA_ERROR("403", "auth", "forbidden")). +-define(ERR_GONE, + ?STANZA_ERROR("302", "modify", "gone")). +-define(ERR_INTERNAL_SERVER_ERROR, + ?STANZA_ERROR("500", "wait", "internal-server-error")). +-define(ERR_ITEM_NOT_FOUND, + ?STANZA_ERROR("404", "cancel", "item-not-found")). +-define(ERR_JID_MALFORMED, + ?STANZA_ERROR("400", "modify", "jid-malformed")). +-define(ERR_NOT_ACCEPTABLE, + ?STANZA_ERROR("406", "modify", "not-acceptable")). +-define(ERR_NOT_ALLOWED, + ?STANZA_ERROR("405", "cancel", "not-allowed")). +-define(ERR_NOT_AUTHORIZED, + ?STANZA_ERROR("401", "auth", "not-authorized")). +-define(ERR_PAYMENT_REQUIRED, + ?STANZA_ERROR("402", "auth", "payment-required")). +-define(ERR_RECIPIENT_UNAVAILABLE, + ?STANZA_ERROR("404", "wait", "recipient-unavailable")). +-define(ERR_REDIRECT, + ?STANZA_ERROR("302", "modify", "redirect")). +-define(ERR_REGISTRATION_REQUIRED, + ?STANZA_ERROR("407", "auth", "registration-required")). +-define(ERR_REMOTE_SERVER_NOT_FOUND, + ?STANZA_ERROR("404", "cancel", "remote-server-not-found")). +-define(ERR_REMOTE_SERVER_TIMEOUT, + ?STANZA_ERROR("504", "wait", "remote-server-timeout")). +-define(ERR_RESOURCE_CONSTRAINT, + ?STANZA_ERROR("500", "wait", "resource-constraint")). +-define(ERR_SERVICE_UNAVAILABLE, + ?STANZA_ERROR("503", "cancel", "service-unavailable")). +-define(ERR_SUBSCRIPTION_REQUIRED, + ?STANZA_ERROR("407", "auth", "subscription-required")). +-define(ERR_UNEXPECTED_REQUEST, + ?STANZA_ERROR("400", "wait", "unexpected-request")). +-define(ERR_UNEXPECTED_REQUEST_CANCEL, + ?STANZA_ERROR("401", "cancel", "unexpected-request")). +%-define(ERR_, +% ?STANZA_ERROR("", "", "")). + +-define(STANZA_ERRORT(Code, Type, Condition, Lang, Text), + {xmlelement, "error", + [{"code", Code}, {"type", Type}], + [{xmlelement, Condition, [{"xmlns", ?NS_STANZAS}], []}, + {xmlelement, "text", [{"xmlns", ?NS_STANZAS}], + [{xmlcdata, translate:translate(Lang, Text)}]}]}). + +-define(ERRT_BAD_FORMAT(Lang, Text), + ?STANZA_ERRORT("406", "modify", "bad-format", Lang, Text)). +-define(ERRT_BAD_REQUEST(Lang, Text), + ?STANZA_ERRORT("400", "modify", "bad-request", Lang, Text)). +-define(ERRT_CONFLICT(Lang, Text), + ?STANZA_ERRORT("409", "cancel", "conflict", Lang, Text)). +-define(ERRT_FEATURE_NOT_IMPLEMENTED(Lang, Text), + ?STANZA_ERRORT("501", "cancel", "feature-not-implemented", Lang, Text)). +-define(ERRT_FORBIDDEN(Lang, Text), + ?STANZA_ERRORT("403", "auth", "forbidden", Lang, Text)). +-define(ERRT_GONE(Lang, Text), + ?STANZA_ERRORT("302", "modify", "gone", Lang, Text)). +-define(ERRT_INTERNAL_SERVER_ERROR(Lang, Text), + ?STANZA_ERRORT("500", "wait", "internal-server-error", Lang, Text)). +-define(ERRT_ITEM_NOT_FOUND(Lang, Text), + ?STANZA_ERRORT("404", "cancel", "item-not-found", Lang, Text)). +-define(ERRT_JID_MALFORMED(Lang, Text), + ?STANZA_ERRORT("400", "modify", "jid-malformed", Lang, Text)). +-define(ERRT_NOT_ACCEPTABLE(Lang, Text), + ?STANZA_ERRORT("406", "modify", "not-acceptable", Lang, Text)). +-define(ERRT_NOT_ALLOWED(Lang, Text), + ?STANZA_ERRORT("405", "cancel", "not-allowed", Lang, Text)). +-define(ERRT_NOT_AUTHORIZED(Lang, Text), + ?STANZA_ERRORT("401", "auth", "not-authorized", Lang, Text)). +-define(ERRT_PAYMENT_REQUIRED(Lang, Text), + ?STANZA_ERRORT("402", "auth", "payment-required", Lang, Text)). +-define(ERRT_RECIPIENT_UNAVAILABLE(Lang, Text), + ?STANZA_ERRORT("404", "wait", "recipient-unavailable", Lang, Text)). +-define(ERRT_REDIRECT(Lang, Text), + ?STANZA_ERRORT("302", "modify", "redirect", Lang, Text)). +-define(ERRT_REGISTRATION_REQUIRED(Lang, Text), + ?STANZA_ERRORT("407", "auth", "registration-required", Lang, Text)). +-define(ERRT_REMOTE_SERVER_NOT_FOUND(Lang, Text), + ?STANZA_ERRORT("404", "cancel", "remote-server-not-found", Lang, Text)). +-define(ERRT_REMOTE_SERVER_TIMEOUT(Lang, Text), + ?STANZA_ERRORT("504", "wait", "remote-server-timeout", Lang, Text)). +-define(ERRT_RESOURCE_CONSTRAINT(Lang, Text), + ?STANZA_ERRORT("500", "wait", "resource-constraint", Lang, Text)). +-define(ERRT_SERVICE_UNAVAILABLE(Lang, Text), + ?STANZA_ERRORT("503", "cancel", "service-unavailable", Lang, Text)). +-define(ERRT_SUBSCRIPTION_REQUIRED(Lang, Text), + ?STANZA_ERRORT("407", "auth", "subscription-required", Lang, Text)). +-define(ERRT_UNEXPECTED_REQUEST(Lang, Text), + ?STANZA_ERRORT("400", "wait", "unexpected-request", Lang, Text)). + +% Auth stanza errors +-define(ERR_AUTH_NO_RESOURCE_PROVIDED(Lang), + ?ERRT_NOT_ACCEPTABLE(Lang, "No resource provided")). +-define(ERR_AUTH_BAD_RESOURCE_FORMAT(Lang), + ?ERRT_NOT_ACCEPTABLE(Lang, "Illegal resource format")). +-define(ERR_AUTH_RESOURCE_CONFLICT(Lang), + ?ERRT_CONFLICT(Lang, "Resource conflict")). + + +-define(STREAM_ERROR(Condition), + {xmlelement, "stream:error", + [], + [{xmlelement, Condition, [{"xmlns", ?NS_STREAMS}], []}]}). + +-define(SERR_BAD_FORMAT, + ?STREAM_ERROR("bad-format")). +-define(SERR_BAD_NAMESPACE_PREFIX, + ?STREAM_ERROR("bad-namespace-prefix")). +-define(SERR_CONFLICT, + ?STREAM_ERROR("conflict")). +-define(SERR_CONNECTION_TIMEOUT, + ?STREAM_ERROR("connection-timeout")). +-define(SERR_HOST_GONE, + ?STREAM_ERROR("host-gone")). +-define(SERR_HOST_UNKNOWN, + ?STREAM_ERROR("host-unknown")). +-define(SERR_IMPROPER_ADDRESSING, + ?STREAM_ERROR("improper-addressing")). +-define(SERR_INTERNAL_SERVER_ERROR, + ?STREAM_ERROR("internal-server-error")). +-define(SERR_INVALID_FROM, + ?STREAM_ERROR("invalid-from")). +-define(SERR_INVALID_ID, + ?STREAM_ERROR("invalid-id")). +-define(SERR_INVALID_NAMESPACE, + ?STREAM_ERROR("invalid-namespace")). +-define(SERR_INVALID_XML, + ?STREAM_ERROR("invalid-xml")). +-define(SERR_NOT_AUTHORIZED, + ?STREAM_ERROR("not-authorized")). +-define(SERR_POLICY_VIOLATION, + ?STREAM_ERROR("policy-violation")). +-define(SERR_REMOTE_CONNECTION_FAILED, + ?STREAM_ERROR("remote-connection-failed")). +-define(SERR_RESOURSE_CONSTRAINT, + ?STREAM_ERROR("resource-constraint")). +-define(SERR_RESTRICTED_XML, + ?STREAM_ERROR("restricted-xml")). +% TODO: include hostname or IP +-define(SERR_SEE_OTHER_HOST, + ?STREAM_ERROR("see-other-host")). +-define(SERR_SYSTEM_SHUTDOWN, + ?STREAM_ERROR("system-shutdown")). +-define(SERR_UNSUPPORTED_ENCODING, + ?STREAM_ERROR("unsupported-encoding")). +-define(SERR_UNSUPPORTED_STANZA_TYPE, + ?STREAM_ERROR("unsupported-stanza-type")). +-define(SERR_UNSUPPORTED_VERSION, + ?STREAM_ERROR("unsupported-version")). +-define(SERR_XML_NOT_WELL_FORMED, + ?STREAM_ERROR("xml-not-well-formed")). +%-define(SERR_, +% ?STREAM_ERROR("")). + +-define(STREAM_ERRORT(Condition, Lang, Text), + {xmlelement, "stream:error", + [], + [{xmlelement, Condition, [{"xmlns", ?NS_STREAMS}], []}, + {xmlelement, "text", [{"xml:lang", Lang}, {"xmlns", ?NS_STREAMS}], + [{xmlcdata, translate:translate(Lang, Text)}]}]}). + +-define(SERRT_BAD_FORMAT(Lang, Text), + ?STREAM_ERRORT("bad-format", Lang, Text)). +-define(SERRT_BAD_NAMESPACE_PREFIX(Lang, Text), + ?STREAM_ERRORT("bad-namespace-prefix", Lang, Text)). +-define(SERRT_CONFLICT(Lang, Text), + ?STREAM_ERRORT("conflict", Lang, Text)). +-define(SERRT_CONNECTION_TIMEOUT(Lang, Text), + ?STREAM_ERRORT("connection-timeout", Lang, Text)). +-define(SERRT_HOST_GONE(Lang, Text), + ?STREAM_ERRORT("host-gone", Lang, Text)). +-define(SERRT_HOST_UNKNOWN(Lang, Text), + ?STREAM_ERRORT("host-unknown", Lang, Text)). +-define(SERRT_IMPROPER_ADDRESSING(Lang, Text), + ?STREAM_ERRORT("improper-addressing", Lang, Text)). +-define(SERRT_INTERNAL_SERVER_ERROR(Lang, Text), + ?STREAM_ERRORT("internal-server-error", Lang, Text)). +-define(SERRT_INVALID_FROM(Lang, Text), + ?STREAM_ERRORT("invalid-from", Lang, Text)). +-define(SERRT_INVALID_ID(Lang, Text), + ?STREAM_ERRORT("invalid-id", Lang, Text)). +-define(SERRT_INVALID_NAMESPACE(Lang, Text), + ?STREAM_ERRORT("invalid-namespace", Lang, Text)). +-define(SERRT_INVALID_XML(Lang, Text), + ?STREAM_ERRORT("invalid-xml", Lang, Text)). +-define(SERRT_NOT_AUTHORIZED(Lang, Text), + ?STREAM_ERRORT("not-authorized", Lang, Text)). +-define(SERRT_POLICY_VIOLATION(Lang, Text), + ?STREAM_ERRORT("policy-violation", Lang, Text)). +-define(SERRT_REMOTE_CONNECTION_FAILED(Lang, Text), + ?STREAM_ERRORT("remote-connection-failed", Lang, Text)). +-define(SERRT_RESOURSE_CONSTRAINT(Lang, Text), + ?STREAM_ERRORT("resource-constraint", Lang, Text)). +-define(SERRT_RESTRICTED_XML(Lang, Text), + ?STREAM_ERRORT("restricted-xml", Lang, Text)). +% TODO: include hostname or IP +-define(SERRT_SEE_OTHER_HOST(Lang, Text), + ?STREAM_ERRORT("see-other-host", Lang, Text)). +-define(SERRT_SYSTEM_SHUTDOWN(Lang, Text), + ?STREAM_ERRORT("system-shutdown", Lang, Text)). +-define(SERRT_UNSUPPORTED_ENCODING(Lang, Text), + ?STREAM_ERRORT("unsupported-encoding", Lang, Text)). +-define(SERRT_UNSUPPORTED_STANZA_TYPE(Lang, Text), + ?STREAM_ERRORT("unsupported-stanza-type", Lang, Text)). +-define(SERRT_UNSUPPORTED_VERSION(Lang, Text), + ?STREAM_ERRORT("unsupported-version", Lang, Text)). +-define(SERRT_XML_NOT_WELL_FORMED(Lang, Text), + ?STREAM_ERRORT("xml-not-well-formed", Lang, Text)). +%-define(SERRT_(Lang, Text), +% ?STREAM_ERRORT("", Lang, Text)). + + +-record(jid, {user, server, resource, + luser, lserver, lresource}). + +-record(iq, {id = "", + type, + xmlns = "", + lang = "", + sub_el}). + +-record(rsm_in, {max, direction, id, index}). +-record(rsm_out, {count, index, first, last}). diff --git a/ejabberd-dev/include/mod_muc/mod_muc_room.hrl b/ejabberd-dev/include/mod_muc/mod_muc_room.hrl new file mode 100644 index 0000000..a7a6ca0 --- /dev/null +++ b/ejabberd-dev/include/mod_muc/mod_muc_room.hrl @@ -0,0 +1,90 @@ +%%%---------------------------------------------------------------------- +%%% +%%% ejabberd, Copyright (C) 2002-2012 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., 59 Temple Place, Suite 330, Boston, MA +%%% 02111-1307 USA +%%% +%%%---------------------------------------------------------------------- + +-define(MAX_USERS_DEFAULT, 200). + +-define(SETS, gb_sets). +-define(DICT, dict). + +-record(lqueue, {queue, len, max}). + +-record(config, {title = "", + description = "", + allow_change_subj = true, + allow_query_users = true, + allow_private_messages = true, + allow_private_messages_from_visitors = anyone, + allow_visitor_status = true, + allow_visitor_nickchange = true, + public = true, + public_list = true, + persistent = false, + moderated = true, + captcha_protected = false, + members_by_default = true, + members_only = false, + allow_user_invites = false, + password_protected = false, + password = "", + anonymous = true, + allow_voice_requests = true, + voice_request_min_interval = 1800, + max_users = ?MAX_USERS_DEFAULT, + logging = false, + captcha_whitelist = ?SETS:empty() + }). + +-record(user, {jid, + nick, + role, + last_presence}). + +-record(activity, {message_time = 0, + presence_time = 0, + message_shaper, + presence_shaper, + message, + presence}). + +-record(state, {room, + host, + server_host, + mod, + access, + jid, + config = #config{}, + users = ?DICT:new(), + last_voice_request_time = treap:empty(), + robots = ?DICT:new(), + nicks = ?DICT:new(), + affiliations = ?DICT:new(), + history, + subject = "", + subject_author = "", + just_created = false, + activity = treap:empty(), + room_shaper, + room_queue = queue:new()}). + +-record(muc_online_users, {us, + resource, + room, + host}). diff --git a/ejabberd-dev/include/mod_pubsub/pubsub.hrl b/ejabberd-dev/include/mod_pubsub/pubsub.hrl new file mode 100644 index 0000000..8cc20c4 --- /dev/null +++ b/ejabberd-dev/include/mod_pubsub/pubsub.hrl @@ -0,0 +1,193 @@ +%%% ==================================================================== +%%% ``The contents of this file are subject to the Erlang Public License, +%%% Version 1.1, (the "License"); you may not use this file except in +%%% compliance with the License. You should have received a copy of the +%%% Erlang Public License along with this software. If not, it can be +%%% retrieved via the world wide web at http://www.erlang.org/. +%%% +%%% Software distributed under the License is distributed on an "AS IS" +%%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%%% the License for the specific language governing rights and limitations +%%% under the License. +%%% +%%% The Initial Developer of the Original Code is ProcessOne. +%%% Portions created by ProcessOne are Copyright 2006-2011, ProcessOne +%%% All Rights Reserved.'' +%%% This software is copyright 2006-2011, ProcessOne. +%%% +%%% +%%% copyright 2006-2011 ProcessOne +%%% +%%% This file contains pubsub types definition. +%%% ==================================================================== + +%% ------------------------------- +%% Pubsub constants +-define(ERR_EXTENDED(E,C), mod_pubsub:extended_error(E,C)). + +%% The actual limit can be configured with mod_pubsub's option max_items_node +-define(MAXITEMS, 10). + +%% this is currently a hard limit. +%% Would be nice to have it configurable. +-define(MAX_PAYLOAD_SIZE, 60000). + +%% ------------------------------- +%% Pubsub types + +%% @type hostPubsub() = string(). +%%

hostPubsub is the name of the PubSub service. For example, it can be +%% "pubsub.localhost".

+ +%% @type hostPEP() = {User, Server, Resource} +%% User = string() +%% Server = string() +%% Resource = []. +%%

For example, it can be : +%% ```{"bob", "example.org", []}'''.

+ +%% @type host() = hostPubsub() | hostPEP(). + +%% @type nodeId() = binary(). +%%

A node is defined by a list of its ancestors. The last element is the name +%% of the current node. For example: +%% ```<<"/home/localhost/user">>'''

+ +%% @type nodeIdx() = integer(). + +%% @type itemId() = string(). + +%% @type subId() = string(). + +%% @type payload() = [#xmlelement{} | #xmlcdata{}]. + +%% @type stanzaError() = #xmlelement{}. +%% Example: +%% ```{xmlelement, "error", +%% [{"code", Code}, {"type", Type}], +%% [{xmlelement, Condition, [{"xmlns", ?NS_STANZAS}], []}]}''' +%% @type pubsubIQResponse() = #xmlelement{}. +%% Example: +%% ```{xmlelement, "pubsub", +%% [{"xmlns", ?NS_PUBSUB_EVENT}], +%% [{xmlelement, "affiliations", [], +%% []}]}''' + +%% @type nodeOption() = {Option, Value} +%% Option = atom() +%% Value = term(). +%% Example: +%% ```{deliver_payloads, true}''' + +%% @type nodeType() = string(). +%%

The nodeType is a string containing the name of the PubSub +%% plugin to use to manage a given node. For example, it can be +%% "flat", "hometree" or "blog".

+ +%% @type jid() = {jid, User, Server, Resource, LUser, LServer, LResource} +%% User = string() +%% Server = string() +%% Resource = string() +%% LUser = string() +%% LServer = string() +%% LResource = string(). + +%% @type ljid() = {User, Server, Resource} +%% User = string() +%% Server = string() +%% Resource = string(). + +%% @type affiliation() = 'none' | 'owner' | 'publisher' | 'publish-only' | 'member' | 'outcast'. + +%% @type subscription() = 'none' | 'pending' | 'unconfigured' | 'subscribed'. + +%% @type accessModel() = 'open' | 'presence' | 'roster' | 'authorize' | 'whitelist'. + +%% @type pubsubIndex() = {pubsub_index, Index, Last, Free} +%% Index = atom() +%% Last = integer() +%% Free = [integer()]. +%% internal pubsub index table +-record(pubsub_index, +{ + index, + last, + free +}). + +%% @type pubsubNode() = {pubsub_node, NodeId, Id, Parents, Type, Owners, Options} +%% NodeId = {host() | ljid(), nodeId()} +%% Id = nodeIdx() +%% Parents = [nodeId()] +%% Type = nodeType() +%% Owners = [ljid()] +%% Options = [nodeOption()]. +%%

This is the format of the nodes table. The type of the table +%% is: set,ram/disc.

+%%

The Parents and type fields are indexed.

+%% id can be anything you want. +-record(pubsub_node, +{ + nodeid, + id, + parents = [], + type = "flat", + owners = [], + options = [] +}). + +%% @type pubsubState() = {pubsub_state, StateId, Items, Affiliation, Subscriptions} +%% StateId = {ljid(), nodeIdx()} +%% Items = [itemId()] +%% Affiliation = affiliation() +%% Subscriptions = [{subscription(), subId()}]. +%%

This is the format of the affiliations table. The type of the +%% table is: set,ram/disc.

+-record(pubsub_state, +{ + stateid, + items = [], + affiliation = 'none', + subscriptions = [] +}). + +%% @type pubsubItem() = {pubsub_item, ItemId, Creation, Modification, Payload} +%% ItemId = {itemId(), nodeIdx()} +%% Creation = {now(), ljid()} +%% Modification = {now(), ljid()} +%% Payload = payload(). +%%

This is the format of the published items table. The type of the +%% table is: set,disc,fragmented.

+-record(pubsub_item, +{ + itemid, + creation = {'unknown','unknown'}, + modification = {'unknown','unknown'}, + payload = [] +}). + +%% @type pubsubSubscription() = {pubsub_subscription, SubId, Options} +%% SubId = subId() +%% Options = [nodeOption()]. +%%

This is the format of the subscriptions table. The type of the +%% table is: set,ram/disc.

+-record(pubsub_subscription, +{ + subid, + options +}). + +%% @type pubsubLastItem() = {pubsub_last_item, NodeId, ItemId, Creation, Payload} +%% NodeId = nodeIdx() +%% ItemId = itemId() +%% Creation = {now(),ljid()} +%% Payload = payload(). +%%

This is the format of the last items table. it stores last item payload +%% for every node

+-record(pubsub_last_item, +{ + nodeid, + itemid, + creation, + payload +}). diff --git a/ejabberd-dev/include/mod_roster.hrl b/ejabberd-dev/include/mod_roster.hrl new file mode 100644 index 0000000..7e1acaa --- /dev/null +++ b/ejabberd-dev/include/mod_roster.hrl @@ -0,0 +1,33 @@ +%%%---------------------------------------------------------------------- +%%% +%%% ejabberd, Copyright (C) 2002-2012 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., 59 Temple Place, Suite 330, Boston, MA +%%% 02111-1307 USA +%%% +%%%---------------------------------------------------------------------- + +-record(roster, {usj, + us, + jid, + name = "", + subscription = none, + ask = none, + groups = [], + askmessage = [], + xs = []}). + +-record(roster_version, {us, + version}). diff --git a/ejabberd-dev/include/web/ejabberd_http.hrl b/ejabberd-dev/include/web/ejabberd_http.hrl new file mode 100644 index 0000000..7cd6da1 --- /dev/null +++ b/ejabberd-dev/include/web/ejabberd_http.hrl @@ -0,0 +1,34 @@ +%%%---------------------------------------------------------------------- +%%% +%%% ejabberd, Copyright (C) 2002-2012 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., 59 Temple Place, Suite 330, Boston, MA +%%% 02111-1307 USA +%%% +%%%---------------------------------------------------------------------- + +-record(request, {method, + path, + q = [], + us, + auth, + lang = "", + data = "", + ip, + host, % string() + port, % integer() + tp, % transfer protocol = http | https + headers + }). diff --git a/ejabberd-dev/include/web/ejabberd_web_admin.hrl b/ejabberd-dev/include/web/ejabberd_web_admin.hrl new file mode 100644 index 0000000..73f792e --- /dev/null +++ b/ejabberd-dev/include/web/ejabberd_web_admin.hrl @@ -0,0 +1,76 @@ +%%%---------------------------------------------------------------------- +%%% +%%% ejabberd, Copyright (C) 2002-2012 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., 59 Temple Place, Suite 330, Boston, MA +%%% 02111-1307 USA +%%% +%%%---------------------------------------------------------------------- + +-define(X(Name), {xmlelement, Name, [], []}). +-define(XA(Name, Attrs), {xmlelement, Name, Attrs, []}). +-define(XE(Name, Els), {xmlelement, Name, [], Els}). +-define(XAE(Name, Attrs, Els), {xmlelement, Name, Attrs, Els}). +-define(C(Text), {xmlcdata, Text}). +-define(XC(Name, Text), ?XE(Name, [?C(Text)])). +-define(XAC(Name, Attrs, Text), ?XAE(Name, Attrs, [?C(Text)])). + +-define(T(Text), translate:translate(Lang, Text)). +-define(CT(Text), ?C(?T(Text))). +-define(XCT(Name, Text), ?XC(Name, ?T(Text))). +-define(XACT(Name, Attrs, Text), ?XAC(Name, Attrs, ?T(Text))). + +-define(LI(Els), ?XE("li", Els)). +-define(A(URL, Els), ?XAE("a", [{"href", URL}], Els)). +-define(AC(URL, Text), ?A(URL, [?C(Text)])). +-define(ACT(URL, Text), ?AC(URL, ?T(Text))). +-define(P, ?X("p")). +-define(BR, ?X("br")). +-define(INPUT(Type, Name, Value), + ?XA("input", [{"type", Type}, + {"name", Name}, + {"value", Value}])). +-define(INPUTT(Type, Name, Value), ?INPUT(Type, Name, ?T(Value))). +-define(INPUTS(Type, Name, Value, Size), + ?XA("input", [{"type", Type}, + {"name", Name}, + {"value", Value}, + {"size", Size}])). +-define(INPUTST(Type, Name, Value, Size), ?INPUT(Type, Name, ?T(Value), Size)). +-define(ACLINPUT(Text), ?XE("td", [?INPUT("text", "value" ++ ID, Text)])). + +-define(TEXTAREA(Name, Rows, Cols, Value), + ?XAC("textarea", [{"name", Name}, + {"rows", Rows}, + {"cols", Cols}], + Value)). + +%% Build an xmlelement for result +-define(XRES(Text), ?XAC("p", [{"class", "result"}], Text)). +-define(XREST(Text), ?XRES(?T(Text))). + +%% Guide Link +-define(GL(Ref, Title), + ?XAE("div", + [{"class", "guidelink"}], + [?XAE("a", + [{"href", "/admin/doc/guide.html#"++ Ref}, + {"target", "_blank"}], + [?C("[Guide: " ++ Title ++ "]")]) + ])). + + +%% h1 with a Guide Link +-define(H1GL(Name, Ref, Title), [?XC("h1", Name), ?GL(Ref, Title)]). diff --git a/ejabberd-dev/src/gen_mod.erl b/ejabberd-dev/src/gen_mod.erl new file mode 100644 index 0000000..49f4991 --- /dev/null +++ b/ejabberd-dev/src/gen_mod.erl @@ -0,0 +1,236 @@ +%%%---------------------------------------------------------------------- +%%% File : gen_mod.erl +%%% Author : Alexey Shchepin +%%% Purpose : +%%% Created : 24 Jan 2003 by Alexey Shchepin +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2008 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., 59 Temple Place, Suite 330, Boston, MA +%%% 02111-1307 USA +%%% +%%%---------------------------------------------------------------------- + +-module(gen_mod). +-author('alexey@process-one.net'). + +-export([start/0, + start_module/3, + stop_module/2, + stop_module_keep_config/2, + get_opt/2, + get_opt/3, + get_opt_host/3, + get_module_opt/4, + get_module_opt_host/3, + loaded_modules/1, + loaded_modules_with_opts/1, + get_hosts/2, + get_module_proc/2, + is_loaded/2]). + +-export([behaviour_info/1]). + +-include("ejabberd.hrl"). + +-record(ejabberd_module, {module_host, opts}). + +behaviour_info(callbacks) -> + [{start, 2}, + {stop, 1}]; +behaviour_info(_Other) -> + undefined. + +start() -> + ets:new(ejabberd_modules, [named_table, + public, + {keypos, #ejabberd_module.module_host}]), + ok. + + +start_module(Host, Module, Opts) -> + set_module_opts_mnesia(Host, Module, Opts), + ets:insert(ejabberd_modules, + #ejabberd_module{module_host = {Module, Host}, + opts = Opts}), + case catch Module:start(Host, Opts) of + {'EXIT', Reason} -> + del_module_mnesia(Host, Module), + ets:delete(ejabberd_modules, {Module, Host}), + ?ERROR_MSG("~p", [Reason]); + _ -> + ok + end. + +%% @doc Stop the module in a host, and forget its configuration. +stop_module(Host, Module) -> + case stop_module_keep_config(Host, Module) of + error -> + error; + ok -> + del_module_mnesia(Host, Module) + end. + +%% @doc Stop the module in a host, but keep its configuration. +%% As the module configuration is kept in the Mnesia local_config table, +%% when ejabberd is restarted the module will be started again. +%% This function is useful when ejabberd is being stopped +%% and it stops all modules. +stop_module_keep_config(Host, Module) -> + case catch Module:stop(Host) of + {'EXIT', Reason} -> + ?ERROR_MSG("~p", [Reason]), + error; + {wait, ProcList} when is_list(ProcList) -> + lists:foreach(fun wait_for_process/1, ProcList), + ets:delete(ejabberd_modules, {Module, Host}), + ok; + {wait, Process} -> + wait_for_process(Process), + ets:delete(ejabberd_modules, {Module, Host}), + ok; + _ -> + ets:delete(ejabberd_modules, {Module, Host}), + ok + end. + +wait_for_process(Process) -> + MonitorReference = erlang:monitor(process, Process), + wait_for_stop(Process, MonitorReference). + +wait_for_stop(Process, MonitorReference) -> + receive + {'DOWN', MonitorReference, _Type, _Object, _Info} -> + ok + after 5000 -> + catch exit(whereis(Process), kill), + wait_for_stop1(MonitorReference) + end. + +wait_for_stop1(MonitorReference) -> + receive + {'DOWN', MonitorReference, _Type, _Object, _Info} -> + ok + after 5000 -> + ok + end. + +get_opt(Opt, Opts) -> + case lists:keysearch(Opt, 1, Opts) of + false -> + % TODO: replace with more appropriate function + throw({undefined_option, Opt}); + {value, {_, Val}} -> + Val + end. + +get_opt(Opt, Opts, Default) -> + case lists:keysearch(Opt, 1, Opts) of + false -> + Default; + {value, {_, Val}} -> + Val + end. + +get_module_opt(global, Module, Opt, Default) -> + Hosts = ?MYHOSTS, + [Value | Values] = lists:map( + fun(Host) -> + get_module_opt(Host, Module, Opt, Default) + end, + Hosts), + Same_all = lists:all( + fun(Other_value) -> + Other_value == Value + end, + Values), + case Same_all of + true -> Value; + false -> Default + end; + +get_module_opt(Host, Module, Opt, Default) -> + OptsList = ets:lookup(ejabberd_modules, {Module, Host}), + case OptsList of + [] -> + Default; + [#ejabberd_module{opts = Opts} | _] -> + get_opt(Opt, Opts, Default) + end. + +get_module_opt_host(Host, Module, Default) -> + Val = get_module_opt(Host, Module, host, Default), + element(2, regexp:gsub(Val, "@HOST@", Host)). + +get_opt_host(Host, Opts, Default) -> + Val = get_opt(host, Opts, Default), + element(2, regexp:gsub(Val, "@HOST@", Host)). + +loaded_modules(Host) -> + ets:select(ejabberd_modules, + [{#ejabberd_module{_ = '_', module_host = {'$1', Host}}, + [], + ['$1']}]). + +loaded_modules_with_opts(Host) -> + ets:select(ejabberd_modules, + [{#ejabberd_module{_ = '_', module_host = {'$1', Host}, + opts = '$2'}, + [], + [{{'$1', '$2'}}]}]). + +set_module_opts_mnesia(Host, Module, Opts) -> + Modules = case ejabberd_config:get_local_option({modules, Host}) of + undefined -> + []; + Ls -> + Ls + end, + Modules1 = lists:keydelete(Module, 1, Modules), + Modules2 = [{Module, Opts} | Modules1], + ejabberd_config:add_local_option({modules, Host}, Modules2). + +del_module_mnesia(Host, Module) -> + Modules = case ejabberd_config:get_local_option({modules, Host}) of + undefined -> + []; + Ls -> + Ls + end, + Modules1 = lists:keydelete(Module, 1, Modules), + ejabberd_config:add_local_option({modules, Host}, Modules1). + +get_hosts(Opts, Prefix) -> + case catch gen_mod:get_opt(hosts, Opts) of + {'EXIT', _Error1} -> + case catch gen_mod:get_opt(host, Opts) of + {'EXIT', _Error2} -> + [Prefix ++ Host || Host <- ?MYHOSTS]; + Host -> + [Host] + end; + Hosts -> + Hosts + end. + +get_module_proc(Host, {frontend, Base}) -> + get_module_proc("frontend_" ++ Host, Base); +get_module_proc(Host, Base) -> + list_to_atom(atom_to_list(Base) ++ "_" ++ Host). + +is_loaded(Host, Module) -> + ets:member(ejabberd_modules, {Module, Host}). + diff --git a/ejabberdPrefs/English.lproj/InfoPlist.strings b/ejabberdPrefs/English.lproj/InfoPlist.strings new file mode 100644 index 0000000000000000000000000000000000000000..cc357ab1f888e22fd37c5aa52cbba60859be6618 GIT binary patch literal 532 zcmbV|OAo<76ot>)uV}0gudomi8xoI15KZjS*1M!d!jH!{^-82RGMV1nJ7>;0bFMcP zl#!*LcA9CTjh4E|mBZ{r4HPoj$){RWk8hVhk3O$`gb#IKx(0oYP#HKAtfpFO=m^>Y z=c*6?mTIZaPSUNIOj$<9VN#2^Wov;}fW|n&z5Lir0=)T}zZpw-DzeKzxY{rr-u~kl zzCC@XVCkcgMYoB!J?}P7<~TJ;q)0MNE#al8igM4cDsy89)@1FHRj2{}T^v8&-j}To aiEYWn*NBq|xtNShesNllB;!v0c5w$h30C9) literal 0 HcmV?d00001 diff --git a/ejabberdPrefs/English.lproj/ejabberdPref.nib/classes.nib b/ejabberdPrefs/English.lproj/ejabberdPref.nib/classes.nib new file mode 100644 index 0000000..6e0dced --- /dev/null +++ b/ejabberdPrefs/English.lproj/ejabberdPref.nib/classes.nib @@ -0,0 +1,30 @@ +{ + IBClasses = ( + { + CLASS = NSPreferencePane; + LANGUAGE = ObjC; + OUTLETS = { + "_firstKeyView" = id; + "_initialKeyView" = id; + "_lastKeyView" = id; + "_window" = id; + }; + SUPERCLASS = NSObject; + }, + { + ACTIONS = {automaticStartAction = id; startStopAction = id; }; + CLASS = ejabberdController; + LANGUAGE = ObjC; + OUTLETS = { + actionProgress = NSProgressIndicator; + automaticBox = NSButton; + imagestarted = NSImageView; + imagestopped = NSImageView; + startStopButton = NSButton; + status = NSTextField; + }; + SUPERCLASS = NSObject; + } + ); + IBVersion = 1; +} \ No newline at end of file diff --git a/ejabberdPrefs/English.lproj/ejabberdPref.nib/info.nib b/ejabberdPrefs/English.lproj/ejabberdPref.nib/info.nib new file mode 100644 index 0000000..7914eb6 --- /dev/null +++ b/ejabberdPrefs/English.lproj/ejabberdPref.nib/info.nib @@ -0,0 +1,14 @@ + + + + + IBDocumentLocation + 32 22 356 240 0 0 1680 1028 + IBFramework Version + 446.1 + IBOldestOS + 2 + IBSystem Version + 8S2167 + + diff --git a/ejabberdPrefs/English.lproj/ejabberdPref.nib/keyedobjects.nib b/ejabberdPrefs/English.lproj/ejabberdPref.nib/keyedobjects.nib new file mode 100644 index 0000000000000000000000000000000000000000..e39337f1b6fa7669a5e4d4b2808edb6567a0f717 GIT binary patch literal 8128 zcmaJ`34Bw<);}{h`_kOpBsVvJTCj=;lqPL~f&!&17HBE7l!`zN>7@ihQj(NXYh@6X zUB&hJRD>3gMMPNy7u@&tiMSvSeeU~$h&<#8-;nQ61TcUROkf7vm096rWJYwZnykvguT(SZ-C5(`KrXbgEi(m6nfI z8`YY^wt*6mn^wOI7O+k!9-6HWs|lyWjBFL(1Uon&2>H+#E`!UV5Qf8em;^~kK^p3z z5t`vfxE&sV)$lkx0nft=@G`s(Z@}B|4(x-E;dA%`euiJ*82kyx2@p!m#6s*OgGeMm zI+1SVLUIY|M+TB1q>vPmk>m)@hZXip@ zQnHNPLT)E_kruL+JV-W>&14(dPM#*ukmty&lspxkw3`^ zN~u5{XFjJs8ldOV^XUaNlXj)u=tcBmdMWKgLo}D>(E>Vv4yHrsa5{n((^5K?R?rD_ z61|FEO~Z6Lt)-2$iLRjc(=G53eVA^gkI+YP?l!#JPIu6q^f9`NK0)`;kLgkPgZ`Ty zqkq!l^gr}3dV-!5Kp+AY1i>H}1(RSFEP_?A33kCDILYHehTsw;!6zsavf}lz*r%cd zPRIZkh~NeZJm3W%$lwPB0?-LMLl-y?&W8&i6S_h-xDc}7BDfg3!zIuIE`^@Z3wlE~ z^nn}=7h`pdREA^qDk3FNT3#55sHs$RS~M0-Hx?!0al}v}siUYU7EYzqls(B?8d8;65xyQ5 zsH=g{=6Bjs`YgO_vp`?W|6lr_rr?MqYS@rS&cZn(67h6NI3BK5lUJfVbzT|=R+g5J zPbAXp@9b5!nLWhrWt-URYzs57H<*Rp$Nt7%V@3wzbByjqkB1-^^3ZV2SgsN?FrivR zE6S4UbTz5QBWhVVuI`0?&>sq5K$UKenwq3v#Bojw41j@CQAm9%otUkgQq^t~3M(F3 zl!zq49lGy@LAdl_7=q4kOWf3IX4cmj3Pr6@1jAq;dbXrK9Y$A{tLX`)<@l}790B7t z!AQ6QieVIthA~hAr7#xCn3XMLi`WvjjNQy`Ww)_A*j=pOJ}8F@TyFwY!bGTo9CY0< zHIQn2-Kh)6{bTi z%z!A&gjo=S*${^W)WMu;(Op`uN4-4V7*k8asaYC4^=CuFky+7rErzNj8ZVDtqoN-A zn~G>UcIr+G1=_B^8;`)noAO9fRpb1I&kxVz^E<{=M%4y%+8wGe1?fP4}dY2Q7!tLn(}jdW#;DQHTTMF$}b4^$_(WNo13de-({C&)+93H ziF9Tx5edg|vCKh(&_lXTdtom6p#kRMqS>i*60NG>OEQ-p!fT+Z6`J5$7^s=w9z<;* zce8e}53Ymxz);Wv%;be|1A?Kve%f4=qo*kc^msK@mx$M>NsN*PALeGju4R37AlwAw zHp3!V3`<}sEQ95+0&a#|;8s}4Bz6Jo%|fh@jboMUDt0xiVRfwEhq&e)xays77p{Ca zhAs|&hkM~ZxF1%b=gRBr)FjsdO&cEP*sDQ}=W33{)GAVAF(%@l$LcwWABJ^|ozJ|Q zGjsFMo1vT__hu;AAFp$pn-9VoXn|H(3+rG#JO~?b*^RIXHp4@(1s;a2@CaH^3)^5j z?0}u{7(U(IUhIa?sgKSL$JBUQOUc4mT1_Hq)#BNh^r#_fTbD?t(R0WxX&lx@GwEa^ z#sh*mIkY~VPQ*3+L)D^$d95a+iJEAn7$+oAIHK^hG1|)QLx!oba3d!Nv|5u;Eqaie z+O=q36LHuvBE-6|0F#-INn${{;<9lk*?HotQWiM@tcwWMqEc9o9D$h^$xNlg$#iCz zW2`n2-v>{^Q|O#M@H9LF&%$%)ZjN&N;k<-Vn6By2t*{IM>}QJG;GI0CJ`$}7k4%OU zBRck8gqK?2B^Zd)I6v-%S5W4w@EV$-c}?TEPOLM>VM8<>nW0a66W(fpw>V(YLfs$Q zaGes$^g;iCch|wYuovD#)-?U5I%zaFc4SQ z=VB1K8+p9+v_(BTKz}Z<9z?Ky3jc!7G#F-D-3Y*5tg8mVV6Yztpq9?TU|z4xV87<( zkKjxAsttfL0`DRC8ooi;eTQlD1N;a-X|%=3sWzFYkJl6>Vu>VbQGw~FU9$_>B^c3e zm{2_>0|rupA%Kx|t24vXrm4vq9>{ckDs#a5@GBgG-{3I1{C7A4f51`rH)d|TNbQ#g zfFp)4Q(hO2aN;Y%whcj`#pYsmk!WIBtb1j7V=ArAo*dErqfK}Z{)6&m_zO;Gk|)lH zrq$6o*?sj-0f++Yt~s0#__YP~8F-pfL?8xTDRwFA!PmgFY7^gKZrc*X%9j|L1MTu0 z#JQF@_0Vfs!e@+#sF8Ci|CFhazkqa@bq^lOTAj?4(>?y%e9%+ck5#?^O_9^_Ke6By}5dV@@|Ngs_rPrz;xBDo}wWLtn8m5OFt!}L#tlu(mIq9gn8^p4F?^WPHM>vxEDh+ zEE%rleotXd8D10Bx-TsP#a33vMzNt*Jp{90$a)eZvq_vJNFABOir6qVoQ+^3*%i!; z{c>SlT};g^8#W@dE{thBEfGedNNb1)ma0`1nZx6ea9yfChUKg#6Ty+j7AIPl&YT;} z4xK&cd~Hre@rV(bbMvxu&K}@HzK>$1M0QD)X{+h*>(r)5aZ$zDliIZ`9eMUU8S#^k zxC!O+nCAxj_R1{4pnpV~$hEMRG?VL)hd7 zcoZw1n&N$x3n?uVff^O*~IYPE#E> z`p`Sbo$JV*r*WG0p7$3Yaa57JkrDm|caeM03XZA!$o*s$d4Q}YYcP@PqFUKTH(=YU z?RStM5M~V}VeA9vad5eVHdb-rkSoy__;DM7jb{_yMTTi*<+=*%$odwt9tNV6GL*w7 zR*O7BxfquAiielMzU^7Qk!)H^Ht8v#74@=YqBeZa|+t4Gj(1b;$(ZQuj{8 z)$F*M?xV*0v}F8?Wz$(L zo57-NCY#0SkQWdv8o^vGCjD`r(EprVvKv|C_$e-7aU_a%dN|4Hfh9N}V5Udo*mdFN zyjts`)tdjIIyLF4Q!BN#sXmXZ-pJS0NiVrf>-=CTH6KAY}P zMJk`B;x(sKq@6Sso6c4-QCFwdREu78LmMtn-)>?eYn7Cj>c0JXgOd~L-`qR}cTu_6 z+F{5s+p5J*O}W{5_$H+G%*EZ+X*YUhrx((!7Mj&Ygl$`Eb#@&uF%zpBwBs0!PH1;} zNh`gC_TWRkxW9-_8(WVp3HG~f2Sv!j_^{*l)DglG+6$^^Ha1l^;TgdVZAU0H$PDa8 z>^a)8g)@0iA5)W+dUJeA!gAVIlQ51g<`SlcwT(hMJDnQ0f?lSLt7J?0IBcY}NSEWG zsJ?aR7)rD0P+9~l*bQtsTfr(39UVjUZO8!K?2+_}R(geQHr7ksLayi8hN8^Tcve!0 zGH=1n&zRs3*|AL~u#JyZ8o=Gca~nmlt?6U5K>U=WLS$F3!G`b`k`ZKvJ@ar=s{5Ax1k zJ8{VCxC-gk?xyorbB}V(pFHjQY3|QXc!Y9xXi5TZFM&L9_9&Z#4RM zB_8=LgC!WhQuJ9d9`W(`<*}PFeoOGEkH_#1j9d-I?gl&zT!wM1V+%2!y)mk{VH_5< zYj8kYmHv|+q@U1F>A&b_^mF`J%C@oXYzN!P9%H-MZuU5Pf<4KeVtd%r>>2hfdyYNNUSKb>m)Ohf6?Tuf zQ*?;a#jWB-ah>>#*do@5d&FttHgT=ESnMP|DAtPG#XH1hqAG3@9}`+g(jg{m@h04 z778~Ci-aY@GGT>qm(U`t71j%n3(pI02pM=w#?@IL~mgp|@dxVThsF zFx7CiVVa@FFx@c2Fw+n-#0_e8;%o_=)jf#_x>3 znyATc>SF3{DliqAicG^zBTdDo@usM0mT9&rVVYw~nd(gqrbbhfso6B&w7|5=w8^x? zw8!+K>1ETarq@kxnD&`IFnwhD&D`BwU>;+xG>6Rz^L6Ii&1=lt&Ci(MH-B#a()_jg zTl4qkAI(3T51WsekDC8uK4Ae%XGhxIY*ZtD})r>sv~pS8YZeck$r^|1B0&2019y4kX97uzneU25xP%e4)$mDncO z!nTM_wZ&~I+hW^N+j85@wp(qjwnuE+Z98pG+g`Elw|#E=#dh3ov0r5GZtr35Y42sv zw&&PK*=N~j+Y|OV_LRNe-eAAh{&)Lk`y=*k_8s8;2spYsf{t8AzN4R` zz%kHqxnq!Hh@;ps#!>1hbA%lchw7+xL><>T7CCNq-0fK9*y7mfc+0WRamaDlal~=d zam;btank8^%1*`E$=Ssjat?M5bq;fmb5=XU&Y8}Xv(dTAxyiZHxy$*u^GWAZ&ZnKv zI-hsG;C$1$-+91!(D|wJGv^o1ubkgx2pRn{sxs;_R%C3#og7N<1TOybXU12xhK1)x~tt`cf{S~zR$hNz1rR4Uh7`(-r(NiJ}Oxxr{t2{ zl2-~!xl+E=PwFoXkS>!dqW=ReD`|LwZYkM|xM-paEm)Gm>HpaOng0j>5&sGQNrfmj#jZFNSt(J*D&v%LWr8wMxl*}GnW9uH zVI`udO05!AW+}6kgfd4-DfLQ&(x@~k&B}abfwE9puDqbUq`acMro5rNrM#oOtGuVY zuY9QNR}Lr#l~0w=lrNO8ly8*plpmCzlwXuX%3c7!?>DC<#;suEHHC Sekebcz>m;b|84&c%=|y+n#0Zj literal 0 HcmV?d00001 diff --git a/ejabberdPrefs/Info.plist b/ejabberdPrefs/Info.plist new file mode 100644 index 0000000..065688c --- /dev/null +++ b/ejabberdPrefs/Info.plist @@ -0,0 +1,30 @@ + + + + + CFBundleDevelopmentRegion + English + CFBundleExecutable + ejabberd + CFBundleIconFile + + CFBundleIdentifier + com.apple.prefpanel + CFBundleInfoDictionaryVersion + 6.0 + CFBundlePackageType + BNDL + CFBundleSignature + process-one + CFBundleVersion + 2.0.3 + NSMainNibFile + ejabberdPref + NSPrefPaneIconFile + ejabberdPref.tiff + NSPrefPaneIconLabel + ejabberd + NSPrincipalClass + ejabberdPref + + diff --git a/ejabberdPrefs/LICENSE b/ejabberdPrefs/LICENSE new file mode 100644 index 0000000..17fb0c7 --- /dev/null +++ b/ejabberdPrefs/LICENSE @@ -0,0 +1,18 @@ +ejabberd's prefPane Licence + +Except as described below, all of the source code to prefPane is +available under the Mozilla Public Licence (MPL). + +The complete text of the Mozilla Public License can be found in +the MPL file in the same directory as this file or consulted at +http://www.mozilla.org/MPL/MPL-1.1.html. + +Exceptions + +The following portions are not available under the above +terms: + + * Image files containing the trademarks and logos of + ProcessOne, which may not be reproduced without + permission. (Copyright ©2001-2009 ProcessOne. All Rights + Reserved.) diff --git a/ejabberdPrefs/MPL b/ejabberdPrefs/MPL new file mode 100644 index 0000000..7714141 --- /dev/null +++ b/ejabberdPrefs/MPL @@ -0,0 +1,470 @@ + MOZILLA PUBLIC LICENSE + Version 1.1 + + --------------- + +1. Definitions. + + 1.0.1. "Commercial Use" means distribution or otherwise making the + Covered Code available to a third party. + + 1.1. "Contributor" means each entity that creates or contributes to + the creation of Modifications. + + 1.2. "Contributor Version" means the combination of the Original + Code, prior Modifications used by a Contributor, and the Modifications + made by that particular Contributor. + + 1.3. "Covered Code" means the Original Code or Modifications or the + combination of the Original Code and Modifications, in each case + including portions thereof. + + 1.4. "Electronic Distribution Mechanism" means a mechanism generally + accepted in the software development community for the electronic + transfer of data. + + 1.5. "Executable" means Covered Code in any form other than Source + Code. + + 1.6. "Initial Developer" means the individual or entity identified + as the Initial Developer in the Source Code notice required by Exhibit + A. + + 1.7. "Larger Work" means a work which combines Covered Code or + portions thereof with code not governed by the terms of this License. + + 1.8. "License" means this document. + + 1.8.1. "Licensable" means having the right to grant, to the maximum + extent possible, whether at the time of the initial grant or + subsequently acquired, any and all of the rights conveyed herein. + + 1.9. "Modifications" means any addition to or deletion from the + substance or structure of either the Original Code or any previous + Modifications. When Covered Code is released as a series of files, a + Modification is: + A. Any addition to or deletion from the contents of a file + containing Original Code or previous Modifications. + + B. Any new file that contains any part of the Original Code or + previous Modifications. + + 1.10. "Original Code" means Source Code of computer software code + which is described in the Source Code notice required by Exhibit A as + Original Code, and which, at the time of its release under this + License is not already Covered Code governed by this License. + + 1.10.1. "Patent Claims" means any patent claim(s), now owned or + hereafter acquired, including without limitation, method, process, + and apparatus claims, in any patent Licensable by grantor. + + 1.11. "Source Code" means the preferred form of the Covered Code for + making modifications to it, including all modules it contains, plus + any associated interface definition files, scripts used to control + compilation and installation of an Executable, or source code + differential comparisons against either the Original Code or another + well known, available Covered Code of the Contributor's choice. The + Source Code can be in a compressed or archival form, provided the + appropriate decompression or de-archiving software is widely available + for no charge. + + 1.12. "You" (or "Your") means an individual or a legal entity + exercising rights under, and complying with all of the terms of, this + License or a future version of this License issued under Section 6.1. + For legal entities, "You" includes any entity which controls, is + controlled by, or is under common control with You. For purposes of + this definition, "control" means (a) the power, direct or indirect, + to cause the direction or management of such entity, whether by + contract or otherwise, or (b) ownership of more than fifty percent + (50%) of the outstanding shares or beneficial ownership of such + entity. + +2. Source Code License. + + 2.1. The Initial Developer Grant. + The Initial Developer hereby grants You a world-wide, royalty-free, + non-exclusive license, subject to third party intellectual property + claims: + (a) under intellectual property rights (other than patent or + trademark) Licensable by Initial Developer to use, reproduce, + modify, display, perform, sublicense and distribute the Original + Code (or portions thereof) with or without Modifications, and/or + as part of a Larger Work; and + + (b) under Patents Claims infringed by the making, using or + selling of Original Code, to make, have made, use, practice, + sell, and offer for sale, and/or otherwise dispose of the + Original Code (or portions thereof). + + (c) the licenses granted in this Section 2.1(a) and (b) are + effective on the date Initial Developer first distributes + Original Code under the terms of this License. + + (d) Notwithstanding Section 2.1(b) above, no patent license is + granted: 1) for code that You delete from the Original Code; 2) + separate from the Original Code; or 3) for infringements caused + by: i) the modification of the Original Code or ii) the + combination of the Original Code with other software or devices. + + 2.2. Contributor Grant. + Subject to third party intellectual property claims, each Contributor + hereby grants You a world-wide, royalty-free, non-exclusive license + + (a) under intellectual property rights (other than patent or + trademark) Licensable by Contributor, to use, reproduce, modify, + display, perform, sublicense and distribute the Modifications + created by such Contributor (or portions thereof) either on an + unmodified basis, with other Modifications, as Covered Code + and/or as part of a Larger Work; and + + (b) under Patent Claims infringed by the making, using, or + selling of Modifications made by that Contributor either alone + and/or in combination with its Contributor Version (or portions + of such combination), to make, use, sell, offer for sale, have + made, and/or otherwise dispose of: 1) Modifications made by that + Contributor (or portions thereof); and 2) the combination of + Modifications made by that Contributor with its Contributor + Version (or portions of such combination). + + (c) the licenses granted in Sections 2.2(a) and 2.2(b) are + effective on the date Contributor first makes Commercial Use of + the Covered Code. + + (d) Notwithstanding Section 2.2(b) above, no patent license is + granted: 1) for any code that Contributor has deleted from the + Contributor Version; 2) separate from the Contributor Version; + 3) for infringements caused by: i) third party modifications of + Contributor Version or ii) the combination of Modifications made + by that Contributor with other software (except as part of the + Contributor Version) or other devices; or 4) under Patent Claims + infringed by Covered Code in the absence of Modifications made by + that Contributor. + +3. Distribution Obligations. + + 3.1. Application of License. + The Modifications which You create or to which You contribute are + governed by the terms of this License, including without limitation + Section 2.2. The Source Code version of Covered Code may be + distributed only under the terms of this License or a future version + of this License released under Section 6.1, and You must include a + copy of this License with every copy of the Source Code You + distribute. You may not offer or impose any terms on any Source Code + version that alters or restricts the applicable version of this + License or the recipients' rights hereunder. However, You may include + an additional document offering the additional rights described in + Section 3.5. + + 3.2. Availability of Source Code. + Any Modification which You create or to which You contribute must be + made available in Source Code form under the terms of this License + either on the same media as an Executable version or via an accepted + Electronic Distribution Mechanism to anyone to whom you made an + Executable version available; and if made available via Electronic + Distribution Mechanism, must remain available for at least twelve (12) + months after the date it initially became available, or at least six + (6) months after a subsequent version of that particular Modification + has been made available to such recipients. You are responsible for + ensuring that the Source Code version remains available even if the + Electronic Distribution Mechanism is maintained by a third party. + + 3.3. Description of Modifications. + You must cause all Covered Code to which You contribute to contain a + file documenting the changes You made to create that Covered Code and + the date of any change. You must include a prominent statement that + the Modification is derived, directly or indirectly, from Original + Code provided by the Initial Developer and including the name of the + Initial Developer in (a) the Source Code, and (b) in any notice in an + Executable version or related documentation in which You describe the + origin or ownership of the Covered Code. + + 3.4. Intellectual Property Matters + (a) Third Party Claims. + If Contributor has knowledge that a license under a third party's + intellectual property rights is required to exercise the rights + granted by such Contributor under Sections 2.1 or 2.2, + Contributor must include a text file with the Source Code + distribution titled "LEGAL" which describes the claim and the + party making the claim in sufficient detail that a recipient will + know whom to contact. If Contributor obtains such knowledge after + the Modification is made available as described in Section 3.2, + Contributor shall promptly modify the LEGAL file in all copies + Contributor makes available thereafter and shall take other steps + (such as notifying appropriate mailing lists or newsgroups) + reasonably calculated to inform those who received the Covered + Code that new knowledge has been obtained. + + (b) Contributor APIs. + If Contributor's Modifications include an application programming + interface and Contributor has knowledge of patent licenses which + are reasonably necessary to implement that API, Contributor must + also include this information in the LEGAL file. + + (c) Representations. + Contributor represents that, except as disclosed pursuant to + Section 3.4(a) above, Contributor believes that Contributor's + Modifications are Contributor's original creation(s) and/or + Contributor has sufficient rights to grant the rights conveyed by + this License. + + 3.5. Required Notices. + You must duplicate the notice in Exhibit A in each file of the Source + Code. If it is not possible to put such notice in a particular Source + Code file due to its structure, then You must include such notice in a + location (such as a relevant directory) where a user would be likely + to look for such a notice. If You created one or more Modification(s) + You may add your name as a Contributor to the notice described in + Exhibit A. You must also duplicate this License in any documentation + for the Source Code where You describe recipients' rights or ownership + rights relating to Covered Code. You may choose to offer, and to + charge a fee for, warranty, support, indemnity or liability + obligations to one or more recipients of Covered Code. However, You + may do so only on Your own behalf, and not on behalf of the Initial + Developer or any Contributor. You must make it absolutely clear than + any such warranty, support, indemnity or liability obligation is + offered by You alone, and You hereby agree to indemnify the Initial + Developer and every Contributor for any liability incurred by the + Initial Developer or such Contributor as a result of warranty, + support, indemnity or liability terms You offer. + + 3.6. Distribution of Executable Versions. + You may distribute Covered Code in Executable form only if the + requirements of Section 3.1-3.5 have been met for that Covered Code, + and if You include a notice stating that the Source Code version of + the Covered Code is available under the terms of this License, + including a description of how and where You have fulfilled the + obligations of Section 3.2. The notice must be conspicuously included + in any notice in an Executable version, related documentation or + collateral in which You describe recipients' rights relating to the + Covered Code. You may distribute the Executable version of Covered + Code or ownership rights under a license of Your choice, which may + contain terms different from this License, provided that You are in + compliance with the terms of this License and that the license for the + Executable version does not attempt to limit or alter the recipient's + rights in the Source Code version from the rights set forth in this + License. If You distribute the Executable version under a different + license You must make it absolutely clear that any terms which differ + from this License are offered by You alone, not by the Initial + Developer or any Contributor. You hereby agree to indemnify the + Initial Developer and every Contributor for any liability incurred by + the Initial Developer or such Contributor as a result of any such + terms You offer. + + 3.7. Larger Works. + You may create a Larger Work by combining Covered Code with other code + not governed by the terms of this License and distribute the Larger + Work as a single product. In such a case, You must make sure the + requirements of this License are fulfilled for the Covered Code. + +4. Inability to Comply Due to Statute or Regulation. + + If it is impossible for You to comply with any of the terms of this + License with respect to some or all of the Covered Code due to + statute, judicial order, or regulation then You must: (a) comply with + the terms of this License to the maximum extent possible; and (b) + describe the limitations and the code they affect. Such description + must be included in the LEGAL file described in Section 3.4 and must + be included with all distributions of the Source Code. Except to the + extent prohibited by statute or regulation, such description must be + sufficiently detailed for a recipient of ordinary skill to be able to + understand it. + +5. Application of this License. + + This License applies to code to which the Initial Developer has + attached the notice in Exhibit A and to related Covered Code. + +6. Versions of the License. + + 6.1. New Versions. + Netscape Communications Corporation ("Netscape") may publish revised + and/or new versions of the License from time to time. Each version + will be given a distinguishing version number. + + 6.2. Effect of New Versions. + Once Covered Code has been published under a particular version of the + License, You may always continue to use it under the terms of that + version. You may also choose to use such Covered Code under the terms + of any subsequent version of the License published by Netscape. No one + other than Netscape has the right to modify the terms applicable to + Covered Code created under this License. + + 6.3. Derivative Works. + If You create or use a modified version of this License (which you may + only do in order to apply it to code which is not already Covered Code + governed by this License), You must (a) rename Your license so that + the phrases "Mozilla", "MOZILLAPL", "MOZPL", "Netscape", + "MPL", "NPL" or any confusingly similar phrase do not appear in your + license (except to note that your license differs from this License) + and (b) otherwise make it clear that Your version of the license + contains terms which differ from the Mozilla Public License and + Netscape Public License. (Filling in the name of the Initial + Developer, Original Code or Contributor in the notice described in + Exhibit A shall not of themselves be deemed to be modifications of + this License.) + +7. DISCLAIMER OF WARRANTY. + + COVERED CODE IS PROVIDED UNDER THIS LICENSE ON AN "AS IS" BASIS, + WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, + WITHOUT LIMITATION, WARRANTIES THAT THE COVERED CODE IS FREE OF + DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR PURPOSE OR NON-INFRINGING. + THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE COVERED CODE + IS WITH YOU. SHOULD ANY COVERED CODE PROVE DEFECTIVE IN ANY RESPECT, + YOU (NOT THE INITIAL DEVELOPER OR ANY OTHER CONTRIBUTOR) ASSUME THE + COST OF ANY NECESSARY SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER + OF WARRANTY CONSTITUTES AN ESSENTIAL PART OF THIS LICENSE. NO USE OF + ANY COVERED CODE IS AUTHORIZED HEREUNDER EXCEPT UNDER THIS DISCLAIMER. + +8. TERMINATION. + + 8.1. This License and the rights granted hereunder will terminate + automatically if You fail to comply with terms herein and fail to cure + such breach within 30 days of becoming aware of the breach. All + sublicenses to the Covered Code which are properly granted shall + survive any termination of this License. Provisions which, by their + nature, must remain in effect beyond the termination of this License + shall survive. + + 8.2. If You initiate litigation by asserting a patent infringement + claim (excluding declatory judgment actions) against Initial Developer + or a Contributor (the Initial Developer or Contributor against whom + You file such action is referred to as "Participant") alleging that: + + (a) such Participant's Contributor Version directly or indirectly + infringes any patent, then any and all rights granted by such + Participant to You under Sections 2.1 and/or 2.2 of this License + shall, upon 60 days notice from Participant terminate prospectively, + unless if within 60 days after receipt of notice You either: (i) + agree in writing to pay Participant a mutually agreeable reasonable + royalty for Your past and future use of Modifications made by such + Participant, or (ii) withdraw Your litigation claim with respect to + the Contributor Version against such Participant. If within 60 days + of notice, a reasonable royalty and payment arrangement are not + mutually agreed upon in writing by the parties or the litigation claim + is not withdrawn, the rights granted by Participant to You under + Sections 2.1 and/or 2.2 automatically terminate at the expiration of + the 60 day notice period specified above. + + (b) any software, hardware, or device, other than such Participant's + Contributor Version, directly or indirectly infringes any patent, then + any rights granted to You by such Participant under Sections 2.1(b) + and 2.2(b) are revoked effective as of the date You first made, used, + sold, distributed, or had made, Modifications made by that + Participant. + + 8.3. If You assert a patent infringement claim against Participant + alleging that such Participant's Contributor Version directly or + indirectly infringes any patent where such claim is resolved (such as + by license or settlement) prior to the initiation of patent + infringement litigation, then the reasonable value of the licenses + granted by such Participant under Sections 2.1 or 2.2 shall be taken + into account in determining the amount or value of any payment or + license. + + 8.4. In the event of termination under Sections 8.1 or 8.2 above, + all end user license agreements (excluding distributors and resellers) + which have been validly granted by You or any distributor hereunder + prior to termination shall survive termination. + +9. LIMITATION OF LIABILITY. + + UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, WHETHER TORT + (INCLUDING NEGLIGENCE), CONTRACT, OR OTHERWISE, SHALL YOU, THE INITIAL + DEVELOPER, ANY OTHER CONTRIBUTOR, OR ANY DISTRIBUTOR OF COVERED CODE, + OR ANY SUPPLIER OF ANY OF SUCH PARTIES, BE LIABLE TO ANY PERSON FOR + ANY INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY + CHARACTER INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF GOODWILL, + WORK STOPPAGE, COMPUTER FAILURE OR MALFUNCTION, OR ANY AND ALL OTHER + COMMERCIAL DAMAGES OR LOSSES, EVEN IF SUCH PARTY SHALL HAVE BEEN + INFORMED OF THE POSSIBILITY OF SUCH DAMAGES. THIS LIMITATION OF + LIABILITY SHALL NOT APPLY TO LIABILITY FOR DEATH OR PERSONAL INJURY + RESULTING FROM SUCH PARTY'S NEGLIGENCE TO THE EXTENT APPLICABLE LAW + PROHIBITS SUCH LIMITATION. SOME JURISDICTIONS DO NOT ALLOW THE + EXCLUSION OR LIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO + THIS EXCLUSION AND LIMITATION MAY NOT APPLY TO YOU. + +10. U.S. GOVERNMENT END USERS. + + The Covered Code is a "commercial item," as that term is defined in + 48 C.F.R. 2.101 (Oct. 1995), consisting of "commercial computer + software" and "commercial computer software documentation," as such + terms are used in 48 C.F.R. 12.212 (Sept. 1995). Consistent with 48 + C.F.R. 12.212 and 48 C.F.R. 227.7202-1 through 227.7202-4 (June 1995), + all U.S. Government End Users acquire Covered Code with only those + rights set forth herein. + +11. MISCELLANEOUS. + + This License represents the complete agreement concerning subject + matter hereof. If any provision of this License is held to be + unenforceable, such provision shall be reformed only to the extent + necessary to make it enforceable. This License shall be governed by + California law provisions (except to the extent applicable law, if + any, provides otherwise), excluding its conflict-of-law provisions. + With respect to disputes in which at least one party is a citizen of, + or an entity chartered or registered to do business in the United + States of America, any litigation relating to this License shall be + subject to the jurisdiction of the Federal Courts of the Northern + District of California, with venue lying in Santa Clara County, + California, with the losing party responsible for costs, including + without limitation, court costs and reasonable attorneys' fees and + expenses. The application of the United Nations Convention on + Contracts for the International Sale of Goods is expressly excluded. + Any law or regulation which provides that the language of a contract + shall be construed against the drafter shall not apply to this + License. + +12. RESPONSIBILITY FOR CLAIMS. + + As between Initial Developer and the Contributors, each party is + responsible for claims and damages arising, directly or indirectly, + out of its utilization of rights under this License and You agree to + work with Initial Developer and Contributors to distribute such + responsibility on an equitable basis. Nothing herein is intended or + shall be deemed to constitute any admission of liability. + +13. MULTIPLE-LICENSED CODE. + + Initial Developer may designate portions of the Covered Code as + "Multiple-Licensed". "Multiple-Licensed" means that the Initial + Developer permits you to utilize portions of the Covered Code under + Your choice of the NPL or the alternative licenses, if any, specified + by the Initial Developer in the file described in Exhibit A. + +EXHIBIT A -Mozilla Public License. + + ``The contents of this file are subject to the Mozilla Public License + Version 1.1 (the "License"); you may not use this file except in + compliance with the License. You may obtain a copy of the License at + http://www.mozilla.org/MPL/ + + Software distributed under the License is distributed on an "AS IS" + basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the + License for the specific language governing rights and limitations + under the License. + + The Original Code is ______________________________________. + + The Initial Developer of the Original Code is ________________________. + Portions created by ______________________ are Copyright (C) ______ + _______________________. All Rights Reserved. + + Contributor(s): ______________________________________. + + Alternatively, the contents of this file may be used under the terms + of the _____ license (the "[___] License"), in which case the + provisions of [______] License are applicable instead of those + above. If you wish to allow use of your version of this file only + under the terms of the [____] License and not to allow others to use + your version of this file under the MPL, indicate your decision by + deleting the provisions above and replace them with the notice and + other provisions required by the [___] License. If you do not delete + the provisions above, a recipient may use your version of this file + under either the MPL or the [___] License." + + [NOTE: The text of this Exhibit A may differ slightly from the text of + the notices in the Source Code files of the Original Code. You should + use the text of this Exhibit A rather than the text found in the + Original Code Source Code for Your Modifications.] + diff --git a/ejabberdPrefs/config.txt b/ejabberdPrefs/config.txt new file mode 100644 index 0000000..c1ff9d0 --- /dev/null +++ b/ejabberdPrefs/config.txt @@ -0,0 +1 @@ +/Applications/ejabberd-2.0.3 diff --git a/ejabberdPrefs/ejabberd.plist b/ejabberdPrefs/ejabberd.plist new file mode 100644 index 0000000..4d15b20 --- /dev/null +++ b/ejabberdPrefs/ejabberd.plist @@ -0,0 +1,16 @@ + + + + + Label + ejabberd.etc.rc.command + ProgramArguments + + /Applications/ejabberd-2.0.3/bin/ejabberdctl + start + + RunAtLoad + + + diff --git a/ejabberdPrefs/ejabberd.xcodeproj/project.pbxproj b/ejabberdPrefs/ejabberd.xcodeproj/project.pbxproj new file mode 100644 index 0000000..9769d16 --- /dev/null +++ b/ejabberdPrefs/ejabberd.xcodeproj/project.pbxproj @@ -0,0 +1,439 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 42; + objects = { + +/* Begin PBXBuildFile section */ + 837D1CC80B635DA9003F99C1 /* ejabberd.plist in Resources */ = {isa = PBXBuildFile; fileRef = 837D1CC70B635DA9003F99C1 /* ejabberd.plist */; }; + 837F6DD209F777FF00914317 /* ejabberdController.h in Headers */ = {isa = PBXBuildFile; fileRef = 837F6DD009F777FF00914317 /* ejabberdController.h */; }; + 837F6DD309F777FF00914317 /* ejabberdController.m in Sources */ = {isa = PBXBuildFile; fileRef = 837F6DD109F777FF00914317 /* ejabberdController.m */; }; + 837F6DDD09F7E55000914317 /* logo_ejabberd.png in Resources */ = {isa = PBXBuildFile; fileRef = 837F6DDB09F7E55000914317 /* logo_ejabberd.png */; }; + 837F6DDF09F7E6FD00914317 /* logo_ejabberd.png in Resources */ = {isa = PBXBuildFile; fileRef = 837F6DDE09F7E6FD00914317 /* logo_ejabberd.png */; }; + 837F6DE409F7E8BF00914317 /* instance_started.png in Resources */ = {isa = PBXBuildFile; fileRef = 837F6DE209F7E8BF00914317 /* instance_started.png */; }; + 837F6DE509F7E8BF00914317 /* instance_stopped.png in Resources */ = {isa = PBXBuildFile; fileRef = 837F6DE309F7E8BF00914317 /* instance_stopped.png */; }; + 837F6DEE09F8D41200914317 /* config.txt in Resources */ = {isa = PBXBuildFile; fileRef = 837F6DED09F8D41200914317 /* config.txt */; }; + 8D202CEA0486D31800D8A456 /* ejabberd_Prefix.pch in Headers */ = {isa = PBXBuildFile; fileRef = 32DBCFA20370C41700C91783 /* ejabberd_Prefix.pch */; }; + 8D202CEB0486D31800D8A456 /* ejabberdPref.h in Headers */ = {isa = PBXBuildFile; fileRef = F506C03C013D9D7901CA16C8 /* ejabberdPref.h */; }; + 8D202CED0486D31800D8A456 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 089C167DFE841241C02AAC07 /* InfoPlist.strings */; }; + 8D202CEE0486D31800D8A456 /* ejabberdPref.tiff in Resources */ = {isa = PBXBuildFile; fileRef = F506C040013D9D8001CA16C8 /* ejabberdPref.tiff */; }; + 8D202CEF0486D31800D8A456 /* ejabberdPref.nib in Resources */ = {isa = PBXBuildFile; fileRef = F506C042013D9D8C01CA16C8 /* ejabberdPref.nib */; }; + 8D202CF10486D31800D8A456 /* ejabberdPref.m in Sources */ = {isa = PBXBuildFile; fileRef = F506C03D013D9D7901CA16C8 /* ejabberdPref.m */; }; + 8D202CF30486D31800D8A456 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1058C7ADFEA557BF11CA2CBB /* Cocoa.framework */; }; + 8D202CF40486D31800D8A456 /* PreferencePanes.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F506C035013D953901CA16C8 /* PreferencePanes.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 089C1672FE841209C02AAC07 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = /System/Library/Frameworks/Foundation.framework; sourceTree = ""; }; + 089C167EFE841241C02AAC07 /* English */ = {isa = PBXFileReference; fileEncoding = 10; lastKnownFileType = text.plist.strings; name = English; path = English.lproj/InfoPlist.strings; sourceTree = ""; }; + 089C167FFE841241C02AAC07 /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = /System/Library/Frameworks/AppKit.framework; sourceTree = ""; }; + 1058C7ADFEA557BF11CA2CBB /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = /System/Library/Frameworks/Cocoa.framework; sourceTree = ""; }; + 32DBCFA20370C41700C91783 /* ejabberd_Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ejabberd_Prefix.pch; sourceTree = ""; }; + 837D1CC70B635DA9003F99C1 /* ejabberd.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist; path = ejabberd.plist; sourceTree = ""; }; + 837F6DD009F777FF00914317 /* ejabberdController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ejabberdController.h; sourceTree = ""; }; + 837F6DD109F777FF00914317 /* ejabberdController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ejabberdController.m; sourceTree = ""; }; + 837F6DDB09F7E55000914317 /* logo_ejabberd.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = logo_ejabberd.png; sourceTree = ""; }; + 837F6DDE09F7E6FD00914317 /* logo_ejabberd.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = logo_ejabberd.png; sourceTree = ""; }; + 837F6DE209F7E8BF00914317 /* instance_started.png */ = {isa = PBXFileReference; explicitFileType = image.png; path = instance_started.png; sourceTree = ""; }; + 837F6DE309F7E8BF00914317 /* instance_stopped.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = instance_stopped.png; sourceTree = ""; }; + 837F6DED09F8D41200914317 /* config.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = config.txt; sourceTree = ""; }; + 8D202CF70486D31800D8A456 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + 8D202CF80486D31800D8A456 /* ejabberd.prefPane */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ejabberd.prefPane; sourceTree = BUILT_PRODUCTS_DIR; }; + F506C035013D953901CA16C8 /* PreferencePanes.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PreferencePanes.framework; path = /System/Library/Frameworks/PreferencePanes.framework; sourceTree = ""; }; + F506C03C013D9D7901CA16C8 /* ejabberdPref.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ejabberdPref.h; sourceTree = ""; }; + F506C03D013D9D7901CA16C8 /* ejabberdPref.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ejabberdPref.m; sourceTree = ""; }; + F506C040013D9D8001CA16C8 /* ejabberdPref.tiff */ = {isa = PBXFileReference; lastKnownFileType = image.tiff; path = ejabberdPref.tiff; sourceTree = ""; }; + F506C043013D9D8C01CA16C8 /* English */ = {isa = PBXFileReference; lastKnownFileType = wrapper.nib; name = English; path = English.lproj/ejabberdPref.nib; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 8D202CF20486D31800D8A456 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 8D202CF30486D31800D8A456 /* Cocoa.framework in Frameworks */, + 8D202CF40486D31800D8A456 /* PreferencePanes.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 089C166AFE841209C02AAC07 /* ejabberd */ = { + isa = PBXGroup; + children = ( + 08FB77AFFE84173DC02AAC07 /* Classes */, + 32DBCFA10370C40200C91783 /* Other Sources */, + 089C167CFE841241C02AAC07 /* Resources */, + 089C1671FE841209C02AAC07 /* Frameworks and Libraries */, + 19C28FB8FE9D52D311CA2CBB /* Products */, + ); + name = ejabberd; + sourceTree = ""; + }; + 089C1671FE841209C02AAC07 /* Frameworks and Libraries */ = { + isa = PBXGroup; + children = ( + 1058C7ACFEA557BF11CA2CBB /* Linked Frameworks */, + 1058C7AEFEA557BF11CA2CBB /* Other Frameworks */, + ); + name = "Frameworks and Libraries"; + sourceTree = ""; + }; + 089C167CFE841241C02AAC07 /* Resources */ = { + isa = PBXGroup; + children = ( + 837F6DE209F7E8BF00914317 /* instance_started.png */, + 837F6DE309F7E8BF00914317 /* instance_stopped.png */, + 8D202CF70486D31800D8A456 /* Info.plist */, + 089C167DFE841241C02AAC07 /* InfoPlist.strings */, + F506C040013D9D8001CA16C8 /* ejabberdPref.tiff */, + F506C042013D9D8C01CA16C8 /* ejabberdPref.nib */, + 837F6DDB09F7E55000914317 /* logo_ejabberd.png */, + 837F6DDE09F7E6FD00914317 /* logo_ejabberd.png */, + 837F6DED09F8D41200914317 /* config.txt */, + ); + name = Resources; + sourceTree = ""; + }; + 08FB77AFFE84173DC02AAC07 /* Classes */ = { + isa = PBXGroup; + children = ( + F506C03C013D9D7901CA16C8 /* ejabberdPref.h */, + F506C03D013D9D7901CA16C8 /* ejabberdPref.m */, + ); + name = Classes; + sourceTree = ""; + }; + 1058C7ACFEA557BF11CA2CBB /* Linked Frameworks */ = { + isa = PBXGroup; + children = ( + 1058C7ADFEA557BF11CA2CBB /* Cocoa.framework */, + F506C035013D953901CA16C8 /* PreferencePanes.framework */, + ); + name = "Linked Frameworks"; + sourceTree = ""; + }; + 1058C7AEFEA557BF11CA2CBB /* Other Frameworks */ = { + isa = PBXGroup; + children = ( + 089C1672FE841209C02AAC07 /* Foundation.framework */, + 089C167FFE841241C02AAC07 /* AppKit.framework */, + ); + name = "Other Frameworks"; + sourceTree = ""; + }; + 19C28FB8FE9D52D311CA2CBB /* Products */ = { + isa = PBXGroup; + children = ( + 8D202CF80486D31800D8A456 /* ejabberd.prefPane */, + ); + name = Products; + sourceTree = ""; + }; + 32DBCFA10370C40200C91783 /* Other Sources */ = { + isa = PBXGroup; + children = ( + 837D1CC70B635DA9003F99C1 /* ejabberd.plist */, + 837F6DD009F777FF00914317 /* ejabberdController.h */, + 837F6DD109F777FF00914317 /* ejabberdController.m */, + 32DBCFA20370C41700C91783 /* ejabberd_Prefix.pch */, + ); + name = "Other Sources"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 8D202CE90486D31800D8A456 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 8D202CEA0486D31800D8A456 /* ejabberd_Prefix.pch in Headers */, + 8D202CEB0486D31800D8A456 /* ejabberdPref.h in Headers */, + 837F6DD209F777FF00914317 /* ejabberdController.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 8D202CE80486D31800D8A456 /* ejabberd */ = { + isa = PBXNativeTarget; + buildConfigurationList = 83231E620B57EBEA006434E8 /* Build configuration list for PBXNativeTarget "ejabberd" */; + buildPhases = ( + 8D202CE90486D31800D8A456 /* Headers */, + 8D202CEC0486D31800D8A456 /* Resources */, + 8D202CF00486D31800D8A456 /* Sources */, + 8D202CF20486D31800D8A456 /* Frameworks */, + 8D202CF50486D31800D8A456 /* Rez */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = ejabberd; + productInstallPath = "$(HOME)/Library/PreferencePanes"; + productName = ejabberd; + productReference = 8D202CF80486D31800D8A456 /* ejabberd.prefPane */; + productType = "com.apple.product-type.bundle"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 089C1669FE841209C02AAC07 /* Project object */ = { + isa = PBXProject; + buildConfigurationList = 83231E660B57EBEA006434E8 /* Build configuration list for PBXProject "ejabberd" */; + hasScannedForEncodings = 1; + mainGroup = 089C166AFE841209C02AAC07 /* ejabberd */; + projectDirPath = ""; + targets = ( + 8D202CE80486D31800D8A456 /* ejabberd */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 8D202CEC0486D31800D8A456 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 8D202CED0486D31800D8A456 /* InfoPlist.strings in Resources */, + 8D202CEE0486D31800D8A456 /* ejabberdPref.tiff in Resources */, + 8D202CEF0486D31800D8A456 /* ejabberdPref.nib in Resources */, + 837F6DDD09F7E55000914317 /* logo_ejabberd.png in Resources */, + 837F6DDF09F7E6FD00914317 /* logo_ejabberd.png in Resources */, + 837F6DE409F7E8BF00914317 /* instance_started.png in Resources */, + 837F6DE509F7E8BF00914317 /* instance_stopped.png in Resources */, + 837F6DEE09F8D41200914317 /* config.txt in Resources */, + 837D1CC80B635DA9003F99C1 /* ejabberd.plist in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXRezBuildPhase section */ + 8D202CF50486D31800D8A456 /* Rez */ = { + isa = PBXRezBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXRezBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 8D202CF00486D31800D8A456 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 8D202CF10486D31800D8A456 /* ejabberdPref.m in Sources */, + 837F6DD309F777FF00914317 /* ejabberdController.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 089C167DFE841241C02AAC07 /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + 089C167EFE841241C02AAC07 /* English */, + ); + name = InfoPlist.strings; + sourceTree = ""; + }; + F506C042013D9D8C01CA16C8 /* ejabberdPref.nib */ = { + isa = PBXVariantGroup; + children = ( + F506C043013D9D8C01CA16C8 /* English */, + ); + name = ejabberdPref.nib; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 83231E630B57EBEA006434E8 /* Development */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = ( + ppc, + i386, + ); + COPY_PHASE_STRIP = NO; + DEBUGGING_SYMBOLS = YES; + FRAMEWORK_SEARCH_PATHS = ""; + GCC_DYNAMIC_NO_PIC = NO; + GCC_ENABLE_FIX_AND_CONTINUE = YES; + GCC_ENABLE_TRIGRAPHS = NO; + GCC_GENERATE_DEBUGGING_SYMBOLS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = ejabberd_Prefix.pch; + GCC_WARN_ABOUT_MISSING_PROTOTYPES = NO; + GCC_WARN_FOUR_CHARACTER_CONSTANTS = NO; + GCC_WARN_UNKNOWN_PRAGMAS = NO; + HEADER_SEARCH_PATHS = ""; + INFOPLIST_FILE = Info.plist; + INSTALL_PATH = "$(HOME)/Library/PreferencePanes"; + LIBRARY_SEARCH_PATHS = ""; + LIBRARY_STYLE = Bundle; + OPTIMIZATION_CFLAGS = "-O0"; + OTHER_CFLAGS = ""; + OTHER_LDFLAGS = ( + "-bundle", + "-twolevel_namespace", + ); + OTHER_REZFLAGS = ""; + PRODUCT_NAME = ejabberd; + SECTORDER_FLAGS = ""; + WARNING_CFLAGS = ( + "-Wmost", + "-Wno-four-char-constants", + "-Wno-unknown-pragmas", + ); + WRAPPER_EXTENSION = prefPane; + ZERO_LINK = YES; + }; + name = Development; + }; + 83231E640B57EBEA006434E8 /* Deployment */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = ( + ppc, + i386, + ); + COPY_PHASE_STRIP = YES; + FRAMEWORK_SEARCH_PATHS = ""; + GCC_ENABLE_FIX_AND_CONTINUE = NO; + GCC_ENABLE_TRIGRAPHS = NO; + GCC_GENERATE_DEBUGGING_SYMBOLS = NO; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = ejabberd_Prefix.pch; + GCC_WARN_ABOUT_MISSING_PROTOTYPES = NO; + GCC_WARN_FOUR_CHARACTER_CONSTANTS = NO; + GCC_WARN_UNKNOWN_PRAGMAS = NO; + HEADER_SEARCH_PATHS = ""; + INFOPLIST_FILE = Info.plist; + INSTALL_PATH = "$(HOME)/Library/PreferencePanes"; + LIBRARY_SEARCH_PATHS = ""; + LIBRARY_STYLE = Bundle; + OTHER_CFLAGS = ""; + OTHER_LDFLAGS = ( + "-bundle", + "-twolevel_namespace", + ); + OTHER_REZFLAGS = ""; + PRODUCT_NAME = ejabberd; + SECTORDER_FLAGS = ""; + WARNING_CFLAGS = ( + "-Wmost", + "-Wno-four-char-constants", + "-Wno-unknown-pragmas", + ); + WRAPPER_EXTENSION = prefPane; + ZERO_LINK = NO; + }; + name = Deployment; + }; + 83231E650B57EBEA006434E8 /* Default */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = ( + ppc, + i386, + ); + FRAMEWORK_SEARCH_PATHS = ""; + GCC_ENABLE_TRIGRAPHS = NO; + GCC_GENERATE_DEBUGGING_SYMBOLS = NO; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = ejabberd_Prefix.pch; + GCC_WARN_ABOUT_MISSING_PROTOTYPES = NO; + GCC_WARN_FOUR_CHARACTER_CONSTANTS = NO; + GCC_WARN_UNKNOWN_PRAGMAS = NO; + HEADER_SEARCH_PATHS = ""; + INFOPLIST_FILE = Info.plist; + INSTALL_PATH = "$(HOME)/Library/PreferencePanes"; + LIBRARY_SEARCH_PATHS = ""; + LIBRARY_STYLE = Bundle; + OTHER_CFLAGS = ""; + OTHER_LDFLAGS = ( + "-bundle", + "-twolevel_namespace", + ); + OTHER_REZFLAGS = ""; + PRODUCT_NAME = ejabberd; + SECTORDER_FLAGS = ""; + WARNING_CFLAGS = ( + "-Wmost", + "-Wno-four-char-constants", + "-Wno-unknown-pragmas", + ); + WRAPPER_EXTENSION = prefPane; + }; + name = Default; + }; + 83231E670B57EBEA006434E8 /* Development */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = ( + ppc, + i386, + ); + SDKROOT = /Developer/SDKs/MacOSX10.4u.sdk; + }; + name = Development; + }; + 83231E680B57EBEA006434E8 /* Deployment */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = ( + ppc, + i386, + ); + SDKROOT = /Developer/SDKs/MacOSX10.4u.sdk; + }; + name = Deployment; + }; + 83231E690B57EBEA006434E8 /* Default */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = ( + ppc, + i386, + ); + SDKROOT = /Developer/SDKs/MacOSX10.4u.sdk; + }; + name = Default; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 83231E620B57EBEA006434E8 /* Build configuration list for PBXNativeTarget "ejabberd" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 83231E630B57EBEA006434E8 /* Development */, + 83231E640B57EBEA006434E8 /* Deployment */, + 83231E650B57EBEA006434E8 /* Default */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Deployment; + }; + 83231E660B57EBEA006434E8 /* Build configuration list for PBXProject "ejabberd" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 83231E670B57EBEA006434E8 /* Development */, + 83231E680B57EBEA006434E8 /* Deployment */, + 83231E690B57EBEA006434E8 /* Default */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Deployment; + }; +/* End XCConfigurationList section */ + }; + rootObject = 089C1669FE841209C02AAC07 /* Project object */; +} diff --git a/ejabberdPrefs/ejabberdController.h b/ejabberdPrefs/ejabberdController.h new file mode 100644 index 0000000..c42d1de --- /dev/null +++ b/ejabberdPrefs/ejabberdController.h @@ -0,0 +1,34 @@ +// +// ejabberdController.h +// ejabberd preference pane +// +// Created on Wed Apr 19 2006. +// Copyright (c) 2006-2009 ProcessOne. +// + +#import + +@interface ejabberdController : NSObject +{ + IBOutlet NSTextField *status; + //IBOutlet NSImageView *image; + IBOutlet NSImageView *imagestarted; + IBOutlet NSImageView *imagestopped; + IBOutlet NSProgressIndicator *actionProgress; + IBOutlet NSButton *startStopButton; + IBOutlet NSButton *automaticBox; + BOOL started; + char startscript[128]; + char stopscript[128]; + char statusscript[128]; + char waitstartedscript[128]; + char waitstoppedscript[128]; +} +- (void)setStarted:(BOOL)flag; +- (BOOL)isStarted; +- (void)getRunningStatus; +- (void)waitRunningStatus; +- (void)updateRunningStatus; +- (IBAction)startStopAction:(id)sender; +- (IBAction)automaticStartAction:(id)sender; +@end diff --git a/ejabberdPrefs/ejabberdController.m b/ejabberdPrefs/ejabberdController.m new file mode 100644 index 0000000..ca474f3 --- /dev/null +++ b/ejabberdPrefs/ejabberdController.m @@ -0,0 +1,125 @@ +// +// ejabberdController.m +// ejabberd preference pane +// +// Created on Wed Apr 19 2006. +// Copyright (c) 2006-2009 ProcessOne. +// + + +#import "ejabberdController.h" +#include +#include +#include + +@implementation ejabberdController + +- (id) init +{ + if((self = [super init])) { + [self performSelector:@selector(getRunningStatus) withObject:self afterDelay:0.5]; + /* + [automaticBox setState:1]; + [automaticBox setNeedsDisplay]; + NSDictionary *defaultDefaults = [[NSDictionary alloc] initWithContentsOfFile: + [bundle pathForResource:@"ejabberdPath" ofType:@"plist"]]; + [[EjabberdPreferences preferences] registerDefaults:defaultDefaults]; + [defaultDefaults release]; + */ + //TODO: This is very dirty code. need rewrite by MacOS/ObjC coder. + FILE *file; + char conf[255]; + strcpy(conf, getenv("HOME")); + strcat(conf, "/Library/PreferencePanes/ejabberd.prefPane/Contents/Resources/config.txt"); + file = fopen(conf,"r"); + char path[255], *ptr; + memset(path, 0, 128); + fgets(path, 127, file); + for(ptr=path; *ptr; ptr++) + { + if(*ptr==10) *ptr=0; + if(*ptr==13) *ptr=0; + } + fclose(file); + sprintf(startscript, "%s/bin/ejabberdctl start", path); + sprintf(stopscript, "%s/bin/ejabberdctl stop", path); + sprintf(statusscript, "%s/bin/ejabberdctl status", path); + sprintf(waitstartedscript, "%s/bin/ejabberdctl started", path); + sprintf(waitstoppedscript, "%s/bin/ejabberdctl stopped", path); + } + return self; +} + +- (void) setStarted:(BOOL)flag +{ + started = flag; +} + +- (BOOL) isStarted +{ + return started; +} + +- (void) getRunningStatus +{ + [actionProgress startAnimation:self]; + started = (system(statusscript) == 0); + [self updateRunningStatus]; +} + +- (void) waitRunningStatus +{ + [actionProgress startAnimation:self]; + system(started?waitstartedscript:waitstoppedscript); + [self getRunningStatus]; +} + +- (void) updateRunningStatus +{ + //[startStopButton setTitle:isStarted + // ? NSLocalizedStringFromTableInBundle(@"Stop ejabberd",nil,bundle,@"") + // : NSLocalizedStringFromTableInBundle(@"Start ejabberd",nil,bundle,@"")]; + if(started) + { + [startStopButton setTitle:@"Stop ejabberd"]; + [status setStringValue:@"ejabberd is started."]; + [imagestarted setHidden:NO]; + [imagestopped setHidden:YES]; + } else { + [startStopButton setTitle:@"Start ejabberd"]; + [status setStringValue:@"ejabberd is stopped."]; + [imagestarted setHidden:YES]; + [imagestopped setHidden:NO]; + } + [actionProgress stopAnimation:self]; +} + +-(IBAction)startStopAction:(id)sender +{ + [actionProgress startAnimation:self]; + if(started) + { + [status setStringValue:@"Stopping ejabberd..."]; + started = !(system(stopscript) == 0); + } else { + [status setStringValue:@"Starting ejabberd..."]; + started = (system(startscript) == 0); + } + [self performSelector:@selector(waitRunningStatus) withObject:self afterDelay:0.5]; +} + +-(IBAction)automaticStartAction:(id)sender +{ + //TODO: implement autostart + if([automaticBox state]) + { + system("mkdir -p ~/Library/LaunchDaemons"); + system("cp /Library/PreferencePanes/ejabberd.prefPane/Contents/Resources/ejabberd.plist ~/Library/LaunchDaemons"); + system("launchctl load ~/Library/LaunchDaemons/ejabberd.plist"); + } else { + system("launchctl unload ~/Library/LaunchDaemons/ejabberd.plist"); + system("rm ~/Library/LaunchDaemons/ejabberd.plist"); + } +} + +@end diff --git a/ejabberdPrefs/ejabberdPref.h b/ejabberdPrefs/ejabberdPref.h new file mode 100644 index 0000000..59f6627 --- /dev/null +++ b/ejabberdPrefs/ejabberdPref.h @@ -0,0 +1,19 @@ +// +// ejabberdPref.h +// ejabberd preference pane +// +// Created on Wed Apr 19 2006. +// Copyright (c) 2006-2009 ProcessOne. +// + +#import + + +@interface ejabberdPref : NSPreferencePane +{ + +} + +- (void) mainViewDidLoad; + +@end diff --git a/ejabberdPrefs/ejabberdPref.m b/ejabberdPrefs/ejabberdPref.m new file mode 100644 index 0000000..ce7e168 --- /dev/null +++ b/ejabberdPrefs/ejabberdPref.m @@ -0,0 +1,19 @@ +// +// ejabberdPref.m +// ejabberd preference pane +// +// Created on Wed Apr 19 2006. +// Copyright (c) 2006-2009 ProcessOne. +// + +#import "ejabberdPref.h" + + +@implementation ejabberdPref + +- (void) mainViewDidLoad +{ + //ejabberdPref->ejabberdController +} + +@end diff --git a/ejabberdPrefs/ejabberdPref.tiff b/ejabberdPrefs/ejabberdPref.tiff new file mode 100644 index 0000000000000000000000000000000000000000..ea838cb3b694e3249e0432ac029d968366c86e90 GIT binary patch literal 1086 zcmebEWzb?^VEV?uynvB`nSp_U5r`QUFfp(|*$NAo5p0lL1A{_X9% zS^f@}q(F-$VY=cyZpnc=o|M_jPw+?yvQ(Mo%frIz#=syUqsUsdQ$nbKVMPw30@t%A z3_lhy>9aOF2>p;UxbatGZ(IU}zLBLVf56m}D9b~YL*=*T!j#GE~q+U0sg_+jHxJ|_Q92aI+^>sI}bP(81InOAG3=z|6$ON*!F&Wn3W+Ss28 z@Yb(tDWAusV*TUMf$;r14fpf+S)FQNd+8_9z<%>dff8?^P? z?O2$(so1&l$snLGF)-C61dQy-Kze8 zncu=+$*pvz3I{`}q)w8UzodiWy&p>Xhre*^J!C0qai935QNY7fgqKCymBFdS-T9}} zceM_c&GHNT-#AS^vR2!#$!D`=y~%>&m~H>ndWAhcD-^G}Xu}|UxxBfY>OnAH27K; zu!UclvgLb-ib*U(JFnqEI|cn$UG6~(LryuB&0=X_*nXH}YtxH}9wvoD8(t&}9bLHE z=?ZIeb`S?EbKWLydcBKZO8HG)`m!U5iGe``n9L!m zT>+TnVJRQPW&(=?Q$OWqEI$aH=`JY4P-Jf zio?`19cGkb0GbBMWz1g~rQz&9j50{(Xh7K@y9^nCB!e6S0}BhI5tuCkWGgY+BB=>N z(i;qB1FdB-YtMw)%fJCNg8@i_q5JQ%;A{?~9aHj?QWXL+@=NlIGx7@*oP&)Od=ry1 f^FVwCBLf2qD?=kILvsZq11keVD + #import +#endif diff --git a/ejabberdPrefs/instance_started.png b/ejabberdPrefs/instance_started.png new file mode 100644 index 0000000000000000000000000000000000000000..06da6a27b9a61805d24193b839f9509f21cbd446 GIT binary patch literal 4422 zcmV-M5xMS(P)V%OPuH!)0xB= zO>E*4M3@*85k$oWO(F_{7(sRgl(5N;Kmk=$)tqy=44rZh!~P8RcM}p4E|is(mAAFEwc?Gsj*gDU z6%`c^PM$n@8Lz*G{WGI`qzeuX`4P{%1_dootSEn| zD=a9{eY|^*&eL=LXL#NMyY&CVcvI|~4j(?8s>G%<(6m~u=HS7DH)p!fESNXX;|4xA z2fL9>Ci6TVpQyc=bxU_4?XvE`{;zfC&Zg;Bu3TApHuZd8N%1{h`sJ(aT5(aCZfW4s zM2SS=Z*On^Doy3v!FTVI<&{$t^@!}3x7hC*p+2^-kQXCdU}-GMuI?)2j781LBe!TB?( zDSyG&)^a$F+4w;8_|~YXsLRV^x!ec@+1ul zR0@c7C@U+4_V#udH*OsG`Yr+^BO}Poy#*(K_&z@|F<}$FwqUe)5RA9P{swwz2M%T; zT

IKuJdtcv^dnI)SZQ-v+-${ur$G%s`n;3Q{y)CY7P_GG;u52K}Z<$>nlTD!ahU z%$(id+}s2uB_;6evqA99q1fu(yLY{Yll=)FQjIhLGM+Ft2ZM8&*l=oUtr}j=35T}} z-h#TGT7$-ihJ--I^=s^y3}hXD$S}ch;TjD}t6*Yc3K%b@2WWV80 z?vY3&BeDg-Cp|qKDk{q1=bwL~zpciPAO8lYX(L0%+fJA;!OoCp$>=XDK4+7l$qa3l zmX-(uC8*Wvej^3%OGb3sz(iGQ6=0x)m9-U=qwzCm&VrEOPr*zWH zjKDXBM(`mR?J3(e>>34UXJ^L%WQH0#2vS2P)!W+(&#zns&#eeXX0ii~Mgx{sR`Q65 z^-+r!`F(@8IB-TB)a2MT_^<{CmyKq;lZi8Io&GL_Ij$OXo;=>&-OX4-CX?fDIl@LZ z2p%J?f`kV$O@N?A!F=xASujU`j{3Ge z<95d5lwFm|Zg6mLfS4^?Szyux2AGJ)hhVrupPOVv16F6 zWATY9>kq%-Z8k@Qh;5RaFz>MkB z;q}*}6K?-{I|ffRQ_u<-mRcwFqZ>DF%-XqgX9+F0p+=aDiHTu`zx>jA#OFk?wzg&- zy?7z*=gXHb{d;|V-FBSdaUA3lK5_+nN_O_mkB~L`6bj=3rqQce&~UQh!i6*p;8ctu zC7e8YD)O0UmVCu)3mNE1y*A^=jsMBt-+%6!wQC?aI2f0C0$Wx}-Kgi`yZ`+TPA8wn zK_n2lK9VgnwUW{jO>FGJuTl5^z>5~@9bBJixJFTI-n=DuYs}j-iBSZ+i#svSty?*8 z_G~Kj;!2vCnL@|o4p_hb6=-U1>WGfsv;f~zgOlu%Gb%JOd3x&9X+g*cKFgLab0?1e z@rP5ed-rbirj`W~1z>Nl3Y?srVC|Z4nCIaEy?ll4-Mgndc<`&w5to#HAJO9Q%4?`H z)KpbHEJJM194YIP=ZlJq82rqUtw;lrz)MQ{5ll=?N$Xe(?quwoW7ou#D} zNEiqApy!=&HTqa4Bq93kw?|4JJ*to66xLwC@O~X+VQDLV|IlWH`k%NSVF+PiE8%)Z z2I>5}NCNS=V#GLY9EvPp<=3wL(pveTa&QV4=@B|sc6G6&LAihiCoNN3R|{!r=My-- zn>bXo24Y>|Hg)O@Lj?vgO+diY;Qo|5G++*)*g(a401dy&dA^?0B}g$4b518Gt$6?a z?dcC5KF|@qhp9J7f9A7BtgYP7Cj$$~i)+|g^E=0310(58s#P_zg zwya|e!n$=YM00K5!X2a$JWm7OMI+v$NZ08RQ9=Tt>q0hEsnl>2D{U^8sH;cI4YOy@ zUN>vzEV)Y6JEBacF$j{6F_x-ps=*!c|Bp}p$7kENzbD|TFX8%LB4``3SL5R1@;nBH zM61yZld{5clK1)6IeM*b<9;6_YBuhua|BpKwO{rmR;COD!+ z5b{mXv&-SHZ~U9f-TeG_&YnxX#S@iUYGGmd+?qAv8QZtN|DOZ}dcm-z#2>;tDMq?Z z10kQ&WEwgCMy>Xd6n2D#hB=|Q(jTbkaVpG7z)`EIsbS9Avu8KewG8_emE~2ODw#}` zhp!EfU%h&b9a0@jMEaxwd7BiX3VFV@tre`Wa-wXC=li(f2EC4nZ0$!M?fRRMk%Ac{ zhNX~E>HPis_h8wwAizwDGGi5$i?7 zIjO@!Tv8g4SO+06ZeVdnXy)eT%Mr6jkv$}WBp!)nh|WWC z>gwO#zk2mb6v8Nj#}`{^DHgni$BxB8T->)T36Uz)^L7+seK6U@h2bzSFAr>NY>4yc za-E@%sxPauC%qb z#^DPr4S9zKL!;rh>S~xiZN_AzzfYQ)nlBg|E7W$jcGJAPycaKDzG9Jwho^*MNbtCT zZ86SCv(UsaLo@A8lR)|fj6+uBJa|yK@$TKblvOJ@73&7qzdV0q$56@#0Fxdn>_nq*&}`bmi`8Q!I`exxOxIt6+8cc7}O zipA7?&-v*6K$b_eS`Ea0^UtcJq*E*S1XSWUAyC-83%8#1-hiurw)icJ`6Tk1w}+hzn)l&iGg;lyuG}Urj|3$4!{Pj zBX6-y)FCkHKOVfnD%>aDrxa5$;k&4)kSSRza%d9GEzQbLKmFwI$S@^%%M?xq*Z8(~ z<51Wz;I+56C-2+0_Y*7vI?3x;sxjJ>>tUV&wt%6KdP1xHtOA2lfWlb8U_}n0_i7N& zv?oI{AR=NtGk)*h&%Q;&bEv+-PKC{PoE}^BX(z$D9%P1{X!OOJn(7!Vlvmi=*~y44 zgbgERv}Ew!IGuWOtQ#C7{BQC&dASn3ix6OXJt88KeQZcgJp;u>MbOgR469bHM&UNc z7E8c(?q#KjP&E1yp=8Lur73#FA5-}mtY#E6ctb-2+qt3`pp7Ik1 z2m^$5^q@*OX@^Q)(S9Yxp#hkmlI|K$UgOL>gsEIQ_dv+12KP{OMq@enu#`@C z>BJ<}RJ5bv_pn!TkGBe2dWHv>@}k4w*A%F4>}P)CP%b#-o?GiUA;@+28RI|=jV zc`#`}I=!a02AmOMq&j`j@A7K5Z3R2;UWWS8HDWQ#@k& zDC(W(@&0HhCnw9_s;W8)iwcwR*)Q;Y_qn(Kr7z%POy(@|6+Gj4Bb{#c^IIGc8X9JC z^yuM>$Qp%c*ekdy_LO)c*T2HPQ*GGz%2rHD>D<8CSQRbl?&;3O&J?3L&i%u z{$*U7EBJ3C?s?&wg3}nu=4r?@UTEB1UJzTLhhM_DScLgNV`^&h8@ByCL$-1UnDh7b za-+L>0@84}Xn3Ka*3&iO5>PPSgm+WkP53`VNDV+qJP{c;rcX36;qyx;pO`>CQp5rlN{{I9R08K1m(1$_^%m4rY M07*qoM6N<$g46zuWB>pF literal 0 HcmV?d00001 diff --git a/ejabberdPrefs/instance_stopped.png b/ejabberdPrefs/instance_stopped.png new file mode 100644 index 0000000000000000000000000000000000000000..232d4f81824da64409b957204bcf38a27e60bdc3 GIT binary patch literal 4339 zcmVw7WAq|ch>t8J60X_^?T zi8X4pi6&|koQ;Ym5#s>bf`EWBh|DuG50}$_?YoaT7cWSXZoc(#xo28??SK92U;AJn z%cCDG9QFik3hYT(C~O4G8P))6wREAUr`O`yvuD2*GIreS6&2;aO-)Ug@pTu>d+ywM zpZR$EPAn}g(Ho7%0(_^?j~t5f@TCUn>~Ht(-TPHePEKxJU0p3sR2mu@?&sy@6&yTx z@Enf+4mKNR3)5+?w^{Y_EBV=3ca+no&nnxu?@;E2q>hw8f>z0p{lP69q3l=QMKXLq2XV$ITO5(W-;@GXs9A)~n>HBm#-6T&> z&*ypKKV%~8yg?K3|JuKQ|8k7k))>8AhzIJz!^3??j0p59FDoDH?%}bxtgN&lDr(|0 zF)@orVQyq^Zzmqm*w{!@r%rbrJlIdqe-{@QQ+s+q zhUesDe~a5UNtpFI)~#FjV9BQ;;;ZB0;^zI(<%^F`pu>mbY1-6j6c7+VR#w)S9HL<` zkgct){98wzot>oD>*07i!AYvGuP0YmSDG*(k~mh^ucy$#FAt>c+qd^Q+%_H7*;`I< zAlP{)*o&>Lt$)PB?0yXXRPN=^Q5e-tf;xU ziTd?(5a(A{S5a107Crf7G=02dd(ry!Z@qxYevOOTdzt_{&ln3va?UVw98nSB>&f`I zX3U#Amy#}BR3T+D{IFxHNPPdR7k)kH%{SjZ;OOWW zgG=nX5rWxZ=Z8l{MFrAN7!l(m5V>2@ag&oK$IbGjN48-2Bqk77oyL!!K+Z1Clndua zjtru?bLP?W&&5R{q5sqyomz20>c`;wiG!1~v9SR+vlo!*?x-20x=pIBt&N^q@U-}z zp@afPPR`C&OO`B+JAXdW^~#mYzrkshFk^S;jkwr|hZ}yZ;~eppmKMPp27?uTTLCr( zGkEm03Obp{JOPFpC-cdZC&&Tubaip1dGqGSU{$-)(o%no)9-61z{^nuj;;JDQP0oM zr*=Ht+1Z6^YHO&dsF=#j%f+Oys?=lI=r$ofj~#DpYNSDf29t+}2cbSt=(x~WOl+={ zEjn%F^U~APv;F=32mV;+j~zQk%)0Nr_bydbR!|cj-qocL7=tDN&kGmEkdKd#SRF0i zyF-g3&nr+_SxLS=^l5`_t8H zS5VAyn0>l7YLVf|XuyDhA|XxmZ(a}Bd8SQ2zrhp{J|6O6C}m`1 zbltdd{adL{tE8G$SRSZJdhgV!Q|`lu5APow9PHhf39#cEHf*HNKi@}BJ+%;8#-6N1 z!WsyYw-pafD!5y401)`}{{8!hFo82rrOq*nVu!HyQ8BOA>CIS(uXk{86!&v*aG*}3 zQUB8at&B`bPIf6RD@~NJF>{{FaBf0ELI50hij0i(;H>XyFecZoT}!CU^vho?B_D4e zvaz)x$Ywfq^29eO*Hbn^knKk#Q*e3~><<4fDJi)%<%uaveSHV%P0=@ZTsr~C6%?*> zu(LEecnqyr@vAYZss9SNd$-^W#}3Xb-TBK6c>8+Nq)DR}#Vn#ZbLL=~2Z&|m(v8T` z7oUGY2}cqDeH|@Zx=buHkF4x0_KEedPWTZ%IkbyBujbgKM<*MteUwL&TGm1)+ zT4TmZNlB&?CyrAaL>$+RhWib)bm?-cs;+8SvEn&yl8QmWO(ML=>L&sMhDC!BB4$jV z5y%{U=*z>je*Jpjvx{tOY=n?%YilQOZ*RiV)0nYisjb!2Ox?bHTiLel!}ozpPO5TQ zP?{_#Lt|m#-5jjQ$eyY$*M-bmnWADlfCN|rvA~OuKSXw{vq2Z?{n>e|MGIqg#l|ik z#upm7!Xe7fJoBuGcx!7560TOPKv`KCA+d$APRE@QmHV`1>)$?u<419?Qt5ni51L$o zByP0S!l|PL&qI?VD(;S*A5$&rhNp)oHQ;&2PaL~wR$(zh!owqkEuMh`bM0qf=Bu7NWa%@5(MOFWZAtE2K5|)=W4#I=S~o2-eX5%=LgF&7Z%3E+r+g z&cBHs;8Uy^Gftg^qTqR}ixO&V~Gc+LJv0hs{BHB5=67gToc+}S*QpM+CtVU3S!->_OiP>_C1v(y80cmmA#?(Xiw zkb|^%@y}OCZC@h;WK<*154;H{e#e=vSP)Ta0pa6n8E1^$Bx(Z{6&Fz;@c);0{u;4n&Dyr`#S{u=%K{GLK$=(>!? zhor2!oE&}AXFXRZSJ@>T&`+51fg_0_TOtp%Q ziT!lp!bKjSI#Gx?!j^Ez3b7qLUt3oT@#Q4+3BK=?j`tdM6tb8<|M{)|wYIhq4ryU2 zb^)ZGzjNm{&6p8QQ>IJ>P41wxXU~v5cz*BR&j<}k2c&n3M2bilJ)$ih%uZW!{6W$J z=U!f3lz}DX2Qupb0zI!_<4~^hwdsyu5N1MMp?*4&gCUr#9~(YgN>)~UTHT=+spaGAO9kkhDk>^Caouj-ym?S+mgAMq77xT-(!f>1TT8=rhYOJ*K3jpnjZWD&C3j({O5uq{(JgaMc49p9vEt(7mD}8b0i?!Jxl)s;a8LvbDA8@NoAS78)8h zapuffkz>b(=s1T|$5m`=an6o&;-f+f?aq?`{Sxv}G&uzY`OhLaTvhXAa1`_YSJsb^ zu^}Hp@jVY#5x{3TV31P5WCqb4F*f;_9Vm~nsR&553%EH#hjT9QBs)7hzHhND$X@Qc z#CGA~!<3r#FDfi76gf36WE@SMI!)9O*8ST*{(E73{NY)00`1y7;mGSpj~=u6rI%iQ zfvM6&z{*1#LyD<KIJ{LG@rPjJ;^MLdoyxuv6b9b4 z3JVPbP0bXMeE=JH$I%j-sNG;>`fR?zs-CCar_@q0<2y4mLnv8pa(EKeHPy}UzWdHv zFbsEJL*#DRMVYXb-C?6Q?W@)w*}QqvJ7@wLIqGR?se(2wwSwwODAXNN-_xohjLb>^ zw}}iZ4hY}Z2t4zi49kEeOO^`fH*I=vHylsq{szX4%@>j$Ybd7ecrNyQ2lHmr-uk5Q@=KA=Gu-bhSrRCZUucLpNi?4`)8-oh!}(-bm6i zjzP;n0#YhmzK0RFajaQ3eegaF957HwX}&MFcvd){w?gXc>qH=`YpUvY?b`V?{J0?{ zcCR_3t&A)an*IHUMk8^AsK0*WI-xA1WS>%tl38)lX;y-N_&7TU&KIB6g08yfD5Z9=ZtwKcUaxc4B*ERWoOc}UElEN3EdLLrp=QGIC;miK;r zJ@228KMINJ3{#@5;Ch2>G^|Chd-mKJI(qboXdj0R@fXMHAm#!C18Lf{>C^@-pOK!? zm64Ht>00u&x3Y7xt9`tE#+H_qv>iQmc^W&+<>dJc=^g@%FM_R$@bgV{-58yiq+sm=a&r0$;r$dEQ7@axvAiK;DJUO z%|7G52;JK{ZXGKtE6?xTwIdPFwMTJFmF;A6U*6GLl97B-JGXa;&UY0R7zbuQ3TS0z9hS0TZ*HAU@3g}%*x=8>0=M5pb zN1!*&&CRVpe<6{Xq==h#IDQ+JFC$*7Zs|ShZD3E^VjWU?ew&k%lL{Rj+tk#!IyiXr z5RN1}z&iQ6g*n$0D#24Nxd>dU+t)0F7D6p1o`pugSXU=|g9DM(sK&LCCq9#tBH*dbkZDH~!(D9T?T zFCtM7jP~|+_b|4|3Rx=y;2`hUCY^4P1;{AjVw9`hVZn^r)lr?dlV&f?PX0qgPduyZ zHW7n#rc*O9_4P}moR~^`s@Uo3156a@bgR6+MUx|){67#n&D#cpjEc3KgpK@L8+rBN z`fi^U4ZN&1?@l{4yTsaFSJ7CP=C>MoHTvz+`3}trDcxbym!XB2&cb(=@}$c`G|57O hDnAtW?EJq47yx#HKVAp&V~PL(002ovPDHLkV1f!Cgh>DZ literal 0 HcmV?d00001 diff --git a/ejabberdPrefs/logo_ejabberd.png b/ejabberdPrefs/logo_ejabberd.png new file mode 100644 index 0000000000000000000000000000000000000000..edd9a536cd0aaeec9aa18a7a57f6e10b3123f8ef GIT binary patch literal 4501 zcmdT|_ct5-_m9~Knpln2-m55DD;g2IFTNSacF+|N003Y&Hqx^I0O;VfJsQYN zn*&#>BWVjufRSA=0Km%i-=G6Lf58g?a2Oiv>D&#c+j2q$5sWxL!5$GQENqHWP1i8= zx>6tNpTGW>b!3-Cy!dkv8bJxNt~3*-GX7&|(tkA*-udB>t31M-sjycZF^{6}8cgAq z-N<{g|8~b#m~Qo`ySrN@6#}70x)=dZm0xwfpnt6^JAL--39h1u$>1yz!L2k=hx4sfpj7}`4HY=1MuJTw)KVV z5^XK5e5^x{&6SU?YP`cpz>eUfX!BWyI+cCsDBARSgAdNNs%XKQ$wH$P`m2>1sW2N2hs{$56ju@GCaok7j1=rrO_9seI=G38pu3|=Mg zZU_7Vtl9mgQ#J}Gmg^h)&qa)Oy9mpyX{5uv8ef|F3M9os0Q488$PzHE+{~Lwdy*nZ zI^(RxFdw^k%21X-;xUvSS*F2gPs>v<%xY#S6Z9Cv-qk8=+qiXKe6Fqm<~{P4A75Fb zog2D1bqho%0${Wk!@C#s4^DaOymA{~Q1G|=a4zx|!yI_6CK?!4xo|mVAo_v?cH#`^ zcn1{MEjj`s$5H6n7UUjU1gNu)FW?%g2DVm-y6q68fm2G5IqA>Dy9eDzoVgF1c2egZ5YN`?wqr<#FrqlBt#S< zs;ISY&T@dmU50ter7Plib=7muLC>_8;<&j^jM|Z)nZb_Tsi59rTTn_Ut~{^*fW#|i z>V(|Bl96ZImcTqx-vM0XJ!Wq~pyl{C29NE04%Fg5cBucT$4P8H3Yye_>wj=W@eh7s zeeqzer*1Eg)O+!qACkax{V^x>_};Oxu^8?(z0MgxR>#|6Igu;3En&|tnj}FMeHTI6|V5Wm$N*p)OYz=1-E8nM7d+)f_&dE4HM(sK^zj*H>E1qxx)^w;}u0=R4ia?TMVjm@duI*#0ein#Phi5G(&prg7@sVLJtE zpIqpsX5Ql`WY^1Nlm*8l8QwerUUn?PCtiCy6t`WavNqk5oD&de^#r9V9)WB|Xw+JI z*}R^yosjD3;Txg%Ch?;Y=4HdKF^R|NnTnQ$qT7(JaEsJz^<;cRMtW^C)mdb?PaP>Z zp!m5qGeV+=4VhH%rk58j(QyL5N}6!1alAf5S@Q zdA~|7cdC>CSU0UaSemS9obR@Hx|B}fK^KBhlQODW_*m1|IN)9r)7WrBrm47Op4bqu3;I2(M+(YYL+)$4v@fm?k|V$%G5 z=uIBN?wajt$E*w#$P)@$CfZK^h9fP0=)t537?U*? z$F8-SeSG+k#2|X>`yzE<^5h$g!n0MKsEHb{UA)=ZR0$(AX!mnb%=eer=ZjA+ON@#u zqVMsx8fSe8z|A$D=BsGh~W^C0B+oBy&Igq}{I%FZ0Q-pRJLy`KJ^=~abhX>xD zZ{O$EH{Z2f2yxx?9phLSmHVCQT(6C@+ZcGWFE|+o8^CB4o?v6_b72c%Lg!VL=UtP8 zW%4pLfkl!TADpMgTcwrhGU1?ZBDi3=E%I@ZDn01USR9W&5DZc7n>!zeEG({RU=Qi)^auidd|w!i}o1X z2emf&Js0+UZDP&a-;wh8$d(tR-J}_I9n5d}t zqB>6uUQag>5LAKe%+~y(r?UN*qX`*f2L%%a-@y>6Pic%+SDtw75v*s=wCjCuMBKU@ zlIl-)O`()in5mDG=Z#x+&zCam9FHdkNd$MIVu7kBv3h(5%k|2zWY#&Cn2Gh=IwBr8 zE++W4{yc6bzWa!Y=xH|n6LmBi8WW8!HklBG6Q)G=)EUDzay=Tvz-mj2dTpamM4v`8-3vRaL8hWuVJu=QC3XafPnj zng#}6t3}`0A)RL3OeqpExR9ZO!%<_EuFqPH)OX{Ls!gK?VEZ*EsyiU^uEJrH3oKH` zHHPl$YUR)Sp`O7L_x*ns6V%rR<{f5FLQw>?Gg`GHJ@^Gy!Hi2gEBP^7fWchh~h2 zPk-kKiI4g^=XR)JQhLW-cJ4O|gx`4$8OXk1B)|Qr}W(U4FU0Cq84;MALS@ojMVf9%-WAVd9 zULPp;w?BoUvtorUmxmRy*(_BwocSV4kB)P`9sXJA>#03K=F&bKhc(hLS<^jCjLNQ@ z@<6uWv*#VJoBwEUx84%i-lj{>|=E$d6 zp4x&NehT9=|GZ}9Kbk8HZ9Dz*_XW`8TxGGjX@;-yY*%Alp>Z9jk`kxLudNL*JNuSd zADK859k9z->_1LWRTtF0bU=;EWM%7~7ttWP#GXYTK9hCSKAtvBPTH>L}>#Eztyv(Q3nsFlnYx0TrfZpC0E8us?2cP|mnEE2?vDmMfEHuxl%dq-j)jPNE zPt~`a`JW?c8elM3i()n{3^%)L%ba=z=X0v8UnJLMj zqA2Sr@wI_D&?g$Qxs>?|cEOG2Vv@wDPHHH}dQccSyn@GXB{Y2Vfqhe^nEpMu;*UNn zUpFj47;fMo|4YN(?|X9!_>D_rSP}!&K}JKA+zZUl(@^9I`1zI86O+6gAZVv)k!sc6K6e zbhDbZzP;_CdBK|j$Mru}Y6MOVdg&jhKM5{LGR`=ac)0QcTtY=W5<#>iU*d^B22wdv z6p=*q{nESohR>~rw~XRT^-VWbHz34eM0SAG%&Jm#YEu+3!n?z~Bp_tq22N-hrX1xM z`N4WM{XuPnw;Q%7Yc%3vE2=<2X(d4tzZiKlw*+PvYCZ(&T@h|&nr1RG&%t-qItBWR z9&fk6ZJ9S61O=}!^65(n(BX|g4N z<^b;wg|hSvi9nvGG%@_o!fPYv##rXJUmTOsj2Xkv)2Q|roCPQxbR1|QPkGosb1CbC zP~vTSATaI*y(s?1r886Cjsf?i&KXJ=BFzz4pB|fnbBZb=lLY0b4|=5-sFitJHnHRg z%iK~unh_i0l^A0k1EH^P6JjItHzMancO99GEZ=8S>IrpkzW6c4b;6r}fSw1fi|x#* zAQgS{X8sf>P%bqf8fXfl2)Jqy#+9XDPlSvsC_7Y~icy{z;nK&d3R_ZQ6+&9oO~tX$Fs5bFLbPTC1w`WmSBdJRc^zS- zVI*(oga7A01!pt%+J(li7d;KbCH*y-7I!-m(KAuKX7Pb$dp(ZE5DWK`E-eISk)4=| Xt(u%2)T9cn&jK*kH`A*@y2Sq<9vW@w literal 0 HcmV?d00001 diff --git a/ejabberdPrefs/version.plist b/ejabberdPrefs/version.plist new file mode 100644 index 0000000..c12b708 --- /dev/null +++ b/ejabberdPrefs/version.plist @@ -0,0 +1,16 @@ + + + + + BuildVersion + 54 + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1.0 + ProjectName + PreferencePaneTemplate + SourceVersion + 120000 + + diff --git a/ejabberd_xmlrpc/COPYING b/ejabberd_xmlrpc/COPYING new file mode 100644 index 0000000..8b325f5 --- /dev/null +++ b/ejabberd_xmlrpc/COPYING @@ -0,0 +1,343 @@ +As a special exception, the authors give permission to link this program +with the OpenSSL library and distribute the resulting binary. + + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Library General +Public License instead of this License. diff --git a/ejabberd_xmlrpc/ChangeLog b/ejabberd_xmlrpc/ChangeLog new file mode 100644 index 0000000..f7a0342 --- /dev/null +++ b/ejabberd_xmlrpc/ChangeLog @@ -0,0 +1,130 @@ +2009-09-11 Badlop + + * src/ejabberd_xmlrpc.erl: Update to work with ejabberd 2.1.0 + +2009-04-28 Badlop + + * src/ejabberd_xmlrpc.erl: Improve handling of errors in arguments + +2009-04-17 Badlop + + * src/ejabberd_xmlrpc.erl: Change access_commands to use the new + AccessCommands feature of ejabberd. Syntax is similar (EJAB-910) + +2009-03-02 Badlop + + * src/ejabberd_xmlrpc.erl: Replace the listener option 'access' + with the much more powerful 'access_commands', that allows to + grant selective permission to execute commands with certain + arguments + * README.txt: Likewise + +2009-02-24 Badlop + + * src/ejabberd_xmlrpc.erl: Update to work with recent ejabberd + trunk SVN (thanks to Artem Yurov) + * README.txt: Likewise + +2008-10-21 Badlop + + * README.txt: Updated PHP example + +2008-10-12 Badlop + + * src/ejabberd_xmlrpc.erl: Major rewrite of mod_xmlrpc to be an + ejabberd independent listener, and to be a frontend of ejabberd + commands (EJAB-694) + +2008-08-31 Badlop + + * README.txt: Added Java example client and fixed the PHP + example (thanks to Calder) + +2008-07-24 Badlop + + * src/mod_xmlrpc.erl: Fixed typo in get_roster + +2008-07-08 Badlop + + * src/mod_xmlrpc.erl: Detect if mod_roster or mod_roster_odbc is + enabled. New call send_message (thanks to Darren Ferguson) + * README.txt: Likewise + +2008-05-20 Badlop + + * src/mod_xmlrpc.erl: New call get_roster; works only with + mod_roster_odbc + * README.txt: Likewise + +2008-05-19 Badlop + + * src/mod_xmlrpc.erl: New function check_account (thanks to + Zbyszek Żółkiewski) + * README.txt: Likewise + +2008-05-18 Badlop + + * README.txt: Added example client in PHP (thanks to Zbyszek + Żółkiewski) + + * src/mod_xmlrpc.erl: Convert from DOS to Unix line format + * README.txt: Likewise + * ChangeLog: Likewise + +2008-05-16 Badlop + + * src/mod_xmlrpc.erl: New calls muc_room_change_option and + muc_room_set_affiliation. Improved calls add_rosteritem and + delete_rosteritem: they push the changed roster item to connected + resources (thanks to Darren Ferguson). New call check_password. + * README.txt: Likewise + +2008-05-06 Badlop + + * src/mod_xmlrpc.erl: Added new calls delete_account, + delete_rosteritem, create_muc_room and destroy_muc_room (thanks to + Darren Ferguson) + * README.txt: Likewise + +2008-02-29 Badlop + + * src/mod_xmlrpc.erl: Added module options: maxsessions, timeout + * README.txt: Likewise + +2007-08-24 Badlop + + * README.txt: Removed requirement of Xmerl 0.20, since it is + included in Erlang/OTP. Instead, require updated XMLRPC-Erlang. + +2007-08-21 Badlop + + * README.txt: New Ruby example (thanks to Diddek). Patched xmlrpc + Erlang library for Ruby compatibility (thanks to Cstar). + + * mod_xmlrpc/*: Initial commit. + +Old Changelog: + +0.2.4 - 7 November 2006 + * Fixed a bug that required clients to provide attributes in a fixed order + +0.2.3 - 22 September 2006 + * New feature: bind to a specific IP address, requires the patched XML-RPC-Erlang-1.13-IP (thanks to Zeank) + +0.2.2 - 15 August 2006 + * Fixed small bug on resource_num (thanks to Flipkick) + +0.2.1 - 20 July 2006 + * Fixed small bug on add_rosteritem (thanks to Leonexis) + +0.2.0 - 16 April 2006 + * Added two new calls: num_resources and resouce_num + * Added support for vhosts + +0.1.2 - 28 December 2005 + * Now compatible with ejabberd 1.0.0 + * The XMLRPC server is started only once, not once for every virtual host + * Added comments for handlers. Every available handler must be explained + +0.1.0 - 4 April 2005 + * Initial version diff --git a/ejabberd_xmlrpc/Emakefile b/ejabberd_xmlrpc/Emakefile new file mode 100644 index 0000000..fb9dc10 --- /dev/null +++ b/ejabberd_xmlrpc/Emakefile @@ -0,0 +1,2 @@ +{'../ejabberd-dev/src/gen_mod', [{outdir, "../ejabberd-dev/ebin"},{i,"../ejabberd-dev/include"}]}. +{'src/ejabberd_xmlrpc', [{outdir, "ebin"},{i,"../ejabberd-dev/include"}]}. diff --git a/ejabberd_xmlrpc/README.txt b/ejabberd_xmlrpc/README.txt new file mode 100644 index 0000000..a1a07fd --- /dev/null +++ b/ejabberd_xmlrpc/README.txt @@ -0,0 +1,499 @@ + + ejabberd_xmlrpc - XML-RPC server + + Homepage: http://www.ejabberd.im/ejabberd_xmlrpc + Author: Badlop + + + DESCRIPTION + ----------- + +ejabberd_xmlrpc is an ejabberd listener that starts a XML-RPC server +and waits for external calls. + +ejabberd_xmlrpc implements some example calls that can be used to test +during the development of a new XML-RPC client. But most +importantly, ejabberd_xmlrpc is also a frontend to execute ejabberd +commands. This way a XML-RPC client can execute any ejabberd command. + +This allows external programs written in any language like websites or +administrative tools to communicate with ejabberd to get information +or to make changes without the need to know ejabberd internals. One +example usage is a corporate site in PHP that creates a Jabber user +every time a new user is created on the website. + +Some benefits of interfacing with the Jabber server by XML-RPC instead +of modifying directly the database are: + - external programs are more simple and easy to develop and debug + - can communicate with a server in a different machine, and even on Internet + + + REQUIREMENTS + ------------ + + ejabberd 2.1.0 or higher + XMLRPC-Erlang 1.13 with IP, Ruby and Xmerl 1.x patches + Optional: mod_admin_extra implements many ejabberd commands for general server administration + Optional: mod_muc_admin implements ejabberd commands for MUC administration + + + CONFIGURE EJABBERD + ------------------ + +1. You need to get and install XMLRPC-Erlang. +You can download XMLRPC-Erlang binary files from + http://www.ejabberd.im/ejabberd_xmlrpc +or compile it yourself: + wget http://www.ejabberd.im/files/contributions/xmlrpc-1.13-ipr2.tgz + tar -xzvf xmlrpc-1.13-ipr2.tgz + cd xmlrpc-1.13/src + make + cd ../../ +Then you can copy the *.beam files to ejabberd ebin directory, +or add an option like this to the ejabberd start script: +$ erl -pa '/home/jabber/xmlrpc-1.13/ebin' ... + +2. Configure ejabberd to start this listener at startup: +edit ejabberd.cfg and add on the 'listen' section: +{listen, [ + {4560, ejabberd_xmlrpc, []}, + ... + ]}. + +3. Start ejabberd. + +4. Verify that ejabberd is listening in that port: +$ netstat -n -l | grep 4560 +tcp 0 0 0.0.0.0:4560 0.0.0.0:* LISTEN + +5. If there is any problem, check ejabberd.log and sasl.log files + + + CONFIGURE + --------- + +The listener allow several configurable options: + + {maxsessions, Integer} + Number of concurrent connections allowed. + Default: 10 + + {timeout, Integer} + Timeout of the connections, expressed in milliseconds. + Default: 5000 + + {access_commands, AccessCommands} + This option allows to define a list of access restrictions. + If this option is present, then XML-RPC calls must include as + first argument a struct with a user, server and password of an + account in ejabberd that has privileges in Access. + If the option is not present, such struct must not be provided. + The default value is to not define any restriction: [] + When one or several access restrictions are defined and the + XML-RPC call provides authentication for an account, each + restriction is verified until one matches completely: + the account matches the Access rule, + the command name is listed in CommandNames, + and the provided arguments do not contradict Arguments. + There is more information about AccessCommands in the ejabberd Guide. + + +Example configuration: XML-RPC calls can execute any command, with any +argument, and no authentication information must be provided: +{listen, [ + {4560, ejabberd_xmlrpc, [{maxsessions, 10}, {timeout, 5000}]}, + ... + ]}. + +In this case authentication information must be provided, but it is +enough that the account exists and the password is valid to execute +any command: +{listen, [ + {4560, ejabberd_xmlrpc, [{maxsessions, 10}, {timeout, 5000}, + {access_commands, [{all, all, []}]}]}, + ... + ]}. + +In this example the local Jabber account xmlrpc-robot@jabber.example.org +can execute any command with no argument restriction: +{acl, xmlrpcbot, {user, "xmlrpc-robot", "jabber.example.org"}}. +{access, xmlrpcaccess, [{allow, xmlrpcbot}]}. +{listen, [ + {4560, ejabberd_xmlrpc, [{maxsessions, 10}, {timeout, 5000}, + {access_commands, [{xmlrpcaccess, all, []}]}]}, + ... + ]}. + +Finally, in this complex example the listener only listens in port +4560 of IP address 127.0.0.1, and several access restrictions are +defined (the corresponding ACL and ACCESS are not shown): +{listen, [ + {{4560, "127.0.0.1"}, ejabberd_xmlrpc, [ + {access_commands, [ + %% This bot can execute any command: + {xmlrpc_bot, all, []}, + %% This bot can execute any command, + %% but if a 'host' argument is provided, it must be "example.org": + {xmlrpc_bot_all_example, all, [{host, "example.org"}]}, + %% This bot can only execute the command 'dump'. No argument restriction: + {xmlrpc_bot_backups, [dump], []} + %% This bot can only execute the command 'register', + %% and if argument 'host' is provided, it must be "example.org": + {xmlrpc_bot_reg_example, [register], [{host, "example.org"}]}, + %% This bot can execute the commands 'register' and 'unregister', + %% if argument host is provided, it must be "test.org": + {xmlrpc_bot_reg_test, [register, unregister], [{host, "test.org"}]} + ]} + ]}, + ... + ]}. + + + + USAGE + ----- + +You can send calls to http://host:4560/ + +Call: Arguments: Returns: + + -- debug +echothis String String +echothisnew struct[{sentence, String}] struct[{repeated, String}] +multhis struct[{a, Integer}, {b, Integer}] Integer +multhisnew struct[{a, Integer}, {b, Integer}] struct[{mu, Integer}] + + -- statistics +tellme_title String String +tellme_value String String +tellme String struct[{title, String}, {value. String}] + + +With ejabberd_xmlrpc you can execute any ejabberd command with a XML-RPC call. + +1. Get a list of available ejabberd commands, for example: +$ ejabberdctl help +Available commands in this ejabberd node: + connected_users List all established sessions + connected_users_number Get the number of established sessions + delete_expired_messages Delete expired offline messages from database + delete_old_messages days Delete offline messages older than DAYS + dump file Dump the database to text file + register user host password Register a user + registered_users host List all registered users in HOST + reopen_log Reopen the log files + restart Restart ejabberd + restore file Restore the database from backup file + status Get ejabberd status + stop Stop ejabberd + unregister user host Unregister a user + user_resources user host List user's connected resources + +2. When you found the command you want to call, get some additional + help of the arguments and result: +$ ejabberdctl help user_resources + Command Name: user_resources + Arguments: user::string + host::string + Returns: resources::[ resource::string ] + Tags: session + Description: List user's connected resources + +3. You can try to execute the command in the shell for the account testuser@localhost: +$ ejabberdctl user_resources testuser localhost +Home +Psi + +4. Now implement the proper XML-RPC call in your XML-RPC client. + This example will use the Erlang library: +$ erl +1> xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, user_resources, [{struct, [{user, "testuser"}, {host, "localhost"}]}]}). +{ok,{response,[{struct,[{resources,{array,[{struct,[{resource,"Home"}]}, + {struct,[{resource,"Psi"}]}]}}]}]}} + +5. Note: if ejabberd_xmlrpc has the option 'access_commands' + configured with some access restriction (see the example + configurations provided above), the XML-RPC must include first an + argument providing information of a valid account. For example: +1> xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, user_resources, [ + {struct, [{user, "adminuser"}, {server, "localhost"}, {password, "aeiou"}]}, + {struct, [{user, "testuser"}, {host, "localhost"}]} ]}). + + +Arguments in XML-RPC calls can be provided in any order; +This module will sort the arguments before calling the ejabberd command. + +If auth is provided in the call when ejabberd_xmlrpc does not require it, +the call will return the error: -112 Unknown call + + + + EXAMPLE IN PHP + -------------- + +This is an XML-RPC client in PHP, thanks to Zbyszek Żółkiewski and Calder. +It requires "allow_url_fopen = On" in your php.ini. + +------- +"testuser", "host"=>"localhost"); +$request = xmlrpc_encode_request('user_resources', $param, (array('encoding' => 'utf-8'))); + +$context = stream_context_create(array('http' => array( + 'method' => "POST", + 'header' => "User-Agent: XMLRPC::Client mod_xmlrpc\r\n" . + "Content-Type: text/xml\r\n" . + "Content-Length: ".strlen($request), + 'content' => $request +))); + +$file = file_get_contents("http://127.0.0.1:4560/RPC2", false, $context); + +$response = xmlrpc_decode($file); + +if (xmlrpc_is_fault($response)) { + trigger_error("xmlrpc: $response[faultString] ($response[faultCode])"); +} else { + print_r($response); +} + +?> +------- + +The response, following the example would be like this: +------- +$ php5 call.php +Array +( + [resources] => Array + ( + [0] => Array + ( + [resource] => Home + ) + [1] => Array + ( + [resource] => Psi + ) + ) +) +------- + +If you configured the option access_commands, you have to provide authentication +information by replacing the first lime with something like this: +------- +$param_auth=array("user"=>"analloweduser", "server"=>"localhost", "password"=>"MyPasS997"); +$param_comm=array("user"=>"testuser", "host"=>"localhost"); +$param=array($param_auth, $param_comm); +------- + + + **** WARNING: all the remaining text was written for mod_xmlrpc and + is NOT valid for ejabberd_xmlrpc **** + + + TEST + ---- + + - You can easily try the XML-RPC server starting a new Erlang Virtual Machine + and making calls to ejabberd's XML-RPC: + +1. Start Erlang with this option: +$ erl -pa '/home/jabber/xmlrpc-1.13/ebin' + +2. Now on the Erlang console, write commands and check the results: + +1> xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, echothis, [800]}). +{ok,{response,[800]}} + +2> xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, echothis, ["blot cloc 557.889 kg"]}). +{ok,{response,["blot cloc 557.889 kg"]}} + +3> xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, multhis, [{struct,[{a, 83}, {b, 689}]}]}). +{ok,{response,[57187]}} + +4> xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, register, +[{struct, [{user, "ggeo"}, {host, "example.com"}, {password, "gogo11"}]}]}). +{ok,{response,[0]}} + +5> xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, register, +[{struct, [{user, "ggeo"}, {host, "example.com"}, {password, "gogo11"}]}]}). +{ok,{response,[409]}} + +6> xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, muc_room_change_option, +[{struct, [{name, "test"}, {service, "conference.localhost"}, + {option, "title"}, {value, "Test Room"}]}]}). + +7> xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, muc_room_set_affiliation, +[{struct, [{name, "test"}, {service, "conference.example.com"}, +{jid, "ex@example.com"}, {affiliation, "member"}]}]}). + +8> xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, muc_room_set_affiliation, +[{struct, [{name, "test"}, {service, "conference.example.com"}, +{jid, "ex@example.com"}, {affiliation, "none"}]}]}). + + + - Some possible XML-RPC error messages: + + + Client: connection refused: wrong IP, wrong port, the server is not started... + +2> xmlrpc:call({127, 0, 0, 1}, 44444, "/", {call, echothis, [800]}). +{error,econnrefused} + + + Client: bad value: a800 is a string, so it must be put into "" + +7> xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, echothis, [a800]}). +{error,{bad_value,a800}} + + + Server: unknown call: you sent a call that the server does not implement + +3> xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, bububu, [800]}). +{ok,{response,{fault,-1,"Unknown call: {call,bububu,[800]}"}}} + + + EXAMPLE IN PYTHON + ----------------- + +This is an example XML-RPC client in Python, thanks to Diddek: +------- +import xmlrpclib + +server_url = 'http://127.0.0.1:4560'; +server = xmlrpclib.Server(server_url); + +params = {} +params["user"] = "ggeo" +params["host"] = "localhost" +params["password"] = "gogo11" + +result = server.register(params) +print result +------- + +This Python example shows how to provide authentication in the call, thanks to Muelli: +------- +import xmlrpclib + +server_url = 'http://127.0.0.1:4560' +server = xmlrpclib.ServerProxy(server_url) + +EJABBERD_XMLRPC_LOGIN = {'user': 'adminuser', 'server': 'localhost', 'password': 'aeiou'} + +def ejabberdctl(command, data): + fn = getattr(server, command) + return fn(EJABBERD_XMLRPC_LOGIN, data) + +result = ejabberdctl('register', {'user':'ggeo', 'host':'localhost', 'password':'gogo11'}) +print result +------- + + + EXAMPLE IN RUBY + --------------- + +This is an example XML-RPC client in Ruby, thanks to Diddek: +------- +require 'xmlrpc/client' + +host = "172.16.29.6:4560" +timeout = 3000000 +client = XMLRPC::Client.new2("http://#{host}", "#{host}", timeout) +result = client.call("echothis", "800") +puts result +------- + + + EXAMPLE IN JAVA + --------------- + +This is an XML-RPC client in Java, thanks to Calder. +It requires Apache XML-RPC available at http://ws.apache.org/xmlrpc/ + +------- +import java.net.URL; +import java.util.HashMap; +import java.util.Map; + +import org.apache.xmlrpc.client.XmlRpcClient; +import org.apache.xmlrpc.client.XmlRpcClientConfigImpl; + +public class Test { + + public static void main(String[] args) { + try { + XmlRpcClientConfigImpl config = new XmlRpcClientConfigImpl(); + config.setServerURL(new URL("http://127.0.0.1:4560/RPC2")); + XmlRpcClient client = new XmlRpcClient(); + client.setConfig(config); + + /* Command string */ + String command = "check_password"; + + /* Parameters as struct */ + Map struct = new HashMap(); + struct.put("user", "test1"); + struct.put("host", "localhost"); + struct.put("password", "test"); + + Object[] params = new Object[]{struct}; + Integer result = (Integer) client.execute(command, params); + System.out.println(result); + } catch (Exception e) { + System.out.println(e); + } + } + +} +------- + + + EXAMPLE IN C# + ------------- + +This is an XML-RPC client in C#, thanks to Mitchel Constantin. + +------- +// Copyright: 2010 Weavver, Inc. +// Author: Mitchel Constantin +// License: Public Domain (Limited to this file) + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using CookComputing.XmlRpc; + +namespace Weavver.Vendors.ProcessOne +{ + public struct send_message_chat + { + public string from; + public string to; + public string body; + } + public struct status {} + public class ejabberdRPC + { + [XmlRpcUrl("http://205.134.225.18:4560/RPC2")] + public interface IStateName : IXmlRpcProxy + { + [XmlRpcMethod("send_message_chat")] + object SendMessageChat(send_message_chat message); + [XmlRpcMethod("status")] + object Status(status s); + } + public CookComputing.XmlRpc.XmlRpcStruct SendMessageChat(send_message_chat message) + { + IStateName proxy = XmlRpcProxyGen.Create(); + proxy.KeepAlive = false; + return (CookComputing.XmlRpc.XmlRpcStruct) proxy.SendMessageChat(message); + } + public CookComputing.XmlRpc.XmlRpcStruct Status(status status) + { + IStateName proxy = XmlRpcProxyGen.Create(); + proxy.KeepAlive = false; + return (CookComputing.XmlRpc.XmlRpcStruct) proxy.Status(status); + } + } +} +------- diff --git a/ejabberd_xmlrpc/build.bat b/ejabberd_xmlrpc/build.bat new file mode 100644 index 0000000..fff9b3b --- /dev/null +++ b/ejabberd_xmlrpc/build.bat @@ -0,0 +1 @@ +erl -pa ../../ejabberd-dev/trunk/ebin -pa ebin -make diff --git a/ejabberd_xmlrpc/build.sh b/ejabberd_xmlrpc/build.sh new file mode 100755 index 0000000..f0a0cfa --- /dev/null +++ b/ejabberd_xmlrpc/build.sh @@ -0,0 +1,2 @@ +#!/bin/sh +erl -pa ../ejabberd-dev/ebin -pz ebin -make diff --git a/ejabberd_xmlrpc/src/ejabberd_xmlrpc.erl b/ejabberd_xmlrpc/src/ejabberd_xmlrpc.erl new file mode 100644 index 0000000..0e958a1 --- /dev/null +++ b/ejabberd_xmlrpc/src/ejabberd_xmlrpc.erl @@ -0,0 +1,467 @@ +%%%---------------------------------------------------------------------- +%%% File : ejabberd_xmlrpc.erl +%%% Author : Badlop +%%% Purpose : XML-RPC server that frontends ejabberd commands +%%% Created : 21 Aug 2007 by Badlop +%%% Id : $Id: ejabberd_xmlrpc.erl 595 2008-05-20 11:39:31Z badlop $ +%%%---------------------------------------------------------------------- + +%%% TODO: Implement a command in ejabberdctl 'help COMMAND LANGUAGE' that shows +%%% a coding example to call that command in a specific language (python, php). + +%%% TODO: Remove support for plaintext password + +%%% TODO: commands strings should be strings without ~n + +-module(ejabberd_xmlrpc). +-author('badlop@process-one.net'). + +-export([ + start_listener/2, + handler/2, + socket_type/0 + ]). + +-include("ejabberd.hrl"). +-include("mod_roster.hrl"). +-include("jlib.hrl"). + +-record(state, {access_commands, auth = noauth, get_auth}). + + +%% Test: + +%% xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, take_integer, [{struct, [{thisinteger, 5}]}]}). +%% {ok,{response,[{struct,[{zero,0}]}]}} +%% +%% xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, echo_string, [{struct, [{thisstring, "abcd"}]}]}). +%% {ok,{response,[{struct,[{thatstring,"abcd"}]}]}} +%% +%% xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, tell_tuple_3integer, [{struct, [{thisstring, "abcd"}]}]}). +%% {ok,{response, +%% [{struct, +%% [{thattuple, +%% {array, +%% [{struct,[{first,123}]}, +%% {struct,[{second,456}]}, +%% {struct,[{third,789}]}]}}]}]}} +%% +%% xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, pow, [{struct, [{base, 5}, {exponent, 7}]}]}). +%% {ok,{response,[{struct,[{pow,78125}]}]}} +%% +%% xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, seq, [{struct, [{from, 3}, {to, 7}]}]}). +%% {ok,{response,[{array,[{struct,[{intermediate,3}]}, +%% {struct,[{intermediate,4}]}, +%% {struct,[{intermediate,5}]}, +%% {struct,[{intermediate,6}]}, +%% {struct,[{intermediate,7}]}]}]}} +%% +%% xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, substrs, [{struct, [{word, "abcd"}]}]}). +%% NO: +%% {ok,{response,[{array,[{struct,[{miniword,"a"}]}, +%% {struct,[{miniword,"ab"}]}, +%% {struct,[{miniword,"abc"}]}, +%% {struct,[{miniword,"abcd"}]}]}]}} +%% {ok,{response, +%% [{struct, +%% [{substrings, +%% {array, +%% [{struct,[{miniword,"a"}]}, +%% {struct,[{miniword,"ab"}]}, +%% {struct,[{miniword,"abc"}]}, +%% {struct,[{miniword,"abcd"}]}]}}]}]}} +%% +%% xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, splitjid, [{struct, [{jid, "abcd@localhost/work"}]}]}). +%% {ok,{response, +%% [{struct, +%% [{jidparts, +%% {array, +%% [{struct,[{user,"abcd"}]}, +%% {struct,[{server,"localhost"}]}, +%% {struct,[{resource,"work"}]}]}}]}]}} +%% +%% xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, echo_integer_string, [{struct, [{thisstring, "abc"}, {thisinteger, 55}]}]}). +%% {ok,{response, +%% [{struct, +%% [{thistuple, +%% {array, +%% [{struct,[{thisinteger,55}]}, +%% {struct,[{thisstring,"abc"}]}]}}]}]}} +%% +%% +%% xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, echo_list_integer, [{struct, [{thislist, {array, [{struct, [{thisinteger, 55}, {thisinteger, 4567}]}]}}]}]}). +%% {ok,{response, +%% [{struct, +%% [{thatlist, +%% {array, +%% [{struct,[{thatinteger,55}]}, +%% {struct,[{thatinteger,4567}]}]}}]}]}} +%% +%% +%% xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, echo_integer_list_string, [{struct, [{thisinteger, 123456}, {thislist, {array, [{struct, [{thisstring, "abc"}, {thisstring, "bobo baba"}]}]}}]}]}). +%% {ok, +%% {response, +%% [{struct, +%% [{thistuple, +%% {array, +%% [{struct,[{thatinteger,123456}]}, +%% {struct, +%% [{thatlist, +%% {array, +%% [{struct,[{thatstring,"abc"}]}, +%% {struct,[{thatstring,"bobo baba"}]}]}}]}]}}]}]}} +%% +%% xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, take_tuple_2integer, [{struct, [{thistuple, {array, [{struct, [{thisinteger1, 55}, {thisinteger2, 4567}]}]}}]}]}). +%% {ok,{response,[{struct,[{zero,0}]}]}} +%% +%% xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, echo_isatils, [{struct, +%% [{thisinteger, 123456990}, +%% {thisstring, "This is ISATILS"}, +%% {thisatom, "test_isatils"}, +%% {thistuple, {array, [{struct, [ +%% {listlen, 2}, +%% {thislist, {array, [{struct, [ +%% {contentstring, "word1"}, +%% {contentstring, "word 2"} +%% ]}]}} +%% ]}]}} +%% ]}]}). +%% {ok,{response, +%% [{struct, +%% [{results, +%% {array, +%% [{struct,[{thatinteger,123456990}]}, +%% {struct,[{thatstring,"This is ISATILS"}]}, +%% {struct,[{thatatom,"test_isatils"}]}, +%% {struct, +%% [{thattuple, +%% {array, +%% [{struct,[{listlen,123456990}]}, +%% {struct,[{thatlist,...}]}]}}]}]}}]}]}} + +%% ecommand doesn't exist: +%% xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, echo_integer_string2, [{struct, [{thisstring, "abc"}]}]}). +%% {ok,{response,{fault,-1, "Unknown call: {call,echo_integer_string2,[{struct,[{thisstring,\"abc\"}]}]}"}}} +%% +%% Duplicated argument: +%% xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, echo_integer_string, [{struct, [{thisstring, "abc"}, {thisinteger, 44}, {thisinteger, 55}]}]}). +%% {ok,{response,{fault,-104, "Error -104\nAttribute 'thisinteger' duplicated:\n[{thisstring,\"abc\"},{thisinteger,44},{thisinteger,55}]"}}} +%% +%% Missing argument: +%% xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, echo_integer_string, [{struct, [{thisstring, "abc"}]}]}). +%% {ok,{response,{fault,-106, "Error -106\nRequired attribute 'thisinteger' not found:\n[{thisstring,\"abc\"}]"}}} +%% +%% Duplicated tuple element: +%% xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, take_tuple_2integer, [{struct, [{thistuple, {array, [{struct, [{thisinteger1, 55}, {thisinteger1, 66}, {thisinteger2, 4567}]}]}}]}]}). +%% {ok,{response,{fault,-104, "Error -104\nAttribute 'thisinteger1' defined multiple times:\n[{thisinteger1,55},{thisinteger1,66},{thisinteger2,4567}]"}}} +%% +%% Missing element in tuple: +%% xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, take_tuple_2integer, [{struct, [{thistuple, {array, [{struct, [{thisinteger1, 55}, {thisintegerc, 66}, {thisinteger, 4567}]}]}}]}]}). +%% {ok,{response,{fault,-106, "Error -106\nRequired attribute 'thisinteger2' not found:\n[{thisintegerc,66},{thisinteger,4567}]"}}} +%% +%% The ecommand crashed: +%% xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, this_crashes, [{struct, []}]}). +%% {ok,{response,{fault,-100, "Error -100\nA problem 'error' occurred executing the command this_crashes with arguments []: badarith"}}} + + +%% ----------------------------- +%% Listener interface +%% ----------------------------- + +start_listener({Port, Ip, tcp = _TranportProtocol}, Opts) -> + %% get options + MaxSessions = gen_mod:get_opt(maxsessions, Opts, 10), + Timeout = gen_mod:get_opt(timeout, Opts, 5000), + AccessCommands = gen_mod:get_opt(access_commands, Opts, []), + GetAuth = case [ACom || {Ac, _, _} = ACom <- AccessCommands, Ac /= all] of + [] -> false; + _ -> true + end, + + %% start the XML-RPC server + Handler = {?MODULE, handler}, + State = #state{access_commands = AccessCommands, get_auth = GetAuth}, + xmlrpc:start_link(Ip, Port, MaxSessions, Timeout, Handler, State). + +socket_type() -> + independent. + + +%% ----------------------------- +%% Access verification +%% ----------------------------- + +%% @spec (AuthList) -> {User, Server, Password} +%% where +%% AuthList = [{user, string()}, {server, string()}, {password, string()}] +%% It may throw: {error, missing_auth_arguments, Attr} +get_auth(AuthList) -> + %% Check AuthList contains all the required data + [User, Server, Password] = + try get_attrs([user, server, password], AuthList) of + [U, S, P] -> [U, S, P] + catch + exit:{attribute_not_found, Attr, _} -> + throw({error, missing_auth_arguments, Attr}) + end, + {User, Server, Password}. + + +%% ----------------------------- +%% Handlers +%% ----------------------------- + +%% Call: Arguments: Returns: + + +%% ............................. +%% Access verification + +%% xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, echothis, [152]}). +%% {ok,{response,{fault,-103, "Error -103\nRequired authentication: {call,echothis,[152]}"}}} +%% +%% xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, echothis, [{struct, [{user, "badlop"}, {server, "localhost"}, {password, "ada"}]}, 152]}). +%% {ok,{response,{fault,-103, +%% "Error -103\nAuthentication non valid: [{user,\"badlop\"},\n +%% {server,\"localhost\"},\n +%% {password,\"ada\"}]"}}} +%% +%% xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, echothis, [{struct, [{user, "badlop"}, {server, "localhost"}, {password, "ada90ada"}]}, 152]}). +%% {ok,{response,[152]}} +%% +%% xmlrpc:call({127, 0, 0, 1}, 4560, "/", {call, echothis, [{struct, [{user, "badlop"}, {server, "localhost"}, {password, "79C1574A43BC995F2B145A299EF97277"}]}, 152]}). +%% {ok,{response,[152]}} + +handler(#state{get_auth = true, auth = noauth} = State, {call, Method, [{struct, AuthList} | Arguments] = AllArgs}) -> + try get_auth(AuthList) of + Auth -> + handler(State#state{get_auth = false, auth = Auth}, {call, Method, Arguments}) + catch + {error, missing_auth_arguments, _Attr} -> + handler(State#state{get_auth = false, auth = noauth}, {call, Method, AllArgs}) + end; + + +%% ............................. +%% Debug + +%% echothis String String +handler(_State, {call, echothis, [A]}) -> + {false, {response, [A]}}; + +%% echothisnew struct[{sentence, String}] struct[{repeated, String}] +handler(_State, {call, echothisnew, [{struct, [{sentence, A}]}]}) -> + {false, {response, [{struct, [{repeated, A}]}]}}; + +%% multhis struct[{a, Integer}, {b, Integer}] Integer +handler(_State, {call, multhis, [{struct, [{a, A}, {b, B}]}]}) -> + {false, {response, [A*B]}}; + +%% multhisnew struct[{a, Integer}, {b, Integer}] struct[{mu, Integer}] +handler(_State, {call, multhisnew, [{struct, [{a, A}, {b, B}]}]}) -> + {false, {response, [{struct, [{mu, A*B}]}]}}; + +%% ............................. +%% Statistics + +%% tellme_title String String +handler(_State, {call, tellme_title, [A]}) -> + {false, {response, [get_title(A)]}}; + +%% tellme_value String String +handler(_State, {call, tellme_value, [A]}) -> + N = node(), + {false, {response, [get_value(N, A)]}}; + +%% tellme String struct[{title, String}, {value. String}] +handler(_State, {call, tellme, [A]}) -> + N = node(), + T = {title, get_title(A)}, + V = {value, get_value(N, A)}, + R = {struct, [T, V]}, + {false, {response, [R]}}; + +%% ............................. +%% ejabberd commands + +handler(State, {call, Command, []}) -> + %% The XMLRPC request may not contain a struct parameter, + %% but our internal functions need such struct, even if it's empty + %% So let's add it and do a recursive call: + handler(State, {call, Command, [{struct, []}]}); + +handler(State, {call, Command, [{struct, AttrL}]} = Payload) -> + case ejabberd_commands:get_command_format(Command) of + {error, command_unknown} -> + build_fault_response(-112, "Unknown call: ~p", [Payload]); + {ArgsF, ResultF} -> + try_do_command(State#state.access_commands, State#state.auth, Command, AttrL, ArgsF, ResultF) + end; + +%% If no other guard matches +handler(_State, Payload) -> + build_fault_response(-112, "Unknown call: ~p", [Payload]). + + +%% ----------------------------- +%% Command +%% ----------------------------- + +try_do_command(AccessCommands, Auth, Command, AttrL, ArgsF, ResultF) -> + try do_command(AccessCommands, Auth, Command, AttrL, ArgsF, ResultF) of + {command_result, ResultFormatted} -> + {false, {response, [ResultFormatted]}} + catch + exit:{duplicated_attribute, ExitAt, ExitAtL} -> + build_fault_response(-114, "Attribute '~p' duplicated:~n~p", [ExitAt, ExitAtL]); + exit:{attribute_not_found, ExitAt, ExitAtL} -> + build_fault_response(-116, "Required attribute '~p' not found:~n~p", [ExitAt, ExitAtL]); + exit:{additional_unused_args, ExitAtL} -> + build_fault_response(-120, "The call provided additional unused arguments:~n~p", [ExitAtL]); + throw:Why -> + build_fault_response(-118, "A problem '~p' occurred executing the command ~p with arguments~n~p", [Why, Command, AttrL]) + end. + +build_fault_response(Code, ParseString, ParseArgs) -> + FaultString = "Error " ++ integer_to_list(Code) ++ "\n" ++ + lists:flatten(io_lib:format(ParseString, ParseArgs)), + ?WARNING_MSG(FaultString, []), %% Show Warning message in ejabberd log file + {false, {response, {fault, Code, FaultString}}}. + +do_command(AccessCommands, Auth, Command, AttrL, ArgsF, ResultF) -> + ArgsFormatted = format_args(AttrL, ArgsF), + Result = ejabberd_commands:execute_command(AccessCommands, Auth, Command, ArgsFormatted), + ResultFormatted = format_result(Result, ResultF), + {command_result, ResultFormatted}. + + +%%----------------------------- +%% Format arguments +%%----------------------------- + +get_attrs(Attribute_names, L) -> + [get_attr(A, L) || A <- Attribute_names]. + +get_attr(A, L) -> + case lists:keysearch(A, 1, L) of + {value, {A, Value}} -> Value; + false -> + %% Report the error and then force a crash + exit({attribute_not_found, A, L}) + end. + +%% Get an element from a list and delete it. +%% The element must be defined once and only once, +%% otherwise the function crashes on purpose. +get_elem_delete(A, L) -> + case proplists:get_all_values(A, L) of + [Value] -> + {Value, proplists:delete(A, L)}; + [_, _ | _] -> + %% Crash reporting the error + exit({duplicated_attribute, A, L}); + [] -> + %% Report the error and then force a crash + exit({attribute_not_found, A, L}) + end. + + +format_args(Args, ArgsFormat) -> + {ArgsRemaining, R} = + lists:foldl( + fun({ArgName, ArgFormat}, {Args1, Res}) -> + {ArgValue, Args2} = get_elem_delete(ArgName, Args1), + Formatted = format_arg(ArgValue, ArgFormat), + {Args2, Res ++ [Formatted]} + end, + {Args, []}, + ArgsFormat), + case ArgsRemaining of + [] -> R; + L when is_list(L) -> + exit({additional_unused_args, L}) + end. +format_arg({array, [{struct, Elements}]}, {list, {ElementDefName, ElementDefFormat}}) + when is_list(Elements) -> + lists:map( + fun({ElementName, ElementValue}) -> + true = (ElementDefName == ElementName), + format_arg(ElementValue, ElementDefFormat) + end, + Elements); +format_arg({array, [{struct, Elements}]}, {tuple, ElementsDef}) + when is_list(Elements) -> + FormattedList = format_args(Elements, ElementsDef), + list_to_tuple(FormattedList); +format_arg({array, Elements}, {list, ElementsDef}) + when is_list(Elements) and is_atom(ElementsDef) -> + [format_arg(Element, ElementsDef) || Element <- Elements]; +format_arg(Arg, integer) + when is_integer(Arg) -> + Arg; +format_arg(Arg, string) + when is_list(Arg) -> + Arg. + + +%% ----------------------------- +%% Result +%% ----------------------------- + +format_result({error, Error}, _) -> + throw({error, Error}); + +format_result(String, string) -> + lists:flatten(String); + +format_result(Atom, {Name, atom}) -> + {struct, [{Name, atom_to_list(Atom)}]}; + +format_result(Int, {Name, integer}) -> + {struct, [{Name, Int}]}; + +format_result(String, {Name, string}) -> + {struct, [{Name, lists:flatten(String)}]}; + +format_result(Code, {Name, rescode}) -> + {struct, [{Name, make_status(Code)}]}; + +format_result({Code, Text}, {Name, restuple}) -> + {struct, [{Name, make_status(Code)}, + {text, lists:flatten(Text)}]}; + +%% Result is a list of something: [something()] +format_result(Elements, {Name, {list, ElementsDef}}) -> + FormattedList = lists:map( + fun(Element) -> + format_result(Element, ElementsDef) + end, + Elements), + {struct, [{Name, {array, FormattedList}}]}; + +%% Result is a tuple with several elements: {something1(), something2(), ...} +format_result(ElementsTuple, {Name, {tuple, ElementsDef}}) -> + ElementsList = tuple_to_list(ElementsTuple), + ElementsAndDef = lists:zip(ElementsList, ElementsDef), + FormattedList = lists:map( + fun({Element, ElementDef}) -> + format_result(Element, ElementDef) + end, + ElementsAndDef), + {struct, [{Name, {array, FormattedList}}]}. +%% TODO: should be struct instead of array? + + +make_status(ok) -> 0; +make_status(true) -> 0; +make_status(false) -> 1; +make_status(error) -> 1; +make_status(_) -> 1. + + +%% ----------------------------- +%% Internal +%% ----------------------------- + +get_title(A) -> mod_statsdx:get_title(A). +get_value(N, A) -> mod_statsdx:get(N, [A]). diff --git a/extract-mod-translations.sh b/extract-mod-translations.sh new file mode 100755 index 0000000..b72b8cd --- /dev/null +++ b/extract-mod-translations.sh @@ -0,0 +1,27 @@ + +MODULES=`pwd` + +# Put here the path to ejabberd source +EJADIR=$MODULES/../git/ejabberd/ + +# TODO: Support extraction of multiple modules +#MODULE=mod_webpresence +MODULE=mod_register_web + +RUNDIR=$MODULES/$MODULE/trunk/ +PREPARESCRIPT=$EJADIR/contrib/extract_translations/prepare-translation.sh + +# 1. Create the directory $MODULE/msgs/ + +# 2. Create the $MODULE.pot +#$PREPARESCRIPT -rundir $RUNDIR -ejadir $EJADIR -project $MODULE -src2pot + +# 3. Create a language +# cp $MODULE.pot $LANG.$MODULE.po +# echo "" > $LANG.$MODULE.msg + +# 3.b Convert msg to po. But it doesn't work! :( +#$PREPARESCRIPT -rundir $RUNDIR -ejadir $EJADIR -project $MODULE -srcmsg2po we + +# 4. Update strings +$PREPARESCRIPT -rundir $RUNDIR -ejadir $EJADIR -project $MODULE -updateall diff --git a/ircd/Emakefile b/ircd/Emakefile new file mode 100644 index 0000000..93e965e --- /dev/null +++ b/ircd/Emakefile @@ -0,0 +1 @@ +{'src/ejabberd_ircd', [{outdir, "ebin"},{i,"../ejabberd-dev/include"}]}. diff --git a/ircd/README.txt b/ircd/README.txt new file mode 100644 index 0000000..e44b2d5 --- /dev/null +++ b/ircd/README.txt @@ -0,0 +1,83 @@ + + ircd - IRC-to-XMPP interface + + Author: + Magnus Henoch + xmpp:legoscia@jabber.cd.chalmers.se, + mailto:henoch@dtek.chalmers.se + Homepage: + http://www.dtek.chalmers.se/~henoch/text/ejabberd-ircd.html + Requirements: + ejabberd trunk SVN 1631 or newer + + + DESCRIPTION + =========== + +This is an IRC server frontend to ejabberd. It supports a subset of +the IRC protocol, allowing IRC users to use a subset of Jabber MUC +functions. Users log in with their username and password, just as if +they were Jabber users. Therefore, configuring the IRC interface to +use an anonymous authentication backend is probably what users expect. +Channel names are translated to MUC rooms on a particular MUC service. + +The most obvious missing functions in this module are operator actions +and a command to list channels. + + + CONFIGURATION + ============= + +Something like this should be inserted in the "listen" section of the +configuration file: + +{listen, [ + ... + {6667, ejabberd_ircd, [{access, c2s}, + {host, "example.org"}, + {muc_host, "conference.example.org"}, + {encoding, "utf-8"}, + {mappings, + [{"#esperanto", "esperanto@conference.jabber.org"}]} ]}, + ... +]}. + +Configurable module options: + access: ACL matching users allowed to use the IRC backend. + host: hostname part of the JIDs of IRC users. + muc_host: MUC service hosting IRC "channels". + encoding: encoding that IRC users are expected to use. + mappings: optional list of mappings from channel names to MUC rooms + on other MUC services. + + + AUTHENTICATION + ============== + +The IRC client needs to login in ejabberd. If the 'internal' auth +method is enabled, then the IRC client must provide the username and +password of an existing Jabber account. + +If you want to allow an IRC client to join in MUC rooms without +requiring authentication, you can enable anonyous authentication in +ejabberd. + +Note that this module doesn't do SASL ANONYMOUS authentication. This +means that to use anonymous authentication, the "anonymous_protocol" +option needs to be either "login_anon" or "both". + +For example, you can define a new Jabber virtual host used only for +anonymous authentication by ejabberd_ircd: + +{hosts, ["example.org", "anonymous.example.org"]}. +{host_config, "anonymous.example.org", + [{auth_method, anonymous}, + {anonymous_protocol, both}]}. +{listen, [ + ... + {6667, ejabberd_ircd, [{access, c2s}, + {host, "anonymous.example.org"}, + {muc_host, "conference.example.org"}, + {encoding, "utf-8"} ]}, + ... +]}. diff --git a/ircd/build.bat b/ircd/build.bat new file mode 100644 index 0000000..fff9b3b --- /dev/null +++ b/ircd/build.bat @@ -0,0 +1 @@ +erl -pa ../../ejabberd-dev/trunk/ebin -pa ebin -make diff --git a/ircd/build.sh b/ircd/build.sh new file mode 100755 index 0000000..f0a0cfa --- /dev/null +++ b/ircd/build.sh @@ -0,0 +1,2 @@ +#!/bin/sh +erl -pa ../ejabberd-dev/ebin -pz ebin -make diff --git a/ircd/src/ejabberd_ircd.erl b/ircd/src/ejabberd_ircd.erl new file mode 100644 index 0000000..3f6e203 --- /dev/null +++ b/ircd/src/ejabberd_ircd.erl @@ -0,0 +1,885 @@ +-module(ejabberd_ircd). +-author('henoch@dtek.chalmers.se'). +-update_info({update, 0}). + +-behaviour(gen_fsm). + +%% External exports +-export([start/2, + start_link/2, + socket_type/0]). + +%% gen_fsm callbacks +-export([init/1, + wait_for_nick/2, + wait_for_cmd/2, + handle_event/3, + handle_sync_event/4, + code_change/4, + handle_info/3, + terminate/3 + ]). + +%-define(ejabberd_debug, true). + +-include("ejabberd.hrl"). +-include("jlib.hrl"). + +-define(DICT, dict). + +-record(state, {socket, + sockmod, + access, + encoding, + shaper, + host, + muc_host, + sid = none, + pass = "", + nick = none, + user = none, + %% joining is a mapping from room JIDs to nicknames + %% received but not yet forwarded + joining = ?DICT:new(), + joined = ?DICT:new(), + %% mapping certain channels to certain rooms + channels_to_jids = ?DICT:new(), + jids_to_channels = ?DICT:new() + }). +-record(channel, {participants = [], + topic = ""}). + +-record(line, {prefix, command, params}). + +%-define(DBGFSM, true). + +-ifdef(DBGFSM). +-define(FSMOPTS, [{debug, [trace]}]). +-else. +-define(FSMOPTS, []). +-endif. + +%%%---------------------------------------------------------------------- +%%% API +%%%---------------------------------------------------------------------- +start(SockData, Opts) -> + supervisor:start_child(ejabberd_ircd_sup, [SockData, Opts]). + +start_link(SockData, Opts) -> + gen_fsm:start_link(ejabberd_ircd, [SockData, Opts], ?FSMOPTS). + +socket_type() -> + raw. + +%%%---------------------------------------------------------------------- +%%% Callback functions from gen_fsm +%%%---------------------------------------------------------------------- + +%%---------------------------------------------------------------------- +%% Func: init/1 +%% Returns: {ok, StateName, StateData} | +%% {ok, StateName, StateData, Timeout} | +%% ignore | +%% {stop, StopReason} +%%---------------------------------------------------------------------- +init([{SockMod, Socket}, Opts]) -> + iconv:start(), + Access = case lists:keysearch(access, 1, Opts) of + {value, {_, A}} -> A; + _ -> all + end, + Shaper = case lists:keysearch(shaper, 1, Opts) of + {value, {_, S}} -> S; + _ -> none + end, + Host = case lists:keysearch(host, 1, Opts) of + {value, {_, H}} -> H; + _ -> ?MYNAME + end, + MucHost = case lists:keysearch(muc_host, 1, Opts) of + {value, {_, M}} -> M; + _ -> "conference." ++ ?MYNAME + end, + Encoding = case lists:keysearch(encoding, 1, Opts) of + {value, {_, E}} -> E; + _ -> "utf-8" + end, + ChannelMappings = case lists:keysearch(mappings, 1, Opts) of + {value, {_, C}} -> C; + _ -> [] + end, + {ChannelToJid, JidToChannel} = + lists:foldl(fun({Channel, Room}, {CToJ, JToC}) -> + RoomJID = jlib:string_to_jid(Room), + BareChannel = case Channel of + [$#|R] -> R; + _ -> Channel + end, + {?DICT:store(BareChannel, RoomJID, CToJ), + ?DICT:store(RoomJID, BareChannel, JToC)} + end, {?DICT:new(), ?DICT:new()}, + ChannelMappings), + inet:setopts(Socket, [list, {packet, line}, {active, true}]), + %%_ReceiverPid = start_ircd_receiver(Socket, SockMod), + {ok, wait_for_nick, #state{socket = Socket, + sockmod = SockMod, + access = Access, + encoding = Encoding, + shaper = Shaper, + host = Host, + muc_host = MucHost, + channels_to_jids = ChannelToJid, + jids_to_channels = JidToChannel + }}. + +handle_info({tcp, _Socket, Line}, StateName, StateData) -> + DecodedLine = iconv:convert(StateData#state.encoding, "utf-8", Line), + Parsed = parse_line(DecodedLine), + ?MODULE:StateName({line, Parsed}, StateData); +handle_info({tcp_closed, _}, _StateName, StateData) -> + {stop, normal, StateData}; +handle_info({route, _, _, _} = Event, StateName, StateData) -> + ?MODULE:StateName(Event, StateData); +handle_info(Info, StateName, StateData) -> + ?ERROR_MSG("Unexpected info: ~p", [Info]), + {next_state, StateName, StateData}. + +handle_sync_event(Event, _From, StateName, StateData) -> + ?ERROR_MSG("Unexpected sync event: ~p", [Event]), + Reply = ok, + {reply, Reply, StateName, StateData}. + +handle_event(_Event, StateName, StateData) -> + {next_state, StateName, StateData}. + +code_change(_OldVsn, StateName, StateData, _Extra) -> + {ok, StateName, StateData}. +terminate(_Reason, _StateName, #state{socket = Socket, sockmod = SockMod, + sid = SID, host = Host, nick = Nick, + joined = JoinedDict} = State) -> + ?INFO_MSG("closing IRC connection for ~p", [Nick]), + case SID of + none -> + ok; + _ -> + Packet = {xmlelement, "presence", + [{"type", "unavailable"}], []}, + FromJID = user_jid(State), + ?DICT:map(fun(ChannelJID, _ChannelData) -> + ejabberd_router:route(FromJID, ChannelJID, Packet) + end, JoinedDict), + ejabberd_sm:close_session_unset_presence(SID, Nick, Host, "irc", "Logged out") + end, + gen_tcp = SockMod, + ok = gen_tcp:close(Socket), + ok. + + +wait_for_nick({line, #line{command = "PASS", params = Params}}, State) -> + ?DEBUG("in wait_for_nick", []), + Pass = hd(Params), + ?DEBUG("got password", []), + {next_state, wait_for_nick, State#state{pass = Pass}}; +wait_for_nick({line, #line{command = "NICK", params = Params}}, State) -> + ?DEBUG("in wait_for_nick", []), + Nick = hd(Params), + Pass = State#state.pass, + Server = State#state.host, + + JID = jlib:make_jid(Nick, Server, "irc"), + case JID of + error -> + ?DEBUG("invalid nick '~p'", [Nick]), + send_reply('ERR_ERRONEUSNICKNAME', [Nick, "Erroneous nickname"], State), + {next_state, wait_for_nick, State}; + _ -> + case acl:match_rule(Server, State#state.access, JID) of + deny -> + ?DEBUG("access denied for '~p'", [Nick]), + send_reply('ERR_NICKCOLLISION', [Nick, "Nickname collision"], State), + {next_state, wait_for_nick, State}; + allow -> + case ejabberd_auth:check_password(Nick, Server, Pass) of + false -> + ?DEBUG("auth failed for '~p'", [Nick]), + send_reply('ERR_NICKCOLLISION', [Nick, "Authentication failed"], State), + {next_state, wait_for_nick, State}; + true -> + ?DEBUG("good nickname '~p'", [Nick]), + SID = {now(), self()}, + ejabberd_sm:open_session( + SID, Nick, Server, "irc", peerip(gen_tcp, State#state.socket)), + ejabberd_sm:set_presence(SID, Nick, Server, "irc", + 3, "undefined", + [{'ip', peerip(gen_tcp, State#state.socket)}, {'conn','c2s'}, {'state',"+"}]), + send_text_command("", "001", [Nick, "IRC interface of ejabberd server "++Server], State), + send_reply('RPL_MOTDSTART', [Nick, "- "++Server++" Message of the day - "], State), + send_reply('RPL_MOTD', [Nick, "- This is the IRC interface of the ejabberd server "++Server++"."], State), + send_reply('RPL_MOTD', [Nick, "- Your full JID is "++Nick++"@"++Server++"/irc."], State), + send_reply('RPL_MOTD', [Nick, "- Channel #whatever corresponds to MUC room whatever@"++State#state.muc_host++"."], State), + send_reply('RPL_MOTD', [Nick, "- This IRC interface is quite immature. You will probably find bugs."], State), + send_reply('RPL_MOTD', [Nick, "- Have a good time!"], State), + send_reply('RPL_ENDOFMOTD', [Nick, "End of /MOTD command"], State), + {next_state, wait_for_cmd, State#state{nick = Nick, sid = SID, pass = ""}} + end + end + end; +wait_for_nick(Event, State) -> + ?DEBUG("in wait_for_nick", []), + ?INFO_MSG("unexpected event ~p", [Event]), + {next_state, wait_for_nick, State}. + +peerip(SockMod, Socket) -> + IP = case SockMod of + gen_tcp -> inet:peername(Socket); + _ -> SockMod:peername(Socket) + end, + case IP of + {ok, IPOK} -> IPOK; + _ -> undefined + end. + +wait_for_cmd({line, #line{command = "USER", params = [_Username, _Hostname, _Servername, _Realname]}}, State) -> + %% Yeah, like we care. + {next_state, wait_for_cmd, State}; +wait_for_cmd({line, #line{command = "JOIN", params = Params}}, State) -> + {ChannelsString, KeysString} = + case Params of + [C, K] -> + {C, K}; + [C] -> + {C, []} + end, + Channels = string:tokens(ChannelsString, ","), + Keys = string:tokens(KeysString, ","), + NewState = join_channels(Channels, Keys, State), + {next_state, wait_for_cmd, NewState}; + +%% USERHOST command +wait_for_cmd({line, #line{command = "USERHOST", params = Params}}, State) -> + case Params of + [] -> + send_reply('ERR_NEEDMOREPARAMS', ["USERHOST", "Not enough parameters"], State); + UserParams -> + Users = lists:sublist(string:tokens(UserParams, " "), 5), %% RFC 1459 specifies 5 items max + lists:foreach( + fun(UserSubList) -> + User = lists:last(UserSubList), + case ejabberd_sm:get_user_info(User, State#state.host, "irc") of + offline -> + send_reply('RPL_USERHOST',[State#state.nick, User++" offline"], State); + [_Node, _Conn, Ip] -> + {_,{{IP1,IP2,IP3,IP4}, _}} = Ip, + send_reply('RPL_USERHOST',[State#state.nick, User ++ "=+" ++ integer_to_list(IP1) ++ "." ++ + integer_to_list(IP2) ++ "." ++ integer_to_list(IP3) ++ "." ++ integer_to_list(IP4)], State) + end + end, Users) + end, + {next_state, wait_for_cmd, State}; + +wait_for_cmd({line, #line{command = "PART", params = [ChannelsString | MaybeMessage]}}, State) -> + Message = case MaybeMessage of + [] -> nothing; + [M] -> M + end, + Channels = string:tokens(ChannelsString, ","), + NewState = part_channels(Channels, State, Message), + {next_state, wait_for_cmd, NewState}; + +wait_for_cmd({line, #line{command = "PRIVMSG", params = [To, Text]}}, State) -> + Recipients = string:tokens(To, ","), + FromJID = user_jid(State), + lists:foreach( + fun(Rcpt) -> + case Rcpt of + [$# | Roomname] -> + Packet = {xmlelement, "message", + [{"type", "groupchat"}], + [{xmlelement, "body", [], + filter_cdata(translate_action(Text))}]}, + ToJID = channel_to_jid(Roomname, State), + ejabberd_router:route(FromJID, ToJID, Packet); + _ -> + case string:tokens(Rcpt, "#") of + [Nick, Channel] -> + Packet = {xmlelement, "message", + [{"type", "chat"}], + [{xmlelement, "body", [], + filter_cdata(translate_action(Text))}]}, + ToJID = channel_nick_to_jid(Nick, Channel, State), + ejabberd_router:route(FromJID, ToJID, Packet); + _ -> + send_text_command(Rcpt, "NOTICE", [State#state.nick, + "Your message to "++ + Rcpt++ + " was dropped. " + "Try sending it to "++Rcpt++ + "#somechannel."], State) + end + end + end, Recipients), + {next_state, wait_for_cmd, State}; + +wait_for_cmd({line, #line{command = "PING", params = Params}}, State) -> + {Token, Whom} = + case Params of + [A] -> + {A, ""}; + [A, B] -> + {A, B} + end, + if Whom == ""; Whom == State#state.host -> + %% Ping to us + send_command("", "PONG", [State#state.host, Token], State); + true -> + %% Ping to someone else + ?DEBUG("ignoring ping to ~s", [Whom]), + ok + end, + {next_state, wait_for_cmd, State}; + +wait_for_cmd({line, #line{command = "TOPIC", params = Params}}, State) -> + case Params of + [Channel] -> + %% user asks for topic + case ?DICT:find(channel_to_jid(Channel, State), + State#state.joined) of + {ok, #channel{topic = Topic}} -> + case Topic of + "" -> + send_reply('RPL_NOTOPIC', ["No topic is set"], State); + _ -> + send_reply('RPL_TOPIC', [Topic], State) + end; + _ -> + send_reply('ERR_NOTONCHANNEL', ["You're not on that channel"], State) + end; + [Channel, NewTopic] -> + Packet = + {xmlelement, "message", + [{"type", "groupchat"}], + [{xmlelement, "subject", [], filter_cdata(NewTopic)}]}, + FromJID = user_jid(State), + ToJID = channel_to_jid(Channel, State), + ejabberd_router:route(FromJID, ToJID, Packet) + end, + {next_state, wait_for_cmd, State}; + +wait_for_cmd({line, #line{command = "MODE", params = [ModeOf | Params]}}, State) -> + case ModeOf of + [$# | Channel] -> + ChannelJid = channel_to_jid(Channel, State), + Joined = ?DICT:find(ChannelJid, State#state.joined), + case Joined of + {ok, _ChannelData} -> + case Params of + [] -> + %% This is where we could mirror some advanced MUC + %% properties. + %%send_reply('RPL_CHANNELMODEIS', [Channel, Modes], State); + send_reply('ERR_NOCHANMODES', [Channel], State); + ["b"] -> + send_reply('RPL_ENDOFBANLIST', [Channel, "Ban list not available"], State); + _ -> + send_reply('ERR_UNKNOWNCOMMAND', ["MODE", io_lib:format("MODE ~p not understood", [Params])], State) + end; + _ -> + send_reply('ERR_NOTONCHANNEL', [Channel, "You're not on that channel"], State) + end; + Nick -> + if Nick == State#state.nick -> + case Params of + [] -> + send_reply('RPL_UMODEIS', [], State); + [Flags|_] -> + send_reply('ERR_UMODEUNKNOWNFLAG', [Flags, "No MODE flags supported"], State) + end; + true -> + send_reply('ERR_USERSDONTMATCH', ["Can't change mode for other users"], State) + end + end, + {next_state, wait_for_cmd, State}; + +wait_for_cmd({line, #line{command = "QUIT"}}, State) -> + %% quit message is ignored for now + {stop, normal, State}; + +wait_for_cmd({line, #line{command = Unknown, params = Params} = Line}, State) -> + ?INFO_MSG("Unknown command: ~p", [Line]), + send_reply('ERR_UNKNOWNCOMMAND', [Unknown, "Unknown command or arity: " ++ + Unknown ++ "/" ++ integer_to_list(length(Params))], State), + {next_state, wait_for_cmd, State}; + +wait_for_cmd({route, From, _To, {xmlelement, "presence", Attrs, Els} = El}, State) -> + Type = xml:get_attr_s("type", Attrs), + FromRoom = jlib:jid_remove_resource(From), + FromNick = From#jid.resource, + + Channel = jid_to_channel(From, State), + MyNick = State#state.nick, + IRCSender = make_irc_sender(FromNick, FromRoom, State), + + Joining = ?DICT:find(FromRoom, State#state.joining), + Joined = ?DICT:find(FromRoom, State#state.joined), + case {Joining, Joined, Type} of + {{ok, BufferedNicks}, _, ""} -> + case BufferedNicks of + [] -> + %% If this is the first presence, tell the + %% client that it's joining. + send_command(make_irc_sender(MyNick, FromRoom, State), + "JOIN", [Channel], State); + _ -> + ok + end, + + NewRole = case find_el("x", ?NS_MUC_USER, Els) of + nothing -> + ""; + XMucEl -> + xml:get_path_s(XMucEl, [{elem, "item"}, {attr, "role"}]) + end, + NewBufferedNicks = [{FromNick, NewRole} | BufferedNicks], + ?DEBUG("~s is present in ~s. we now have ~p.", + [FromNick, Channel, NewBufferedNicks]), + %% We receive our own presence last. XXX: there + %% are some status codes here. See XEP-0045, + %% section 7.1.3. + NewState = + case FromNick of + MyNick -> + send_reply('RPL_NAMREPLY', + [MyNick, "=", + Channel, + lists:append( + lists:map( + fun({Nick, Role}) -> + case Role of + "moderator" -> + "@"; + "participant" -> + "+"; + _ -> + "" + end ++ Nick ++ " " + end, NewBufferedNicks))], + State), + send_reply('RPL_ENDOFNAMES', + [Channel, + "End of /NAMES list"], + State), + NewJoiningDict = ?DICT:erase(FromRoom, State#state.joining), + ChannelData = #channel{participants = NewBufferedNicks}, + NewJoinedDict = ?DICT:store(FromRoom, ChannelData, State#state.joined), + State#state{joining = NewJoiningDict, + joined = NewJoinedDict}; + _ -> + NewJoining = ?DICT:store(FromRoom, NewBufferedNicks, State#state.joining), + State#state{joining = NewJoining} + end, + {next_state, wait_for_cmd, NewState}; + {{ok, _BufferedNicks}, _, "error"} -> + NewState = + case FromNick of + MyNick -> + %% we couldn't join the room + {ReplyCode, ErrorDescription} = + case xml:get_subtag(El, "error") of + {xmlelement, _, _, _} = ErrorEl -> + {ErrorName, ErrorText} = parse_error(ErrorEl), + {case ErrorName of + "forbidden" -> 'ERR_INVITEONLYCHAN'; + _ -> 'ERR_NOSUCHCHANNEL' + end, + if is_list(ErrorText) -> + ErrorName ++ ": " ++ ErrorText; + true -> + ErrorName + end}; + _ -> + {'ERR_NOSUCHCHANNEL', "Unknown error"} + end, + send_reply(ReplyCode, [Channel, ErrorDescription], State), + + NewJoiningDict = ?DICT:erase(FromRoom, State#state.joining), + State#state{joining = NewJoiningDict}; + _ -> + ?ERROR_MSG("ignoring presence of type ~s from ~s while joining room", + [Type, jlib:jid_to_string(From)]), + State + end, + {next_state, wait_for_cmd, NewState}; + %% Presence in a channel we have already joined + {_, {ok, _}, ""} -> + %% Someone enters + send_command(IRCSender, "JOIN", [Channel], State), + {next_state, wait_for_cmd, State}; + {_, {ok, _}, _} -> + %% Someone leaves + send_command(IRCSender, "PART", [Channel], State), + {next_state, wait_for_cmd, State}; + _ -> + ?INFO_MSG("unexpected presence from ~s", [jlib:jid_to_string(From)]), + {next_state, wait_for_cmd, State} + end; + +wait_for_cmd({route, From, _To, {xmlelement, "message", Attrs, Els} = El}, State) -> + Type = xml:get_attr_s("type", Attrs), + case Type of + "groupchat" -> + ChannelJID = jlib:jid_remove_resource(From), + case ?DICT:find(ChannelJID, State#state.joined) of + {ok, #channel{} = ChannelData} -> + FromChannel = jid_to_channel(From, State), + FromNick = From#jid.resource, + Subject = xml:get_path_s(El, [{elem, "subject"}, cdata]), + Body = xml:get_path_s(El, [{elem, "body"}, cdata]), + XDelay = lists:any(fun({xmlelement, "x", XAttrs, _}) -> + xml:get_attr_s("xmlns", XAttrs) == ?NS_DELAY; + (_) -> + false + end, Els), + if + Subject /= "" -> + CleanSubject = lists:map(fun($\n) -> + $\ ; + (C) -> C + end, Subject), + send_text_command(make_irc_sender(From, State), + "TOPIC", [FromChannel, CleanSubject], State), + NewChannelData = ChannelData#channel{topic = CleanSubject}, + NewState = State#state{joined = ?DICT:store(jlib:jid_remove_resource(From), NewChannelData, State#state.joined)}, + {next_state, wait_for_cmd, NewState}; + not XDelay, FromNick == State#state.nick -> + %% there is no message echo in IRC. + %% we let the backlog through, though. + {next_state, wait_for_cmd, State}; + true -> + BodyLines = string:tokens(Body, "\n"), + lists:foreach( + fun(Line) -> + Line1 = + case Line of + [$/, $m, $e, $ | Action] -> + [1]++"ACTION "++Action++[1]; + _ -> + Line + end, + send_text_command(make_irc_sender(From, State), + "PRIVMSG", [FromChannel, Line1], State) + end, BodyLines), + {next_state, wait_for_cmd, State} + end; + error -> + ?ERROR_MSG("got message from ~s without having joined it", + [jlib:jid_to_string(ChannelJID)]), + {next_state, wait_for_cmd, State} + end; + "error" -> + MucHost = State#state.muc_host, + ErrorFrom = + case From of + #jid{lserver = MucHost, + luser = Room, + lresource = ""} -> + [$#|Room]; + #jid{lserver = MucHost, + luser = Room, + lresource = Nick} -> + Nick++"#"++Room; + #jid{} -> + %% ??? + jlib:jid_to_string(From) + end, + %% I think this should cover all possible combinations of + %% XMPP and non-XMPP error messages... + ErrorText = + error_to_string(xml:get_subtag(El, "error")), + send_text_command("", "NOTICE", [State#state.nick, + "Message to "++ErrorFrom++" bounced: "++ + ErrorText], State), + {next_state, wait_for_cmd, State}; + _ -> + ChannelJID = jlib:jid_remove_resource(From), + case ?DICT:find(ChannelJID, State#state.joined) of + {ok, #channel{}} -> + FromNick = From#jid.lresource++jid_to_channel(From, State), + Body = xml:get_path_s(El, [{elem, "body"}, cdata]), + BodyLines = string:tokens(Body, "\n"), + lists:foreach( + fun(Line) -> + Line1 = + case Line of + [$/, $m, $e, $ | Action] -> + [1]++"ACTION "++Action++[1]; + _ -> + Line + end, + send_text_command(FromNick, "PRIVMSG", [State#state.nick, Line1], State) + end, BodyLines), + {next_state, wait_for_cmd, State}; + _ -> + ?INFO_MSG("unexpected message from ~s", [jlib:jid_to_string(From)]), + {next_state, wait_for_cmd, State} + end + end; + +wait_for_cmd(Event, State) -> + ?INFO_MSG("unexpected event ~p", [Event]), + {next_state, wait_for_cmd, State}. + +join_channels([], _, State) -> + State; +join_channels(Channels, [], State) -> + join_channels(Channels, [none], State); +join_channels([Channel | Channels], [Key | Keys], + #state{nick = Nick} = State) -> + Packet = + {xmlelement, "presence", [], + [{xmlelement, "x", [{"xmlns", ?NS_MUC}], + case Key of + none -> + []; + _ -> + [{xmlelement, "password", [], filter_cdata(Key)}] + end}]}, + From = user_jid(State), + To = channel_nick_to_jid(Nick, Channel, State), + Room = jlib:jid_remove_resource(To), + ejabberd_router:route(From, To, Packet), + NewState = State#state{joining = ?DICT:store(Room, [], State#state.joining)}, + join_channels(Channels, Keys, NewState). + +part_channels([], State, _Message) -> + State; +part_channels([Channel | Channels], State, Message) -> + Packet = + {xmlelement, "presence", + [{"type", "unavailable"}], + case Message of + nothing -> []; + _ -> [{xmlelement, "status", [], + [{xmlcdata, Message}]}] + end}, + From = user_jid(State), + To = channel_nick_to_jid(State#state.nick, Channel, State), + ejabberd_router:route(From, To, Packet), + RoomJID = channel_to_jid(Channel, State), + NewState = State#state{joined = ?DICT:erase(RoomJID, State#state.joined)}, + part_channels(Channels, NewState, Message). + +parse_line(Line) -> + {Line1, LastParam} = + case string:str(Line, " :") of + 0 -> + {Line, []}; + Index -> + {string:substr(Line, 1, Index - 1), + [string:substr(Line, Index + 2) -- "\r\n"]} + end, + Tokens = string:tokens(Line1, " \r\n"), + {Prefix, Tokens1} = + case Line1 of + [$: | _] -> + {hd(Tokens), tl(Tokens)}; + _ -> + {none, Tokens} + end, + [Command | Params] = Tokens1, + UCCommand = upcase(Command), + #line{prefix = Prefix, command = UCCommand, params = Params ++ LastParam}. + +upcase([]) -> + []; +upcase([C|String]) -> + [if $a =< C, C =< $z -> + C - ($a - $A); + true -> + C + end | upcase(String)]. + +%% sender + +send_line(Line, #state{sockmod = SockMod, socket = Socket, encoding = Encoding}) -> + ?DEBUG("sending ~s", [Line]), + gen_tcp = SockMod, + EncodedLine = iconv:convert("utf-8", Encoding, Line), + ok = gen_tcp:send(Socket, [EncodedLine, 13, 10]). + +send_command(Sender, Command, Params, State) -> + send_command(Sender, Command, Params, State, false). + +%% Some IRC software require commands with text to have the text +%% quoted, even it's not if not necessary. +send_text_command(Sender, Command, Params, State) -> + send_command(Sender, Command, Params, State, true). + +send_command(Sender, Command, Params, State, AlwaysQuote) -> + Prefix = case Sender of + "" -> + [$: | State#state.host]; + _ -> + [$: | Sender] + end, + ParamString = make_param_string(Params, AlwaysQuote), + send_line(Prefix ++ " " ++ Command ++ ParamString, State). + +send_reply(Reply, Params, State) -> + Number = case Reply of + 'ERR_UNKNOWNCOMMAND' -> + "421"; + 'ERR_ERRONEUSNICKNAME' -> + "432"; + 'ERR_NICKCOLLISION' -> + "436"; + 'ERR_NOTONCHANNEL' -> + "442"; + 'ERR_NOCHANMODES' -> + "477"; + 'ERR_UMODEUNKNOWNFLAG' -> + "501"; + 'ERR_USERSDONTMATCH' -> + "502"; + 'RPL_UMODEIS' -> + "221"; + 'RPL_CHANNELMODEIS' -> + "324"; + 'RPL_NAMREPLY' -> + "353"; + 'RPL_ENDOFNAMES' -> + "366"; + 'RPL_BANLIST' -> + "367"; + 'RPL_ENDOFBANLIST' -> + "368"; + 'RPL_NOTOPIC' -> + "331"; + 'RPL_TOPIC' -> + "332"; + 'RPL_MOTD' -> + "372"; + 'RPL_MOTDSTART' -> + "375"; + 'RPL_ENDOFMOTD' -> + "376" + end, + send_text_command("", Number, Params, State). + +make_param_string([], _) -> ""; +make_param_string([LastParam], AlwaysQuote) -> + case {AlwaysQuote, LastParam, lists:member($\ , LastParam)} of + {true, _, _} -> + " :" ++ LastParam; + {_, _, true} -> + " :" ++ LastParam; + {_, [$:|_], _} -> + " :" ++ LastParam; + {_, _, _} -> + " " ++ LastParam + end; +make_param_string([Param | Params], AlwaysQuote) -> + case lists:member($\ , Param) of + false -> + " " ++ Param ++ make_param_string(Params, AlwaysQuote) + end. + +find_el(Name, NS, [{xmlelement, N, Attrs, _} = El|Els]) -> + XMLNS = xml:get_attr_s("xmlns", Attrs), + case {Name, NS} of + {N, XMLNS} -> + El; + _ -> + find_el(Name, NS, Els) + end; +find_el(_, _, []) -> + nothing. + +channel_to_jid([$#|Channel], State) -> + channel_to_jid(Channel, State); +channel_to_jid(Channel, #state{muc_host = MucHost, + channels_to_jids = ChannelsToJids}) -> + case ?DICT:find(Channel, ChannelsToJids) of + {ok, RoomJID} -> RoomJID; + _ -> jlib:make_jid(Channel, MucHost, "") + end. + +channel_nick_to_jid(Nick, [$#|Channel], State) -> + channel_nick_to_jid(Nick, Channel, State); +channel_nick_to_jid(Nick, Channel, #state{muc_host = MucHost, + channels_to_jids = ChannelsToJids}) -> + case ?DICT:find(Channel, ChannelsToJids) of + {ok, RoomJID} -> jlib:jid_replace_resource(RoomJID, Nick); + _ -> jlib:make_jid(Channel, MucHost, Nick) + end. + +jid_to_channel(#jid{user = Room} = RoomJID, + #state{jids_to_channels = JidsToChannels}) -> + case ?DICT:find(jlib:jid_remove_resource(RoomJID), JidsToChannels) of + {ok, Channel} -> [$#|Channel]; + _ -> [$#|Room] + end. + +make_irc_sender(Nick, #jid{luser = Room} = RoomJID, + #state{jids_to_channels = JidsToChannels}) -> + case ?DICT:find(jlib:jid_remove_resource(RoomJID), JidsToChannels) of + {ok, Channel} -> Nick++"!"++Nick++"@"++Channel; + _ -> Nick++"!"++Nick++"@"++Room + end. +make_irc_sender(#jid{lresource = Nick} = JID, State) -> + make_irc_sender(Nick, JID, State). + +user_jid(#state{nick = Nick, host = Host}) -> + jlib:make_jid(Nick, Host, "irc"). + +filter_cdata(Msg) -> + [{xmlcdata, filter_message(Msg)}]. + +filter_message(Msg) -> + lists:filter( + fun(C) -> + if (C < 32) and + %% Add color support, but break XML: (see https://support.process-one.net/browse/EJAB-1097 ) + %% (C /= 3) and + (C /= 9) and + (C /= 10) and + (C /= 13) -> + false; + true -> true + end + end, Msg). + +translate_action(Msg) -> + case Msg of + [1, $A, $C, $T, $I, $O, $N, $ | Action] -> + "/me "++Action; + _ -> + Msg + end. + +parse_error({xmlelement, "error", _ErrorAttrs, ErrorEls} = ErrorEl) -> + ErrorTextEl = xml:get_subtag(ErrorEl, "text"), + ErrorName = + case ErrorEls -- [ErrorTextEl] of + [{xmlelement, ErrorReason, _, _}] -> + ErrorReason; + _ -> + "unknown error" + end, + ErrorText = + case ErrorTextEl of + {xmlelement, _, _, _} -> + xml:get_tag_cdata(ErrorTextEl); + _ -> + nothing + end, + {ErrorName, ErrorText}. + +error_to_string({xmlelement, "error", _ErrorAttrs, _ErrorEls} = ErrorEl) -> + case parse_error(ErrorEl) of + {ErrorName, ErrorText} when is_list(ErrorText) -> + ErrorName ++ ": " ++ ErrorText; + {ErrorName, _} -> + ErrorName + end; +error_to_string(_) -> + "unknown error". diff --git a/jorge/COPYING b/jorge/COPYING new file mode 100644 index 0000000..d511905 --- /dev/null +++ b/jorge/COPYING @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + 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. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/jorge/README b/jorge/README new file mode 100644 index 0000000..ef2151a --- /dev/null +++ b/jorge/README @@ -0,0 +1,44 @@ +iJorge by Zbyszek Zolkiewski (C) 2009 VERSION 1.5-RC1 + +About. + Jorge is set of php scripts that are front-end for Oleg Palij mod_logdb. + +Licensing. + Jorge is distributed by GPL License provided in COPYING file as well in parts in all source files. +Jorge uses jQuery library and some plugins, please read license info at http://docs.jquery.com/Licensing + +Bug reporting: +If you found any bug or have any improvement idea you can contact me at (email/xmpp): zbyszek@jabster.pl +Patches are most welcome. + +Requirements: +- working ejabberd 2.x (or higher) with mod_logdb (compatibile version is bundled with Jorge) +- mod_xmlrpc - revision 772 +- mysql5 server +- any http server supporting PHP (5.2.x or higher) with gd, mcrypt and xmlrpc support + +Client Requirements: +- Jorge is tested and compatibile with: Firefox 2.x, Firefox 3.x, Opera 9.x, Apple Safri 3.x,4.x and Google Chrome (IE7 and IE8+ may work but it is not recomended due to IE standards violations, IE6 is not supported at all) +- Web-browser _must_ have enabled javascript as Jorge strongly relay on jQuery and other js related scripts +- Cookies must be enabled + +Installing: +- setup XML-RPC facility (mod_xmlrpc, available at http://svn.process-one.net/ejabberd-modules/mod_xmlrpc/trunk/ - Use rev772!) +- copy Jorge files into your http server (sugested vhost over SSL connection). +- set up database for Jorge (install/jorge.sql), see README inside install dir +- copy config.php.inc to config.php and edit file - READ CAREFULLY +- that's it! + +Archive usage: +We can say that 1 milion messeges collected by the server consume approx. 65 Megabytes. Depending on number of users that use your server and +how many will activate logging, you must compute db partitions. + +NO WARRANTY +THE PROGRAM IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, BUT WITHOUT ANY WARRANTY. IT IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +!!!SECURITY NOTICE!!! + +Before use check if you set registered_globals to off. It is *required* for security reasons! +Jorge is compatibile out of the box with: mod_security2 and php-hardened (suhosin) and easy to chroot. +It is strongly recomended to use encrypted sessions. +If you are using Hardened_PHP or Suhosin alter get.max_value_length to some greater value f.e 1024. diff --git a/jorge/calendar_view.php b/jorge/calendar_view.php new file mode 100644 index 0000000..8c33751 --- /dev/null +++ b/jorge/calendar_view.php @@ -0,0 +1,679 @@ +set_overview('

'.$cal_head[$lang].'

'.$cal_notice[$lang].'. '.$change_view[$lang].'

'); + +if (isset($_GET['left'])) { + + if ($enc->decrypt_url($_GET['left']) === true) { + + $left = $enc->tslice; + + } + else { + + unset($left); + + } + +} + +if (isset($_GET['right'])) { + + if ($enc->decrypt_url($_GET['right']) === true) { + + $right = $enc->tslice; + + } + else { + + unset($left); + + } + +} + +$e_string = $_GET['a']; +$resource_id = $_GET['b']; +$start = $_GET['start']; +$jump_to = $_POST['jump_box']; + +if ($jump_to!="") { + + $mo=$jump_to; + +} + +if ($mo === "jump") { + + unset($mo); + +} + +if ($enc->decrypt_url($e_string) === true) { + + $tslice = $enc->tslice; + $talker = $enc->peer_name_id; + $server = $enc->peer_server_id; + $action = $enc->action; + $lnk = $enc->lnk; + + // reencode string: + $e_string = $enc->crypt_url("tslice=$tslice&peer_name_id=$talker&peer_server_id=$server"); + +} + +// avoid unnessesary validation, actualy... +if ($tslice) { + + if (validate_date($tslice) === false) { + + debug(DEBUG,"Date validation failed: $tslice"); + unset ($tslice); + unset($e_string); + unset($talker); + unset($left); + unset($right); + unset($mo); + unset($action); + + } + else{ + + debug(DEBUG,"Date set to: $tslice"); + + } + +} + +// some validation things... +if ($start) { + + if ((validate_start($start))!==true) { + + $start="0"; + + } + +} + +// set idx +if ($_GET['idx']) { + + $idx = $_GET['idx']; + + if ($enc->decrypt_url($idx) === true) { + + if($db->set_ext_index($enc->single) !== true) { + + unset($idx); + unset($action); + + } + + $idx = $enc->single; + + } + else{ + + unset($idx); + unset($action); + + } + +} + +// undo delete +if ($action === "undelete") { + + if ($db->move_chat_from_trash($talker,$server,$tslice,$lnk,$idx) === true) { + + $html->status_message($undo_info[$lang],"message"); + + } + else { + + unset($talker); + $html->alert_message($oper_fail[$lang],"message"); + + } + +} + +if ($action === "delete") { + + if ($db->move_chat_to_trash($talker,$server,$tslice,$lnk) === true) { + + $undo = $enc->crypt_url("tslice=$tslice&peer_name_id=$talker&peer_server_id=$server&lnk=$lnk&action=undelete"); + unset($talker); + $idx = $enc->crypt_url("single=".$db->get_last_idx().""); + $html->status_message('
'.$del_moved[$lang] + .' Undo
'); + + } + + else { + + $html->alert_message($oper_fail[$lang],"message"); + unset($talker); + + } + +} + +// check few condition, what we're doing... +if ($tslice!="") { + + list($y,$m,$selected) = split("-", $tslice); + $mo="$y-$m"; + + } + + else { + + if (isset($left)) { + + $mo=$left; + + } + + if (isset($right)) { + + $mo=$right; + + } + +} + +if (!isset($mo)) { + + $mo = date("Y-n"); + +} + +// validate mo if fail, silently fallback to current date +if (validate_date($mo."-1") === false) { + + unset ($tslice); + unset ($e_string); + unset ($talker); + $mo = date("Y-m"); + +} + +// master div +$html->set_body('
'); + +// calendar div +if ($talker) { + + $float="left;"; + + } + else { + + $float="none;"; + +} + +// select list +$db->get_user_stats_drop_down(); +$ch_mo = $db->result; + +// check if user have some chats +if (count($ch_mo)!=0) { + + $html->set_body('
+
+
'); + + // now generate calendar + $db->get_user_stats_calendar($mo); + $result_for_days = $db->result; + + $i=0; + // days + foreach($result_for_days as $result) { + + $i++; + $days[$i] = str_replace("-","",$result[days]); + + } + + list($y,$m) = split("-", $mo); + $html->set_body(calendar($db,$user_id,$xmpp_host,$y,$m,$days,TOKEN,$url_key,$left,$right,$selected,$lang,$view_type,1,$null_a=0,$null_b=0,$cal_days,$enc,$months_names,$weekdays)); + unset($days); + + } + else { + + $html->status_message($no_archives[$lang]); + +} + +// if we got day, lets display chats from that day but only if there are some +if ($tslice) { + + $db->get_user_chats($tslice); + $result = $db->result; + if (count($result)>0) { + + $display_conversations = true; + + } + else{ + + $display_conversations = false; + + } + +} + +if ($display_conversations === true) { + + // we need to sort list by nickname so we need to combine 2 results: roster and mod_logdb chatlist: + foreach($result as $sort_me) { + + $roster_name = query_nick_name($ejabberd_roster,$sort_me[username],$sort_me[server_name]); + $arr_key++; + + if (!$roster_name) { + + // split contact into 2 arrays: one with full jids, second without names - transports, agents.. + $sorted_spec[$arr_key] = array( + "roster_name"=>$roster_name, + "username"=>$sort_me[username], + "server_name"=>$sort_me[server_name], + "todaytalk"=>$sort_me[todaytalk], + "server"=>$sort_me[server], + "lcount"=>$sort_me[lcount] + ); + } + else { + + $sorted_list[$arr_key] = array( + "roster_name"=>$roster_name, + "username"=>$sort_me[username], + "server_name"=>$sort_me[server_name], + "todaytalk"=>$sort_me[todaytalk], + "server"=>$sort_me[server], + "lcount"=>$sort_me[lcount] + ); + } + + } + // sort and split two lists: normal contacts and special contacts. + asort($sorted_list); + + if (!$show_spec) { + + $show_spec="1"; + + } + + if ($sorted_spec AND $show_spec === "1") { + + if ($sorted_list) { + + $sorted_list = array_merge($sorted_list,$sorted_spec); + + } + else{ + + $sorted_list = $sorted_spec; + + } + + } + + $html->set_body(' + + + + + + + + + + + + + + + + +
+ + + + + + + +
'.$chat_list_l[$lang].'
+ '); + + // select chatters + foreach ($sorted_list as $entry) { + + $user_name = $entry[username]; + $server_name = $entry[server_name]; + if ($talker==$entry["todaytalk"] AND $server==$entry[server]) { + + $bold_b=""; + $bold_e=""; + $mrk=1; + + } + else { + + $bold_b=""; + $bold_e=""; + $mrk=0; + + } + + $nickname = $entry[roster_name]; + if (!$nickname) { + + $calday_class="caldays4"; + $nickname = $not_in_r[$lang]; + $spec_con = '
('.htmlspecialchars($server_name).')'; + unset($malpa); + + } + else { + + $calday_class="caldays3"; + unset($spec_con); + $malpa = "@"; + + } + + if ($mrk==1) { + + $db->get_next_prev_day($entry[todaytalk],$entry[server],$tslice,"p"); + $previous_t = $db->result->at; + $to_base_prev = $enc->crypt_url("tslice=$previous_t&peer_name_id=$entry[todaytalk]&peer_server_id=$entry[server]"); + + $db->get_next_prev_day($entry[todaytalk],$entry[server],$tslice,"n"); + $next_t = $db->result->at; + $to_base_next = $enc->crypt_url("tslice=$next_t&peer_name_id=$entry[todaytalk]&peer_server_id=$entry[server]"); + + } + + $to_base2 = $enc->crypt_url("tslice=$tslice&peer_name_id=$entry[todaytalk]&peer_server_id=$entry[server]"); + if ($mrk==1 AND $previous_t != NULL) { + + $html->set_body('<<< '); + + } + + $html->set_body(''.$bold_b.cut_nick($nickname).$bold_e.''); + + if ($mrk==1 AND $next_t != NULL) { + + $html->set_body(' >>>'); + } + + if ($spec_con) { + + $html->set_body($bold_b.$spec_con.$bold_e); + + } + + $html->set_body('
'); + + } + + $html->set_body(' +
+
+ '); +} + +$html->set_body('
'); + +// Chat thread: +if ($talker) { + + $html->set_body(''); + if (!$start) { + + $start="0"; + + } + $db->get_num_lines($tslice,$talker,$server); + $nume = $db->result->cnt; + if ($start>$nume) { + + $start=$nume-$num_lines_bro; + + } + + $db->get_user_name($talker); + $talker_name = $db->result->username; + $db->get_server_name($server); + $server_name = $db->result->server_name; + $nickname = query_nick_name($ejabberd_roster,$talker_name,$server_name); + if ($nickname === "") { + + $nickname=$not_in_r[$lang]; + $spec_mark = true; + + } + else { + + $spec_mark = false; + + } + + $predefined = $enc->crypt_url("jid=$talker_name@$server_name"); + + $html->set_body('
+ + '); + + if ($_GET['loc']) { + + $loc_id=$_GET['loc']; + if ($loc_id=="2") { + + $back_link_message=$chat_map_back[$lang]; + $back_link="chat_map.php?chat_map=$predefined"; + + } + elseif($loc_id=="3") { + + $back_link_message=$fav_back[$lang]; + $back_link="favorites.php"; + + } + elseif($loc_id=="4") { + + $back_link_message=$myl_back[$lang]; + $back_link="my_links.php"; + + } + $html->set_body(''); + + } + if ($resource_id) { + + $db->get_resource_name($resource_id); + $res_display = $db->result->resource_name; + $html->set_body(''); + + } + + $action_link = $enc->crypt_url("tslice=$tslice&peer_name_id=$talker&peer_server_id=$server&lnk=$e_string&action=delete"); + $sess->set('export_nickname',$nickname); // pass to export + $html->set_body(' + + + + + + '); + + if($db->get_user_chat($tslice,$talker,$server,$resource_id,$start,$num_lines_bro) === false) { + + $html->alert_message($oper_fail[$lang]); + + } + // processing messages. this should be handled as separate message_processor, so that tree view and calendar view can share the same code withoud redundancy. To be done in 2.0 + $result = $db->result; + + // some strings to pass to message_processor + $lang_pack = array( + $cont_chat_p[$lang], + $message_type_message[$lang], + $message_type_error[$lang], + $message_type_headline[$lang], + $resource_only[$lang], + $muc_message[$lang], + $my_links_save[$lang], + $verb_h[$lang], + $in_min[$lang], + $cont_chat[$lang] + ); + + // Sent all data to parsing function (message processor) + if (message_processor($tslice,$server_name,$start,$nickname,$result,$db,$html,$enc,TOKEN,$split_line,$lang_pack,$lang,$spec_mark,$e_string,$to_base_prev,$to_base_next) !== true) { + + $html->alert_message($oper_fail[$lang]); + $html->destroy_content(); + + } + + // limiting code + $html->set_body(''); + // limiting code - end + + if (($nume-$start)>40) { + + $html->set_body(''); + + } + + $html->set_body('
+
'.$back_link_message.'
'.$resource_warn[$lang].cut_nick(htmlspecialchars($res_display)).'. ' + .$resource_discard[$lang].''.$resource_discard2[$lang].'
'.$time_t[$lang].' '.$user_t[$lang].' '.$thread[$lang].' + '); + + // check favorite + $db->check_favorite($talker,$server,$tslice); + if ($db->result->cnt < 1) { + + $html->set_body(' +
+ + + +
+ '); + } + else { + + $html->set_body(' +
+ + + '.$fav_favorited[$lang].' +
+ '); + + + } + + $html->set_body(''.$export_link[$lang].'  |  '.$all_for_u[$lang].' + '.$all_for_u_m2[$lang].' +  |  + '.$all_for_u_m[$lang].' +   |   + '.$del_t[$lang].' +
'); + + for($i=0;$i < $nume;$i=$i+$num_lines_bro){ + + if ($i!=$start) { + + if ($resource_id) { + + $add_res="&b=$resource_id"; + } + else { + + $add_res=""; + } + + $html->set_body(' ['.$i.'] '); + } + + else { + + $html->set_body('-'.$i.'- '); + + } + + } + + $html->set_body('
'.$back_t[$lang].'
'); +} + +$html->set_body('
'); + +require_once("footer.php"); +?> diff --git a/jorge/chat_map.php b/jorge/chat_map.php new file mode 100644 index 0000000..cc64e3a --- /dev/null +++ b/jorge/chat_map.php @@ -0,0 +1,152 @@ +set_overview('

'.$chat_map[$lang].'

'.''.$chat_select[$lang].''); + +if ($_POST['chat_map']) { + + $con_map = $enc->decrypt_url($_POST['chat_map']); + + } + elseif ($_GET['chat_map']) { + + $con_map = $enc->decrypt_url($_GET['chat_map']); + + } + +if($con_map === true) { + + $con_map = $enc->jid; + } + else{ + + unset($con_map); +} + +// prepare roster object +$ejabberd_roster->sort_by_nick("az"); +$roster_chat = $ejabberd_roster->get_roster(); + +$html->set_body('


+
+

'.$filter_tip[$lang].'

+ '.$chat_m_select[$lang].' +
'); + +if ($con_map AND $_POST['chat_map'] != "null") { + + // split username and server name + list($name_peer,$server_peer) = split("@",$con_map); + + // get the id's of user and server + $db->get_user_id($name_peer); + $peer_name_id = $db->result->user_id; + $db->get_server_id($server_peer); + $peer_server_id = $db->result->server_id; + + if ($peer_name_id !== null AND $peer_server_id !== null) { + + //first get the months + $db->get_chat_map($peer_name_id,$peer_server_id); + $result1 = $db->result; + $cc_cmp = count($result1); + foreach ($result1 as $row_m) { + + // hack for proper date parsing + list($y,$m) = split("-",$row_m[at]); + $mo="$y-$m"; + + // now get the days in with user was talking + $db->get_chat_map_specyfic($peer_name_id,$peer_server_id,$mo); + $result2 = $db->result; + + foreach($result2 as $row_day) { + + // now scan day for chats, yep thats weak, but as long as we dont have right stats table this will work... + $i++; + list($y,$m,$d) = split("-",$row_day[at]); + $days[$i] = $d; + } + + if (count($days)>=1) { + + $html->set_body('
'); + $html->set_body(calendar($db,$user_id,$xmpp_host,$y,$m,$days,TOKEN,$url_key,$left,$right,$selected,$lang,$view_type,2,$peer_name_id,$peer_server_id,$cal_days,$enc,$months_names,$weekdays)); + $html->set_body('
'); + unset($days); + + } + else { + + $score++; + + } + $i=0; + + } + + } + + else { + + $cc_cmp = $score; + } + + + if ($score==$cc_cmp) { + + $html->set_body('

'.$chat_no_chats[$lang].'

'); + + } + +} + +$html->set_body('
*-'.$ff_notice[$lang].''); +require_once("footer.php"); +?> diff --git a/jorge/class.db.php b/jorge/class.db.php new file mode 100644 index 0000000..2ee23c4 --- /dev/null +++ b/jorge/class.db.php @@ -0,0 +1,3096 @@ +setData($db_host,$db_name,$db_user,$db_password,$db_driver,$xmpp_host); + + } + + + private function setData($db_host,$db_name,$db_user,$db_password,$db_driver,$xmpp_host) { + + $this->db_host = $db_host; + $this->db_name = $db_name; + $this->db_user = $db_user; + $this->db_password = $db_password; + $this->db_driver = $db_driver; + $this->xmpp_host = $this->sql_validate($xmpp_host,"string"); + $this->vhost = str_replace("_",".", $this->sql_validate($xmpp_host,"string")); + + try { + + $this->db_connect(); + + } + catch(Exception $e) { + + echo "
Exception: ".$e->getMessage(); + echo ", Code: ".$e->getCode(); + + } + + if ($this->vhost) { + + $this->set_ignore_id(); + + } + + } + + private function db_mysql() { + + $conn = mysql_connect("$this->db_host", "$this->db_user", "$this->db_password"); + if (!$conn) { + + return false; + + } + if (mysql_select_db($this->db_name)) { + + return true; + + } + + else { + + return false; + + } + + } + + private function do_query($query) { + + $this->show_debug_info($query, $time = false); + if ($this->is_error === false) { + + $this->time_start(); + $result = mysql_query($query); + $this->time_end(); + $this->show_debug_info($query = null, $time = true); + + } + elseif($this->is_error === true) { + + if ($this->is_debug === true) { + if ($this->is_debug === true) { + + throw new Exception("Error before queryID:".$this->id_query,3); + } + } + return false; + } + + if ($result === false ) { + + $this->is_error = true; + if ($this->is_debug === true) { + + throw new Exception("Query error in QueryID:".$this->id_query,2); + } + return false; + + } + else { + + if ($this->query_type === "select" OR $this->query_type="create_table") { + + return $result; + } + elseif($this->query_type === "update" OR $this->query_type === "insert" OR $this->query_type === "replace") { + + return mysql_affected_rows(); + } + elseif($this->query_type === "delete") { + + return $result; + + } + elseif($this->query_type === "transaction") { + + return $result; + + } + } + + return false; + + } + + private function db_query($query) { + + try { + + $result = $this->do_query($query); + + } + catch (Exception $e) { + + echo "
Exception: ".$e->getMessage(); + echo ", Code: ".$e->getCode(); + return false; + } + + return $result; + } + + private function db_connect() { + + if ($this->db_driver === "mysql") { + + if ($this->db_mysql() === true) { + + return true; + } + else { + $this->is_error = true; + if ($this->is_debug === true) { + + throw new Exception("
DB Connection failed!",1); + } + } + } + + return false; + + } + + private function select($query,$return_type = null) { + + $this->query_type="select"; + if (strpos(strtolower($query),"select") === 0) { + + try{ + $this->result = $this->db_query($query); + } + catch(Exception $e) { + echo "
Exception: ".$e->getMessage(); + echo ", Code: ".$e->getCode(); + } + + if($this->is_error===false) { + + if($return_type === null) { + + $this->result = mysql_fetch_object($this->result); + + } + elseif($return_type === "raw") { + + return true; + + } + + return true; + + } + + else{ + + return false; + + } + + } + + else { + + return false; + + } + } + + private function update($query) { + + $this->query_type = "update"; + if (strpos(strtolower($query),"update") === 0) { + + try{ + $this->result = $this->db_query($query); + } + catch(Exception $e) { + echo "
Exception: ".$e->getMessage(); + echo ", Code: ".$e->getCode(); + } + + if($this->is_error===false) { + + return true; + + } + + else{ + + return false; + + } + + } + + else { + + return false; + + } + } + + private function insert($query) { + + $this->query_type = "insert"; + if (strpos(strtolower($query),"insert") === 0) { + + try{ + $this->result = $this->db_query($query); + } + catch(Exception $e) { + echo "
Exception: ".$e->getMessage(); + echo ", Code: ".$e->getCode(); + } + + if($this->is_error===false) { + + return true; + + } + + else{ + + return false; + + } + + } + + else { + + return false; + + } + } + + private function delete($query) { + + $this->query_type = "delete"; + if (strpos(strtolower($query),"delete") === 0) { + + try{ + $this->result = $this->db_query($query); + } + catch(Exception $e) { + echo "
Exception: ".$e->getMessage(); + echo ", Code: ".$e->getCode(); + } + + if ($this->is_error===false) { + + return true; + + } + else { + + return false; + + } + + } + else { + + return false; + + } + } + + private function replace_q($query) { + + $this->query_type = "replace"; + if (strpos(strtolower($query),"replace") === 0) { + + try{ + $this->result = $this->db_query($query); + } + catch(Exception $e) { + echo "
Exception: ".$e->getMessage(); + echo ", Code: ".$e->getCode(); + } + + if ($this->is_error === false) { + + return true; + + } + else { + + return false; + + } + + } + else { + + return false; + + } + } + + public function begin() { + + $this->id_query = "Q001"; + $this->query_type = "transaction"; + return $this->db_query("begin"); + + } + + public function commit() { + + $this->id_query = "Q002"; + $this->query_type = "transaction"; + return $this->db_query("commit"); + + } + + public function rollback() { + + $this->id_query = "Q003"; + $this->query_type = "transaction"; + $this->is_error = false; + return $this->db_query("rollback"); + + } + + public function get_mylinks_count() { + + $this->id_query = "Q004"; + $this->vital_check(); + $query="SELECT + count(id_link) as cnt + FROM + jorge_mylinks + WHERE + owner_id='".$this->user_id."' + AND + vhost='".$this->vhost."' + AND + ext is NULL + + "; + + return $this->select($query); + } + + public function get_trash_count() { + + $this->id_query = "Q005"; + $this->vital_check(); + $query="SELECT + count(*) as cnt + FROM + pending_del + WHERE + owner_id='".$this->user_id."' + AND + vhost='".$this->vhost."' + "; + + return $this->select($query); + + } + + private function row_count($query) { + + $this->id_query = "Q006"; + $result = mysql_num_rows($this->db_query($query)); + if ($result === false) { + + return false; + + } + else{ + + $this->result = $result; + return true; + + } + + } + + public function get_user_id($user) { + + $this->id_query = "Q007"; + $user = $this->sql_validate($user,"string"); + $query="SELECT + user_id + FROM + `logdb_users_".$this->xmpp_host."` + WHERE + username = '$user' + + "; + + return $this->select($query); + + } + + public function get_user_name($user_id,$vhost = null) { + + $this->id_query = "Q008"; + $user_id = $this->sql_validate($user_id,"integer"); + if ($vhost !== null) { + + $vh = $this->vh($vhost,true); + + } + else{ + + $vh = $this->xmpp_host; + + + } + $query="SELECT + username + FROM + `logdb_users_".$vh."` + WHERE + user_id = '$user_id' + + "; + + return $this->select($query); + + } + + public function get_server_id($server) { + + $this->id_query = "Q009"; + $server = $this->sql_validate($server,"string"); + $query="SELECT + server_id + FROM + `logdb_servers_".$this->xmpp_host."` + WHERE + server = '$server' + + "; + + return $this->select($query); + + } + + public function get_server_name($server_id,$vhost = null){ + + $this->id_query = "Q010"; + $server_id = $this->sql_validate($server_id,"integer"); + if ($vhost !== null) { + + $vh = $this->vh($vhost,true); + + } + else{ + + $vh = $this->xmpp_host; + + + } + $query="SELECT + server as server_name + FROM + `logdb_servers_".$vh."` + WHERE + server_id = '$server_id' + + "; + + return $this->select($query); + + } + + public function get_resource_name($resource_id) { + + $this->id_query = "Q012"; + $resource_id = $this->sql_validate($resource_id,"integer"); + $query="SELECT + resource as resource_name + FROM + `logdb_resources_".$this->xmpp_host."` + WHERE + resource_id = '$resource_id' + + "; + + return $this->select($query); + } + + public function get_resource_id($resource) { + + $this->id_query = "Q013"; + $resource = $this->sql_validate($resource,"string"); + $query="SELECT + resource_id + FROM + `logdb_resources_".$this->xmpp_host."` + WHERE + resource = '$resource' + + "; + + return $this->select($query); + + } + + public function get_user_talker_stats($peer_name_id,$peer_server_id){ + + $this->id_query = "Q014"; + $this->prepare($peer_name_id,$peer_server_id,$tslice); + $query="SELECT + at + FROM + `logdb_stats_".$this->xmpp_host."` + WHERE + owner_id='".$this->user_id."' + AND + peer_name_id='".$this->peer_name_id."' + AND + peer_server_id='".$this->peer_server_id."' + ORDER BY + str_to_date(at,'%Y-%m-%d') + ASC + + "; + + $this->select($query,"raw"); + return $this->commit_select(array("at")); + } + + public function get_num_lines($tslice,$peer_name_id,$peer_server_id) { + + $this->id_query = "Q015"; + $this->prepare($peer_name_id,$peer_server_id,$tslice); + $table = $this->construct_table($this->tslice); + $query="SELECT + count(timestamp) as cnt + FROM + `$table` + WHERE + owner_id = '".$this->user_id."' + AND + peer_name_id='".$this->peer_name_id."' + AND + peer_server_id='".$this->peer_server_id."' + + "; + + return $this->select($query); + + } + + public function is_log_enabled() { + + $this->id_query = "Q016"; + + // Return false on non-digit characters, workaround for user not being in sql dictionary. + if (!ctype_digit($this->user_id)) { + + return false; + + } + + $query="SELECT + dolog_default as is_enabled + FROM + `logdb_settings_".$this->xmpp_host."` + WHERE + owner_id='".$this->user_id."' + "; + + if ($this->select($query) === true) { + + if ($this->result->is_enabled === "0") { + + $this->result->is_enabled = false; + return true; + + } + elseif($this->result->is_enabled === "1") { + + $this->result->is_enabled = true; + return true; + + } + else{ + + $this->result->is_enabled = null; + return true; + + } + + + + } + + return false; + + } + + public function total_messages($vhost = null) { + + $this->id_query = "Q017"; + if ($vhost === null) { + + $vh = $this->xmpp_host; + + } + else{ + + $vh = $this->vh($vhost,true); + + } + $query="SELECT + sum(count) as total_messages, + count(owner_id) as total_chats + FROM + `logdb_stats_".$vh."` + "; + + $this->select($query,"raw"); + return $this->commit_select(array("total_messages","total_chats")); + + } + + public function total_chats() { + + $this->id_query = "Q018"; + $query="SELECT + count(owner_id) as total_chats + FROM + `logdb_stats_".$this->xmpp_host."` + "; + return $this->select($query); + + } + + public function get_log_list() { + + $this->id_query = "Q019"; + $this->vital_check(); + $query="SELECT + donotlog_list as donotlog + FROM + `logdb_settings_".$this->xmpp_host."` + WHERE + owner_id = '".$this->user_id."' + "; + + $this->select($query); + $split = explode("\n",$this->result->donotlog); + $this->result = $split; + return true; + + } + + public function set_log($bool) { + + $this->id_query = "Q020"; + $this->vital_check(); + if ($bool === true) { + + $val = 1; + + } + elseif($bool === false) { + + $val = 0; + + } + else{ + + return false; + } + + $query="UPDATE + `logdb_settings_".$this->xmpp_host."` + SET + dolog_default = '$val' + WHERE + owner_id = '".$this->user_id."' + "; + + return $this->update($query); + + } + + public function set_logger($event_id,$event_level,$extra = null) { + + $this->id_query = "Q021"; + $this->vital_check(); + $id_log_detail = $this->sql_validate($event_id,"integer"); + $id_log_level = $this->sql_validate($event_level,"integer"); + $extra = $this->sql_validate($extra,"string"); + $query="INSERT INTO + jorge_logger (id_user,id_log_detail,id_log_level,log_time,extra,vhost) + VALUES + ('".$this->user_id."','$id_log_detail','$id_log_level',NOW(),'$extra','".$this->vhost."') + + "; + + return $this->insert($query); + } + + public function get_user_stats_drop_down() { + + $this->id_query = "Q022"; + $this->vital_check(); + if ($this->spec_ignore === true) { + + $sql = "AND peer_name_id != '".$this->ignore_id."'"; + + } + $query="SELECT + substring(at,1,7) as at_send, + at + FROM + `logdb_stats_".$this->xmpp_host."` + WHERE + owner_id = '".$this->user_id."' + $sql + GROUP BY + substring(at,1,7) + ORDER BY + str_to_date(at,'%Y-%m-%d') + DESC + + "; + + $this->select($query,"raw"); + return $this->commit_select(array("at_send","at")); + } + + public function get_user_stats_calendar($mo) { + + $this->id_query = "Q023"; + $this->vital_check(); + $mo = $this->sql_validate($mo,"string"); + if ($this->spec_ignore === true) { + + $sql = "AND peer_name_id != '".$this->ignore_id."'"; + + } + $query="SELECT + distinct(substring(at,8,9)) as days + FROM + `logdb_stats_".$this->xmpp_host."` + WHERE + owner_id = '".$this->user_id."' + AND + at like '$mo-%' + $sql + ORDER BY + str_to_date(at,'%Y-%m-%d') + DESC + + "; + + $this->select($query,"raw"); + return $this->commit_select(array("days")); + } + + public function get_user_chats($tslice) { + + $this->id_query = "Q024"; + $this->vital_check(); + $xmpp_host = $this->xmpp_host; + $tslice_table = $this->sql_validate($tslice,"string"); + $query="SELECT + a.username, + b.server as server_name, + c.peer_name_id as todaytalk, + c.peer_server_id as server, + c.count as lcount + FROM + `logdb_users_$xmpp_host` a, + `logdb_servers_$xmpp_host` b, + `logdb_stats_$xmpp_host` c + WHERE + c.owner_id = '".$this->user_id."' + AND + a.user_id=c.peer_name_id + AND + b.server_id=c.peer_server_id + AND + c.at = '$tslice' + ORDER BY + lower(username) + + "; + + $this->select($query,"raw"); + return $this->commit_select(array("username","server_name","todaytalk","server","lcount")); + + } + + public function get_user_chat($tslice,$peer_name_id,$peer_server_id,$resource_id = null,$start = null,$num_lines = null) { + + $this->id_query = "Q025"; + $this->prepare($peer_name_id,$peer_server_id,$tslice); + if ($resource_id !== null) { + + $resource_id = $this->sql_validate($resource_id,"integer"); + $sql = "AND (peer_resource_id='$resource_id' OR peer_resource_id='1')"; + + } + else{ + + settype($sql,"null"); + } + + $offset_start = $start; + if ($offset_start === null) { + + $offset_start = "0"; + + } + + $offset_end = $start + $num_lines; + $offset_start = $this->sql_validate($offset_start,"integer"); + $offset_end = $this->sql_validate($offset_end,"integer"); + $tslice_table = $this->construct_table($this->tslice); + $query="SELECT + from_unixtime(timestamp+0) as ts, + direction, + type, + subject, + peer_name_id, + peer_server_id, + peer_resource_id, + body + FROM + `$tslice_table` + FORCE INDEX + (search_i) + WHERE + owner_id = '".$this->user_id."' + AND + peer_name_id='".$this->peer_name_id."' + AND + peer_server_id='".$this->peer_server_id."' + $sql + AND + ext is NULL + ORDER BY + ts + LIMIT + $offset_start,$offset_end + "; + + $this->select($query,"raw"); + return $this->commit_select(array("ts","direction","type","subject","peer_name_id","peer_server_id","peer_resource_id","body")); + + } + + public function get_uniq_chat_dates($limit_start = null, $limit_end = null, $limited = false, $start = null, $peer_name_id = null, $peer_server_id = null) { + + $this->id_query = "Q026"; + $this->vital_check(); + $user_id = $this->user_id; + $xmpp_host = $this->xmpp_host; + if ($limit_start !== null AND $limit_end !== null) { + + $limit_start = $this->sql_validate($limit_start,"date"); + $limit_end = $this->sql_validate($limit_end,"date"); + $sql=" AND str_to_date(at,'%Y-%m-%d') >= str_to_date('$limit_start','%Y-%m-%d') AND str_to_date(at,'%Y-%m-%d') <= str_to_date('$limit_end','%Y-%m-%d')"; + + } + else{ + + settype($sql,"null"); + + } + + if ($limited === true) { + + if ($start == "" OR $start === null) { + + $start = "0"; + + } + + $start = $this->sql_validate($start,"integer"); + $sql2=" limit $start,10000"; + + } + else{ + + settype($sql2,"null"); + + } + + if ($peer_name_id !== null AND $peer_server_id !== null) { + + $peer_name_id = $this->sql_validate($peer_name_id,"integer"); + $peer_server_id = $this->sql_validate($peer_server_id,"integer"); + $sql3 = "AND peer_name_id = '$peer_name_id' AND peer_server_id = '$peer_server_id'"; + + } + else{ + + settype($sql3,"null"); + + } + + $query="SELECT + distinct(at) + FROM + `logdb_stats_$xmpp_host` + WHERE + owner_id='$user_id' $sql3 $sql + ORDER BY + str_to_date(at,'%Y-%m-%d') + ASC + $sql2 + "; + + $this->select($query,"raw"); + return $this->commit_select(array("at")); + + } + + public function check_thread($tslice,$peer_name_id,$peer_server_id,$begin_hour,$end_hour) { + + $this->id_query = "Q027"; + $this->prepare($peer_name_id,$peer_server_id,$tslice); + $xmpp_host = $this->xmpp_host; + $tslice_table = $this->construct_table($this->tslice); + $query="SELECT + 1 + FROM + `$tslice_table` + WHERE + owner_id='".$this->user_id."' + AND + peer_name_id='".$this->peer_name_id."' + AND + peer_server_id='".$this->peer_server_id."' + AND + from_unixtime(timestamp) >= str_to_date('".$this->tslice." $begin_hour','%Y-%m-%d %H:%i:%s') + AND + from_unixtime(timestamp) <= str_to_date('".$this->tslice." $end_hour','%Y-%m-%d %H:%i:%s') + ORDER BY + from_unixtime(timestamp) + + "; + + return $this->row_count($query); + + } + + public function get_chat_map($peer_name_id,$peer_server_id) { + + $this->id_query = "Q028"; + $this->prepare($peer_name_id,$peer_server_id,$tslice); + $query="SELECT + substring(at,1,7) as at + FROM + `logdb_stats_".$this->xmpp_host."` + WHERE + owner_id='".$this->user_id."' + AND + peer_name_id='".$this->peer_name_id."' + AND + peer_server_id='".$this->peer_server_id."' + GROUP BY + substring(at,1,7) + ORDER BY + str_to_date(at,'%Y-%m-%d') + ASC + + "; + + $this->select($query,"raw"); + return $this->commit_select(array("at")); + + } + + public function get_chat_map_specyfic($peer_name_id,$peer_server_id,$month) { + + $this->id_query = "Q029"; + $this->prepare($peer_name_id,$peer_server_id,$tslice); + $mo = $this->sql_validate($month,"string"); + $query="SELECT + at + FROM + `logdb_stats_".$this->xmpp_host."` + WHERE + owner_id='".$this->user_id."' + AND + peer_name_id='".$this->peer_name_id."' + AND + peer_server_id='".$this->peer_server_id."' + AND + at like '$mo-%' + + "; + + $this->select($query,"raw"); + return $this->commit_select(array("at")); + + } + + public function add_mylink($peer_name_id,$peer_server_id,$link_date,$link,$desc) { + + $this->id_query = "Q030"; + $this->prepare($peer_name_id,$peer_server_id,$tslice); + $datat = $this->sql_validate($link_date,"string"); + $lnk = $this->sql_validate($link,"string"); + $desc = $this->sql_validate($desc,"string"); + $query="INSERT INTO + jorge_mylinks (owner_id,peer_name_id,peer_server_id,datat,link,description,vhost) + VALUES ( + '".$this->user_id."', + '".$this->peer_name_id."', + '".$this->peer_server_id."', + '$datat', + '$lnk', + '$desc', + '".$this->vhost."' + ) + + "; + + return $this->insert($query); + + } + + public function del_mylink($link_id) { + + $this->id_query = "Q031"; + $this->vital_check(); + $link_id = $this->sql_validate($link_id,"integer"); + $query="DELETE FROM + jorge_mylinks + WHERE + owner_id='".$this->user_id."' + AND + vhost='".$this->vhost."' + AND + id_link='$link_id' + + "; + + return $this->delete($query); + + } + + public function get_mylink() { + + $this->id_query = "Q032"; + $this->vital_check(); + $query="SELECT + id_link, + peer_name_id, + peer_server_id, + datat, + link, + description, + ext + FROM + jorge_mylinks + WHERE + owner_id='".$this->user_id."' + AND + vhost='".$this->vhost."' + AND + ext is NULL + ORDER BY + str_to_date(datat,'%Y-%m-%d') + DESC + + "; + + $this->select($query,"raw"); + return $this->commit_select(array("id_link","peer_name_id","peer_server_id","datat","link","description","ext")); + + + } + + public function update_log_list($log_list) { + + $this->id_query = "Q033"; + $this->vital_check(); + $log_list = $this->sql_validate($log_list,"string"); + $query="UPDATE + `logdb_settings_".$this->xmpp_host."` + SET + donotlog_list='$log_list' + WHERE + owner_id='".$this->user_id."' + "; + return $this->update($query); + + } + + public function logger_get_events($event_id = null,$level_id = null, $offset = null,$lang = null) { + + $this->id_query = "Q034"; + $this->vital_check(); + $offset = $this->sql_validate($offset,"integer"); + if ($event_id !== null) { + + $event_id = $this->sql_validate($event_id,"integer"); + $sql_1 = "and id_log_detail='$event_id'"; + + } + if ($level_id !== null) { + + $level_id = $this->sql_validate($level_id,"integer"); + $sql_2 = "and id_log_level='$level_id'"; + + } + $query="SELECT + b.id_event, + b.event AS event, + c.level AS level, + c.id_level, + a.log_time, + a.extra + FROM + jorge_logger a, + jorge_logger_dict b, + jorge_logger_level_dict c + WHERE + a.id_log_detail=b.id_event + AND + c.id_level=a.id_log_level + AND + id_user='".$this->user_id."' + AND + a.vhost='".$this->vhost."' + AND + b.lang = '$lang' + AND + c.lang = '$lang' + + $sql_1 + $sql_2 + + ORDER BY + log_time + DESC LIMIT + $offset,300 + "; + + $this->select($query,"raw"); + return $this->commit_select(array("id_event","event","level","id_level","log_time","extra")); + + } + + public function get_num_events($event_id = null,$level_id = null) { + + $this->id_query = "Q035"; + $this->vital_check(); + if ($event_id !== null) { + + $event_id = $this->sql_validate($event_id,"integer"); + $sql_1 = "AND id_log_detail='$event_id'"; + } + if ($level_id !== null) { + + $level_id = $this->sql_validate($level_id,"integer"); + $sql_2 = "AND id_log_level='$level_id'"; + } + $query="SELECT + count(id_user) AS cnt + FROM + jorge_logger + WHERE + id_user='".$this->user_id."' + AND + vhost='".$this->vhost."' + + $sql_1 + $sql_2 + "; + + return $this->select($query); + + } + + public function get_trashed_items() { + + $this->id_query = "Q036"; + $this->vital_check(); + $query="SELECT + peer_name_id, + peer_server_id, + date, + timeframe, + type, + idx + FROM + pending_del + WHERE + owner_id = '".$this->user_id."' + AND + vhost='".$this->vhost."' + ORDER BY + str_to_date(date,'%Y-%m-%d') + DESC + "; + + $this->select($query,"raw"); + return $this->commit_select(array("peer_name_id","peer_server_id","date","timeframe","type","idx")); + + } + + public function move_chat_to_trash($peer_name_id,$peer_server_id,$tslice,$link) { + + $this->id_query = "Q037"; + $this->prepare($peer_name_id,$peer_server_id,$tslice); + $xmpp_host = $this->xmpp_host; + $table = $this->construct_table($this->tslice); + + if ($this->get_ext_index($table) !== true) { + + return false; + + } + else{ + + if (!$this->result->idx) { + + $this->ext_idx = "1"; + + } + else{ + + $this->ext_idx = $this->result->idx; + + } + + } + + $this->begin(); + if ($this->set_undo_table($this->peer_name_id,$this->peer_server_id,$this->tslice,"chat") === false) { + + $this->rollback(); + return false; + + } + + if ($this->remove_user_stats($this->peer_name_id,$this->peer_server_id,$this->tslice) === false) { + + $this->rollback(); + return false; + + } + + if ($this->move_mylink_to_trash($peer_name_id,$link) === false) { + + $this->rollback(); + return false; + + } + + if ($this->move_fav_to_trash($peer_name_id,$peer_server_id,$tslice) === false) { + + $this->rollback(); + return false; + + } + + $this->id_query = "Q037a"; + $query="UPDATE + `$table` + SET + ext = '".$this->ext_idx."' + WHERE + owner_id='".$this->user_id."' + AND + peer_name_id='".$this->peer_name_id."' + AND + peer_server_id='".$this->peer_server_id."' + AND + ext is NULL + + "; + + if ($this->update($query) === false) { + + $this->rollback(); + return false; + + } + else{ + + $this->commit(); + $this->set_logger("4","1"); + return true; + } + } + + private function remove_user_stats($peer_name_id,$peer_server_id,$tslice) { + + $this->id_query = "Q038"; + $query="DELETE FROM + `logdb_stats_".$this->xmpp_host."` + WHERE + owner_id='".$this->user_id."' + AND + peer_name_id='$peer_name_id' + AND + peer_server_id='$peer_server_id' + AND + at='$tslice' + "; + + return $this->delete($query); + + } + + public function move_mylink_to_trash($peer_name_id,$link) { + + $this->id_query = "Q039"; + $this->vital_check(); + $peer_name_id = $this->sql_validate($peer_name_id,"integer"); + $lnk = $this->sql_validate($link,"string"); + $query="UPDATE + jorge_mylinks + SET + ext='".$this->ext_idx."' + WHERE + owner_id ='".$this->user_id."' + AND + vhost='".$this->vhost."' + AND + peer_name_id='$peer_name_id' + AND + link like '$lnk%' + AND + ext is NULL + "; + + return $this->update($query); + + } + + public function move_fav_to_trash($peer_name_id,$peer_server_id,$tslice) { + + $this->id_query = "Q040"; + $this->prepare($peer_name_id,$peer_server_id,$tslice); + $query="UPDATE + jorge_favorites + SET + ext='".$this->ext_idx."' + WHERE + owner_id='".$this->user_id."' + AND + peer_name_id='".$this->peer_name_id."' + AND + peer_server_id='".$this->peer_server_id."' + AND + tslice='".$this->tslice."' + AND + vhost='".$this->vhost."' + AND + ext is NULL + "; + + return $this->update($query); + + } + + private function set_undo_table($peer_name_id,$peer_server_id,$tslice,$type = null) { + + $this->id_query = "Q041"; + $query="INSERT INTO + pending_del(owner_id,peer_name_id,date,peer_server_id,type,idx,vhost) + VALUES ( + '".$this->user_id."', + '$peer_name_id', + '$tslice', + '$peer_server_id', + '$type', + '".$this->ext_idx."', + '".$this->vhost."' + ) + + "; + + return $this->insert($query); + + } + + private function unset_undo_table($peer_name_id,$peer_server_id,$tslice,$type = null) { + + $this->id_query = "Q042"; + $query="DELETE FROM + pending_del + WHERE + owner_id='".$this->user_id."' + AND + peer_name_id='$peer_name_id' + AND + date='$tslice' + AND + peer_server_id='$peer_server_id' + AND + idx = '".$this->ext_idx."' + AND + vhost='".$this->vhost."' + "; + + return $this->delete($query); + } + + public function move_chat_from_trash($peer_name_id,$peer_server_id,$tslice,$link,$idx = null) { + + $this->id_query = "Q043"; + $this->prepare($peer_name_id,$peer_server_id,$tslice); + $xmpp_host = $this->xmpp_host; + $table = $this->construct_table($this->tslice); + if (!$idx) { + + $this->ext_idx = "1"; + } + else{ + + if (ctype_digit($idx)) { + + $this->ext_idx = $idx; + + } + else{ + + return false; + + } + + } + + // Message tables are not transactional, so this make some trouble for us to control all error conditions :/ + $query="UPDATE + `$table` + SET + ext = NULL + WHERE + owner_id='".$this->user_id."' + AND + peer_name_id='".$this->peer_name_id."' + AND + peer_server_id='".$this->peer_server_id."' + AND + ext = '".$this->ext_idx."' + "; + + if ($this->update($query) === false) { + + return false; + + } + + $this->begin(); + if ($this->unset_undo_table($this->peer_name_id,$this->peer_server_id,$this->tslice) === false) { + + $this->rollback(); + return false; + + } + + if ($this->recount_messages($this->peer_name_id,$this->peer_server_id,$this->tslice) === true) { + + $stats = $this->result->cnt; + + } + else { + + $this->rollback(); + return false; + } + + if ($this->if_chat_exist($this->peer_name_id,$this->peer_server_id,$this->tslice) === true) { + + + if ($this->result->cnt == 1) { + + if ($this->update_stats($this->peer_name_id,$this->peer_server_id,$this->tslice,$stats) === false) { + + $this->rollback(); + return false; + } + + } + else { + + if ($this->insert_stats($this->peer_name_id,$this->peer_server_id,$this->tslice,$stats) === false) { + + $this->rollback(); + return false; + + } + } + + } + else{ + + $this->rollback(); + return false; + } + + if ($this->move_mylink_from_trash($peer_name_id,$link) === false) { + + $this->rollback(); + return false; + + } + + if ($this->move_fav_from_trash($peer_name_id,$peer_server_id,$tslice) === false) { + + $this->rollback(); + return false; + } + + $this->commit(); + return true; + + + } + + private function if_chat_exist($peer_name_id,$peer_server_id,$tslice) { + + $this->id_query = "Q044"; + $query="SELECT + 1 as cnt + FROM + `logdb_stats_".$this->xmpp_host."` + WHERE + owner_id = '".$this->user_id."' + AND + peer_name_id='$peer_name_id' + AND + peer_server_id='$peer_server_id' + AND + at = '$tslice' + + "; + + return $this->select($query); + + } + + private function insert_stats($peer_name_id,$peer_server_id,$tslice,$stats) { + + $this->id_query = "Q045"; + $query="INSERT INTO + `logdb_stats_".$this->xmpp_host."` (owner_id,peer_name_id,peer_server_id,at,count) + VALUES + ( + '".$this->user_id."', + '$peer_name_id', + '$peer_server_id', + '$tslice', + '$stats + ') + + "; + + return $this->insert($query); + } + + private function update_stats($peer_name_id,$peer_server_id,$tslice,$stats) { + + $this->id_query = "Q046"; + $query="UPDATE + `logdb_stats_".$this->xmpp_host."` + SET + count='$stats' + WHERE + owner_id='".$this->user_id."' + AND + peer_name_id='$peer_name_id' + AND + peer_server_id='$peer_server_id' + AND + at='$tslice' + + "; + + return $this->update($query); + } + + private function recount_messages($peer_name_id,$peer_server_id,$tslice) { + + $this->id_query = "Q047"; + $table = $this->construct_table($tslice); + $query="SELECT + count(timestamp) as cnt + FROM + `$table` + WHERE + owner_id='".$this->user_id."' + AND + peer_name_id='$peer_name_id' + AND + peer_server_id='$peer_server_id' + AND + ext is NULL + "; + + return $this->select($query); + + } + + private function move_mylink_from_trash($peer_name_id,$link) { + + $this->id_query = "Q048"; + $lnk = $this->sql_validate($link,"string"); + $query="UPDATE + jorge_mylinks + SET + ext = NULL + WHERE + owner_id ='".$this->user_id."' + AND + vhost='".$this->vhost."' + AND + peer_name_id='$peer_name_id' + AND + ext = '".$this->ext_idx."' + AND + link like '$link%' + "; + + return $this->update($query); + + } + + private function move_fav_from_trash($peer_name_id,$peer_server_id,$tslice) { + + $this->id_query = "Q049"; + $query="UPDATE + jorge_favorites + SET + ext = NULL + WHERE + owner_id='".$this->user_id."' + AND + peer_name_id='$peer_name_id' + AND + peer_server_id='$peer_server_id' + AND + tslice='$tslice' + AND + ext = '".$this->ext_idx."' + AND + vhost='".$this->vhost."' + "; + + return $this->update($query); + + } + + public function delete_messages($peer_name_id,$peer_server_id,$tslice) { + + $this->id_query = "Q050"; + $this->prepare($peer_name_id,$peer_server_id,$tslice); + $table = $this->construct_table($this->tslice,"date"); + $query="DELETE FROM + `$table` + WHERE + owner_id='".$this->user_id."' + AND + peer_name_id='".$this->peer_name_id."' + AND + peer_server_id='".$this->peer_server_id."' + AND + ext = '".$this->ext_idx."' + + "; + + return $this->delete($query); + + } + + public function delete_mylinks($peer_name_id,$peer_server_id,$tslice) { + + $this->id_query = "Q051"; + $this->prepare($peer_name_id,$peer_server_id,$tslice); + $query="DELETE FROM + jorge_mylinks + WHERE + owner_id='".$this->user_id."' + AND + vhost='".$this->vhost."' + AND + ext='".$this->ext_idx."' + AND + peer_name_id = '".$this->peer_name_id."' + AND + peer_server_id='".$this->peer_server_id."' + AND + datat = '".$this->tslice."' + + "; + + return $this->delete($query); + + } + + public function delete_favorites($peer_name_id,$peer_server_id,$tslice) { + + $this->id_query = "Q052"; + $this->prepare($peer_name_id,$peer_server_id,$tslice); + $query="DELETE FROM + jorge_favorites + WHERE + owner_id='".$this->user_id."' + AND + peer_name_id='".$this->peer_name_id."' + AND + peer_server_id='".$this->peer_server_id."' + AND + tslice='".$this->tslice."' + AND + ext = '".$this->ext_idx."' + AND + vhost='".$this->vhost."' + "; + + return $this->delete($query); + + } + + public function search_query($tslice) { + + $this->id_query = "Q053"; + $this->vital_check(); + if ($this->user_query === null) { + + return false; + + } + $table = $this->construct_table($this->sql_validate($tslice,"date")); + $query="SELECT + timestamp AS ts, + peer_name_id, + peer_server_id, + direction, + ext, + body, + MATCH(body) AGAINST('".$this->user_query."' IN BOOLEAN MODE) AS score + FROM + `$table` + FORCE INDEX + (search_i) + WHERE + MATCH(body) AGAINST('".$this->user_query."' IN BOOLEAN MODE) + AND + owner_id='".$this->user_id."' + LIMIT + 0,10000 + "; + + $this->select($query,"raw"); + return $this->commit_select(array("ts","peer_name_id","peer_server_id","direction","ext","body","score")); + + } + + public function search_query_chat_stream($peer_name_id,$peer_server_id,$tslice,$start_tag = null) { + + $this->id_query = "Q054"; + $this->prepare($peer_name_id,$peer_server_id,$tslice); + $table = $this->construct_table($this->sql_validate($this->tslice,"date")); + if ($start_tag === null) { + + $start_tag="0"; + } + $start_tag = $this->sql_validate($start_tag,"integer"); + + $query="SELECT + from_unixtime(timestamp+0) AS ts, + peer_name_id, + peer_server_id, + direction, + ext, + body + FROM + `$table` + FORCE INDEX + (search_i) + WHERE + owner_id='".$this->user_id."' + AND + peer_name_id='".$this->peer_name_id."' + AND + peer_server_id='".$this->peer_server_id."' + LIMIT + $start_tag,10000 + "; + + $this->select($query,"raw"); + return $this->commit_select(array("ts","peer_name_id","peer_server_id","direction","ext","body")); + + } + + public function search_query_in_user_chat($peer_name_id,$peer_server_id,$tslice,$start_tag) { + + $this->id_query = "Q055"; + if ($this->user_query === null) { + + return false; + + } + $this->prepare($peer_name_id,$peer_server_id,$tslice); + $table = $this->construct_table($this->sql_validate($this->tslice,"date")); + if ($start_tag === null) { + + $start_tag="0"; + } + $start_tag = $this->sql_validate($start_tag,"integer"); + $query="SELECT + timestamp AS ts, + peer_name_id, + peer_server_id, + direction, + ext, + body , + MATCH(body) AGAINST('".$this->user_query."' IN BOOLEAN MODE) AS score + FROM + `$table` + FORCE INDEX + (search_i) + WHERE + match(body) against('".$this->user_query."' IN BOOLEAN MODE) + AND + owner_id='".$this->user_id."' + AND + peer_name_id='".$this->peer_name_id."' + AND + peer_server_id='".$this->peer_server_id."' + LIMIT + $start_tag,10000 + "; + + $this->select($query,"raw"); + return $this->commit_select(array("ts","peer_name_id","peer_server_id","direction","ext","body","score")); + + } + + public function create_search_results_table() { + + $this->id_query = "Q055"; + $this->query_type = "create_table"; + $query="CREATE TEMPORARY TABLE + jorge_results_table + ( + ts VARCHAR(30), + time_slice VARCHAR(10), + peer_name_id MEDIUMINT, + peer_server_id SMALLINT, + direction ENUM('to','from'), + body TEXT, + score FLOAT, + ext TINYINT + ) + "; + + return $this->db_query($query); + } + + public function insert_data_to_result_table($ts,$time_slice,$peer_name_id,$peer_server_id,$direction,$body,$score,$ext){ + + $this->id_query = "Q056"; + $query="INSERT INTO jorge_results_table + (ts,time_slice,peer_name_id,peer_server_id,direction,body,score,ext) + VALUES ( + '$ts', + '$time_slice', + '$peer_name_id', + '$peer_server_id', + '$direction', + '$body', + '$score', + '$ext' + ) + "; + + return $this->insert($query); + + } + + public function get_search_results() { + + $this->id_query = "Q057"; + $query="SELECT + FROM_UNIXTIME(ts+0) AS ts, + time_slice, + peer_name_id, + peer_server_id, + direction, + body, + score, + ext + FROM + jorge_results_table + ORDER BY + score + DESC LIMIT 100 + + "; + + $this->select($query,"raw"); + return $this->commit_select(array("ts","time_slice","peer_name_id","peer_server_id","direction","body","score","ext")); + + } + + public function get_folder_content($at) { + + $this->id_query = "Q058"; + $this->vital_check(); + $at = $this->sql_validate($at,"string"); + if ($this->spec_ignore === true) { + + $sql = "AND peer_name_id != '".$this->ignore_id."'"; + + } + $query="SELECT + distinct(at) AS at + FROM + `logdb_stats_".$this->xmpp_host."` + WHERE + owner_id = '".$this->user_id."' + AND + substring(at,1,7) = '$at' + $sql + ORDER BY + str_to_date(at,'%Y-%m-%d') + DESC + "; + + $this->select($query,"raw"); + return $this->commit_select(array("at")); + + } + + public function insert_user_id($user_name) { + + $this->id_query = "Q059"; + $user_name = $this->sql_validate($user_name,"string"); + $query="INSERT INTO + `logdb_users_".$this->xmpp_host."` + SET + username='$user_name' + "; + return $this->insert($query); + + } + + public function insert_new_settings($user_name) { + + $this->id_query = "Q060"; + $user_name = $this->sql_validate($user_name,"string"); + $query="INSERT INTO + `logdb_settings_".$this->xmpp_host."` (owner_id,dolog_default) + VALUES + ((SELECT user_id FROM `logdb_users_".$this->xmpp_host."` WHERE username='$user_name'), '1') + + "; + return $this->insert($query); + + } + + public function get_jorge_pref($pref_id = null) { + + $this->id_query = "Q061"; + if ($pref_id !== null) { + + $pref_id = $this->sql_validate($pref_id,"integer"); + $sql = "AND pref_id = '$pref_id'"; + + } + $query="SELECT + pref_id, + pref_value + FROM + jorge_pref + WHERE + owner_id='".$this->user_id."' + $sql + AND + vhost = '".$this->vhost."' + "; + + if ($pref_id === null) { + + $this->select($query,"raw"); + return $this->commit_select(array("pref_id","pref_value")); + + } + else{ + + return $this->select($query); + + } + + return false; + + } + + public function set_jorge_pref($pref_id,$pref_value) { + + $this->id_query = "Q062"; + $this->vital_check(); + $pref_id = $this->sql_validate($pref_id,"integer"); + $pref_value = $this->sql_validate($pref_value,"integer"); + if ($this->row_count("SELECT pref_id FROM jorge_pref WHERE owner_id='".$this->user_id."' AND pref_id='$pref_id' AND vhost='".$this->vhost."'") === false) { + + return false; + + } + if ($this->result > 0) { + + $query="UPDATE + jorge_pref + SET + pref_value='$pref_value' + WHERE + owner_id='".$this->user_id."' + AND + pref_id='$pref_id' + AND + vhost = '".$this->vhost."' + "; + return $this->update($query); + + } + else{ + + $query="INSERT INTO + jorge_pref(owner_id,pref_id,pref_value,vhost) + VALUES + ('".$this->user_id."','$pref_id','$pref_value','".$this->vhost."') + "; + return $this->insert($query); + + } + + return false; + + } + + public function sphinx_get_single($peer_name_id, $peer_server_id, $peer_resource_id, $timestamp, $tslice) { + + $this->id_query = "Q063"; + $this->vital_check(); + $this->prepare($peer_name_id,$peer_server_id,$tslice); + $peer_resource_id = $this->sql_validate($peer_resource_id,"integer"); + $timestamp = $this->sql_validate($timestamp,"string"); + $query="SELECT + body + FROM + `".$this->construct_table($this->tslice)."` + WHERE + owner_id = '".$this->user_id."' + AND + peer_name_id = '".$this->peer_name_id."' + AND + peer_server_id = '".$this->peer_server_id."' + AND + peer_resource_id = '".$peer_resource_id."' + AND + timestamp like '".$timestamp."%' + + "; + + return $this->select($query); + + } + + public function get_favorites() { + + $this->id_query = "Q064"; + $this->vital_check(); + $query="SELECT * + FROM + jorge_favorites + WHERE + owner_id='".$this->user_id."' + AND + vhost='".$this->vhost."' + AND + ext is NULL + ORDER BY + str_to_date(tslice,'%Y-%m-%d') + DESC + "; + + $this->select($query,"raw"); + return $this->commit_select(array("link_id","peer_name_id","peer_server_id","resource_id","tslice","comment")); + + } + + public function set_favorites($peer_name_id,$peer_server_id,$peer_resource_id = null, $tslice,$comment) { + + $this->id_query = "Q065"; + $this->vital_check(); + $this->prepare($peer_name_id,$peer_server_id,$tslice); + #$peer_resource_id = $this->sql_validate($peer_resource_id,"integer"); + $comment = $this->sql_validate($comment,"string"); + $query="INSERT INTO + jorge_favorites(owner_id,peer_name_id,peer_server_id,tslice,comment,vhost) + VALUES( + '".$this->user_id."', + '".$this->peer_name_id."', + '".$this->peer_server_id."', + '".$this->tslice."', + '$comment', + '".$this->vhost."' + ) + "; + + return $this->insert($query); + + } + + public function delete_favorites_id($link_id) { + + $this->id_query = "Q066"; + $this->vital_check(); + $link_id = $this->sql_validate($link_id,"string"); + $query="DELETE FROM + jorge_favorites + WHERE + owner_id = ".$this->user_id." + AND + vhost='".$this->vhost."' + AND + link_id = '$link_id'; + "; + + return $this->delete($query); + + } + + public function check_favorite($peer_name_id,$peer_server_id,$tslice) { + + $this->id_query = "Q067"; + $this->vital_check(); + $this->prepare($peer_name_id,$peer_server_id,$tslice); + $query="SELECT + count(*) as cnt + FROM + jorge_favorites + WHERE + owner_id='".$this->user_id."' + AND + tslice='".$this->tslice."' + AND + peer_name_id='".$this->peer_name_id."' + AND + peer_server_id='".$this->peer_server_id."' + AND + vhost='".$this->vhost."' + AND + ext is null + "; + + return $this->select($query); + + } + + public function get_favorites_count() { + + $this->id_query = "Q068"; + $this->vital_check(); + $query="SELECT + count(*) as cnt + FROM + jorge_favorites + WHERE + owner_id = '".$this->user_id."' + AND + vhost='".$this->vhost."' + AND + ext is null + "; + + return $this->select($query); + + } + + public function get_top_ten($date,$vhost = null) { + + $this->id_query = "Q069"; + if ($vhost === null) { + + $vh = $this->xmpp_host; + + } + else{ + + $vh = $this->vh($vhost,true); + + } + $date = $this->sql_validate($date,"date"); + $query="SELECT + at, + owner_id, + peer_name_id, + peer_server_id, + count + FROM + `logdb_stats_".$vh."` + WHERE + at = '$date' + ORDER BY + count + DESC LIMIT 10 + "; + + $this->select($query,"raw"); + return $this->commit_select(array("at","owner_id","peer_name_id","peer_server_id","count")); + + } + + public function get_monthly_stats($vhost = null) { + + $this->id_query = "Q070"; + if ($vhost === null) { + + $vh = $this->xmpp_host; + + } + else{ + + $vh = $this->vh($vhost,true); + + } + // This query need tweak to use ex.: where at between '2009-2' and '2009-3', it speeds up and corrects query, also forces to use index, instead full table scan + $query="SELECT + count(distinct(owner_id)) AS users_total, + unix_timestamp(at)*10000 AS time_unix, + sum(count) AS messages + FROM + `logdb_stats_".$vh."` + GROUP BY + at + ORDER BY + str_to_date(at,'%Y-%m-%d') + DESC LIMIT 30 + "; + + $this->select($query,"raw"); + return $this->commit_select(array("users_total","time_unix","messages")); + + } + + public function get_hourly_stats($date,$vhost = null) { + + $this->id_query = "Q071"; + if ($vhost === null) { + + $vh = $this->vhost; + + } + else{ + + $vh = $this->vh($vhost); + + } + $date = $this->sql_validate($date,"date"); + $query="SELECT + hour, + value + FROM + jorge_stats + WHERE + day='$date' + AND + vhost='".$vh."' + ORDER BY + hour + ASC + "; + + $this->select($query,"raw"); + return $this->commit_select(array("hour","value")); + + } + + public function get_weekly_stats($date_start,$date_end,$vhost = null) { + + $this->id_query = "Q072"; + if ($vhost === null) { + + $vh = $this->vhost; + + } + else{ + + $vh = $this->sql_validate($vhost,"string"); + + } + $date_start = $this->sql_validate($date_start,"date"); + $date_end = $this->sql_validate($date_end,"date"); + $query="SELECT + hour, + value + FROM + jorge_stats + WHERE + day<='$date_end' + AND + day >= '$date_start' + AND + vhost='".$vh."' + ORDER BY + day,hour + ASC + "; + + $this->select($query,"raw"); + return $this->commit_select(array("hour","value")); + + } + + public function get_personal_top() { + + $this->id_query = "Q073"; + $this->vital_check(); + $query="SELECT + peer_name_id, + peer_server_id, + at, + count + FROM + `logdb_stats_".$this->xmpp_host."` + WHERE + owner_id='".$this->user_id."' + AND + peer_name_id!='".$this->ignore_id."' + AND + ext is NULL + ORDER BY + count + DESC LIMIT 10 + "; + + $this->select($query,"raw"); + return $this->commit_select(array("peer_name_id","peer_server_id","at","count")); + + } + + public function get_personal_sum() { + + $this->id_query = "Q074"; + $this->vital_check(); + $query="SELECT + sum(count) as cnt + FROM + `logdb_stats_".$this->xmpp_host."` + WHERE + owner_id='".$this->user_id."' + AND + peer_name_id!='".$this->ignore_id."' + "; + return $this->select($query); + + } + + public function erase_all() { + + $this->id_query = "Q075"; + $this->vital_check(); + $query="SELECT + distinct(at) + FROM + `logdb_stats_".$this->xmpp_host."` + WHERE + owner_id='".$this->user_id."' + "; + $this->select($query,"raw"); + $this->commit_select(array("at")); + $results = $this->result; + $this->id_query = "Q076"; + foreach ($results as $result) { + + $query="DELETE FROM + `logdb_messages_$result[at]_".$this->xmpp_host."` + WHERE + owner_id='".$this->user_id."' + "; + if ($this->delete($query) === false) { + + return false; + + } + + } + $this->id_query = "Q077"; + $query="DELETE FROM + `logdb_stats_".$this->xmpp_host."` + WHERE + owner_id='".$this->user_id."' + "; + if ($this->delete($query) === false) { + + return false; + + } + if ($this->jorge_cleanup_soft() === false) { + + return false; + + } + + return true; + + } + + public function jorge_cleanup() { + + $this->id_query = "Q081"; + $this->vital_check(); + $query="DELETE FROM + jorge_pref + WHERE + owner_id='".$this->user_id."' + AND + vhost='".$this->vhost."' + "; + if ($this->delete($query) === false) { + + return false; + + } + $this->id_query = "Q082"; + $query="DELETE FROM + `logdb_settings_".$this->xmpp_host."` + where + owner_id='".$this->user_id."' + "; + if ($this->delete($query) === false) { + + return false; + + } + + return true; + + } + + public function jorge_cleanup_soft() { + + $this->id_query = "Q083"; + $this->vital_check(); + $query="DELETE FROM + jorge_mylinks + WHERE + owner_id='".$this->user_id."' + AND + vhost='".$this->vhost."' + "; + if ($this->delete($query) === false) { + + return false; + + } + $this->id_query = "Q084"; + $query="DELETE FROM + jorge_favorites + WHERE + owner_id='".$this->user_id."' + AND + vhost='".$this->vhost."' + "; + if ($this->delete($query) === false) { + + return false; + + } + $this->id_query = "Q085"; + $query="DELETE FROM + pending_del + WHERE + owner_id='".$this->user_id."' + AND + vhost='".$this->vhost."' + "; + if ($this->delete($query) === false) { + + return false; + + } + + return true; + + } + + public function get_next_prev_day($peer_name_id, $peer_server_id, $tslice, $np) { + + $this->id_query = "Q086"; + $this->vital_check(); + $this->prepare($peer_name_id,$peer_server_id,$tslice); + if ($np === "n") { + + $sql1 = ">"; + $sql2 = "ASC"; + } + elseif($np === "p") { + + $sql1 = "<"; + $sql2 = "DESC"; + } + else{ + + return false; + + } + $query="SELECT + at + FROM + `logdb_stats_".$this->xmpp_host."` + WHERE + owner_id='".$this->user_id."' + AND + peer_name_id = '".$this->peer_name_id."' + AND + peer_server_id = '".$this->peer_server_id."' + AND + str_to_date(at, '%Y-%m-%d') $sql1 str_to_date('".$this->tslice."', '%Y-%m-%d') + ORDER BY + str_to_date(at,'%Y-%m-%d') + $sql2 LIMIT 1 + "; + + return $this->select($query); + + } + + public function get_last_day() { + + $this->id_query = "Q087"; + $this->vital_check(); + if ($this->spec_ignore === true) { + + $sql = "AND peer_name_id != '".$this->ignore_id."'"; + + } + + $query="SELECT + at + FROM + `logdb_stats_".$this->xmpp_host."` + WHERE + owner_id = '".$this->user_id."' + $sql + ORDER BY str_to_date(at,'%Y-%m-%d') DESC LIMIT 1 + "; + + return $this->select($query); + + } + + private function get_ignore_id() { + + $this->id_query = "Q088"; + $query="SELECT + user_id AS ignore_id + FROM + `logdb_users_".$this->xmpp_host."` + WHERE + username='' + "; + + return $this->select($query); + + } + + private function set_ignore_id() { + + if ($this->get_ignore_id() === false) { + + return false; + + } + else{ + + $this->ignore_id = $this->result->ignore_id; + return true; + + } + + } + + private function get_ext_index($table) { + + $this->id_query = "Q089"; + $query="SELECT + ext+1 as idx + FROM + `".$table."` + WHERE + owner_id = '".$this->user_id."' + AND + peer_name_id = '".$this->peer_name_id."' + AND + peer_server_id = '".$this->peer_server_id."' + AND + ext is not NULL + ORDER BY + ext + DESC LIMIT 1 + "; + + return $this->select($query); + + } + + public function get_last_attempt($user_id) { + + $this->id_query = "Q090"; + $user_id = $this->sql_validate($user_id,"integer"); + $query="SELECT + count(id_user) AS cnt + FROM + jorge_logger + WHERE + id_user = '$user_id' + AND + log_time > date_sub(now(),interval 1 minute) + "; + + return $this->select($query); + + } + + public function is_left_or_right($date) { + + $this->id_query = "Q091"; + $query="SELECT + at + FROM + `logdb_stats_".$this->xmpp_host."` + WHERE + owner_id='".$this->user_id."' + AND + at like '$date%' + LIMIT 1 + "; + + return $this->row_count($query); + + } + + public function set_own_name($own_name) { + + $this->id_query = "Q092"; + $this->vital_check(); + $own_name = $this->sql_validate($own_name,"string"); + $query="REPLACE INTO + jorge_self_names (owner_id, own_name, vhost) + VALUES + ( + '".$this->user_id."', + '".$own_name."', + '".$this->vhost."' + ) + "; + + return $this->replace_q($query); + + } + + public function get_own_name() { + + $this->id_query = "Q093"; + $this->vital_check(); + $query="SELECT + own_name + FROM + jorge_self_names + WHERE + owner_id='".$this->user_id."' + AND + vhost='".$this->vhost."' + "; + + return $this->select($query); + + } + + public function set_ext_index($idx) { + + /* + This is bit messy as we use ctype_digit() to check if string is an integer, + so any number need to be set as type string and then pass to sql_validate() for validation. + This should be changed in sql_validate() function to take care of all data that it gets... + */ + + settype($idx,"string"); + if ($this->sql_validate($idx,"integer") === false) { + + return false; + + } + else{ + + $this->ext_idx = $idx; + return true; + + } + + return false; + + } + + public function get_last_idx() { + + return $this->ext_idx; + + } + + public function set_user_query($user_query) { + + $this->user_query = $this->sql_validate($user_query,"string"); + return true; + + } + + public function remove_messages_from_trash($peer_name_id,$peer_server_id,$tslice) { + + if ($this->delete_messages($peer_name_id,$peer_server_id,$tslice) === true) { + + $this->unset_undo_table($peer_name_id,$peer_server_id,$tslice); + $this->delete_mylinks($peer_name_id,$peer_server_id,$tslice); + $this->delete_favorites($peer_name_id,$peer_server_id,$tslice); + + } + + else{ + + return false; + + } + + return true; + + } + + public function db_error() { + + return $this->is_error; + + } + + public function set_user_id($user_id) { + + $user_id = $this->sql_validate($user_id,"integer"); + if ($user_id === false) { + + return false; + + } + else { + + $this->user_id = $user_id; + return true; + } + + return false; + + + } + + protected function sql_validate($val,$type) { + + if($this->db_driver === "mysql") { + + if ($type==="integer") { + + // Default - all this "integer" strings are really characters with should be numeric. Need Test! + settype($val,"string"); + + if(ctype_digit($val)) { + + return $val; + + } + else{ + $this->is_error = true; + return false; + } + } + elseif($type==="string") { + + return mysql_escape_string($val); + + } + + elseif($type==="date") { + + list($ye, $mo, $da) = split("-", $val); + if (!ctype_digit($ye) || !ctype_digit($mo) || !ctype_digit($da)) { + + $this->is_error = true; + return false; + + } + else { + + return $val; + + } + + $this->is_error = true; + return false; + } + else{ + $this->is_error = true; + return false; + } + + + } + + return false; + + } + + private function commit_select($arr) { + + if ($this->is_error === true) { + + return false; + + } + + $this->object_to_array($arr); + return true; + + } + + private function object_to_array($arr) { + + settype($i, "integer"); + settype($z, "integer"); + $result = $this->result; + while($row = mysql_fetch_object($result)) { + + $i++; + foreach ($arr as $key) { + + $z++; + $items[$i][$key] = $row->$key; + + } + + + } + + return $this->result = $items; + + } + + private function prepare($peer_name_id = null,$peer_server_id = null, $tslice = null) { + + if ($peer_name_id !== null) { + + $this->peer_name_id = $this->sql_validate($peer_name_id,"integer"); + + } + + if ($peer_server_id !== null) { + + $this->peer_server_id = $this->sql_validate($peer_server_id,"integer"); + + } + + if ($tslice !== null) { + + $this->tslice = $this->sql_validate($tslice,"date"); + + } + + $this->vital_check(); + return; + + } + + private function construct_table($tslice) { + + return $tslice_table = $this->messages_table.''.$tslice.'_'.$this->xmpp_host; + + } + + private function vital_check() { + + if($this->user_id === false OR !$this->user_id) { + + print "

Operation aborted! Can't continue.

"; + exit; // abort all, user_id MUST be set. + } + return true; + + } + + private function vh($vhost,$dash = null) { + + if ($dash === true) { + + return str_replace(".","_",$this->sql_validate($vhost,"string")); + + + } + else{ + + return $this->sql_validate($vhost,"string"); + + } + + } + + public function set_debug($bool) { + + if($bool === true) { + + $this->is_debug = true; + return true; + + } + elseif($bool === false) { + + $this->is_debug = false; + return true; + + } + + return false; + + } + + public function spec_ignore($bool) { + + if($bool === true) { + + $this->spec_ignore = true; + return true; + + } + elseif($bool === false) { + + $this->spec_ignore = false; + return true; + + } + + return false; + + } + + private function show_debug_info($query = null, $time = null) { + + if ($this->is_debug === true) { + + if ($query !== null) { + + print "
QueryID: ".$this->id_query.": ".htmlspecialchars($query)."
"; + } + if ($query === null AND $time !== null) { + + print "SQL performed in: ".$this->time_result."


"; + + } + + } + } + + private function sql_time() { + + list($usec, $sec) = explode(" ",microtime()); + return ((float)$usec + (float)$sec); + } + + private function time_start() { + + if ($this->is_debug === true) { + + return $this->time_start = $this->sql_time(); + + } + + } + + private function time_end() { + + if ($this->is_debug === true) { + + $start = $this->time_start; + $end = $this->sql_time(); + return $this->time_result = substr($end - $start, 0, 10); + + } + + } + + public function __destruct() { + + mysql_free_result(); + mysql_close(); + $this->user_id = null; + $this->result = null; + + } + +} + +?> diff --git a/jorge/class.ejabberd_xmlrpc.php b/jorge/class.ejabberd_xmlrpc.php new file mode 100644 index 0000000..2055921 --- /dev/null +++ b/jorge/class.ejabberd_xmlrpc.php @@ -0,0 +1,257 @@ +set_user("username","password","optional-new_password"); + +this is usefull especialy for scripts like bulk account creation/deletion etc... + +Method: Returned values: +crete_account() true|false|exist +delete_account() true|false +auth() true|false +check_account() true|flase +change_password() true|false +get_roster() Array() +test_rpc() String + +Example of authentication: + +try { + if($ejabberd_rpc->auth() === true) { + + print "Auth OK"; + } + else { + print "Unknown account or bad password"; + } +} +catch(Exception $e) { + echo "Exception: ".$e->getMessage(); + echo ", Code: ".$e->getCode(); +} + +*/################################################## + +class rpc_connector { + + protected $rpc_server; + protected $rpc_port; + protected $username; + protected $password; + protected $newpass; + protected $vhost; + protected $parms; + protected $method; + + public function __construct($rpc_server,$rpc_port,$vhost,$username = null,$password = null,$newpass = null) { + $this->setData($rpc_server,$rpc_port,$vhost,$username,$password,$newpass); + } + + + protected function setData($rpc_server, $rpc_port, $vhost, $username, $password,$newpass) { + $this->rpc_server = $rpc_server; + $this->rpc_port = $rpc_port; + $this->vhost = $vhost; + $this->username = $username; + $this->password = $this->clean_password($password); + $this->newpass = $this->clean_password($newpass); + } + + public function set_user($username,$password,$newpass = null) { + + $this->username = $username; + $this->password = $this->clean_password($password); + $this->newpass = $this->clean_password($newpass); + + } + + protected function commit_rpc() { + + $request = xmlrpc_encode_request($this->method,$this->parms); + $context = stream_context_create(array('http' => array( + 'method' => "POST", + 'header' => "Content-Type: text/xml; charset=utf-8\r\n" . + "User-Agent: XMLRPC::Client JorgeRPCclient", + 'content' => $request + ))); + + $file = file_get_contents("http://$this->rpc_server".":"."$this->rpc_port", false, $context); + $response = xmlrpc_decode($file,"utf8"); + if (xmlrpc_is_fault($response)) { + + throw new Exception("XML-RPC Call Failed. Unrecoverable condition",0); + + } else { + + return $response; + } + + } + + protected function is_value($value) { + + if($value === null) { + return false; + } + elseif($value==""){ + return false; + } + else{ + return true; + } + + + } + + protected function clean_password($password) { + + if (get_magic_quotes_gpc() === 1) { + return stripslashes($password); + } + + return $password; + } + + public function auth() { + + $this->method = "check_password"; + $this->parms = array("user"=>"$this->username","host"=>"$this->vhost","password"=>"$this->password"); + if ($this->commit_rpc() === 0 ) { + + return true; + + } + else{ + + return false; + + } + + + } + + public function create_account() { + + if ($this->is_value($this->username) === false OR $this->is_value($this->password) === false) { return false; } + $this->method = "create_account"; + $this->parms = array("user"=>"$this->username","host"=>"$this->vhost","password"=>"$this->password"); + $call = $this->commit_rpc(); + if ($call === 0) { + + return true; + } + elseif($call === 409) { + return "exist"; + } + elseif($call === 1) { + return false; + } + + } + + public function check_account() { + + if ($this->is_value($this->username) === false) { return false; } + $this->method = "check_account"; + $this->parms = array("user"=>"$this->username","host"=>"$this->vhost"); + if ($this->commit_rpc() === 1) { + + return false; + + } + else{ + + return true; + + } + + } + + public function change_password() { + + if ($this->is_value($this->newpass) === false OR $this->is_value($this->username) === false) { return false; } + $this->method = "change_password"; + $this->parms = array("user"=>"$this->username","host"=>"$this->vhost","newpass"=>"$this->newpass"); + + if ($this->commit_rpc() === 0) { + + $this->password = $this->newpass; + return true; + + } + else{ + return false; + + } + + } + + public function get_roster() { + + $this->method = "get_roster"; + $this->parms = array("user"=>"$this->username","server"=>"$this->vhost"); + return $this->commit_rpc(); + } + + public function delete_account() { + + if ($this->is_value($this->password) === false OR $this->is_value($this->username) === false) { return false; } + $this->method = "delete_account"; + $this->parms = array("user"=>"$this->username","host"=>"$this->vhost","password"=>"$this->password"); + $this->commit_rpc(); + if ($this->check_account() === false) { + + return true; + + } + else { + + return false; + } + } + + public function online_users() { + + $this->method = "online_users"; + $this->parms = "null"; + return $this->commit_rpc(); + } + + public function test_rpc() { + + $this->method = "echothis"; + $this->parms = "If you can read this then RPC is working..."; + return $this->commit_rpc(); + + } + + +} + +?> diff --git a/jorge/class.helper.php b/jorge/class.helper.php new file mode 100644 index 0000000..0aa85bf --- /dev/null +++ b/jorge/class.helper.php @@ -0,0 +1,358 @@ +td = $td; + + } + + public function __destruct() { + + mcrypt_generic_deinit($this->td); + mcrypt_module_close($this->td); + + } + + public function crypt_url($url) { + + return str_replace("+", "kezyt2s0", $this->url_encrypt($url)); + } + + public function decrypt_url($url) { + + $url = str_replace("kezyt2s0", "+",$url); + return $this->decode_string($this->url_decrypt(base64_decode($url))); + + } + + private function url_encrypt($url) { + + $prepared_string = "begin&".$url; + $integrity = md5($prepared_string); + $url = "integrity=$integrity&".$prepared_string; + $td = $this->td; + $c_t = mcrypt_generic($td, $url); + return base64_encode($c_t); + + } + + private function url_decrypt($url) { + + $td = $this->td; + $p_t = mdecrypt_generic($td, $url); + return trim($p_t); + + } + +} + + +Class parser { + + public $tslice = null; + public $peer_name_id = null; + public $peer_name = null; + public $peer_server_id = null; + public $peer_server = null; + public $jid = null; + public $ismylink = null; + public $linktag = null; + public $strt = null; + public $lnk = null; + public $action = null; + public $search_phase = null; + public $offset_arch = null; + public $offset_day = null; + public $tag_count = null; + public $time_start = null; + public $time_end = null; + public $single = null; + + + protected function decode_string($url) { + + parse_str($url); + $reconstructed = strstr($url,"begin"); + settype($integrity,"string"); + if ($integrity === md5($reconstructed)) { + + if (isset($tslice)) { + $this->tslice = $tslice; + } + if (isset($peer_name_id)) { + $this->peer_name_id = $peer_name_id; + } + if (isset($peer_server_id)) { + $this->peer_server_id = $peer_server_id; + } + if (isset($jid)) { + $this->jid = $jid; + } + if (isset($lnk)) { + $this->lnk = $lnk; + } + if (isset($ismylink)) { + $this->ismylink = $ismylink; + } + if (isset($linktag)) { + $this->linktag = $linktag; + } + if (isset($strt)) { + $this->strt = $strt; + } + if (isset($action)) { + $this->action = $action; + } + if (isset($peer_name)) { + $this->peer_name = $peer_name; + } + if (isset($peer_server)) { + $this->peer_server = $peer_server; + } + if (isset($search_phase)) { + $this->search_phase = $search_phase; + } + if (isset($offset_arch)) { + $this->offset_arch = $offset_arch; + } + if (isset($offset_day)) { + $this->offset_day = $offset_day; + } + if (isset($tag_count)) { + $this->tag_count = $tag_count; + } + if (isset($time_start)) { + $this->time_start = $time_start; + } + if (isset($time_end)) { + $this->time_end = $time_end; + } + if (isset($single)) { + $this->single = $single; + } + + return true; + + } + else { + + return false; + + } + + return false; + } + + +} + +Class render_html { + + protected $html_head = array(); + protected $html_menu = array(); + protected $html_over; + protected $html_main = array(); + protected $html_body = array(); + protected $html_foot = array(); + private $head_items = integer; + private $menu_items = integer; + private $main_items = integer; + private $body_items = integer; + private $foot_itesm = integer; + + public function system_message($html) { + + $this->html_main = array("sys_message"=>$this->render_system($html)); + return; + + } + + public function status_message($html) { + + $this->html_main = array("status_message"=>$this->render_status($html)); + return; + + } + + public function alert_message($html) { + + $this->html_main = array("alert_message"=>$this->render_alert($html)); + return; + + } + + public function headers($html) { + + if ($this->head_items === 0) { + + $this->html_head = array("0"=>$html); + $this->head_items = 1; + + } + else{ + + $this->head_items = $this->head_items + 1; + $this->html_head = $this->html_head += array($this->head_items=>$html); + + } + return; + + } + + public function menu($html) { + + if ($this->menu_items === 0) { + + $this->html_menu = array("0"=>$html); + $this->menu_items = 1; + + } + else{ + + $this->menu_items = $this->menu_items + 1; + $this->html_menu = $this->html_menu += array($this->menu_items=>$html); + } + return; + + } + + public function set_overview($html) { + + $this->html_over = $html; + return; + + } + + public function set_body($html) { + + if ($this->body_items === 0) { + + $this->html_body = array("0"=>$html); + $this->body_items = 1; + + } + else{ + + $this->body_items = $this->body_items + 1; + $this->html_body = $this->html_body += array($this->body_items=>$html); + } + return; + + } + + public function foot($html) { + + if ($this->foot_items === 0) { + + $this->html_foot = array("0"=>$html); + $this->foot_items = 1; + } + else{ + + $this->foot_items = $this->foot_items + 1; + $this->html_foot = $this->html_foot += array($this->foot_items=>$html); + } + return; + + } + + public function commit_render() { + + $html_head = $this->html_head; + $html_menu = $this->html_menu; + $html_over = $this->html_over; + $html_main = $this->html_main; + $html_body = $this->html_body; + $html_foot = $this->html_foot; + + for ($z=0;$z<=$this->head_items;$z++) { + + $out .= $html_head[$z]; + + } + $out .= $html_main[sys_message]; + for ($z=0;$z<=$this->menu_items;$z++) { + + $out .= $html_menu[$z]; + + } + $out .= $html_over; + $out .= $html_main[alert_message]; + $out .= $html_main[status_message]; + for ($z=0;$z<=$this->body_items;$z++) { + + $out .= $html_body[$z]; + + } + for ($z=0;$z<=$this->foot_items;$z++) { + + $out .= $html_foot[$z]; + + } + echo $out; + return; + + } + + public function destroy_content() { + + $this->html_body = array(); + return; + + } + + protected function render_alert($message, $class = "message") { + + + return '
'.$message.'

'; + + } + + protected function render_status($message, $class = "message") { + + return '
'.$message.'

'; + + } + + protected function render_system($message, $class = null) { + + return '
'.$message.'

'; + + } + + +} + +?> diff --git a/jorge/class.roster.php b/jorge/class.roster.php new file mode 100644 index 0000000..beb0c91 --- /dev/null +++ b/jorge/class.roster.php @@ -0,0 +1,147 @@ +$nick,"group"=>$group); + if ($this->roster) { + + $this->roster = $this->roster += $new_item; + + } + else { + + $this->roster = $new_item; + + } + + } + + public function get_roster() { + + return $this->roster; + + } + + public function get_nick($jid) { + + $roster = $this->roster; + $nick = $roster[$jid][nick]; + return htmlspecialchars($nick); + + } + + public function get_group($jid) { + + $roster = $this->roster; + $group = $roster[$jid][group]; + return htmlspecialchars($group); + } + + public function get_nick_group($jid) { + + $roster = $this->roster; + $result = array($roster[$jid][nick],$roster[$jid][group]); + return $result; + + } + + public function sort_by_jid($dir) { + + $arr = $this->roster; + if ($dir==="az") { + ksort($arr); + } + + elseif($dir==="za") { + krsort($arr); + } + else{ + return false; + } + + $this->roster = $arr; + + } + + public function sort_by_nick($dir) { + + $this->sort_roster($dir,"nick"); + } + + + public function sort_by_group($dir) { + + $this->sort_roster($dir,"group"); + + } + + public function sort_by_nick_group() { + + $arr = $this->roster; + array_multisort($this->prepare_multisort("group"),SORT_ASC, $this->prepare_multisort("nick"),SORT_ASC,$arr); + $this->roster = $arr; + } + + protected function sort_roster($dir,$field) { + + $arr = $this->roster; + if ($dir ==="az") { + + array_multisort($this->prepare_multisort("$field"),SORT_ASC,$arr); + + } + elseif($dir==="za") { + + array_multisort($this->prepare_multisort("$field"),SORT_DESC,$arr); + + } + else{ + return false; + } + + $this->roster = $arr; + + } + + protected function prepare_multisort($val) { + $arr = $this->roster; + foreach ($arr as $key => $row) { + $field[$key] = $row[$val]; + } + $ret = array_map('strtolower', $field); + return $ret; + + } + +} + +?> diff --git a/jorge/class.sessions.php b/jorge/class.sessions.php new file mode 100644 index 0000000..fbb3a6a --- /dev/null +++ b/jorge/class.sessions.php @@ -0,0 +1,68 @@ +id(); + @session_unset(); + @session_destroy(); + } + +} +?> diff --git a/jorge/config.php.inc b/jorge/config.php.inc new file mode 100644 index 0000000..9e16e50 --- /dev/null +++ b/jorge/config.php.inc @@ -0,0 +1,99 @@ + array ("pol","1"), + "English"=> array("eng", "2") + + ); + +// Define default language. Should be the name from language_support array. +define(default_language, "English"); + +// vhost and RPC settings +// RPC port: +$rpc_port="4666"; + +// vhost settings, format "xmpp.server.com"=>array of RPC servers (WARNING! Do not mistake vhost and RPC servers as this is crutial for authentication!) +$vhosts = array( + + "jabber.example.com"=> + array("10.0.0.1","10.0.0.2"), + + "jabber.example.eu"=> + array("10.0.0.3","10.0.0.4") + + ); + +// array of admins for domain +$vhosts_admins = array( + + "jabber.example.com"=> array("admin1","admin2","admin3"), + "jabber.example.eu"=> array("admin10","admin20") + + ); + +// MySQL database where mod_logdb is running on: +define("MYSQL_USER", ""); // username +define("MYSQL_PASS", ""); // password +define("MYSQL_NAME",""); // db name +define("MYSQL_HOST", ""); // host ip + +// Since version 1.5 Jorge is using reCAPTCHA code. Before running Jorge please go to: http://recaptcha.net/ and setup your account and get private key and public key: +define("CAPTCHA_PRIVATE",""); +define("CAPTCHA_PUBLIC",""); + +// secret key for scrambling URLs. Put here some random data (32 chars): +define("ENC_KEY","PleaseChangeMe"); + +// Turn SSL redirection in PHP +define("SSL_REDIRECT", "false"); + +// number of chat lines in browser (default: 300) +$num_lines_bro = "300"; + +// number of search results (default: 100) +$num_search_results = "100"; + +// splitting line. Value in seconds. Default 900s = 15 minutes +$split_line="900"; + +// links +$links='jabster.pl     + poczta     + kalendarz     + dokumenty + '; + +// copyright +$copy = "jabster.pl © 2009"; + +// custom logo +$brand_logo = "logo_jabster.png"; + +?> diff --git a/jorge/contacts.php b/jorge/contacts.php new file mode 100644 index 0000000..d6dc3e0 --- /dev/null +++ b/jorge/contacts.php @@ -0,0 +1,191 @@ +set_overview('

'.$con_head[$lang].'

'.$con_notice[$lang].''); + +if ($_POST) { + + while (array_keys($_POST)) { + + $jid = base64_decode(str_replace("kezyt2s0", "+", key($_POST))); + $val = array_shift($_POST); + + if ($val=="n") { + + $do_not_log_list .=$jid."\n"; + + } + + } + + if ($db->update_log_list($do_not_log_list) === true) { + + $html->status_message($con_saved[$lang]); + + } + else { + + $html->alert_message($oper_fail[$lang]); + } + +} + +// sorting options +$get_sort = $_GET['sort']; +$get_dir = $_GET['o']; + +// validate +if (!ctype_digit($get_sort) AND !ctype_digit($get_dir)) { unset($get_sort); unset($get_dir); } + +if ($get_dir === "1") { + + $dir = "za"; + $get_dir = "2"; + + } + elseif($get_dir === "2"){ + + $dir = "az"; + $get_dir = "1"; + + } + else{ + + $dir = "az"; + $get_dir = "2"; + + } + +if ($get_sort === "1") { + + $ejabberd_roster->sort_by_jid($dir); + + } + elseif($get_sort === "2") { + + $ejabberd_roster->sort_by_nick($dir); + + } + elseif($get_sort === "3") { + + $ejabberd_roster->sort_by_group($dir); + } + else{ + $ejabberd_roster->sort_by_nick_group(); + } + + +$roster_con = $ejabberd_roster->get_roster(); + +if ($roster_con) { + + $db->get_log_list(); + $do_notlog_list = $db->result; + + $html->set_body('
'); + + if ($get_sort) { + + $html->set_body(''); + + } + + $html->set_body(' + + + + + + + + + '); + + while (array_keys($roster_con)) { + + $jid = key($roster_con); + $roster_item = array_shift($roster_con); + $nick = $roster_item[nick]; + $grp = $roster_item[group]; + $predefined = $enc->crypt_url("jid=$jid"); + $prepared_jid=str_replace("+", "kezyt2s0", base64_encode($jid)); + if ($col=="e0e9f7") { + + $col="e8eef7"; + + } + else { + + $col="e0e9f7"; + + } + if (in_array($jid,$do_notlog_list) === true) { + + $selected="selected"; + + } + else { + + $selected=""; + + } + if ($selected!="") { + + $col="b7b7b7"; + + } + + $html->set_body(' + + + + + + + '); + + } + + $html->set_body(' + + +
'.$reset_sort[$lang].'
'.$con_tab2[$lang].' ↑↓'.$con_tab3[$lang].' ↑↓'.$con_tab6[$lang].' ↑↓'.$show_chats[$lang].':'.$con_tab4[$lang].'
'.cut_nick(htmlspecialchars($nick)).'('.htmlspecialchars($jid).')'.cut_nick(htmlspecialchars($grp)).''.$show_chat_as_map[$lang].'| + '.$show_chat_stream[$lang].' +
+
+ '); + + } + + else { + + $html->status_message('

'.$no_contacts[$lang].'

'); + +} +require_once("footer.php"); + +?> diff --git a/jorge/export.php b/jorge/export.php new file mode 100644 index 0000000..40b7947 --- /dev/null +++ b/jorge/export.php @@ -0,0 +1,208 @@ +get('language')) { + + // Validate language setting in session + if (is_language_supported($sess->get('language'),$language_support) === true) { + + require('lang/'.$sess->get('language').'.php'); + + } + else{ + + // In case of invalid session, overwrite value + require('lang/'.$language_support[default_language][0].'.php'); + $sess->set('language',$language_support[default_language][0]); + + } + } + else{ + + // If no lang in sess, set it anyway... + require('lang/'.$language_support[default_language][0].'.php'); + $sess->set('language',$language_support[default_language][0]); + +} + +// language +$lang = $sess->get('language'); + +define(XMPP_HOST,$sess->get('vhost')); +$xmpp_host = str_replace(".","_", XMPP_HOST); + +$rpc_host = check_rpc_server($vhosts[XMPP_HOST],$rpc_port); + +// in case no RPC servers are available stop jorge +if ($rpc_host===false) { + + print "
Currently service is unavailable. Please try again later.
"; + exit; + } + +// connect to xmpp server +$ejabberd_rpc = new rpc_connector("$rpc_host","$rpc_port",XMPP_HOST); + +// initialize encryption system +$enc = new url_crypt(ENC_KEY); + +// authenticate +if (check_registered_user($sess,$ejabberd_rpc,$enc) !== true) { + + header("Location: index.php?act=logout"); + exit; + +} + +// create database object +$db = new db_manager(MYSQL_HOST,MYSQL_NAME,MYSQL_USER,MYSQL_PASS,"mysql","$xmpp_host"); + +// set user data +define(TOKEN,$sess->get('uid_l')); +$db->get_user_id(TOKEN); +define(USER_ID, $db->result->user_id); + +if (!ctype_digit(USER_ID)) { + + // exit on unexpected results + exit; + + } + else{ + + $db->set_user_id(USER_ID); + +} + +// get parameters +$e_string=$_GET['a']; + + +if ($enc->decrypt_url($e_string) === true) { + + $tslice = $enc->tslice; + $talker = $enc->peer_name_id; + $server = $enc->peer_server_id; + + } + else { + + // if validation of link fail, exit + header('Location: index.php'); + exit; + +} + +$db->get_user_name($talker); +$user_name = $db->result->username; +$db->get_server_name($server); +$server_name = $db->result->server_name; +$nickname = $sess->get('export_nickname'); +$db->get_own_name(); +if ($db->result->own_name) { + + $own_name = $db->result->own_name; + + } + else{ + + $own_name = false; + +} + +// get chat +$db->get_user_chat($tslice,$talker,$server,$resource_id = null,$start = null,10000); +$result = $db->result; + +// set headers +header("Cache-Control: public, must-revalidate"); +header("Pragma: hack"); // this is WEIRD - it is needed to work with Internet Explorer :O +header("Content-Type: application/octet-stream"); +header("Content-Disposition: attachment; filename=\"Jorge_chat_$nickname-$tslice.txt\""); + +$data="$export_head1[$lang]$nickname ($user_name@$server_name) $export_head2[$lang] $tslice:\n"; + +foreach ($result as $results) { + + if ($results["direction"] == "from") + { + $out=$nickname; + $tt=$tt+1; + $aa=0; + } + else + { + if ($own_name !== false) { + + $out = $own_name; + } + else{ + + $out = TOKEN; + + } + $aa=$aa+1; + $tt=0; + } + + + if ($aa<2 AND $tt<2) { + + $data .= "\n(".trim(strstr($results[ts], ' ')).") $out\n"; + $here="1"; + } + else + { + $data .=""; $here="0"; + } + + $data .=" $results[body]\n"; + +} + +$data .="\n\n______\nChat exported by Jorge"; + +echo $data; +ob_end_flush(); + +// log event +$db->set_logger("8","1", "JID: $user_name@$server_name, Date: $tslice"); +$sess->unregister('export_nickname'); + +?> diff --git a/jorge/favicon.ico b/jorge/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..83eccc0464237087f7e03d8fdaa1fd594b8295ab GIT binary patch literal 9662 zcmc&)eNYtV8DCVm;~NAH?zr#cIF8Rl;P@0!lZX|~n5dO#qlq-tw4EB8w$Yk_#$;lg z>DX!iv9{Cx(?6JwX*16BDLV7Yy&rGLiXiiq>z(I55JWa>UBx~rKI3Le6G8YQ0u6Om-?k-D% z*Ie0pNgipwc;0W`=+|4DeQ%{{>GLTX=7CHFoGReuDbAJTI`ojfm!hU0W*V5mf>QS} zZIxFaPmEH$N*dfMeYNK}bbUikbUO#*60adE){IM5(>L-g&QrzpUh{!NW6P!HU+S2` z;!LzQ`L?YrV$UWQ(hp?n5Pd$f3<58;gG+`^vW(0{rPaB!x_uyJsdz)*+k5azio(L~ zgZ*CT^$lI|cKyh>#q|O|-rfYh#9!3(ZC#c7*F|-{j8H7Go>#UGs)`%De+0kjIrbel z?4k?{DX}T6#p!fCqly(bd4AQJA(gV+-G>+m>Ls`V!3m%6$U555Z6jNo7Yk2MhDFo|`#YR}2? zR$oM)Mx1#f)5K0LjNwu0OhV2i(Lc#OG46^qG;c_}E81zYmj0}u%(e5CBR9gv*5c+N z#Y(g5Z_xXU_+C6{l9H?;IW7irhKQZmWe{`dxsm!^Kn6`^KMl61ZxPJ=JG7AQ&Q+Vb`0la=o$a4KBROI zW=yctpv?)_nC~HKjH~Ge_Kc;`cQiv!+fmzqPQ&VJWK*m7%nLm{OLpTjgrU!{EEYbq#|#+;K)$wIb8-~=`CT2&bp8YrM09e4QkO|lxuM=WpG>6*nm~G8au;#01 z8Z5{$xjqc|S&Vo^If#uzSJ2Nbjm_O_uzRvXbrj3_*1ebI<_6D8$oYeEl_TJbV$DMG znt}BrzFS0|sLLCX=e~GE-A}`|{gwvb^BvzD4(djnBUZY))1RoRa@Qc{yoVfmHxwJO zO9N_ek*Bf7^IDQphz+7XqoJa0LMQ14`lHG=U+d0&R}w_N@t_4(M~cbrJD94W$B6C7 z1>aJYx=s`Rul8JDk*cQ8BoVt6@kE22JR|Ue-+#7#`{nk-Z;ZtQvLuh<8edh8i9L-N zHbvGV)5u;}^R!>n-uY=sP7&)%iWYP5IU+CWb}fBRQ{jHi*6v#-IdPl@55GA<71g-% zi|RZ_U_X$-}4TahEwBMj8b#8)un~ZhtjoV%v z)rQ`Gl=Elzj3ksadOLHAoqsmfdY(+zIU2DZ6EVjg*cz!zG!@SMBA(`N_)YM+-y(M2 zNY;=zMsu*+nN%%(eWi*0L|N|oF81-(=ekD|#dnrUZP?#DwQV?~s?FDhIqrn|cU0%F zolHG*6FO)j7)qPGDX^nc*ee9$HCZ3XmqdI@@Dm~nY5=dc%JpMK37cEhI`GK8-mqcl zP`Xy!gA<)adGPwD{{{OiAowly-YtmNHy3@M$bmdT_Mpg9{hCTw$Kjt%tRU|%^{3&f zOFu#E7kUU$Vh_qlpF@34+D%ld+HrIt_W3c5tj+8k*qhwVFLD0PT<^*J*7uhdcgtIR z&muqEmSO)K$~(&QbEqwO{HikNDWY3G=K~|K5uFU&>Ha~snKeT%I@GmhLye~^ z+sK|HxihF6!7t>*&$YBG*TSwv{)@?;@~mz<>4_~RerB4Od&XK%JJDN_-MbeU?k1}p zrwc1w=~BAfzV~W=vE6qDYay()U{AAoa9UmIenVNxq`myZ=yG94*Qjx&nPqwW0-vCM zLTpuX4EjcI^G5`$VU<^xZFc^_+B{G%g`JE&_nk4y);3t2Wn}*;$q)0vx4%%LHqh62 z?Ol7XM2xd<|LRr({P#~W$A8G0gmup~J3rpFf0WN(G98nHBOH8w9C>sZdY-TI`kL=^ z!Zl*T40_PwhSgO$`Ch=b>4kqj1H%laO2R|y}}Cji;?CCy^lPWMPwI!eJ*dI zj)Z=&AAv799oi~Sch~-FVe4_!qBg|w8*};0jS~y*&Dz=w{By8+oj)fO<6`GY`E*89 z`XccFM~arYhkfI@ykh4bYoo6oaUu)#axnLmw+&`NC)l?~z$|$`;DdUb_rRfdCZmcS z8+MG8O6z{!{6t4sgFp^P?Tp-x*fxnZ|3zvc9gGpguMDevorrB;M=ygAJBUnywVp&A z-)3tcT1@p6?kDA2oj;GLe}Y{+ggHozCovj5o}Qiit|o;}w06r#a$bqcCC2dR7l}?is&4$KpH*{4)U=qQsB6L*p;(a~eGYwy@lNxW2=Gps3b&Fu%?E7Z>0H5#YV;8S=HQow5=Zifh&}OreBy62s7v|lxjGFzGj}Y! zzL2A?mp1x3MV#Si+vbrK=;3yurkk~+P(PBy9e&uTwVzwLmi-`GHlnZ;P zUGU|}0AEqBj~C~B8Hi27U#6{11AVU?8J`nxM4aKMT*oAW-){7e0=;O7STKLScwZw< zO=~JWefD)jF@3)fWs>DP5@EZ$QVL;9#BSm(10O%JwuEnJEX6hHlOWFjzNK?`N!Udh z2*o$e9f*sc1bcb>J|&K}M6inodiU5PPZn6%7k+v)x_lmrRqA~0Gb78D<(?Acx09&d zwQ75=!t2Gu`E9f10>Fabo z?Li;yg_e!O{C-qw{&^Z}avn#4O|ik_&kM;pyyk zx{mog*Wx^j@9v%87qQNd7k!oW{S@U3)&gQX|H2S)+4}KTl=P#faIu88fz&nB1 zcP~v(f62!%m17*XH=a}MbfxRr_e$!#2hjVf!}pB5`t|;JLR&JgEzgf?P`}w_1r9s< z3Jq251KoA&FMSWao)+{T>p*TF!bGKw0(Xn%lmQ}}||Pc7$W zc$&#^JOzxi6xEB_xyyw1aNGldJ|y%GkF%E=krAArJlfx{ZdEC9!2O~QEdi)N- z-Amn`TF%p+X@Vb5PZQjDdYa(mDdr!cbW;?MFO#QtC>eeU{)B!ahbWgQFE4+apbzll jdLb`|-+l^`fkI$jZ+Ma5;qU^%$#MP5Ii7O(p{V}?nE9+c literal 0 HcmV?d00001 diff --git a/jorge/favorites.php b/jorge/favorites.php new file mode 100644 index 0000000..c868dce --- /dev/null +++ b/jorge/favorites.php @@ -0,0 +1,178 @@ +set_overview('

'.$fav_main[$lang].'

'.$fav_desc[$lang].''); + +// add to favorites +if ($init == "1") { + + if ($enc->decrypt_url($_POST[a]) === true) { + + $tslice = $enc->tslice; + $peer_name_id = $enc->peer_name_id; + $peer_server_id = $enc->peer_server_id; + $db->get_user_name($peer_name_id); + $user_name = $db->result->username; + $db->get_server_name($peer_server_id); + $server_name = $db->result->server_name; + $nick_name = query_nick_name($ejabberd_roster,$user_name, $server_name); + if (!$nick_name) { + + $nick_name = $not_in_r[$lang]; + unset($malpa); + + } + else{ + + $malpa = "@"; + + } + $html->set_body(' +
'.$fav_add[$lang].':
+ + + + + +
'.$fav_chat[$lang].''.cut_nick($nick_name).' ('.$user_name.$malpa.$server_name.')
+ +
+ + + +
+
+
'); + + } + +} + +// process request +if ($_POST[favorite]) { + + if ($enc->decrypt_url($_POST[favorite]) === true) { + + + if ($db->set_favorites($enc->peer_name_id,$enc->peer_server_id,$peer_resource_id,$enc->tslice,$_POST[desc]) === true) { + + $html->status_message($fav_success[$lang]); + + } + else{ + + $html->alert_message($fav_error[$lang]); + + } + + } + +} + +// delete favorites +if ($_GET[del] == "t") { + + if ($db->delete_favorites_id($_GET[link_id]) === true) { + + + $html->status_message($fav_removed[$lang]); + + } + else{ + + $html->alert_message($oper_fail[$lang]); + + } + +} + +// get favorites +$db->get_favorites(); + +if (count($db->result)>0) { + + $html->set_body(' +
+ + + + '); + $fav_list = $db->result; + foreach ($fav_list as $row) { + + $db->get_user_name($row[peer_name_id]); + $user_name = $db->result->username; + $db->get_server_name($row[peer_server_id]); + $server_name = $db->result->server_name; + $nickname = query_nick_name($ejabberd_roster,$user_name,$server_name); + $to_base = $enc->crypt_url("tslice=$row[tslice]&peer_name_id=$row[peer_name_id]&peer_server_id=$row[peer_server_id]"); + if (!$row[comment] OR $row[comment] == $my_links_optional[$lang]) { + + $comment = $fav_nocomm[$lang]; + + } + else{ + + $comment = htmlspecialchars($row[comment]); + $comment = str_replace("\n","
",$comment); + $comment = wordwrap($comment,30,"
",true); + + } + if (!$nickname) { + + $nickname = $not_in_r[$lang]; + unset($malpa); + + } + else { + + $malpa = "@"; + + } + $html->set_body(' + + + + + + + '); + + } + $html->set_body('
'.$fav_when[$lang].''.$fav_contact[$lang].''.$fav_comment[$lang].'
'.verbose_date($row[tslice],$months_names,$weekdays).' + '.htmlspecialchars(cut_nick($nickname)).' ('.htmlspecialchars($user_name).$malpa.htmlspecialchars($server_name).') + '.$comment.' '.$fav_remove[$lang].' 
'); + + } + + else { + + $html->status_message('
'.$fav_empty[$lang].'
'); + +} + +require_once("footer.php"); +?> diff --git a/jorge/footer.php b/jorge/footer.php new file mode 100644 index 0000000..ea287c6 --- /dev/null +++ b/jorge/footer.php @@ -0,0 +1,126 @@ +foot('
'); + +} + +$html->foot(' +
+
'.$links.'

+
'.$copy.'
+

v1.5-RC1


+ '); + +// footer for admins... +$time_end = getmicrotime(); +$time = substr($time_end - $time_start, 0, 10); +if (TOKEN==ADMIN_NAME) { + + $html->foot(''.$admin_site_gen[$lang].$time.'s.'); + +}; + +// execude following code only when user is logged in +if (!preg_match("/index.php/i",$location) AND !preg_match("/not_enabled.php/i",$location)) { + + $html->foot(' + + + + '); + +} + +$html->foot(''); + +// render html output +$html->commit_render(); + +ob_end_flush(); +?> diff --git a/jorge/func.php b/jorge/func.php new file mode 100644 index 0000000..3455d08 --- /dev/null +++ b/jorge/func.php @@ -0,0 +1,932 @@ +get_nick("$talker"."@"."$server"); + if ($nickname=="") { + + $nickname=$talker; + + } + + return $nickname; + +} + + +function validate_date($tslice) { + + list($ye, $mo, $da) = split("-", $tslice); + return checkdate($mo,$da,$ye); + +} + + +function check_registered_user ($sess,$ejabberd_rpc,$enc) { + + if (!$sess->is_registered('uid_l') OR !$sess->is_registered('uid_p')) { + + return false; + } + else { + + if ($enc->decrypt_url($sess->get('uid_p')) === true) { + + $uid_p = $enc->single; + + } + else { + + return false; + + } + + $ejabberd_rpc->set_user($sess->get('uid_l'),$uid_p); + if ($ejabberd_rpc->auth() === true) { + + return true; + + } + else { + + return false; + } + + + } + + return false; + +} + + +function is_query_from($query) { + + list($from,$talker,$query_p) = split(":",$query); + $from=trim($from); + if ($from=="from") { + + $qquery[from] = "t"; + $qquery[talker] = trim($talker); + $qquery[talker] = str_replace("//","@",$qquery[talker]); // hack for parametrized search + if ($query_p) { + + $qquery[query] = $query_p; + $qquery[words] = "t"; + return $qquery; + + } + else { + + $qquery[words] = "f"; + return $qquery; + + } + + } + else { + + // normal search + return "f"; + + } + + +} + + +function verbose_date($raw_date,$months_names = null,$weekdays = null,$t = false,$y = false) { + + // English calendar arrays. Here we convert names from english calendar to other language. Make sure your locale in php work with default set to english. + $english_months = array("January","February", "March", "April", "May","June","July","August","September","October", "November","December"); + $english_days = array("Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday"); + + // Various formats + if ($t === true) { + + return str_replace($english_days,$weekdays,strftime("%e.%m (%A)",strtotime("$raw_date"))); + + } + elseif($y === true) { + + return str_replace($english_months,$months_names,strftime("%B %Y", strtotime("$raw_date"))); + + } + else{ + + return str_replace($english_months,$months_names,strftime("%e %B %Y", strtotime("$raw_date"))); + } + +} + +function validate_start($start) { + + if (!ctype_digit($start)) { + + return false; + + } + if (fmod($start,10)=="0") { + + return true; + + } + else { + + return false; + + } + +} + + +function db_size() { + + $result = mysql_query("show table status"); + $size = 0; + while($row = mysql_fetch_array($result)) { + + $size += $row["Data_length"]; + + } + + $size = round(($size/1024)/1024, 1); + return $size; + +} + + +function verbose_split_line($in_minutes,$verb_h,$in_min) { + + if ($in_minutes>60) { + return $verb_h; + } + elseif ($in_minutes<60) { + return $in_minutes." ".$in_min; + } + +} + + +function cut_nick($nick) { + + if (strlen($nick)> 25) { + $nick=substr($nick,0,25)."..."; + + } + + return $nick; +} + + +function new_parse_url($text) { + + $text = ereg_replace("([[:alpha:]]+://www|[[:alpha:]]+://)[^<>[:space:]]+[[:alnum:]/]", + + "\\0", $text); + + // disabled for now + #$text = ereg_replace("[^://]?www[^<>[:space:]]+[[:alnum:]/]", + # "\\0", $text); + + return $text; +} + + +function calendar($db,$user_id,$xmpp_host,$y,$m,$days,$token,$url_key,$left,$right,$selected,$lang,$view_type,$c_type,$name_peer=0,$server_peer=0,$cal_days=0,$enc=null,$months_names,$weekdays) { + + $days=$days; + $month = $m; + $year = $y; + +//create arrays for the calendar + + $months_days = array("31","28","31","30","31","30","31","31", + "30","31","30","31"); + + $days_array = array("Mon","Tue","Wed","Thu","Fri","Sat","Sun"); + +//removes the 0 from start of month - can't find array key with 0 + + if(strlen($month)==1){ + $month= str_replace("0","",$month); + } + else{ + $month=$month; + } + +//reset month to the array key match (array starts at 0) + + $month= $month-1; + +//find the days in the month + + $days_in_month = $months_days[$month]; + +//$m is used to find month + + $m = $month+1; + +//find the first day of the month + + $time = date("M D Y H:i:s", mktime(0, 0, 0, $m, 1, $year)); + $first_day = explode(" ",$time); + $time = $first_day[1]; + +//create the links to next and previous months + + $next = $month+2; + $x = $year; + +//if month is 13 then new year + + if($next==13){ + $next=1; + $x = $x+1; + } + $prev = $month; + $y = $year; + +//if month is 0, then previous year + + if($prev==0){ + $prev=12; + $y=$y-1; + } + + $calendar = ""; + +//Build the calendar with css +//links to next and previous month only for browser +if ($c_type=="1") { + + // encode links + $link_left = $enc->crypt_url("tslice=$y-$prev"); + $link_right = $enc->crypt_url("tslice=$x-$next"); + + // check if we have chats in prev and next mo + $db->is_left_or_right("$y-$prev"); + $i_left = $db->result; + $db->is_left_or_right("$x-$next"); + $i_right = $db->result; + + + } + else { + + $i_left=0; + $i_right=0; + + } + + $calendar .=' + + + + + + + + + + + + + + + + +
cal_imgcal_img
cal_img + + + + + + + + + + + + + + + + + + '; + + //checks for leap years and add 1 to February + + if(($year % 4 =="") && ($month==1)){ + $days_in_month=$days_in_month+1; + } + + else{ + $days_in_month=$days_in_month; + } + + $new_time=""; + + //find how many blank spaces at beginning of the month + + foreach($days_array as $key=>$value){ + + if($value == $time){ + $new_time .= $key+1; + } + else{ + $new_time .=""; + } + } + + //loop through the days in the month + $c=0; + + for($k=1;$k<($days_in_month+$new_time);$k++){ + + $c++; + if ($c==1) { $calendar.=''; } + + + + //blank space + + if($k<$new_time){ + $calendar.=' + '; + continue; + } + + //start the actual days + + $n = $k-$new_time+1; + + if(in_array($n,$days)){ + + if ($c_type=="1") { + + $to_base = $enc->crypt_url("tslice=$year-$m-$n"); + $loc_orign=""; + + } + elseif($c_type=="2") { + + $to_base = $enc->crypt_url("tslice=$year-$m-$n&peer_name_id=$name_peer&peer_server_id=$server_peer"); + $loc_orign="&loc=2"; + + } + + if ($selected==$n) { $bgcolor = 'bgcolor="#6daae7"'; } else { $bgcolor=""; } + $calendar .= ' + '; + } + else{ + $calendar .= ' + '; + } + + if ($c==7) { $calendar.=''; $c=0; } + + + } + $calendar .= ' + +
+ '; + +if ($i_left!=0) { $calendar.='<<<'; } + +$verb_date = "$year-$m-1"; + + $calendar.=' + +  '.verbose_date($verb_date,$months_names,$weekdays,false,true).'  + + '; + if ($i_right!=0) { $calendar.='>>>'; } + $calendar.=' + +
'.$cal_days[$lang][1].''.$cal_days[$lang][2].''.$cal_days[$lang][3].''.$cal_days[$lang][4].''.$cal_days[$lang][5].''.$cal_days[$lang][6].''.$cal_days[$lang][7].'
 '.$n.''.$n.'
+
cal_img
cal_imgcal_img
+ + '; + + return($calendar); +} + + +function check_thread($db,$peer_name_id,$peer_server_id,$at,$xmpp_host,$dir=NULL) { + + #adjust this hours as needed, we assume if chat is +/- 1 hour on the edge of day, then chat is related + if ($dir=="1") { + $day="+1 day"; + $bhour="00:00:00"; + $ehour="00:30:00"; + } + elseif($dir=="2"){ + $day="-1 day"; + $bhour="23:30:00"; + $ehour="23:59:59"; + } + + $get_date = date("Y-n-j", strtotime($day, strtotime(date("$at")))); + + $db->check_thread($get_date,$peer_name_id,$peer_server_id,$bhour,$ehour); + if ($db->result > 0) { + + return true; + } + else{ + return false; + + } + + return false; + +} + +function check_rpc_server($rpc_arr,$rpc_port) { + + foreach($rpc_arr as $rpc_host) { + + // assume if response time is greater then 1 second RPC server is down + $fp=fsockopen("$rpc_host", $rpc_port, $errno, $errstr, 1); + if ($fp!=false) { + + return $rpc_host; + + } + + } + + return false; + +} + + +function debug($debug=false,$string) { + + if ($debug===true) { + + print "".htmlspecialchars($string)."
"; + } + + return; + +} + +function message_processor($tslice,$server_name,$start,$nickname,$result_messages,$db,$html,$enc,$token,$split_line,$lang_pack,$lang,$spec_mark,$e_string,$to_base_prev,$to_base_next) { + + /* + This function perform message processing for message archives + tslice - date of chat + server_name - name of server + start - from what point of time of day should chat be displayed + nickname - peer nickname + result_messages - array of messages to be parsed + db - database object + html - html object + enc - encryption object + token - owner name + split_line - config option + lang_pack - array of translations + lang - language pack + spec_mark - marker used to distinguish what we are doing + e_string - url + */ + + // Check if user have set up OwnName + $db->get_own_name(); + if ($db->result->own_name) { + + $own_name = $db->result->own_name; + + } + else{ + + $own_name = false; + + } + + // Main loop + foreach($result_messages as $entry) { + + // always get resource_id if message is type of groupchat + if ($entry[type] !== "groupchat") { + + if ($resource_last !== $entry[peer_resource_id]) { + + if ($db->get_resource_name($entry[peer_resource_id]) === false) { + + return false; + } + + $resource = $db->result->resource_name; + + } + + } + else{ + + if ($db->get_resource_name($entry[peer_resource_id]) === false) { + + return false; + + } + $resource = $db->result->resource_name; + + } + + $resource_last = $entry[peer_resource_id]; + $licz++; + + // marking messages + if ($entry["type"] === "chat" OR $entry["type"] == "") { + + if ($entry["direction"] === "to") { + + $col="main_row_a"; + } + else { + + $col="main_row_b"; + } + } + elseif($entry["type"] === "error") { + + $col="main_row_error"; + + } + elseif($entry["type"] === "normal") { + + $col="main_row_message"; + + } + elseif($entry["type"] === "headline") { + + $col="main_row_headline"; + + } + + $ts = strstr($entry["ts"], ' '); + // time calc + $pass_to_next = $entry["ts"]; + $new_d = $entry["ts"]; + $time_diff = abs((strtotime("$old_d") - strtotime(date("$new_d")))); + $old_d = $pass_to_next; + // end time calc + if ($time_diff>$split_line AND $licz>1) { + + $in_minutes = round(($time_diff/60),0); + $html->set_body(' + '.verbose_split_line($in_minutes,$lang_pack[7],$lang_pack[8]).' +
+ '); + } + + // check if chat is continuation from previous day (work only for calendar view) + if ($to_base_prev !== NULL) { + + if ($ts_mark!="1" AND substr($ts, 0 , strpos($ts, ":")) == 00 ) { + + if ( check_thread($db,$talker,$server,$tslice,$xmpp_host,2) === true) { + + $html->set_body(''.$lang_pack[0].''); + } + // check only first line + $ts_mark="1"; + } + + } + + // run code only if type is not groupchat + if ($entry["type"] !== "groupchat") { + + // setting subject + if ($col==="main_row_message" OR $col==="main_row_headline") { + + if ($entry["subject"]) { + + $subject = ": ".$entry["subject"]; + + } + else{ + + unset($subject); + + } + + } + + // add line in case of special message + if ($col==="main_row_message") { + + $html->set_body(''.$lang_pack[1].' '.htmlspecialchars($subject).''); + + } + + if ($col==="main_row_error") { + + $html->set_body(''.$lang_pack[2].''); + + } + + if ($col==="main_row_headline") { + + $html->set_body(''.$lang_pack[3].' '.htmlspecialchars($subject).''); + + } + + } + + // calculate chat direction, whether to display nick... + if ($entry["direction"] == "from") { + + $out=$nickname; + $tt=$tt+1; + $aa=0; + + } + else{ + + $out = $token; + $aa=$aa+1; + $tt=0; + + } + + // timestamp, beginning of the chatline + if ($entry["type"] !== "groupchat") { + + $html->set_body(''.$ts.''); + + } + else{ + + if ($out!==$token) { + + // colorize + if ($resource_group === $resource OR !$resource_group) { + + if (!$col) { + + $col = "main_row_group_to"; + + } + $col=$col; + + } + else{ + + if($col === "main_row_group_from") { + + $col="main_row_group_to"; + } + else{ + + $col="main_row_group_from"; + + } + + } + $html->set_body(''.$ts.''); + + } + } + + // different bahaviour for groupchat and other messages + if ($entry["type"] !== "groupchat") { + + if ($aa<2 AND $tt<2) { + + $html->set_body(''); + if ($out === TOKEN) { + + // display of Own Name + if ($own_name !== false) { + + $html->set_body(cut_nick(htmlspecialchars($own_name))); + + } + else{ + + $html->set_body(cut_nick(htmlspecialchars($out))); + + } + + } + else{ + + $html->set_body(cut_nick(htmlspecialchars($out))); + + } + + $html->set_body(''); + + if ($out !== $token) { + + if ($spec_mark === false) { + + $html->set_body(' +
+ '); + + } + else{ + + $html->set_body('
+ '.cut_nick(htmlspecialchars($server_name)).'
+ '); + } + + } + + $html->set_body(''); + $here = "1"; + + } + else { + + $html->set_body('-'); + $here = "0"; + + } + + } + else{ + + // do not display own chats sent to MUC. Here resource is actualy nickname as MUC standard specify + if ($out !== $token) { + + if ($resource_group !== $resource) { + + // if message is sent without resource, that must be channel message, advise user. + if ($resource === "") { + + $resource = $lang_pack[5]; + + } + + $html->set_body('MUC: '.cut_nick($out).' +
'.cut_nick(htmlspecialchars($resource)).'
+ '); + $here = "1"; + $resource_group = $resource; + + } + else{ + + $html->set_body('-'); + $here = "0"; + + } + + } + else{ + + $here = "0"; + + } + } + + // process body part, do not show chat if message is type of groupchat + // this is sadly funny i write this 'if' and i dont know what exacly it do :/ + + if ($out !== $token OR $entry["type"] !== "groupchat") { + + // prepare body + $new_s = htmlspecialchars($entry["body"]); + $to_r = array("\n"); + $t_ro = array("
"); + $new_s = str_replace($to_r,$t_ro,$new_s); + $new_s = wordwrap($new_s,107,"
",true); + $new_s = new_parse_url($new_s); + $html->set_body(''.$new_s.''); + // generate mylink only on selected lines + if ($here==="1") { + + $lnk = $enc->crypt_url("tslice=$tslice&peer_name_id=$entry[peer_name_id]&peer_server_id=$entry[peer_server_id]"); + $to_base2 = $enc->crypt_url("tslice=$tslice&peer_name_id=$entry[peer_name_id]&peer_server_id=$entry[peer_server_id]&ismylink=1&linktag=$licz&lnk=$lnk&strt=$start"); + $html->set_body(''.$lang_pack[6].''); + + } + else { + + $html->set_body(''); + + } + + if ($t=2) { $c=1; $t=0; } // WTF!? + + $html->set_body(''); + + } + + } + + + $html->set_body(''); + + // Check thread. Work only for calendar view. + if ($to_base_next !== NULL) { + + if (substr($ts, 0 , strpos($ts, ":")) == 23 AND date("Y-n-j") !== $tslice) { + + if ( check_thread($db,$talker,$server,$tslice,$xmpp_host,1) === true) { + + $html->set_body(''.$lang_pack[9].''); + + } + } + + } + + return true; + +} + +function is_language_supported($l_query,$language_support,$l_key = 0, $return_val = false) { + + while (array_keys($language_support)) { + + $lang_key = key($language_support); + if ($l_query === $language_support[$lang_key][$l_key]) { + + if ($return_val === true) { + + return $language_support[$lang_key][0]; + + } + else { + + return true; + + } + + } + + array_shift($language_support); + + } + + return false; + +} + + +?> diff --git a/jorge/headers.php b/jorge/headers.php new file mode 100644 index 0000000..55cf285 --- /dev/null +++ b/jorge/headers.php @@ -0,0 +1,371 @@ +set('language',$c_language); + debug(DEBUG,"Language found, loading file: $c_language.php"); + require("lang/".$c_language.".php"); + $language_found = true; + + } + else{ + + setcookie("jorge_language",$language_support[default_language][0],time()+2592000); + $sess->set('language',$language_support[default_language][0]); + debug(DEBUG,"Language not found in selection, using defaults"); + require('lang/'.$language_support[default_language][0].'.php'); + } + +} + +// get client addr +$rem_adre = $_SERVER['REMOTE_ADDR']; + +// something for mac users +$mac_user = get_user_agent($_SERVER); + +// location +$location=$_SERVER['PHP_SELF']; + +// init html helper +$html = new render_html(); + +if (!preg_match("/index.php/i",$location)) { + + if ($sess->get('vhost') === null) { + + header("Location: index.php?act=logout"); + + } + if (array_key_exists($sess->get('vhost'), $vhosts) === false) { + + header("Location: index.php?act=logout"); + + } + + define(XMPP_HOST,$sess->get('vhost')); + $rpc_host = check_rpc_server($vhosts[XMPP_HOST],$rpc_port); + debug(DEBUG,"Active RPC host: $rpc_host"); + + // in case no RPC servers are available stop jorge + if ($rpc_host===false) { + + print "
Currently service is unavailable. Please try again later.
+ Please logout +
"; + exit; + } + + // create rpc object + $ejabberd_rpc = new rpc_connector("$rpc_host","$rpc_port",XMPP_HOST); + $xmpp_host = str_replace(".","_", XMPP_HOST); + + } + else{ + + // check if selected host exist in configuration + if (array_key_exists($_POST['vhost'], $vhosts) === true) { + + $rpc_host = check_rpc_server($vhosts[$_POST['vhost']],$rpc_port); + debug(DEBUG,"Selecting RPC server during login: $rpc_host"); + if ($rpc_host === false) { + + print "
Currently service is unavailable. Please try again later.
+ Please logout +
"; + exit; + + } + else { + + define(XMPP_HOST,$_POST['vhost']); + $ejabberd_rpc = new rpc_connector("$rpc_host","$rpc_port",XMPP_HOST); + $xmpp_host = str_replace(".","_", XMPP_HOST); + + } + + } + else{ + + unset($_POST['inpLogin']); + unset($_POST['inpPass']); + + // Try to recreate db object... + if ($_GET['act'] === "logout") { + + if ($sess->get('vhost')!="") { + + $xmpp_host = str_replace(".","_", $sess->get('vhost')); + + } + } + + } + +} + +// create db_manager object +$db = new db_manager(MYSQL_HOST,MYSQL_NAME,MYSQL_USER,MYSQL_PASS,"mysql","$xmpp_host"); +$db->set_debug(SQL_DEBUG); + +// create encryption object +$enc = new url_crypt(ENC_KEY); + +// username (token) +define(TOKEN,$sess->get('uid_l')); + +//debug +debug(DEBUG,"User session:".TOKEN); + +// authentication checks. Ensure if session data is not altered... (only when we are inside Jorge) +if (!preg_match("/index.php/i",$location)) { + + if (check_registered_user($sess,$ejabberd_rpc,$enc) !== true) { + + header("Location: index.php?act=logout"); + exit; + } + + // Load language file based on current session + debug(DEBUG,"Selecting initial language after authentication"); + if ($sess->get('language')) { + + // Validate language setting in session + if (is_language_supported($sess->get('language'),$language_support) === true) { + + debug(DEBUG,"Language selection ok."); + require('lang/'.$sess->get('language').'.php'); + + } + else{ + + debug(DEBUG,"Language in session was altered! Overwritting value..."); + require('lang/'.$language_support[default_language][0].'.php'); + $sess->set('language',$language_support[default_language][0]); + + } + } + + // we need user_id but only if we are not in not_enabled mode: + if(!preg_match("/not_enabled.php/i",$_SERVER['PHP_SELF'])) { + + $db->get_user_id(TOKEN); + $user_id = $db->result->user_id; + // create user_id instance + $db->set_user_id($user_id); + } + +} + +// check if user have admin rights +if (in_array(TOKEN, $vhosts_admins[XMPP_HOST]) === true) { + + define(ADMIN_NAME,TOKEN); + +} + +// run only for admins +if (TOKEN === ADMIN_NAME) { + + $time_start=getmicrotime(); + +} + +// If language not set or not found in cookie, set default language +if (preg_match("/index.php/i",$location) OR preg_match("/not_enabled.php/i",$location)) { + + // Set defaults only if language was not selected + if ($language_found !== true) { + + if (is_language_supported($_COOKIE["jorge_language"],$language_support) === true) { + + debug(DEBUG,"Setting language according to cookie"); + require('lang/'.$_COOKIE["jorge_language"].'.php'); + $sess->set('language',$_COOKIE["jorge_language"]); + + } + else { + + debug(DEBUG,"Language cookie not found, using defaults"); + require('lang/'.$language_support[default_language][0].'.php'); + $sess->set('language',$language_support[default_language][0]); + + } + + + } + +} + +// Get language from session +$lang=$sess->get('language'); + +$html->headers(' + + + + + + + + + + + + + + + '); + +if (preg_match("/main.php/i",$location)) { + + $html->headers(' + + + '); +} +$html->headers(' + + + + + + + '); + +// prevent loading includes as long as user is not admin. +if (TOKEN==ADMIN_NAME) { + + $html->headers(''); +} +$html->headers(' + Jorge Beta + + + + + + + + + '); + +?> + diff --git a/jorge/help.php b/jorge/help.php new file mode 100644 index 0000000..4bea4e4 --- /dev/null +++ b/jorge/help.php @@ -0,0 +1,29 @@ +set_body('

'.$help_but[$lang].'

'.$help_notice[$lang].''.$help_search_tips[$lang].$help_my_links_note[$lang].$help_advanced_tips[$lang]); + +require_once("footer.php"); +?> diff --git a/jorge/img/apple-logo.png b/jorge/img/apple-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..1959bad8eafea466e0927636848a3a4c0b3b5a28 GIT binary patch literal 1410 zcmV-|1%3L7P)Px#0%A)?L;(MXkIcUS000SaNLh0L01m_e01m_fl`9S#00007bV*G`2iXA=3=bqU zs5kro00jm~L_t(&-o06UOqEp_|DAKM1Zb0whA4SQprx=8O9gFip^lntmQJfFABK!I zTQhCBr6y>tv6WN*Xwht|tu&%?WpgQO#>8Zq<%WHLf=FmX@FSU|@o~?uKkj?E_r2o1 z=W^fso_BjMaG#yu?|II1o-YI}Uz~Gj<)MVU^z}usf3*~By_`isaOr@pz#RaXyrn6X zASehfyJEBMw-y!=N+^SXD($uv{^u96D2xXL!P+CXfq#d-IXMQbM}I*F+h3wx1UPNQ zKMn&}5R?R;rj6!*=RK*UnSlFXgH7Or;LIh4)?NZmuCP=8eY;^ep4R~>0QZg-Tkn5+Ndlb|loSM~u(kpAuBOsBN=iZr7T-Ga91yPm zIU^&GG%*qJ$iU2VW%c7Slb&bP+O}Hr^8Z3x&?K54(O))Lv z2jA}(Dh{SyP3%J)W;hd@*WC~eKp#@ZuVe3ml8;t$Dq|@LI+)C5toZ)a1_0}1$ zqxrz|c2W?`W=%5?mVdSZ*xxvfprACR8RNba1T$E{6~~J?)dU(SD+t29iht-=NLN5q z^?^?1i*X_#-QG>2yFco z>fT%W6Y>3wAW~jCIr$`+9umIc!wGI5fqPTXd;BP}u)y$U0+LV%nlvxq8+WXR z2f@`rQlvB;K>Cldg5jssV7X2Jcxt<5_&vG!eoL;N*#?i1E-eepLg$iOme;caOCh>7 z3zF6m2!<{Z!l@YnUfmT;!t4XHVJSQy05ScJHUVd5H0s7AXcHJ3u<{0Y*KK_Q=TAk$ z0_TUROCV|3Y99aq*Q-L8K*mTkny02jCTkMNbb^ZAD z{{&Ef*gRl~)B+CX-{!oqDxRu<837_7;81by6Ln!FkPGOYo$ndSMPvtKJmG%xOxQW> zQ*pFB&TsfOUU*^D1LIn6)q+xCjL}@+zbOLeWxmJ3N7}|% z{JSKW#^Yt<9#)^jBdET((`GrNBg>P_?IvV@Sl| zx6{1&8VopG7C!tJ{#hjMz}k=xKSrX8pW%I&n|VDaxyQt_s`Lb3ZL+Qp@8d=`XAM4#zC2 q4VPqld3IGof8C}Tp8Rh=i_L#3#E~)M%8msflRaJiT-G@yGywqnX;|C< literal 0 HcmV?d00001 diff --git a/jorge/img/bak2b.png b/jorge/img/bak2b.png new file mode 100644 index 0000000000000000000000000000000000000000..d48873918e73a158878bba8d794343ea93b9b10f GIT binary patch literal 231 zcmeAS@N?(olHy`uVBq!ia0vp^tUx@EgAGXjI(HxyNU@|l`Z_W&Z0zU$lL(SzOY(Mi zVfYV%3-&Ib3>4uk@Q5sCVBi)8VMc~ob0mO*>?NMQuI$&j`Gh#QMLeH$1BFsNT^vI^ zjwjoEeQ~&+&E&w52G#`8l*E*T6oq(eU6x1*NlCVulQSOs{%Tc!98mU0z;N=S8W&}C z-fasOyJkA?oZRNpCw(b1^T*R27tffwJg!}#(x!NKlT=HgiIY^Cv(-f*`J)Wt7g=K; TT#*a_+Q;DO>gTe~DWM4f$l6J- literal 0 HcmV?d00001 diff --git a/jorge/img/bar_bg.png b/jorge/img/bar_bg.png new file mode 100644 index 0000000000000000000000000000000000000000..01142421a3d0ce5d8cf72f2756f32d5403a0139a GIT binary patch literal 213 zcmeAS@N?(olHy`uVBq!ia0vp^oItF>!3HFC5-&6ZDaPU;cPEB*=VV?2Ic!PZ?k)`f zL2$v|<&%LToCO|{#S9GGLLkg|>2BR0pdfpRr>`sf4MtfWDZx73jj=$XP)`@f5R21i zryS%wV8FwCb;950xrz&VCREETtmBTJlxqKe`z+_)2B!s)yi>$Vk{0ZKW%KCFatkp7 zzl$BS)4aIbG&jgBf4s&t5L3Q3l@MwB?`=jNv7l`uFLr6!i7rYMwW zmSiZnd-?{1H}Z)C72Wo9aSYKo?>p(>Lg)a6Wo*pXOyx8 zHWV_c2%X6jU{w9gvNX%Ly7!jnTCdL*6U2J&u9+ar7$CdlQK+}`=GU){H}hrrr`4{X zyQQQf?Y4;HIh%q81*edgVbK@2Xa0_fi2o3uw<>$0MCgM}`W&13DLR*!X`h{(o>|rJ1{XE)7O>#Iu{GK7>~T0{2ic>sHcl#2*>qgp1GCN8yg!t8yguN85tDs WGG09WPx$HBd}cMK*T9IeEi9e8pOn)_bq%MT5$8spVms+GL#EcB|%JnAtFLy=R`?Fmk?b zrQ%?j*>b4lJA1@^uj$xc*1d~+)|CvHg~~5e#bR-z+RWwW1HJzo7-=t;&7(pe6Z?qrsG(W)kucSNruc% zi_b@e%SwmMMuf^uiqAoS$1!uhLV?J6uIPEK=rnb}T$R^%tmkQ<-c*p&H+RBjp50ZD z(?fyDTb0*5e8gOq*Ltq$Pm9o2k<>VO!gZ?VSCQ0cpWa=U*fn;+bExD%f5$R&ze0e> zGjzW%alP=Z*@XZA00DGTPE!Ct=GbNc0004EOGiWihy@);00009a7bBm000XU000XU z0RWnu7ytkO2XskIMF-al4+;_w34l-J0002lNklIyoy_KEUrbiqFS*k8pX4s2XQIR zM6(zcEn-?Ui5_t;HpIB19Wf$mM2FZCNwF^0#HMH!+agzKb%`snFTTXC_z<(ATC|IP zkq|ecQVfbAMOore^onyaBi_WEq9gIG=u_;8Qc)~^l$rkL7higHt)j6vX8-^I07*qo IM6N<$f|8atS^xk5 literal 0 HcmV?d00001 diff --git a/jorge/img/cal_bck2.png b/jorge/img/cal_bck2.png new file mode 100644 index 0000000000000000000000000000000000000000..c33521f788e49f62c3273183a13e1b952935ebaa GIT binary patch literal 436 zcmV;l0ZaagP)Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2iFS_ z2?-HUg+Pq}00A^fL_t(o!`)XgZp1JQqkNwF>%OVay18B2m>pUaiDiJ2;|mVWXh@Vu z(MkfS$6nv=@1Gn04(hh?Wgk($NZ9V6fJcl90Nfo`b|KZJkX=L<5WpyKDXNN3L7)#+ z1-eSlr76&SvrBZB;3$p?-4H_8Kz*3@*sm4~QzJOd)7^6{Qy~n)j;Q2lR zO9|)wAyd$L)u9EFpy^vVXSxNqlG(WoK=ZTBbD1;U>HQJ%Sk}pb#v-dPtp}upZRYKf zZC&rNr#UdQ?rRIj*^MxcDSl9;bfBaIb!AmL5nv2l%vOZRn)m$Dh+kETZ zrxEi%CznTAO0D35Vp(!}UDVdn3x(_=%q|$z3NAjviEO6WUSi9U9nz+g4yw0eAKPyw e^5=K|zdYacEhj?+xif750000FdgVotsa{m}Q5Hm=e$wE>9Q7kch)? zBdoa&8}QVc-Lw9F$u>a$%E~Exv0jdnNghn=dR0SQSBmzuD}1T^H2F$KJcYb)Sb$;%L-xN_|mwcavJk&I1;l z*-NcCy`n7>b55`A$vkG0{b}JLd#9tlk#4`5lrw8r?T}r3P~~`v|9siSfwxcP9d6>$ zxUPPBQx4zdl=HUX5-iHbId0D`zOV>5s2UQGbiV2g?{R+1$usWGPu)J-?NrG^Au}tZ z%#xrLGUrr^zHP}Y>3%CO{2));M^fuY_s#6Zo2v`&zKiQyA{(-d|L6y+$r_HTr@jTq ko#J3f;^;6`5c%gny@lgxhO>qMFrXMbUHx3vIVCg!0F3yUSpWb4 literal 0 HcmV?d00001 diff --git a/jorge/img/cal_bck_left2.png b/jorge/img/cal_bck_left2.png new file mode 100644 index 0000000000000000000000000000000000000000..dee5d31e3279cee8b7410d68bd7b267391e51439 GIT binary patch literal 176 zcmeAS@N?(olHy`uVBq!ia0vp^{6Nga!3HGTJ=&%KDaPU;cPEB*=VV?2Ic!PZ?k)`f zL2$v|<&%LToCO|{#S9GG!XV7ZFl&wkP>{XE)7O>#IyXO;lqq|tOdL?iz|+MsgyVX0 z&F`<*>)B)utbAtByvv0D@Y&_@8%`M;K49$O;oEZG5>EY>#c)}Q#z>(elpAl#- O1B0ilpUXO@geCxq)-bmK literal 0 HcmV?d00001 diff --git a/jorge/img/cal_bck_right.png b/jorge/img/cal_bck_right.png new file mode 100644 index 0000000000000000000000000000000000000000..aa6b69529d9a1892e9aff5d03d80b74d9e5f8fee GIT binary patch literal 406 zcmeAS@N?(olHy`uVBq!ia0vp^d_Y{n!3HF^eq=5KQY`6?zK#qG8~eHcB(eheY)Rhk zE)4%caKYZ?lYt_f1s;*b3=G`DAk4@xYmNj^kiEpy*OmP`H=htMpJd1rpb3n^o-U3d z5r^MKIPx(m^2qqSKekx@lVe=%l=DV%duLxc{LWY7q{GB@r`z5%PPyb~t-Iv*-<2nN zJ_mlN*ju=FPSI(Bvt5j-D%{mdSz%?de3jsm@~9jg@1lYS@()(rJRxi}Gt4;Ez4xRiTgTC& z!WnaVLfaP~(w=y=!!lU;{_~!+oiml#Q*Lwa-&U+|#UsPP%swZ=&p}FZ6}v=T{gZsg z2X`*Z_q8oroaegSTt@gC!{5kr>t9IZ%j#{EQZJHuaG=J_P0Q+WJQJ^ky7%gj1wNlM wIz4O~gk`pbmmkdKICb4mlQHwBy7LESeOI>iE!p?}07Hzy)78&qol`;+02^$Z2LJ#7 literal 0 HcmV?d00001 diff --git a/jorge/img/cal_bck_right2.png b/jorge/img/cal_bck_right2.png new file mode 100644 index 0000000000000000000000000000000000000000..464b4472b4cfc3ca54d2aca8b32020f0ed6aa8b4 GIT binary patch literal 173 zcmeAS@N?(olHy`uVBq!ia0vp^d_c^^!3HEVy56n=QjEnx?oJHr&dIz4a@dl*-CY>| zgW!U_%O?XxI14-?iy0WWg+Z8+Vb&Z8pdfpRr>`sfb#8tx5$)ugJez?+x}GkMAsp9} zujt?TJ)e;~VEVJf9Y4QtAKoVxopXst=LM5bPY(}IkHD;njOGjsbunzO6`qB!2Wn*S MboFyt=akR{0E&MxTL1t6 literal 0 HcmV?d00001 diff --git a/jorge/img/cal_bck_top.gif b/jorge/img/cal_bck_top.gif new file mode 100644 index 0000000000000000000000000000000000000000..10bd5bf25cfc63da147345f9d241bc9ba1d38786 GIT binary patch literal 52 zcmZ?wbhEHbWM*Jzn8?I1{lNQ%&2Rqy|Ifg{p!k!8k%57UK?lfY0Le2jvGz1BIvvYk F4FH>64j2Fc literal 0 HcmV?d00001 diff --git a/jorge/img/cal_corn_11.png b/jorge/img/cal_corn_11.png new file mode 100644 index 0000000000000000000000000000000000000000..e3328ad3cf33f3703d6a0e9fe75664bbd63c7d3a GIT binary patch literal 290 zcmeAS@N?(olHy`uVBq!ia0vp^{6Ngk!3HF+w~DI+DaPU;cPEB*=VV?2Ic!PZ?k)`f zL2$v|<&%LToCO|{#S9GG!XV7ZFl&wkP>{XE)7O>#Iyaw?DeFwhMkAomB2O2`5Q)pJ zeb#)33`AIt_bq10DDpmcNN|NE2Wx1Ql*1y|8WjzO_8B+oruy*^!Y{0W!nw= ze{HGUmlg5OYE`1!vWH2`pEjCle*SeK@EOt=1^@s6 literal 0 HcmV?d00001 diff --git a/jorge/img/cal_corn_12.png b/jorge/img/cal_corn_12.png new file mode 100644 index 0000000000000000000000000000000000000000..9892714cda961a2dbccbe9452bdaf7c4d66f3d9c GIT binary patch literal 298 zcmeAS@N?(olHy`uVBq!ia0vp^d_c_3!3HEP+-oF(6id3JuOkD)#(wTUiL5|ATavfC z3&Vd9T(EcfWS|IVfk$L90|U1(2s1Lwnj--eWH0gbb!ETK%_qdA(_Ctt1Qc5C>Eak7 zak=-bHP;~r3D<|l{GK67E6=d8gg?A@r+sd80ee&L+@%rS8V3*YwArN9Ni&;q##uZ) z9{M^~uSPubXi;9IZqk-e9^dp%MQ@&$5sMoxZ%r%KsFv1fgF0p*y&Lw2{0(q>cZWwQVP literal 0 HcmV?d00001 diff --git a/jorge/img/cal_corn_21.png b/jorge/img/cal_corn_21.png new file mode 100644 index 0000000000000000000000000000000000000000..f0a4c4888c7d32f6400a61651854ab19a9db44ec GIT binary patch literal 341 zcmeAS@N?(olHy`uVBq!ia0vp^{6H+g!3HExhN-duDaPU;cPEB*=VV?2Ic!PZ?k)`f zL2$v|<&%LToCO|{#S9GG!XV7ZFl&wkP>{XE)7O>#Iyaw?K2N}l!@fYFtDY{7AsXl3 zUfAfx>?qLiu)aLK`1MnvB*DGMoDMq8cmA-oMl{|fpv7U<7qeTP3l_A><$O!aVObr+ zqJ2<)+3fztLjCtQZ(R6!RChVM@Vog|yJ7-Zx4thu@1V)Gs_*BPq;wUJ4;|lI)=yNt z6EZKRr_B80#A+Ga*Y^U9&uoja%e2jpO^ls#c(37Pt047-Qk;Q*4D77SSki6wrByXA zE<5vYO;QwhD9?_C?Tj&Zb#}&ZX3dLKHu3Z@oY0+joT2;Zq}S~S?rvVb;xJFwt@9t2 h+3bHI|6lSCCp$yHA)m&G13(Wlc)I$ztaD0e0stM_gt-6! literal 0 HcmV?d00001 diff --git a/jorge/img/cal_corn_22.png b/jorge/img/cal_corn_22.png new file mode 100644 index 0000000000000000000000000000000000000000..b44ed85ca2fcbfd0756b89349687378dbe6ad987 GIT binary patch literal 355 zcmeAS@N?(olHy`uVBq!ia0vp^d_XL~!3HGNrubO_DVB6cUq=Rpjs4tz5?O(Kwj^(N z7l!{JxM1({$v_d#0*}aI1_o|n5N2eUHAey{$X?><>&kwen@>nqVCBryKzW7-o-U3d z8t31hwf1686mfW1EdSr;>VyOq`HqN;WQT4>){Ui#V&C2!NZlcNFgM~>L`M^wlA+q; zGZ7qNeXd&E{{IV8{y$pAD8MLqFtnq2Y{|q)>c`ylQ-i{K)Z}@t?fEOgo~+6J#oBQ8 zY01s29zIOJ}bbP0l+XkK6%L3> literal 0 HcmV?d00001 diff --git a/jorge/img/closed.png b/jorge/img/closed.png new file mode 100644 index 0000000000000000000000000000000000000000..26f03a416e3ff4864bd416c1be6e5d8e88b8b8a3 GIT binary patch literal 423 zcmeAS@N?(olHy`uVBq!ia0vp^f{XE)7O>#Iu|R4m_-YRQ7F(9bx#+^5RLP# zlMKC?9R*tVUv7ws<=VJuL$FJrz~vir%EGw*DbzQ4E}fd7c_V@|>?h|>&g&f;1@3iC z5Nz3cWAEvEu4fMzZ9cqj`}?zb&-SK&`uJ%6e7TI{7eBJD&VDUftn~Tb=L-7|t{WZq z$;{$&P(7@eBll+G*5H)Sp6}TH?RTD3_f~%1oK1UgpSB8p{HA6~gy!f%_(Ml~93%T0m>O6cmY&RZcFxmf>ShrW@l<_(e^svDl7#CU zCJKbK1m)&`G|q`uQS#qA`Pck&i=&LvrtT2&DQ?Iu2;kh*;xcVd$6rSU*2QWmPS@5> zEeYBX+<8njLXs`wE>DKdUXN?MWosOn{>~0$;9R07b^KPy`Ko99M}IJydEJuDxH1bA OB@CXfelF{r5}E+5>#8#V literal 0 HcmV?d00001 diff --git a/jorge/img/indicator.gif b/jorge/img/indicator.gif new file mode 100644 index 0000000000000000000000000000000000000000..085ccaecaf5fa5c34bc14cd2c2ed5cbbd8e25dcb GIT binary patch literal 1553 zcma)+TTl~c6vwlh>nb99Af5rT)t{mCEg5urg=A(g z{C|6SPb~9Xage|wB`SrZk2FOMYM!buln2sX?5Y+T78iB(Zu9cS7|LZyZ++}u$^oi1 z_j@S}bW9OzU2R+RMy&~OT>X-oZ98$jq#ogNfJ!BM-42wHGZk*6s2KD}U*IA%epmxb zm}|6BK9YoIF;*xSL!+z@<64lB7->LTW2Vi4ostCA(z&2XniwNIv}fFo-`MbG;)u4G z^p@F!)|9HhZprHd_vXjDoxs6WkK-6P0@lfxnGT>*p(QHoUV=u1FAqb@b%*W=a3{`LsH5k^AvQNL>6fPpy#oU(&MuH(*aEX4b35*} zn4n7)`I2U%=+Z=?BVZQ?vjQFW4gD@~XSOO6b{qu81`4&LFuU2(ilxW+1|ZkNMnWe79C$gs zWT?Ele|HR{JGPe)5BTW>0Ey?-Ls6S#GoV0tbt6ku7B&*0 z;i9QM$W1Rj*rRIdceL)rAOSl+sDe3LkB87<%){;ZdHp6|SNlopDXRx< zxBDF9-lTo&v`8$humFygUij@qgT=Qzhj8{ym2-{Xciwqq_Xwk%=O3B-MNAL_6e`3U zyxwmXex4`g0^1RYw~Dth3av3Dl^AAlpO3mG!nLr#&ZZ7c_wUboI+deC+&%TFjK2Lm z!Y&f1h|T_On%RCV&=4bx`!>(YezqGVhl&QpED?N6GV)HmzJ9&rh$x*i?*@o9#6QI< z5ZI_MRX;0+pY8$`j)eF#TlUyG(eE%E7S!rj;mj^M5vhUicPm zVWQ2z+imFyg}SRABmOBY_@osR!>7Ov!ioK`NB6_Rv}7Ud?35ed5Sb@?yND?kv~RCa wqs^a3Sh>&&L4)!LKI?D2&k@))k(LESaga|C278ChSzn3NWVkcuNoY&{0f?~U_5c6? literal 0 HcmV?d00001 diff --git a/jorge/img/jorge_logo.png b/jorge/img/jorge_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..14316e2b52b8039697eb0aa41e371cb6d3ca0b59 GIT binary patch literal 11845 zcmV-LF1pc)P) zceEu(edj+_9d3Ma=1mTo(ab1~q*0azAwWU_$->weFbCNzu)Ekk$JlEhhvQ?6VV7M4 z&KL)lg+&B=F*pDsNdlpW;*2z-G@6`eUViV!PPKpZ?R)$7t-kNRnKzO+*6*Bq`gV2I zuYOhitKYZ6uc}251WE`2fKrMi>7EH8I_`Ukqf65*U!_#XwcE4!bw2Xw^5|9%N&ac+ zb@I$5d$MlQ>gbh}C+$6+E{Pxrnw-163~k~rh3h5VnWDKGS2rbnx#iKt?X@0XNqK~` z4XjNlQCKjAlu?FBhEZ7jmkf_9VqEa(fAShn7k`$_Gc8@OagscBWz>~nuDrVC-7QX9 zz3TWR<(Y3@rz=kw)_SIuhX78z52ilT!7olqsphX2l2M{GM!Im_{ao>rf~7&xTzSUh zB*VMOsY^SbIB6A|3`?u{x#IM~=Vgsk)<&p)3&!39{?TSSfr818v??oi9Zdfp zr1Jzp5Hx?H^zX3m8mR9Ak)czyw^df&eTxV&M z@+A4BHOe##d8Xw7z&{pt9D@@u{$7Ol5=tq8($}H(Ncb%GdZ^wX#vlECG8qfY1bO0D z;n=T%asUOHjX-EQtmL>;uTK8^olgiL7?x}{Q zp^dKYbjsK5`9jMR)`I~wo?Dw?><*;49-Li(57}#>^5a%Q_W(@Y2~(d3cMqUIDJWbI zZfIF+wHg4nZ3AE!Mo0c#VY-{eh{sR6PiuX>!gMmMSEx$zekEW!9yVWj&IC;NDo;p$ zX#FzRfb)~^zq==Vlzlza9;B(PD9FD7#y$e>zEBAxMJ(L|gbH3Nm8euIIF6G#;4yo# z8r|;G;3fXQmpr|^v|4R!H!O4$pqQ_1U0#d$d>{vuvU zd4#_W{DUBuLNFcVhinoz{oLG=OJ74bT#{D>wjD+T#C!R$TZe-zC8E7s8a zShd6h_a*RV!3`j&K>iApwX&&{CiK>_l6@*&zb_sy8J6tRrR5usuN!nE|LYplsm4j` zccwi%l{m?ApDLfzj$>{C_W`JVmQb03@M>NT&Z9KPwgRxPhT20dz{12qieHJE<6>D2<$07@)DWPb^2Uk2+ka32RDTM?`)8v__4G?ak~N7B{%Ln|3_ zf&3dAeZ-(s5rd#*b^C6H>F>hQ-@=nCs8_2<8Rlo2X6Pu<3xw*zbyl&Lc=Lhjxu7%c zda7}{<<(8TpT;;r5Fn+3+TVbCcZjx4D|uPiZ;ULW^1-|!97+3bXbpWAvRfM#(QQ~s zABxw*^!FR-8t~lTP(*VHY~IJjR2kQGky2vYHnwfQ2Gjr1$@hH>aTqEeY7IJ!%fNdq z*3a|7xE!2^!1o%;=zw)~Xc2uU3{zg%Z=C*Km@a7a9s6#EneT@DHr&Nza5ZCNV?a3X zn9t|Q=ku?@^vg}JCYX-MSF140OTga_@>1}h0AnMFJOsxhm}F2Yf`ATU>A1}3&`Q!14(O%nhq zU(X}ozMRe1{u`z}NNxN#S$6I=cHDmx(^L$6Wfm3@&*i+kf~*0M;xXV*L#h z0PMK;QZ5{RHk=~rza<>h2m#hoSp8X?OqixQn$}stxvW-NJe_5nPrA7jyBFC`#ZvTA z554g1R^C&Q{d6s&Hm=$Y?*9gLC{z-883fy7BWZz^J(tUGdWhoS7LM)xBxA=*EISuQ z*1U&}7rl$DRfEw_gSY@}LCu80oM6jp3)XxJReFHBSi{ij_fnetJ&Gf@Kmx zy2+!9qbpBZoL=&puRKkQ7~BIK36;;d1b7Cd3>8HhAeR&P(@Y${muh(*!^_{#$m%x$ zP#oMsAU88p9ptcc0nGdW3@fyDLc+?A!J-ckm}{}@L6)!kPaLnUhVs4P6p@hOum6^C z?K+r5DTV90VV^OL=CV@B7aR{uYr(pTOIBzv72a(Sm6RpUNB6?I(Ok-|lP68CUM#5Y zna)6*iacSM1mjl_#%b)ZttAQ}_~^YS5h5JH3ju*rh{l4?AKkDdRk`Dps#BUY8Fbd9o6Jex-SW~Y zkWK;8@=EKj#P8GO(dD(!@&v&ur1?GwHiG&h;DNXZ)D93sWD7sX)wh=bC{6w;p6dXI z*nI83whA)&i%@wHqzy6$!3+$&hwAaa^0R3;H zW?aVj_&75&d&p+9WV6{$W$b1xdg@=sl1m{7lmfb^neTl(tk?c+uY7dn*C|U{oKx-7 zo=RS)Baf64$8k{VN-~)%Kz#?)T_Ifa2;)Z{=FqOalqR?1xl^2b#kJu5PhNa#lxle# z&zYh$wVkW4yohXmnu$HX9a^BtzhdvR_pxf_X3iQu2}gdOv3>iY7_fN#hhWJ^ICSVR zCr>^LgexzROWA_Lv?BIGRZIy;>%Y7P(=R7X$KwP+fKs8;YbMhV9BTCE1Rz+n;sP@H z2|VW-iUWg~h6!pNXPkN7=kq zV)nfSCjJfv-iee00L`U9DP?P(F;1rg(~$>;Ky<)#+RKRN`AWfbl1CJ0u7O2TQGS|W z+As`)ARw2^0pAAk9MI@vaRI2ODGm(A)|NMe_XMUX7#to7S6t+l(5*}u6#EL{>P&Vk z#qy6p8AKjF*+n-%>4)Lq=*at6x$Xg~)oOE&(Y9@}*=+aGbT7|3!V)m8nszZgO)oH= zmhXJz?G`twFhLMB!_xL`_`ct@fz2gzw{pz|!M(=mR%XL6u}kpyuRoo#rp4`)M}hhapxjW#t3XWy zYd{TzR#C(iF=#2G_c(13nB5BXM?z(jks`jap@_2e%U|Ev$R!a+-a&s~4%0M|Qidxq zNg;pQM$^gvK@fCUvglqH9w|=zSri_H#s4R%TO6HI#^Xc^8inbUQb(sdnih>g3gW|! zfQGz5*jOy%c~IZeDB>ld&!bYU_uS?M-q^B`dtJllQ9|ZID82*CH4x0g&<8=-U<`xu za2zLn0MjXS_vbI?XgUqf(&UUQTQXetKaEZ`PBQ;6p z$kBb2CQ=Gm*HXl23xT{ObSsN*GqA1-p}FCa6TEiq&iszHGpt?9od322=E>C3L zKXLyU2c}#OOu2mU%3($d=7No;0r4D}K0ZhR2WA**oDdRr7IuMc$GuEl4&m z%W>1jA}jlht}s1a^LcPziJd1M_D(p=)B{RRfF%@t8Of@_44W3)T(L6C`Ac(rY19of)_{YeE!KvcAluWo&`ZuX=VeMbpwu+>g+gC=bxULW~5;7jtly@<(z&D zDSCqGq!PyI;e!=E``84dQB$4e6)&m6Dw*S9a%<}*=t`N}g>+`GGktJ%9= zSAu#~F;=ay{aB5!?U-TpfW_fyw^eT7=(NLcedjoTbo~eyFVA(kgPdF?p=AhEp zQP|*$eH|+Ii;Rv{S+e*>nEY_KE3kS$SnGh6CCp~C*tXrKkdZ&*s}80E1)qL+j34Zp z4WCKbri97=@i?Pp_{>ix_|}VMK61kdOY&j7x!^g@C%XTaXJ`1rlT&T{oASmKSOoz; zcR_zMpNI^@;PJy1{``SaW}H@B9lvBb4^GzG?xVO>$L9wx%{G-W4W{=^I{fR-X=a^v zB@~T0?eLzqdm&)fQQWhq%r&bON?Zc&6Nt=z0qa)w?c0yzI4l~z8LD3b6|P%jUk%n3 zjE&bgbm$QE`Vv;J`WnpqHSho^51^@Z(KJqU%t@Q2Oe)4JIq5xH_xXb#oM=#pNGKN?!qZZMtX6aD|flT!#O+ms^u#c6TT;<~f)O$CpBKi*g7kMBOg zbgfhAqHB^@vf(A4|NQ0we&@O+O{LdO?>u&>!k4#AG2=AmHygDt5yY!KG8R#sP$^sJ z9Qb39{|AnbU&b@f>|knYic+b>@e>uu-Vrjft^)UI$iB6?r)6wxj2Cz9VcdH!Ecq%J z!_;cErou(5Ez!@mZ8Dh*hGD!ilioU536$c0{AiS&C+gZ4)sgocac1fPAHQ#$iR#=i z-Ai6+^tSU`U4n?7J6YpT9~f&m9dzfD%s(y8n>Q5r&sPk0${(|c_v|V2-CZRVEkgid z{Rc6Vpa`dC1e$IV3L%OZg-`K~2afURj+gL#zo|@9Q&W(AJ7nGxuE6yDA;(XQQK?j# z)=x^w)YKG5$A+N)gG^3Nb_`tN4L71!1g3SPzt253&W>Z%X5}X#ZNuU$KYL!0iF36?^Q;}RkV0@|+U1_T6~6iGG$i0T zx^6pKqvrZpQa`$X^iYMrdT2Zx3=LQmZP})5n-<$#zB0#(ev_OfaXiIX#pi|5I*%W& z;5PQzOCh-D@qK*d?#Bu2B16MN{L5a30{{iatdUNC~yEN>Vbu`_yf{m9QRqBB$mw$h5HWh+zKYxG^UO9w{O=$PoaM_A1 zSFSE_|GqMxdO)uL&yUu5_+X9e))nRj(<%tCP01f#zl2Mc+0C@eErUC@gfI4qLuHED zunHpu|H*+e2WQ%qyj36Y2iGrU>zZ~2K5NM0gI5l7@v#eAJanMS4I7Fb z(^wTJ;IAGYCkO&&>hQv7oyB>B)dLo$grB=$fXz!YZLprT1S|Ums|GACTbboc+oqYS z`X~htl}1>1&23GCGEporVvl2*Ci#3G!!W1^7m^wL43m>njs4fM`8=P`W7{@C5MbLj zwOS3&^X62>wkN&x7rQR<)7}S(EaP}3?8HV&8#m8}Veqw`vu%(TSIVna=lIpDhLNHj z3giE;TUX$YErV@oqyIumzP@v&P0n6mT1d$UuN>y`l{xL4;khFj?znWgji>MXJh;D- z%KNS7540&_vM#P(ljp4$3}|H$Qu4^bN{9RaU)wRwbghL9fr67|pJ$HOc>THp7cR?o z@Ql-7*5KV24}?pEF^XRCV}~lpOrHM!{?VzC(R zjqak5vfEmw4x-y&IWgBI8ru;gc&NFG7su-Cop815ya1ymhX?kSP(jcJj{v5GU%7I) zZOu1refe$Y6&cD(EuYZ^n^(=5qL-n72hd|zqZE+GUX1(S=H=TIo55X}V@ zDFnWPW7Tk4ChG4+*GS>wqvUv>Fq+PZo=4Ag)Ui9T+cHy<^spVa%ww}I$7VfzKWwpS zR&D!nC!A)`z~q&y3oI$>r$UpfE*Z;U>)Jeby;$ntBcx=<@fv3j_jm9~8^PXoUVrK+ zTo)GIPt{fGXn5s72E*t+C|on7A7um0DWZ7Zj~=N|c9k~G?dNw|**GQN-+3b$wjZmt zkGdPzgC)t8mesbDn8{?wWHJm54W;Ih9Hew3EllgYL3DBAR|w&Gy5_UZq6|1_#OOh^L-WfmQz4Lf2HJvX1%}eK(H-r|;({)@M)a}`P;>2nD(Q4|m z%U8CSA$q52Yt<|I&4v#|>v=Q^A8mBCqWC&Kr%R^JZ0qj3t${{#VA5r@9D05vCw6t~ zvW8)>f4tTfFP?7wu-%q6qBxKE$au}5-7yM}uE$EAb~Pp~@5Oo9G3wUkpS&N;2u91k zcA|XGc#ZQ$x}W}kVA4sIX)xa!EJgIvXf3rIANa-*EuXAqaMqC3biQaGe}*vKOxKv7 zEH~yYbu%IhMGFekqh-Gha^nh-2GcW*RWBV(FYB{9;>UT!VcIYZhH^s7M@Y$Rz0^z)(w9+{npu3NU z)qTaTF^84?CM)`_w)tQwgchbNZaZo&v?9W3!gNP}QImm?64^!WnVO#p)7g1CSm(^n zjGFzeTKXp+=IToHO1|%N&6;#&`_Y~ebJ25B&zHQNo-s}iOo>s0fCRCvkRG8LwUQm9yE=+8w>HNYpAbyeRR`}N2>9fEC)swS$juvztR1p2gy49|ULHKT6ljofm>ZfV?Ge(CqMnwPku9fs>yLt&~DQ!zf{CPwR zsg(1E>BtK?xz4+HB7GFNvM(K`SM(X|8Lw;89h`DGYdG68Oz)j=Q(=0|p!KrAbaZ|9 z#e)Izzg)x=y6*5OonMsk8f)B188nRY2yLYpql*E4$7&Cea@0xD(r1pk>`Q^6)n6zeZt|o^}WONPY&t%2r0R6S@vat={UXh z`~gai;ww*2xAE?UmJ|%`*fPis8;V`?zHmjJr;aAJ7@z3-jWc0Vi$x1HO^4QCfwTrkL6`l&A!1o(601+Zb* zWL3Y(p(&>$ujv~6<%6UA?sbcMhUtl_&zH7M@f%wgHRHwUjKlQ0VT(i2o{PrX^4Kr3^$#CAH;sl&1`8<4JntywChKetmmCz=*l-#nZ53>tA7>2>y zH}~_*$1ylaleaG8oH z=h&>rjT_yjw@vZdff!7piDWZauft zIUz}xuP$)S>N1ZUYKK`B1Ry1U_TV@VAE|j zt!oQxS>8EW+*X1GhUp*fO`q5vt9Wb(Y$|TR?s12mC+fU7R_DN^%S_$Jl!AgSS=DcG z_9B~W*XHL`-Z!o<@XhCEd1<`89->C}(18luk5;*9W0A{N=2$mm)0dGrzGAZK^Zdy= z4;`%VXsrIfvVDp_xn&v93B_j$rke-Mf9J(n9@t;zrE!N!J$oO5N9}-fsY1rh~w=QDspp`1m$ytZr`1Y~poMW$b){Xwx%}d$5wAB#emh?=( zbn|=DH=JF-@f6jDJCg1f6(X%L@wh*GULUVt*VimW+?6Q#AD};L^0AwiF;XzJd3BRR z7k61P?+und!(Uc^bt+l8iFQn39j)*xnM7r0WAE!%PY9zig2IdS&P4p)`u(tQoZV z%iESWN8hJnNdV4YlHt#9TgIvZtEc=I5~cxe(oDykF4a86$L|}(@zd9XH!aTa>D!lc z&S};kga;(tdQM+Q_?b|`Chs5!7lz|sN5AXE5?^~}TD!9j^e}BoxO!EV_iPztWxtsU zi)k>Oe16})3SZqm#qn~`Gvuruw0ZYM{k;CHymn&#<$`GieEN||?tZD|^l&;b9Z&bG zS1;n$O@+2N$^3bbeO6IRO%i+0?QB-EXiO-u)NP?!$_9%mt@FmmYtG(b)<_EZCyAp>GJ%^ z8ha*OrfME#M^SMD@|GlT!cg8~!y=3GMzX9OvgV!thrV@83pcuhoaRfr#~mI$RAK+5 z%T&$B@fBG!9OAZJOkX6UQ9X24?Y!O6$v000gENklimN(F6KL&D}IHn(i*W29gprEKFFQ9N81CklJ&XpQghnq|DIP%1HiY2=%{ zU)pDJ(}n_DmS>xMlGh~t_IHkJ`Tp6h%jT3Z?Op+2+cC>yhgy^0Ntt`4WV*L+9$@R5 z_DTF?{z6KgI#S`@-6f9AdRjhd@)T{!<*RbMX?=mB#_cX%p9@V*WrXmEv@bq6$=*q~ zTbiz92)Jcau`AScqf$NK3r|gQa57v?=r--58+ZVmHDvMLO9s38QAYH;-#eiV|Ku&b z4-z7ZUebg6D}3{X^z8$uN&^|m@4aCJvrDmN>OOZqHO0YcFSUMpp@JoN+xh+49hYZ{ zn%|pt{D3>3n&j||m+B>xEL?oGBTjekE>m^_ZacSsUZd%gC71vE)D+XT0NW6AR-_UF zp}~}+Gamo&*d)Jv=^(>d(KUbD5Zae^Dx<_GpKZsgJ$V|j#SyQqTLFgiIta#%qWAWkJZSTf}$;`>rS&np!Nx{mZ!Oxu6&ys@CbZ}0}GhOxg>h?(-S9RvC%fAPC zu3w+;cz4kK7@X4~6GCT*eG@KOOR{#z=ID$|UB8QIaZv`x4>&gKacIip$gIm~$*1ZCNC8<> zvM6V;v@dMTD+jEOyy6rn#lJjP!VM&r*~%;G&jVyk*s?s!`eB=;g|H62AUu-#@U+Vd zV-7FH98T5@nyg>c3DHSKTu^^TfP+&Gh6E5;z*&PfC(B-V_OmItcv+US7uodNf%8Kcks0g!u^?DJX3llDJ4oNwk*r?xW>^J!aXaI z!mS&!SvQpKc-*Tj&!4O@R`J^FFO&!fmkHmpv51}6%4-_}J7+MIGq`YBmc!F7-`!PW zs^;^Cb%mblU?Huf2{2I!FlDp_HGDTK`z`!H@#YOh3h{=i!lU9ANy+Qh7Pxp>map%e zWxDQnxE3j;cS&EtVXl2;1|frxAbJ{nj-(j?TE8jBOB$cz=Dz6c;TEr^1RE8TO5J zZ>Gn*0G>G9s=s78HZ8KbWm5qu#hiRr^cno(MSVPSu*%9lN7M%_ePYdBW(xVlhY}*hqtjwG#xMF2K zc>rvaN4&DG-_Xagr>6+otaSMQWZ7rB<~NV?h*P#>Xev5n)V+{oSX>@k!Y^Dnzz<$3 zwH*{6@qT{P;rY>+GqHe-A;{a3rA3p~0~YIs?PhK2@`~tW$)n;1+Hzz~xN&12Qg)Up ztz3N>$xRzO9VA{3G6CM)qnAmtr9>@jNx@{zpvCdo&W@&1it$RjGREoHtcN9a4xF%Y zB-`jd%uNMbvVO?s;8gpxR5bcKKI>_f(Kfn&BYHj*uN&JC{LHz1Y&%+I+tIoA!2~Lt z)+*H<_KZ6`ak$E*D{^e;vVRERSSfw>-OBz@dAh-5T#mDbY>qV#?`iN65|m=J?6GlCXM>2tbSv-EgXt*j zh0z+%ooGO4jCU_o_W~X{Sms#C<(k#G=Bh|Cud$LFDqP#n`|y5sznNN&wAqE|*(*Ww z-r*fTC_H{0&KcM#kXQB6VXx{beLAHWYFM%~n2cLADJ6^YNf|`*T6XpQpEQ|I1*YTj zJbR+X&J%S^Lnlw~back!(Ze-fx3=XmAJ5wfXf)=|E>eHi=vt0fO6k&Q{>Kf(C_Y%| zYjnnSak|-qXu47d(>Z&7leT%QQ-;<)aDAGEgz1RK(HWOrW6pw2e4i|Nyf{|poW-3k za)BZ>pYOyDPMwQ{2pwwEwzV$nGkY5OpAMatGR8-3wjtU^baA*Pc4AdLjSvEol^IJH z%|z(twsc3aSaBH=FsbD0f<3sbR5u(-8D$j7I;mHIosins??s4NprMp=AvWDN-R% zh<3{t|8E&Wuuaa+IF=#k&&2m`>#eNYWxK;d z(P=AVoYG*r8wBQUN!<;!Fzss=F}jhjR4Pm-Cw${FFCEsg)}bS>&ZApfVTK6jG_A-S#;aem8ik_gDYnz01y#m@jKsB| z>INOqkp_GHSwr)+(XisGo}!rP9j5DEfUO<9itvk3I+*ULe#@bCjETL=(;Pf^;mM!CS2F&PrJaxX~#Af{zVhVurcdVA}Rs&=&Ov7*0oED_B*0a>eim6x3K0Z`<*-d2~u;)21X zW{z^7U?|sq)^&d-y)31A zK;8=4wnN10qT&RU>OrbZky0gJ>~P+o;s*Fp8xUZMaN4HoDs0)k%i#y%$E4EUU*3|S zybj+4DSVV{ol2ahj6o1IWsY0TB%NW)sHbELu}#5H&S-u{B0d%rK-E=@mOP5KBx6Qj zYYkg-+4ZTp3QLN__&xleF+}qyVcjXLVV|*;SClTI$%>DYkrXU}DZ}sRRh)oXC;VLR zKt>^@Xf~|8CCJ+X&u{UH^PQ@Oy95iC2>WM*U&X4p0p)tolqX&mML3$x+npafhp;|I z3MLa3A1T5~?6_W)-2f*DcP5&$Wd!_yM&CX_2%3uwS>ZEVsiB0#FeH{~f@nWRTM7q) z2@GQep4a+NXJj+*P4PY;we(+6P@Ro#I40<4q_W(5dAx#l8NfFavIZfaR31T&y2Acyb+ zt4$%}aZS_2^Sma+ODR*W;{wC_7$)l@p+Lr_M_T-&pVS47QsaavL*$T&?4AcdeW6Mo>R+i6?9$dR{OlQnT@ zOTu(o|14{Yp4VgDKs8r#BFdN&DFjt7n2+aJSBl_VcUVG1wjnS?xGHn1`3gV@3n*jA zEQVpn4(k#SxPS!&Ko*pPVOUM;XBZNKTKE&0rV_>#G5H=~ft7I~P9hK@Y~i*MuBf!> z^79GMq2?G*e zS<>}+o9|;G`;6WCd#CDg94JMg!uPIoM-w9KtV;pW5GDx)y6K20td;_486z5$v}6c6 z<{LpGkTPrtx_qMiRg*`n9Pz3c@<<uvX z#7IJg126zD5f+izD3KQ%eHws3(R~>-s<;0S4j?;)&1?tp00000NkvXXu0mjfyum|> literal 0 HcmV?d00001 diff --git a/jorge/img/list.png b/jorge/img/list.png new file mode 100644 index 0000000000000000000000000000000000000000..fe5682a81713451b2c096163be87bf943cfd0e06 GIT binary patch literal 179 zcmeAS@N?(olHy`uVBq!ia0vp^d?3uh1|;P@bT0xa#^NA%Cx&(BWL^R}Y)RhkE)4%c zaKYZ?lYt_f1s;*b3=G`DAk4@xYmNj^kiEpy*OmP`7mE-V@7#m0UI2xRJzX3_G|ngg z`TyUZ*|MRt(Z^w%n^;cM!iNhBzORqlIjgY1V(P?z6_1L(WvmDiS{1Tgkm2ioQPCqG SMRxQwwE>r|SVQ=(9w zS(2gP?&+rBn_rfhs^FfQSfpU0X8=?n>YJF7s*qHv;Fy(I;gg@3qL-PMmd~L0lZBHD zXo3#Nevk(kSOXLk`cg9IEj19yQPgTlShaSB$gzo1rVSt1Zgw<8_p;b9Wwr)P6li7a zY+cm1MpLK3GwR18m81{5JSI)BXjx&ZCR@>Inyp|XEb^&R_UQY5Cv28vC@6HfU+T%w zk~;ce#R>-%*V+RI4qiLT#L+XQAvHrpi09w|1qVScp92jak^|kFnVhyf-4^7V%4F-IXY!+I)obAer*ep)~ z`cV1v1dUrF9$H*S0z^1kk-VBwS8!H(&1omA#!+Il& zTdy(P#o-QLB*P*D4^FXG9fLlnD_niq0$$JF@`T!(mNT=|sATiTX!7%PW3@C0608jh zQEB=62$RK_i&v{2#Y zp~VS}32nTgZp$Q#?nv@2EqJZ@IqiP~R%<|h1IFG82Od{LfkP)Ha$_Zq@09T9xzKh) zgI%DD#e_-WxrUC!GB?FU4~B@~861tdrc1UmCb@WQVPjaeGciert53C|z*sE#i1G&Y zAG~jr*iFpL1Jtx4^cysKdR4Gm8V|M91L!f1Wez3`hc+E^RPvn2v0>K21P;5rR#r~~ zl@5tsIdAh#ydFC22Rn63nzfpmU8O_>mK@f-n6RQF;Q-T50mDuXi4D^o`uUvAGdT1O TS<+ZzG`oA$6d9~R$rB6!Lq-)2 literal 0 HcmV?d00001 diff --git a/jorge/img/logo_jabster2.png b/jorge/img/logo_jabster2.png new file mode 100644 index 0000000000000000000000000000000000000000..4d8042f943c764256e1864561047e462613da9fe GIT binary patch literal 13517 zcmV;;G&0MHP)AjqszRxZ*v)23lF*^ylAYcLI zx}Sf}oPFl3nb~XRnYTReyHrPiNEKC7QN>>h@3kDfx4STU$ z6Hqu2#5qK35D;;QQ{WuNXvD;niW+N695{J6Uwro=jB0DCl1LR*yiv*H$G1EsU%T;f zV&@19cs}i3d~(q>3uuXykFpS8tO)Jh_90v_~uo-#{3}I5bKL7Ke5Ug+g8!*AT%u zmk@}6bplG^oCP!@;GE!@#MKF{!imKw2TtRZ24(5#vmATSaIU%ZO;rM^qKY>Hd6c7w z^FMdHtlHE`eO6PliU5d35@HZRD}xq?6W9~sP%3%God|-UwI+@uL=r0_A(dobYb`ao z0EwZe7=p4W(Fg`jgmngEAYZgh9MQi5(~uled5?s4r9<7?JPwL!w1xmFGlR{ErD222C0}P zdt4m^w1QF;GJa@1zrFgDstZ#^75@O}m;2*~{`-%zzO#>%XFxQ8?-Tew#u$t-;2h~7 zAngYj1%c<`DMcpb-(S8;sT%qtFJj=?qV8>Eqw7zE!HzSVa~8MC9?K-#;Y}u5PC($YNDUkTxg{##4Ak zp>PC&hv3j!@QlIt48CtFnHEK<9O4HFktC!M;QKyd6k#DD1J46x2vPy{H5nRe(ir36 z`#ws8=Xq$IWM@22V~j>AjZ%*0T$<%iZ0DMrpQ;i_6;-^>cu9HshHag4{T=IR%%w4& zpd^b@g3=D7G)^4#O?CA2<}uo!wMMLN`q76zV)=MVq6ZjfqgtSh97htsG zNv8}?u50Jk2R2mQmny3GN0G;m@7_dBCO~O}(FV`+ zFvj4lFk^fR>5NYx25mG(D+1%Afpp+ww4yPaL#agfF{mlWwjFtz>eGbQv7@&{cOj%M zn`O@EX2y?cWZd9dCJ%4s%5&%P{^O@|^*d&B*3zledWQ3ln#S+1Jd!)FK9c`9_dpzu zC<2Va^EJL_Fh=1ADQdGRzW;|c|B{o~-qH8S_4llks2ou$h1_uO8tKay-|*M=Iuy#nydd)Lag9esP}V82xD{VR9K$FF^a=9)Ct z34zf`wgbV5;po|eS-xT`Vl}Zia7iFj#8zSukT#k~6k#MxXv?yBXOW_H#IeQ^(bkl~ z4-CZ+CJwD3SCi$`gNM*IpdRZiMjI?}*OS{=Jfjr>Vkh`o^ZQ44@Zs;?hw+kXT%}Cq zsEL-Ow;1!~ch2F^nQi|vv5(&Vh!d~;y=>cEWY(xAVh4|H+|7g`&D{K{rEl2p;l_K{ z$Ys|)M#^{`I%Ob_Jl{ckzQ`pfOy;8Fr&nWJuLDlI>P}g)dMCqM8yMb{<_{}((NyDe z_kX@gWwYu1EsvkyyMcym28AZ@G}>sC0%Ht8T66Ow+fW3E7E&o6FXf>OXy2ervVTz$ z!U-}St9SMhAt()L-;(w`I*Y>gjw0<{1@3-&Cjrnlpq?mpXez~XE@ttJAz15h7JMMK z5l77)#GuAB#`8%90mdl2iuGZPrY@c4+Pl{OOHJ_Z?xJ+`mT9R=^W64c*0uLAAm_8a zt3aVpdc$7VhV}x!($u6C_pjOkO7J|xhW0}BseTKlTY7NLp|!&)m$*D)h$XJ*Jp`xroT@_kDFOqYr|~@x z&-3tnAJ6m1))>}o?Pph4U+2HdB#$0ir_Nq9mfrpl-#2K1La6xYTc(iBzH#OIA2?~c z8a1e%vUQkBMfT8okIPOyU`+MVe(j-*;ln3QrxaSmXgm%5{Y5T1Y6`;!)vEo-$5MewIZ9F{RB)Su=DrsH7 zc-*yON8`VYUmC{TziJ0TppZBQ3|c#W^QTR3= zkM`?>+n(HvHUds?qVPTV{i7Qxmy%G&UY*Ai8@lj(gJ=(}Vtn?9vpmElt_e;m8VjAo zCPc8-VvNFC*wf%Asgz9$<^X(Op`2j6fY^m7t?>;EXvi{kcmvBH-^PxPJkv)HpubS& zy1Uo2VskgEHV$Ix{NXhCn%}M1#rJMshZlG#os{>BNTOKZTZ!kBa+YT{_Wa9C@TT2) zd3sGZjdf{4X8}tZ%|mN<(cNDh-H^+?A^XGL_e?kWyd_)fAQm(kieWd8Jm1MVM^zu} z*WPat9(bl5qf`>pRDy3(Jhich_O5~qYYmhJ5GSnfhzPX8Xvn3r)YJqRV-V5F+g&M) z)&zcl=li%ym2A?N13XV*%pNXETpR6qc%A_?%F~F%iIsw-sUgjbk+sCuanGs_zJ1#U zzV_=?eB?(@(w!IDyJCL!@Mhk3&7*wa+9$dHnXQa#sbz3u7QbToB$=SZW$`=@-)K^4 z&8Dun(hTHviaYd{ZdkQ(mjLt^LRM|+df9_N{QQo+e}DY>ogxYI^3=L^iOUJAKfAU= z`udWX#hT4s(wC23a;&jK8q|d+PNcUGp|wSvqt{x#c-~w_52#!6_^PdYkM+d!?Xrhy zS3I{v!YHX5TCs7rluNHZKJv)wU36lp&!4UGg)%GFCFc;8B35o_f7O${dP|pd z^%Rx>Y}(!<+jr#!VEe9K*|NPy06IGJSFPQcoKI(e@q*P`cE0TRPpsQ5E==Za#oBf$ z6_dGIwXsXOyNja%Hf-sY9Uc7wuwiG9Y~7vzyWF5#cJ<4aoyk}`cK6HrZIx^2>c49B z#!dk!6(XKppL|DQF=EB)4oRMSSh;rlOP_~QT>17H*w_POF$jI75+6Ew62n>>)LzSD zM@LC|3lWVOAEh945tKbVqp7P+lkY2%&1EQ+%7{v09aLf+3Bd@)7_4<-(Udl+Ysm1cKds|{aRX$`kXmlKXEP&Pb8_hP zR<60_dBzUR$>Fnxa_z0FXsl1m|6IC^5rgYqH0FIz?!xzCoDI-oF)`fn_*QOxctNXdtrClH=i}QsWXd3EsLS&6dscD8v^HsO$ndNC)-i2( zv)uHVH!0Nq>VTi#xmvEg{xPz?&*=+C%K!Vr8nliZFF1ONZ0qPlqfsu#;^-|z{P%C4 zm75>l%;xqU-oAK}-1*FQHg4(U{in>4cOO6HW#{{bKdhEd|M*dw8&k`VJ7_pRS-uu! z;O)yM$u+-ULCU2#_n0yAgXJ5DY>~4L8!x|oWE zajkW7(1cdLf5%G34Q-SorViu#w?D(6rrg@&W{u)U_pBnL;mjkZ$aimj3hQEi@Zn=+ z_V~YE6@ANtYvq#fJV0$K;PfL#%k_7zK`F&)hmVsR?_Wz_U(A`y#>veOZeUwy$OUg2 zC$~SkjSV~dIAhTmd16f`D>ruYjw8p)N8dX4Mdz{n(Vc{p@uC2SV3g+0C%1CRn`ff5 zPExgxKi?^z_}SAmW_`5M7_F0<6(Ai1#8C(aP@2Ga#BrR+#w(pWWKX9sfmC1!Lz|S^ zR{~v0KMw^?QJ2+>7+gbNU&z|_0ysw`FmXsN);d=2$TO%R#i*8A9^J4TUj+y@X(pr< zPCHU!@O=f!P>w9IO{#Upf#++wdqX~Y`UGZ=e(l>9e*1ST=|4 z4$g7&e;og!<4-*I$C6GP3dJ(c8EP^frIHX!OifzRA1SnjOj=Qj6wX@eYdm)MMtttP zIh?v^(tcw;v$kDM{>&}payhgOams+Q^cF3Mqs9;D457uqT~#jjK-EN5tf!* z4Lf>!`QH1MuxR$5Pjf%Fd6%5L6w4M5gBKXeVF*qL z(muU?G0Xq+C?<>?u;2LSfB8WyIme#w-bY+Cog1kAXZ_^4xLeG5lUr9r4)i9EIDd& zhO%=UG^UmRcj=-Rjlbwazm(p5j1x=Q3fYt)3S)XpF}3M{D2fTKkPecUY$+@a_1B4$fYvm!+a}vaaqn?HdP%mZ6HHiQvBq@%Q$4lAV#!gx#DfJUvmGCoi-ES z7cM?w24BA5U~1BucN{sEufA&mjWspAdHx8#_o0R4GCs%69l~`VU5brk=1y$p++|bL zuFl@huAcIeJ>yLtGeEuN@G(TCGH3%frWiUdKW!%edDc9FjN-i~Pv^=r4N6?cx^yhpyl)ZtVv)tOhH=V*(QMw{DPb|*-~DxtmFwH(pb0~H%i*Kx=qd2U zcOAkd$4{Xqo#FDg&gMgJo=#mR;F6Q3@#(YYk}+`c3Dddy9Sf*WY2JDCgclJ9z=uzm z#<n~l*$bq#?9XWtoK7Aam%^pg?B(DbQGKQNjJCcK^4Q6O#ifccxjKv4G z(c0i~*_m_y>>RDx(kIkrkU$5U&`1)HH;h9$SngJp_w`tdYX8uP{olWSHu6H@lqfiJrcxo#*Ubc+p zx-^St4dn-y9Komob<7z*kefcflx*5(dr6Ov8(&9a$!F4)|p++wTV8e zHZVjsDHgr))5kJrYzw1Xb6op@#T+taAcGpS{P2T|ICAzVT51f}esB>-&mBR3G3N5q zW+pG9b^BbfxQ_(uPbbAdHee zq*^PqN}3q!y-k0>s6^i(A~+PvSyH~C7)B@*N_$uvqm{x+V#U<>n!4H)yZb`=i!m7^ zgcyuiiq0~rwS~TXKRsnjUr88J=i{XVx_XObQ-;obgh9}Lvfhif8g0BJV5?Q7lGmXz z6kW{13C*0nWYj;Wn8-P(#EQfU%2W#dT!hkoWjjV`k6Z44PCoqIhZr@uk>lpK@txnT zLVzECY!MUMl85mfSKlfRu5KsoX~b$GD=eEmgfCopxRThSJhL|l#-3g*&Vn}l;MP_0 z`5PW$^6(apIAAbez3CZ-He~q4WlKNT&|G(EAz$G5Pv0tAw)b(tF_U;~Lpu+yZ0DjA zrtrQ~<|v6Hl;>B5jL^p4s}oShylDH+MCU55?LMsC(jjlX>JP+`LnOu;4;?wabm3tf zF>BbXp4adEcBOpjhKCtBq@EMzkK((xJVP!WaKq(CGI&7k%Z}6C)!%f=XP0;M6big^ z=~RCG$Y$1V+R4Yxn#bFhB`(r8e!W7jx#@AlPl0y0vSq}e27dF|W0QyHe_i7v&+L@* zzj_z-DUWxZIEByu{0VFb-+J!?4w~_oi5nk&daJzm8+YQYPtsM#p^VQD-+KttCJlMT zu}=T=&GPidF3w&&p69pbS-xT`XCE<+Pn|bk0bKcm2jyq?t;RDNoJY}y%p5*|pImX| zzEo}GbVcN42(3;08KW_kLO4~4TX>#NDwW!|?ut^F3dw{v$)+fiyeE`4D4nEe(_TP2 z8?d{ppHc*VV2IFo#=|MY;JO@dK4b`2oim-UymK}uAK1#ye9ZjuHGKY@seI|3)4BLf zV;Rz%BhWs+N#XfPFOXE=;Rh9&y62%iAK&e<_vx+Vl)+lT;ZVl%tNYinyR&cBUgD_C*S>Q| zKEGl4O2Fg!O+9?$7f+G%J)Yaz&GHpnKavCkyJh9tc4}%o{`=P}dHC5~q>SO`cdkx` z^!yhL>F=i#c&Y6_qbhjOC*S+sN_u)rK#XAUqnO@efgjxQ^k4K<|MI@)NvAxvca-_s zFCRk$)^6X;4fniq#`kCUtnOI9y%!e?|8e6(tlzquz}NibwpA5UjQO8Cp2O(?C4#Xr zDed#<>RsIa*hYD&YrOHk=jrO{rL$1x(rfO;mO^?9d4BuwU!Ge3-W{vxDJH#xMMvPE zy(7<$?|J0}+aDj>AP=wUB=F!{H$TO_E4I>*gP+}St)UlXE3ZC+PJfkpPQXT7SgEl_KPbZcNIw@T948~|Y zqX^QPo_JF44&_yB*OO%7au!;nZxUFN|OmRCode%+m;OH@ApZYqjc-&X`mVYGW)(^|!R|K49;N!i)kT^PN0yC=W*^ZtB! z?{;5*9{?YD%S=Qpz5_4gh(gVn!HxXevB{fn_JqM|(abjbqY#oX2c=@jg~v{%rK$F@ zS333cPn?EwWsC{&0!_J4WX{-TPB?g+0yud35Ou?qsn*ggi^V**HXd>=Nvg%jUAFhq4?HCOPDuh0GU*pOV6BN zAw`c1-!zFZDuYDCPSF$fvv_7JOXiGx$+y=<$4+PT&<2JzXZZ1@M>4P>$An?coPYGx zzxX@);7JEi<71L{i%&|%%J;wEe(Kw&%ttxSw1*~%kCe(WPAfc>Gzv;eQas9HnKiPB zwgDdb(4i0_D`16X(*_eoj#wuPMD-P8rVeYQA(y5QM<}Q8On~4}%HbIgqaboZYfF|) zI(S`pV`oQ?oOjh9*wWe0C(fM5|J=2TXV!P{rFS33kq3==(GB#4>mQb{{pLx|UNn)` zrW{|q@kx%JJ&YOS2l4rzKFZt)%^W*_93TI|1I!%T%;^iq5crzd3PcXH!%9e@6iRJv zl1)k*jdg*{U%maWF7@h@R4I#9I0UB^+Nl?;u>;?_WU)#l#hT?0Zx-VP#N~*O ze(R6&)4Nvlj$@{xW8pi?pXJQOV`!|Y;j2G?f-{zk`N+rt^?dH}|=?_5}N91CZM8u-WWV76M|0=oSx<^TQh7X=`AfLVN zLBu&edDcOE{>De>D@0s&#(~sie71M=(~vcM|3gQs{aio5>WWPvNyWJmA+}~e*Yt%L z)>^Ob_~KjkWi-Cj_E+C~VL4fnq`zP>8bV6crE)yArk!)HzEgH|b@Iu#9>x#<@HEeF z?&MSFEZ{fyuHk{_+PUKF1F6df#Li(vxbnOO>Wa6`!B3_3j&B@ED^TUol@LX5c^@IE zj{cCJ-M3wS@%S!ke1#~DrxgNtN~5$?)TDxvB=G48NuLcT#Nx2pkg|znJSN6Q3X8)k zAMHvw?cu9EE5%i^GaAAe#y018|A}LwvI3=(Nl_ONM{fai~!29t+T{OPC0-N zoH<`TxoWcg+=T4`Y)+_omK1 zzIov?PF=Qts`<&)+vS+ce$9~j47>AXe5J_Mr0MS~Vzs6|m!hk$gwcwIoT0Bjrfi`u ztLf~Cxb1T%F=_0;S2ZUe{rcT<{XNgqnDdf_#E9w2TNce2!VfMx_Epd8vP*81tsUK@ zv>_xQP!ahsq8tj1wP~z#C7X^is71Ob~n%RIfQFCnZLBGJT7VNuCXmsHha43DnqrmtY}w2!X1 zRK^&z?_qq82t(VTS~4k*0rjs>SM|3n9RIU1tr?;Sa)IC?!S@Uo9y9AD_kZh>iF9`5 zIqC2Ty!ZIow6_;nFl89;J83TMJ-tjD-pFNV9>(^rE=IOCaplx*vu z8RJ^jp%Yr!)>Yu&j+w$yvq#b1(aq^cOyJ~0MzOQK$cYO^a^8`X*|5FD;#tFZ?};<$ z%f}pf;D}dE8~_)eF@u)I6jmH!EzTJlvneh-;V*Mr-hILpL{(~~=%?RWCJt@pix(V% zlQK1d$5rPqgmR41imTtfh?Ey&Vz}b%vuLfafmk?w(Ug~V`;MJO9G5DR1Vv9&k&=$IT z;De{lWX9Mg`uamoIehYq#!h9@oV9d3VdOwOM1(>aPB^TMaU)vxxh<`YK_WW)>kI$@ z6h28rK~zK43hgwgM4RDNI({i71b#pvbPQ|J9MCohOvsb#@@($vBhWR>8DC3lLyC>t zORU(m1M8=dD8_jSa~YBo(0d~AK%g~IO8EH$+iA>=rm4}O-9G)up8Yb$@Z^SGe)ISa zQfY%jo~QI&OWl#5B}EOps^0=U{5 zgNZFVFvQ{z3Ca4YS9QH3ni`2+jOV5CtflM}DU+r?mwUoCt!uf}C%fp+w^?|iq`ELt2_MjwY z(_ELqbCN6VT5D3Nfa4DujU^(T&T`H%(Y52G|%$5^KcB1PJVAobe2&2`xfSq=FpLMtH>jV%{BX!0oT{rY%SC^>T3 z6z3f^nVv#OeNCFPkDg9%Pak#lwOn-4f%Nv~sjI7b$?=iqFtE6c)h$I;kt&uonWNQOwA2N)Ztl%)@OO|ghU(doRgP1e2HW|=| z<&Sl6^JCiy{4~}@iThC*OI6lyQ3^DcvcRDeT39lz1z+pE>%1fhVk$%;Skc_}S3K7PUoRh#+C$^W>I-Xv=_?n2?{Ejb!;DZ2V2E;@D^A3JZs ziw6AqjStCxUi%15^#S>QAqWKLG@}OBa@$u2KIF`;G^O{;8=Ux3fx#Qu@)Z_xNNoIIwPY-7ssnFSdz>N#zVA0dzEIz zT2ohB%L(%ap)_=t6n#a9L2=IF(M%kgqc@D`4juV^;gHGAv<<4kN}N>6;w5CHAe9O* zm6{l*0*2J(STeOGY5HT6m|$$dT8J#fRuMS?u^cwB9zX44On|O5UZ#>t)>?y?^gJC< zqxoxy1K_Mh<0+LQ8tSrq@U%H7tr*_qbMm6eFS-AzhfnJoF(`*thWDO4lYxzC3SpU3 z4oxb@+XgqQWpjtou`A}#Ic=P=a56pp5y#9M_=>~T7hyzafy=`OyHG>1K^!UO~;jCLR%y6IdLZ4eFa7iu3^E{VSgVH z-BFY1D~2qX*2YCAOr@i*mvLU_m%OULgc z`UNA^#IcP;CE?=JW-@8)V2Zu{ES|eJnZ=n)r_fxia{*IDp3-8Dhb?KYtbky znov1Xflo~)AYUve-NYm@rxHshkrx-NqpdmnSAS`zE}g7qjvXMw2Io{%QcJG8WySJ= z1M128aN>u5Axb$ucj4iD_Jf*E|<9PRR)6|BJUYRs>RCy5(sOoid8sA6X+Gzvcl>oHv9+W>4ZH-@1*3)7v=d@Cm%{ z>$h{@*f!2PZW3uvh(aH$Lqvp{tYY$*!F$IaJ+z*?pWI4d1nU$fD;zXsIKRC8Ir-f6 z_i^4)GiYmT;8WN9k<%AVW7?!fuDIqw-ZW+Wx9>V$^z5MXzC%E{OdH4pdy8a<9JZTz(>T3AZ5AWvf$IM{%_@R`3 z`6Pqte7^jiC7gK3M8*wmRO#HyoAr|?3{ZD|<;?yY36qu*Kx$O5nS??`Rds<+hy{Ep?klZ^NyRY4xHR9ldoN}l*yw9 zzlac~j%r5Zl1Ir};xY^wn5M2a^P0OYliM0pF6GPaa)dJui=ozs2_u_6_jeh8L3V`g{QTuEODTuQuD5O+o7$DYRM717*J?!*wq`- zoHd|K@+E4G6~{HVZ=|~r6CwD{5O@lkIPPer5ixisLK}~FE*wTSNMhAm31STGoka#V zWr*XX=q)AC6*_Lbe-n}O0Kqw_v;auzTC@`SBjL0In>k=a=5J>Hi?6;zesljb_-UVV z7!!DjzCU&vLyR^a`C>>mlcrQIuz2=3zJK}AFMHfkr+!1SDW5PD%5jkzKSjwY1V^AD zb`V=j+D}Ng>?idI+4R5DU4K&(7{%e)h=|SUC5!&9+?l`CH_7 z53Z*+2q+aJyo{lXNhb7w2gaL=dn0Kfl@i$r7AC;hPyiC~>!{nkEidaRudTl?{~Mx*Hp z;jWdt30)>J&q+%1wNgp5Ag{8roM-+n!~wA2fELOw#@E6-mQ5jy6xIr-EgXx}n$TI! zJ!%R$-=|oDgQm2+;+Tg`9!7h6A5%uPaMY~P?9LY$*ILJtSwrbAlo`^L;q)UW)74v| zHk;%0MdRtphxkE?w=5h_PXs~AW7@>Q0L+{;SdD0{qqDD@`ICn+Wy~OUbrv~z>Ie>> zGMt{Sn5iQMv2f}Lc6H^MJZ30I&l^QYpJn90dX~)}O?SSVrrHdXMhtlQH_^DkYD7yT zyL$RrHg7BwMmE#g8!~tNP-agW!LIHC3&sy%;p~y@>gZ>}$XX6NU^rc!F{208Gi*@v zUo|gF4je_w_Xx|v8H=V;p9|>8J5D-e42?B)6uTo1oig+l$2oHL2>OdcE=Y0KQ4nTy+l;Y$=N3*l5 zkJjc46Gt??=9b4X^T$yr^%J>>Q;(cV97h`4nrwa z4rn1r8Fuwre*Mr^`Xa?~Q=4cTmctX_;q^WI>6uQ(53S|!NzK$|6dgSgH?QbKJI9eT z2QZ+{Lr^@vF3*ZB9Rvo3G}e&z44ZfN6IqgWh4_dh#al-2QNKER3c}1`8BUp9`*&mf zho9aeEsZ%w4Q*6UtlA+#pqM;*kXpHVhgfk;A3sE`-@IGedkV~*I{Z}~_xAhO$ddVE z6iRW&!|UbXDT7ryo8it!R?C9P!&Q(?ao3|8WXh zmYR%a+^BtT@N%KRy-)9y#dAj~L|FdtdRba|>)rA2YFTt(l6koE!8LNkfuj`KaMwet zW!{wGDw9ib=Ob(7fN=xHG}PCju})v|bg za;)1QcwUyw8>di;yB=ICbEmbbZ1%6uK&;xZOA5u9S(ApSjoaE~XGf8_(}t^^J33|U zjy?{aIpS4)_YXa_Nrtr4GOV>xJ+g9>G}Yx8-PWw0ShZDZQW?gMYEsXyYnT2~!~v6s zs5P5+OK*3HS=0V}>hLDaTIS+#{q%q)xIfKbyDWS>uiVdAb%8_E+K%X#HN!-FyJiW1>sYBDrVp1Zc z1I32+kidu5x`41!%$d<}%gSzcc9+Nm9>vgNycjKsU`U)zS^`vRY!Wr3CUU~Xhc&*d zsm_00Ca#Jq{*|%ca+5=vjGEM1D{FU`(WcTrltiA2Bz~bW3Ppg{Ldn7SK{>R6d<33? z{!kcPn+D~`N1!Aro^!Dyx)pIR>0VjHaZ75d%`}>g`8Fk@6Jc zJVcW$er=L;Y=98D%W{=2Fv|bQ;L&qPaD!&vwC|! z&+dxJDhr}W8%v*s)tw>Avxrut(mrNSk&)48&rkY`=%k9a-+9a&l3{SIR2KuPqKbcn z+p-5|&Z$vrJ0i(jgC|i^lcHfyPXeV>5>fSiiVkjHxtp1z8}L=k?moqeEk#OUgmc2c z`ZQJ|p5A8J*>33#3rrho(8jR4JL0*WWgtYPhL)TojA6~rE_#YWS{q`#y)6ip(m0ZI zuV{sFhS+(uHNvseG774z5mr&f>xKOSrZ2?WPUM?^>?8C&hNIB6O7)I(v@tFdqK+> zVNZZGjza$Z@D^2*a@EWS}eyqWjRvf=ebL5>;tGDQLWktX)*1P(?s= z(qtylW>yrdC{P*;cze>V+9dthb+TlluH<^ONtVA1txdMA!YM=E!o0SiN*q;G@y|#e z0H;h(DXn68c$*@nEmkxsqlkq>J*M`rZrv9dsc;1BtCLk4aQp3_v^|uhSE^J9hok~m zn=fq3QNaViKB`t{wdv-7vlHNh4ROZLN+kOPB7vU(U_8#k5j6o zHJjRxX!a)%g{!Pnn+VGGlD%66>kLc0C;q>m0z!R zfZ*#SxgoFc4FB(@`ul7XK`FQI6O}|d^j;F#v!6{0>8!#k$I;^rQwQlPaa2*o8;Lvs zI*a;(8=i{3y)7@K{GCe}Oz8aarzM8_T0R4Si)t%2!EG z4>6VeibE7Q(dY_cpb}|$5Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2iFP( z2o?i11KV)`00DkUL_t(2&xMjbXjDNIg};ZQn=02zMTJ)e8`A`Lbs;wXbV+GpZ7bSn zo6<_RwbDkgOFAu6B_s%I6YRXHTt&zUgyag*v6$JHw=9xrW|+C>yJyZ!l~Mv=?ZY51 zTI1rC>va2fD^3|_$AI}c!)gOy7D2$;#~rrM{7DPF2i2(%0<2~N0I<|`-mK5Cv$Zne zQX!r^e^pIF3TU=-0zeLk$BP?Nod>Hn?c~SrnFa)~zA3;^%$I#-Zl16Xyc?_}1%NW- z{Lyy^gO0K5j>1^#>i;1W=$E0hHM-ThPB|R9VKy$*v^Zd2LL>2h<1)*S?zA%zy8C<1xGP9Pkuy@MG2q+YUh~=h0T3k0T|)OSg#Z8m07*qoM6N<$f-#TU Ae*gdg literal 0 HcmV?d00001 diff --git a/jorge/img/shadow.png b/jorge/img/shadow.png new file mode 100644 index 0000000000000000000000000000000000000000..904dda9278a3c75e5ffd8d73b56e710c73a01a69 GIT binary patch literal 4648 zcmai0XH=8jvVI{dy_X`J@MKQ~R%(j8sN8DwAMA07MW;-Og;9%Nz?>ykllnYIz?K&8O|`Ksy)kR_y z1g5Cm=mZ4rQ39%bY+WD$ZBX{De(ZEH)cJj#CI!%hOXx~^)PI}3E}@s(b8BOI_VVlx zSfz>u_!_e{%{(MMDNT?~lK|50rUw9SQr~y-J;i9k;{42_H33H4X~`u!Y``H3mp7-# zjUOq`0nEBR#4+f?!Vt*0jpBR#?xe*(e`?iA`thEmH0Aq^xNfl zFVWJ`tqEJ^pl{7ZD4gD&flo*H@10r6c<&A1O{!8#SyNu{$r}qNk8*=Z0`8oOgwsAj zklgk@;3V=|->g*S^BWxDn>DnSI(T*LH^VDAhkpf9EsA#5Wz1X9U=6MvmOiS3THchUOL4G{z zL+RIw3+D>5Bvn$fOCEO>Xa1%x36se(({jxA&=-FaTFfTpo+{jx#;JP_l*SfW}cEC=s+(ldlG1h;8gUcKmW(dS|j7wV#Tigw}ofuxl7 zr?-YBc~WeWnUdj#CfE0_`*OtmVoi`qFiP-GAe1~cbTw2QM3m4yEj45+H8=7rd|+6m zKUYlio3nJVnAh-I@tyK+15bk}{hr6rJPh>Y?V%OJ*O2!QvO_fUo?4+=FMhhr*(&*Z z(?n&+#R#-=!y#G6^4V9?slo{d#W!6PjIEW^8}~=kA9i2v zX6W`21Boe$RSwygS(o{j-5v8D!;D$3xvX)nt&9=foimg&Ze;ArILhq1(=^;^c--*5 z0q@@PM`6Hf1+h%C`)ikRd7@8a*KsFr7yO4fMC{|gdrOM?PV_~X#67Ex&S2FNU6!!;|_8bmUfc({Ga(P^O$R0 z@eZlh4Sm%EZk?U&H^z}8$d1*>`-ABYnHHGQM*Q)o_XXYiOM@0`zEt*I_ENq(u#g;M-#-&JYjf|@ttCU0v*I1 zR0VPX@t;dMS3o6w{?qwy7sTlmI8^9q7)<$H;^0l-Egq89?QxqMseK&3s53ascn)uM zifHoH_OZ>VmvN57al4Sl!r!Xx2W={~Y-#|6$IR zk^oD+oj#xXG||nfd|^Dd9=y2Rjmz|uGnF=t_PzH$nKkLF;zF>1ioQ6P-a?U+4lmo* zyR`Ru>X_V5xsNMRbr0oY7c-0!_U=u{`l%k;hUh(TZ@o>UXCR7*xfok7xUFNPHZ+Os z8GSPlxQGuR2Y%rrGb?Q`Y1^Btg2joWAJjFZy*~sz&O(OgEv*(1icA`OE33kp<4-x^CL6vubqoXU61>a92| z5?&b+?UM$d83kMiCqR+T$j{{mUmnyg&Km1N2Yq)Dj$>n!4^h9?Ry0@gSDcXhrr3&y zXnW#(3p`NAhm&qO-uAnZUDaK`xUMC^2W{-XhOVOg@04-GzV>m7!;8l+&^D?)nTlYyb!`-Mzhirn5cOiC(Rl~5 zZ;2O1Jt~tgm)G!jKAF+1cm`j2y^mWnmMeGL(OHr0BMt-{+BXfl4j&BPe36Af6I)k# zP);Hfm+FKYb`L!E*1(>$>)PWlyraA-jxl2=f2ampzDJMa&gKGEng%)m;4KUQfx!SkIz7|N z0N{QL0RFrL0GVU}0K;RUEe`;IN?l7$`Ki~~>O_x2Jc+kyv*&h-5L;YrVs@o1UaV%{ z+oV8~<86Ei6^kDF=|(nnf7NMFqLN-c_4%B)d^^#3wP58N)y%a*)UPY`&E!XBl|41C zble-jZI4vwWoox3LmMi*jN{iqJZuP(KPu`<5Jtq?h{a0`YU+}< z8#ciPApJ_sBOBt7E~}y!vdUNyhSQ*ql7t_3M=kuC^*{k};}|W4b>y9iDg}rhsKOW- z6h5NkVuD@s=Jxz%nGg)+^g&0pM>J7wOUGBt&Yf8u@iWaR;<6Ds%=a3EGtLmFxG!>= z3fa5ihxCqe-GXdEmS%jVxA2ey%ddD$b1Mu#poNL1ntXTxme|HuFmjZ-RP!qKF2qvE z)NVCcB~*vT?-~=@P{#4*>4_7fWY`Bby3!%U{~?T*MIv=7QcW$d&sM7ZdHoit(yEPd zZLDU+=bI;y?4S5rT2b)8IYYN-0(J-5-P6;ftD`fKmXd;q4ONJzA9k5SE!F@YTJ_LW)ZaMPA9>6!o?}WY8iir%yf$Zl zf8x1|=>(l#hnO#-Dz-+z+ht@tYU0B6ZX8vtm`|QmSKIm&_H<>gEh?*I&>qAn3oFmb z$!Yz-c7?I4yW4;BU~{g*iuBCM5MHBB@@+UyG~4`j=&UAHSX30?zq8Z_l^Ory9ugX= zQ31u0(;nnWu;QTX{9)nX76%T((sINbxYbm>D**A^ zyunNCFlA{u!8zEt!u(`(WMo7)l&dy0?us56k_e48C+&F*KC>`Y-vEU&@so>H>F{j_ z2M3t^0duTpY=I!=&#>;4|K=pxN72>R0&lveQUj~`klC8~DKgT(P2#NB#1~0j_#6fz5E+&}>Avqnf1&x$#DAsvH{3&k|BxoISNWfH|6O@^Ztkqp?o+Vw z9&oAN?V6eEbG>4|vQ44Jl+~n0ENAxIB(2ZR&N_tBDU@qXI^=pjDrIAtu&yHgINJ~& zAC&QGO+WPN6p?*pd7$XjP0j+p{B*S`N$1#{4b8>^58vIGX~y0!{Ng7UKt9&$x4UAB z(?vUQ#1sXgwrku-TMLTVv&L3r6o)bGuTLC3k@@&225TjN?TW>lkx4;eTpz={)#z)@ z%EmB$M`lz^e8=OLL&|nnN2>yKbaly?2`6lLm@&(y48)A3l8faL;$R}7c70h5PJmW( zvCy$pIxqJZoVm#zJo|`j2?Iy1EA3_dDDnu1}(mh+(#iL10vw+)hD1=}`7f5=gwe zClr#)!XmpR{lC{xVPPTTSy_DC{QOzT$;k&f{8Y#&-J$`fGZq8_!KSFNaJ{RuQ?A3x zNjT=Namvz!%hKaYM?EWkEh9ll$hM-W`Ipj!%o~9Y1s4WFJ%1wj=}KOp!0(0Y=fah+x5ye+V3_TK{oPyprru3ImeMK z*4Aj#MpXU*l*L-U(3kW97#VT&ZPc4~NU0sAjhmbg>57mysz3dngC%n1Dre3fUqDM;SFK#-<=cM%i~A6Q literal 0 HcmV?d00001 diff --git a/jorge/img/spinner.gif b/jorge/img/spinner.gif new file mode 100644 index 0000000000000000000000000000000000000000..f4347b8cb79375e44d866ee6c3d5ea6ffc61c67e GIT binary patch literal 2573 zcma*pX;2es8VB%~zPr=ibVMCxot@BWhL8m(5O5I{f{;K&M-T_c8x-VFITV2itAPXv zB;k@n5HuVrSSSJttS1OMIF8CVIU-hlk&!rh;F+Jl;t@*RGBW4~xiFJytJHD zS)^VkTalTYmMn`*PTVC6n6DBqn6)A?DOt9~APY-R%wL|Fm^42vV_PQhc!iLZ_yiy4 z>zx3Ao|4Hb1rV0Jj-_5Le&VP#kJ(}a7MK@XUGcCwH-;EHVtEt=5&_)S7F^4+&-D?i zah-Y0C2>Q3q)BbO_ZyFK@`shdzH?`n6|YLKZggM0tk|T4P?AI;6O)56rIrd?Ws$iN z-q*-uG@7(~V>5-KbhCF(19sGxhZ;cIqNtF8jN`E>8#gNv+6t&stNPS5T?CCr>O&d& z){Qe=xx(1RLs+`nV>-fVCt*Y-5)i|@PE}V&wyfi^Ev9p_V~xb&=FKm15ZPZ^Hr*y7 z0#(&B%1X(*mc>zOsYf4Y!O-o#(hQDBG|Fle<7uTs4M|2JMq|M863olWEOLq#KQ*+B zF(4RYwO3hcGwaI_r<5LMAbML-of+{8DmzdBFt2csm5>fH&hov0@*gg!OUQ?+v2??Q z009#OIv~^`Fm-%4ZmAX&O~%NGYE3 zMZS{R9$o2;5HwAMMl%>9T51l|R~>9_sX8ja6~}mnf*O=fST_bimgpwL&YWEX`j1~~ zMe^;JLv4|lK)1Yg4K{hMsyU-M^Eoxlculm#waf;RL{9erLLU4sz!65zR~Jvv-%*1Y)4Dw406

1G|CGiJtDWy}U(x=Njcn%X@}Lz0Z%dU!0g8aMlTMD3Y7q zg|X^!MWNJdCu)GRns`j;xr>xhO)Z0&T{xjY7-Av7`?>< z%MP#}^M*woCx@*mmsIaV5XUZ`l}Z31<5za>rtr0; zWcpm4aY{5?a`OVAxcZu-(w<~ zY5Hz5X;vT?Kgt*RA1MsY;5`%!jSN@R7@yXA!^uW-K)zvXC_&;lNibztxmv7^lBOo6 zPM&92CSiAYGMPqLByL*2WwVNmr(N0A>s32j03&r35L18yU?I4aGke9a1C*5QLbl!l zoODQSx+Z+te4*8I+Dx5vLK5aAH?y7X)m_1Cgqpe%r5f~^fy@S8>8d%GVoluca)d5k zuJDgq(d7cWxZO#Y!?PwQ%4e1=wb2_^&VBY#TEd>=99p_$t-I}&Q5(N}iT*>UVoX>` zNLjhHsJ@=5-n~bU;*vGXb{4{-a09=&WQ$6&lsM+t8qafF5{qwCM*F$C@+2Eos97qN zhO#62fRYJ0%{t17YO^u)Nga1{Sw<@|LgUc&gMiFY}=u%o}827Jbn&Ha3Y_Tt^q8| zV;6ELF>hbV3B1np3*kn>U`}7=w1)R_`mdoUm_?K`#_w}|^h3WEOT$0Qb45c^*Y&+$ z|M;w7@vW={o;fjr1oGhHw&!t$*TLWKM{$aN-Cf|``^llL!!~1riVMug_9_ea8FR}c z&8i$Zg-b~tfzqj*Sl*Q?QAo)lDa(aRk{H^La_Hf9InF8wB8^0STWR(HAV)Y7+T+S< zaeO%`$wpXu;2RzyQ$72HE5hOmFqAzl$IW)8gCt^R%@hB!2bR}1Nf)P-xo*lRlO&k3Epgtx$jwhto@e@71>!qENx(RV2pSc#pVTj z?Roxix*U<ZXyefZi2~XNs92aj5$q3<8vtDdAX=-F zkb1Q7)3JPv?B#DB=+OlTlN;^w;rwYC1OH*HW1+M;;sCM?u_J!Y0i@EhR2)DmLZ!kk z?qUnF`gzpeh!`(^#}imF&$ZOAFR;OIQ9AZ0C?}<~WoBcRPi^+tp}G~X-zh)zrTt8{ z_@gT}JLeB$9sh@VImVG&LJb9wIGPlASL{Yei7$YO-6w+^7KXsdfbIlZpYRQ83T-9I nHbFyb6kSiZGEmQ9^wMxEz@XD{L*s=9&-aIVfJM#$>izmRQbs2t literal 0 HcmV?d00001 diff --git a/jorge/index.php b/jorge/index.php new file mode 100644 index 0000000..c5f135e --- /dev/null +++ b/jorge/index.php @@ -0,0 +1,384 @@ +get('uid_l')) { + + if ($sess->get('view_type') == "1") { + + header ("Location: main.php"); + + } + else { + + header ("Location: calendar_view.php"); + + } + +} + +// get post data +$inpLogin = strtolower($_POST['inpLogin']); +$inpPass = $_POST['inpPass']; + +if ($wo_sess || $inpLogin || $inpPass) { + + // set attepts in cookies + if ($_COOKIE["auth_attempt"]=="") { + + // show captcha anyway if user comes for the first time or have disabled cookies + setcookie("auth_attempt",1,time()+600); + $auth_attempt = "3"; + + } + else{ + + $auth_attempt = $_COOKIE["auth_attempt"] + 1; + settype($auth_attempt,"string"); + // if it is not numeric let set it to some high value + if (!ctype_digit($auth_attempt)) { + + $auth_attempt = "100"; + + } + + setcookie("auth_attempt",$auth_attempt,time()+600); + } + + // on 3rd attempt - check captcha + if ($auth_attempt >= "3") { + + $resp = recaptcha_check_answer(CAPTCHA_PRIVATE, + $_SERVER["REMOTE_ADDR"], + $_POST["recaptcha_challenge_field"], + $_POST["recaptcha_response_field"]); + + + if (!$resp->is_valid) { + + unset($inpPass); + unset($inpLogin); + $html->system_message($wrong_data2[$lang]); + + } + + } + +} + +if ($_GET['act']==="logout") { + + if ($db->get_user_id(TOKEN) === true) { + + if($db->result->user_id) { + + $db->set_user_id($db->result->user_id); + $db->set_logger("2","1",$rem_adre); + + } + + } + + $sess->finish(); + header("Location: index.php"); + exit; + + } + + else { + + if ($inpLogin!="" || $inpPass!="") { + + $ejabberd_rpc->set_user($inpLogin,$inpPass); + if ($ejabberd_rpc->auth() === true) { + + $sess->set('login',$inpLogin); + $sess->set('uid_l',$inpLogin); + $sess->set('uid_p',$enc->crypt_url("single=$inpPass")); + $sess->set('vhost',XMPP_HOST); + // remember user choice + setcookie("fav_host", XMPP_HOST,time()+2592000); + setcookie("auth_attempt",0,time()+2592000); + // Get user_id if it is possible + if ($db->get_user_id($sess->get('uid_l')) === true) { + + $ui = $db->result->user_id; + $db->set_user_id($ui); + $db->is_log_enabled(); + $ret_v = $db->result->is_enabled; + + } + else { + + $ret_val = null; + } + + if ($ret_v === true OR $ret_v === false) { + + $sess->set('log_status',$ret_v); + $db->set_logger("1","1",$rem_adre); + // get preferences, if not set, fallback to standard view. + $db->get_jorge_pref(); + $pref_res = $db->result; + foreach ($pref_res as $res_pref) { + + if ($res_pref[pref_id]=="1") { + + if ($res_pref[pref_value] == "2") { + + $view_type = "2"; + $tmp_v = "calendar_view.php"; + + } + elseif($res_pref[pref_value] == "1") { + + $view_type = "1"; + $tmp_v = "main.php"; + + } + + $sess->set('view_type',$view_type); + } + + if ($res_pref[pref_id] == "2") { + + // Check if language is supported, return value if it is + $check_language = is_language_supported($res_pref[pref_value],$language_support,1,true); + + // function can return true/false/value, in this case we need value and not false + if ($check_language !== false) { + + // set language according to database setup + setcookie("jorge_language",$check_language,time()+2592000); + $sess->set('language',$check_language); + + } + else{ + + // this is where language was not found in settings, so use default + setcookie("jorge_language",$language_support[default_language][1],time()+2592000); + $sess->set('language',$check_language); + + } + + } + } + + if ($tmp_v=="") { + + $sess->set('view_type',2); + $tmp_v="calendar_view.php"; + } + + header("Location: $tmp_v"); + exit; // lets break script at this point... + + } + + else { + + $sess->set('log_status',null); + header("Location: not_enabled.php"); + exit; + } + + } + + if (no_vhost === true) { + + $html->system_message($vhost_not_selected[$lang]); + + } + else{ + + $html->system_message($wrong_data[$lang]); + + } + $db->get_user_id($inpLogin); + $ui_fail = $db->result->user_id; + + // Workaround, if user_id is not know, do not alter login attempts + if ($ui_fail) { + + $db->get_last_attempt($ui_fail); + $row = $db->result->cnt; + + } + else{ + + $row="0"; + + } + + // bump log_level if more then 3 log attempts in one minute + if ($row > "3") { + + $log_level = "3"; + + } + else { + + $log_level = "2"; + + } + + if ($ejabberd_rpc->check_account() === true) { + + $db->set_user_id($ui_fail); + $db->set_logger("3",$log_level,$rem_adre); + + } + + } + +} + + +$html->set_body(' + + + + '); + +$html->set_body(' +


Branding logo
+ + + + + + + +
'.$welcome_1[$lang].'
+
+ +
+
+
+
+

+ + + + + + '); + + // display captcha on 3rd attempt... + $check_cookie = $_COOKIE["auth_attempt"]; + settype($check_cookie,"string"); + if (!ctype_digit($check_cookie) OR $check_cookie=="") { + + $cookie_failed = true; + + } + else{ + + $cookie_failed = false; + + } + + if ($check_cookie >= "2" OR $cookie_failed === true) { + + $html->set_body(' + '); + + } + + $html->set_body(' +
'.$login_w[$lang].'  + + '); + +// vhost support +$vhost_count = count($vhosts); +if ($vhost_count>1) { + + $html->set_body(''); + + + } + else{ + + // There is only one vhost configured, so do not display select box + $html->set_body('@'.key($vhosts).''); + +} + +$html->set_body('
'.$passwd_w[$lang].' 
'.recaptcha_get_html(CAPTCHA_PUBLIC,$error = null, $use_ssl = true).'
+ '); + +require_once("footer.php"); +?> diff --git a/jorge/install/jorge.sql b/jorge/install/jorge.sql new file mode 100644 index 0000000..a0a1fd0 --- /dev/null +++ b/jorge/install/jorge.sql @@ -0,0 +1,182 @@ +-- MySQL schema for project Jorge + +-- +-- Table structure for table `jorge_favorites` +-- + +DROP TABLE IF EXISTS `jorge_favorites`; +SET @saved_cs_client = @@character_set_client; +SET character_set_client = utf8; +CREATE TABLE `jorge_favorites` ( + `owner_id` int(11) default NULL, + `peer_name_id` int(11) default NULL, + `peer_server_id` int(11) default NULL, + `resource_id` int(11) default NULL, + `tslice` varchar(20) default NULL, + `comment` varchar(50) default NULL, + `ext` int(11) default NULL, + `link_id` int(10) unsigned NOT NULL auto_increment, + `vhost` varchar(255) default NULL, + PRIMARY KEY (`link_id`), + KEY `jorge_favorites_ext_idx` (`owner_id`,`ext`), + KEY `favorites_idx` (`owner_id`,`peer_name_id`,`peer_server_id`,`tslice`,`vhost`) +) ENGINE=InnoDB AUTO_INCREMENT=100 DEFAULT CHARSET=utf8; +SET character_set_client = @saved_cs_client; + +-- +-- Table structure for table `jorge_logger` +-- + +DROP TABLE IF EXISTS `jorge_logger`; +SET @saved_cs_client = @@character_set_client; +SET character_set_client = utf8; +CREATE TABLE `jorge_logger` ( + `id_user` int(11) default NULL, + `id_log_detail` int(11) default NULL, + `id_log_level` int(11) default NULL, + `log_time` timestamp NOT NULL default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP, + `extra` text, + `vhost` varchar(255) default NULL, + KEY `logger_idx` (`id_user`,`id_log_detail`,`id_log_level`,`vhost`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +SET character_set_client = @saved_cs_client; + +-- +-- Table structure for table `jorge_logger_level_dict` +-- + +DROP TABLE IF EXISTS `jorge_logger_level_dict`; +SET @saved_cs_client = @@character_set_client; +SET character_set_client = utf8; +CREATE TABLE `jorge_logger_level_dict` ( + `id_level` smallint(6) NOT NULL, + `level` varchar(20) default NULL, + `lang` varchar(3) NOT NULL default '', + PRIMARY KEY (`id_level`,`lang`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +SET character_set_client = @saved_cs_client; + +-- +-- Table structure for table `jorge_logger_dict` +-- + +DROP TABLE IF EXISTS `jorge_logger_dict`; +SET @saved_cs_client = @@character_set_client; +SET character_set_client = utf8; +CREATE TABLE `jorge_logger_dict` ( + `id_event` smallint(6) NOT NULL, + `event` text, + `lang` varchar(3) NOT NULL default 'eng', + PRIMARY KEY (`id_event`,`lang`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +SET character_set_client = @saved_cs_client; + +-- +-- Table structure for table `jorge_mylinks` +-- + +DROP TABLE IF EXISTS `jorge_mylinks`; +SET @saved_cs_client = @@character_set_client; +SET character_set_client = utf8; +CREATE TABLE `jorge_mylinks` ( + `id_link` int(11) NOT NULL auto_increment, + `owner_id` int(11) default NULL, + `peer_name_id` int(11) default NULL, + `peer_server_id` int(11) default NULL, + `datat` text, + `link` text, + `description` text, + `ext` int(11) default NULL, + `link_id` int(11) default NULL, + `vhost` varchar(255) default NULL, + PRIMARY KEY (`id_link`), + KEY `mylinks_idx` (`owner_id`,`vhost`) +) ENGINE=InnoDB AUTO_INCREMENT=454 DEFAULT CHARSET=utf8; +SET character_set_client = @saved_cs_client; + +-- +-- Table structure for table `jorge_pref` +-- + +DROP TABLE IF EXISTS `jorge_pref`; +SET @saved_cs_client = @@character_set_client; +SET character_set_client = utf8; +CREATE TABLE `jorge_pref` ( + `owner_id` int(11) default NULL, + `pref_id` int(11) default NULL, + `pref_value` int(11) default NULL, + `vhost` varchar(255) default NULL, + KEY `pref_idx` (`owner_id`,`vhost`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +SET character_set_client = @saved_cs_client; + +-- +-- Table structure for table `jorge_stats` +-- + +DROP TABLE IF EXISTS `jorge_stats`; +SET @saved_cs_client = @@character_set_client; +SET character_set_client = utf8; +CREATE TABLE `jorge_stats` ( + `day` date default NULL, + `hour` tinyint(4) default NULL, + `value` int(11) default NULL, + `vhost` varchar(255) default NULL, + KEY `stats_idx` (`day`,`vhost`), + PRIMARY KEY (`day`,`hour`,`vhost`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +SET character_set_client = @saved_cs_client; + +-- +-- Table structure for table `pending_del` +-- + +DROP TABLE IF EXISTS `pending_del`; +SET @saved_cs_client = @@character_set_client; +SET character_set_client = utf8; +CREATE TABLE `pending_del` ( + `owner_id` int(11) default NULL, + `peer_name_id` int(11) default NULL, + `date` varchar(20) default NULL, + `peer_server_id` int(11) default NULL, + `timeframe` timestamp NOT NULL default CURRENT_TIMESTAMP, + `type` enum('chat','favorite','mylink','other') default NULL, + `idx` smallint(6) default NULL, + `vhost` varchar(255) default NULL, + KEY `pending_idx` (`owner_id`,`peer_name_id`,`peer_server_id`,`date`,`type`,`idx`,`vhost`), + KEY `pending_time_idx` (`timeframe`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +SET character_set_client = @saved_cs_client; + +-- +-- Table structure for table `jorge_self_names` +-- + +DROP TABLE IF EXISTS `jorge_self_names`; +SET @saved_cs_client = @@character_set_client; +SET character_set_client = utf8; +CREATE TABLE `jorge_self_names` ( + `owner_id` int(11) NOT NULL, + `own_name` varchar(60) NOT NULL, + `vhost` varchar(255) NOT NULL, + PRIMARY KEY (`owner_id`,`vhost`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +SET character_set_client = @saved_cs_client; + +-- +-- Inserting logger dictionary +-- + +LOCK TABLES `jorge_logger_dict` WRITE; +/*!40000 ALTER TABLE `jorge_logger_dict` DISABLE KEYS */; +INSERT INTO `jorge_logger_dict` VALUES (1,'Logged in','eng'),(2,'Logged out','eng'),(3,'Login failed','eng'),(4,'Deleted chat thread','eng'),(5,'Deleted whole archive','eng'),(6,'Turned off archivization','eng'),(7,'Turned on archivization','eng'),(8,'Chat exported','eng'),(9,'Deleted entire archive','eng'); +/*!40000 ALTER TABLE `jorge_logger_dict` ENABLE KEYS */; +UNLOCK TABLES; + +LOCK TABLES `jorge_logger_level_dict` WRITE; +/*!40000 ALTER TABLE `jorge_logger_level_dict` DISABLE KEYS */; +INSERT INTO `jorge_logger_level_dict` VALUES (1,'normal','eng'),(2,'warn','eng'),(3,'alert','eng'); +/*!40000 ALTER TABLE `jorge_logger_level_dict` ENABLE KEYS */; +UNLOCK TABLES; + +-- EOF diff --git a/jorge/jquery.autocomplete.css b/jorge/jquery.autocomplete.css new file mode 100644 index 0000000..402ab12 --- /dev/null +++ b/jorge/jquery.autocomplete.css @@ -0,0 +1,48 @@ +.ac_results { + padding: 0px; + border: 1px solid black; + background-color: white; + overflow: hidden; + z-index: 99999; +} + +.ac_results ul { + width: 100%; + list-style-position: outside; + list-style: none; + padding: 0; + margin: 0; +} + +.ac_results li { + margin: 0px; + padding: 2px 5px; + cursor: default; + display: block; + /* + if width will be 100% horizontal scrollbar will apear + when scroll mode will be used + */ + /*width: 100%;*/ + font: menu; + font-size: 12px; + /* + it is very important, if line-height not setted or setted + in relative units scroll will be broken in firefox + */ + line-height: 16px; + overflow: hidden; +} + +.ac_loading { + background: white url('img/indicator.gif') right center no-repeat; +} + +.ac_odd { + background-color: #eee; +} + +.ac_over { + background-color: #0A246A; + color: white; +} diff --git a/jorge/lang/core.php b/jorge/lang/core.php new file mode 100644 index 0000000..df6b799 --- /dev/null +++ b/jorge/lang/core.php @@ -0,0 +1,44 @@ + diff --git a/jorge/lang/eng.php b/jorge/lang/eng.php new file mode 100644 index 0000000..746a1f5 --- /dev/null +++ b/jorge/lang/eng.php @@ -0,0 +1,329 @@ + + WARNING - We are still testing the system, that mean it may not work at all or work wrong or even lead to datalost. Use it at your own risk!"; +$welcome_1[eng] = "Welcome to Jorge - message archives. Please login"; +$login_w[eng] = "Login"; +$passwd_w[eng] = "Password"; +$login_act[eng] = "Sign in"; +$devel_info[eng] = "Development version"; +$activate_m[eng] = "Enable message archiving"; +$ch_lan[eng] = "Change language to:"; +$ch_lan2[eng] = "Zmień język na "; +$lang_sw[eng] = "Polski"; +$lang_sw2[eng] = "Angielski"; +$header_l[eng] = "Message archives of server"; +$menu_item_browser[eng] = "Browser"; +$menu_item_map[eng] = "Chat Map"; +$menu_item_fav[eng] = "Favorites"; +$menu_item_search[eng] = "Search"; +$menu_item_links[eng] = "MyLinks"; +$menu_item_panel[eng] = "Control Panel"; +$menu_item_contacts[eng] = "Contacts"; +$menu_item_logs[eng] = "Logs"; +$menu_item_trash[eng] = "Trash"; +$filter_form[eng] = "Filter your contacts *"; +$filter_form_tip[eng] = "Type contact name"; +$filter_tip[eng] = "...and next select it from list, or search contact list by hand:"; +$ff_notice[eng] = "This function work only in Firefox browser"; +$search_box[eng] = "Search in archives"; +$search_tip[eng] = "Displaying"; +$search_why[eng] = " search results (not more then 100). Find out why"; +$search_warn[eng] = "Warning: Showing results only from selected time range"; +$all_for_u[eng] = "Show conversations with this user using: "; +$all_for_u_m[eng] = "stream"; +$all_for_u_m_d[eng] = "Show all conversations with this user as stream"; +$all_for_u_m2[eng] = "map"; +$all_for_u_m2_d[eng] = "Show all conversations with this user as chat map"; +$all_for_u_t[eng] = "Show all chats from this user"; +$arch_on[eng] = "Turn on archivization"; +$arch_off[eng] = "Turn off archivization"; +$log_out_b[eng] = "Sign out"; +$archives_t[eng] = "Archive browser"; +$main_date[eng] = "Date:"; +$talks[eng] = "Conversation list:"; +$thread[eng] = "Content:"; +$time_t[eng] = "Time:"; +$user_t[eng] = "User:"; +$my_links_save[eng] = "MyLinks"; +$my_links_desc_m[eng] = "MyLinks - Your links"; +$my_links_desc_e[eng] = "Here you can find saved fragments of your chats"; +$settings_desc[eng] = "Archive settings"; +$settings_desc_detail[eng] = "The panel consist of options that let you control message archiving as well as options regarding your account"; +$api_access_enable[eng] = "Enable API access for this account"; +$api_access_disable[eng] = "Disable API access for this account"; +$api_access_off[eng] = "API access is enabled for this account"; +$api_access_learn[eng] = "Learn more about public API"; +$print_t[eng] = "print"; +$del_t[eng] = "delete"; +$resource_only[eng] = "Show chat only with this resource"; +$resource_warn[eng] = "Showing chat only with resource: "; +$resource_discard[eng] = "Show "; +$resource_discard2[eng] = "entire chat thread."; +$del_all_conf[eng] = "You are about to delete all your message archives. Are you *really* sure?\\nWARNING: It would be impossible to recover your archives!"; +$deleted_all[eng] = "All your message archive has been deleted"; +$delete_nothing[eng] = "Your message archive is empty. Nothing was deleted"; +$delete_error[eng] = "Ooops...There were errors during processing your request. Please try again later"; +$search_w1[eng] = "Search string cannot be shorter than 3 and longer than 70 characters..."; +$search_res[eng] = "Search results: "; +$my_links_save_d[eng] = "Saving link. Fill the form below"; +$my_links_optional[eng] = "Description (optional, max 120 characters)"; +$my_links_chat[eng] = "Conversation with:"; +$my_links_commit[eng] = "save"; +$my_links_cancel[eng] = "cancel"; +$my_links_link[eng] = "Link from day:"; +$my_links_desc[eng] = "Description:"; +$my_links_added[eng] = "Link succesfuly added!"; +$my_links_back[eng] = "Back to chat"; +$my_links_removed[eng] = "Link succesfuly deleted"; +$my_links_none[eng] = "No decsription"; +$status_msg1[eng] = "Message archiving is disabled by user"; +$status_msg2[eng] = "Message archiving have beed enabled. Changes may take 10s"; +$status_msg3[eng] = "Message archiving have beed disabled. Changes may take 10s"; +$my_links_no_links[eng] = "You don't have any MyLinks saved..."; +$quest1[eng] = "Found error? Fill bug report!"; +$search1[eng] = "Search..."; +$no_result[eng] = "No search results"; +$settings_del[eng] = "Delete entire archive"; +$del_conf[eng] = "Do you really want to delete this chat?"; +$del_conf_my_link[eng] = "Do you really want to remove that link?"; +$not_in_r[eng] = "Special contact"; +$del_moved[eng] = "Conversation moved to trash."; +$del_info[eng] = "Conversation have been deleted"; +$undo_info[eng] = "Conversation restored succesfuly"; +$del_my_link[eng] = "delete"; +$help_but[eng] = "Help"; +$tip_delete[eng] = "Delete this conversation"; +$tip_export[eng] = "Export this conversation to text file"; +$customize1[eng] = "Customize logging"; +$from_u[eng] = "From: "; +$to_u[eng] = "To: "; +$search_next[eng] = "Next results..."; +$search_prev[eng] = "Previous results..."; +$change_pass[eng] = "Change password"; +$no_contacts[eng] = "Your contacts list is currently empty"; +$no_archives[eng] = "Currently you dont have any chats saved"; +$con_tab1[eng] = "No."; +$con_tab2[eng] = "Contact name"; +$con_tab3[eng] = "JabberID"; +$con_tab4[eng] = "Enable archiving"; +$con_tab_act_y[eng] = "Yes"; +$con_tab_act_n[eng] = "No"; +$con_tab_submit[eng] = "Save changes"; +$con_tab6[eng] = "Group"; +$con_no_g[eng] = "No group"; +$map_no_g[eng] = "no group"; +$con_head[eng] = "Contacts managment"; +$con_notice[eng] = "Notice: displaying only contacts with assigned nicknames."; +$con_title[eng] = "Click on contact name to see conversation history"; +$con_saved[eng] = "Changes have beed saved"; +$help_notice[eng] = "Main topics"; +$nx_dy[eng] = "Next day"; +$no_more[eng] = "No more search results"; +$in_min[eng] = "minutes"; +$verb_h[eng] = "interruption in conversation lasting more than an hour"; +$time_range_w[eng] = "Field \"From\" cannot be greater than field \"To\""; +$time_range_from[eng] = "from"; +$time_range_to[eng] = "to"; +$export_link[eng] = "export"; +$export_head1[eng] = "Exported chat between you and "; +$export_head2[eng] = "performed on"; +$help_search_tips[eng] =" +

+
  • Search Tips
  • +
      When searching you can do some more complex queries like:
      + if you want to find all chats from particular user you can type:
      + from:jid@example.com - where jid is user name of the server: example.com
      + or if you want to find phase in chats with that user, you can type:
      + from:jid@example.com:what is jabber - witch will query for phase what is jabber in all chats with user jid@example.com
      + Search engine also of course supports normal search that search all archives:
      + what is jabber - will search in all our chats phase \"what is jabber\" as well as all keywords like: \"what\", \"is\", \"jabber\"
      + If we don't know full name that we are searching we can put instead character: * (wildcard):
      + wor* - will find all words that begin with wor* like: word, work, world... +
    +"; +$help_my_links_note[eng] = " +

    +
  • MyLinks: overview.
  • +
      MyLinks let you store your favorited links. Thanks to MyLinks option you can easly and fast find your favorited talk.
      +To add chat to MyLinks just click on the right side of the chat window onto option called \"save in mylinks\". Then fill the form with description and save link into database. +
    + + + + +"; +$help_advanced_tips[eng] = " +

    +
  • How to search right
  • +
      Search engine of Jorge supports advanced mode called Boolean mode, that means that you can improve your search results.
      + Search engine search all your archives next it sort it and evaluates score and then displays only 100 most relevant matches.
      + To let you make it easy to adjust search results engine supports following arguments:
      + + - means that particular word must be in the results, so: +abc +def means that both words must be there
      + - - it excludes word from search results
      + > and < - increasese or decreases score for particular word
      + ( ) - make it possible to execute sub-query
      + ~ - adds negative score to particular word
      + * - replaces unknown word
      + \" - perform exact match search
      +
    + + +"; +$admin_site_gen[eng] = "Site generated in:"; +$logger_from_day[eng] = " from day: "; +$logger_overview[eng] = "Activity logs on Jorge"; +$logger_f1[eng] = "Event:"; +$logger_f2[eng] = "Event date:"; +$logger_f3[eng] = "Event level:"; +$logger_f4[eng] = "Additional info:"; +$logger_f_ip[eng] = "from IP address: "; +$refresh[eng] = "Refresh"; +$back_t[eng] = "Back to top of the page"; +$trash_name[eng] = "Trash"; +$trash_desc[eng] = "List of trashed conversations. Conversations that are left in trash are automaticly deleted after 30 days."; +$trash_undel[eng] = "Restore"; +$trash_vit[eng] = "View restored chat"; +$trash_del[eng] = "Delete"; +$trash_link[eng] = "Action"; +$trash_empty[eng] = "Trash is empty"; +$trash_recovered[eng] = "Conversation have been moved to archive"; +$cal_head[eng] = "Chat calendar"; +$cal_notice[eng] = "Click on days to see chats"; +$change_view[eng] = "Switch to tree view"; +$change_view_cal[eng] = "Browse archives using calendar view."; +$months_names = array("January","February","March","April","May","June","July","August","September","October","November","December"); +$weekdays = array("Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday"); +$jump_to_l[eng] = "Select month"; +$chat_list_l[eng] = "Conversation list:"; +$select_view[eng] = "Select prefered view for browser:"; +$view_calendar[eng] = "Calendar view"; +$view_standard[eng] = "Tree view"; +$setting_d1[eng] = "Change global archivization policy:"; +$setting_d2[eng] = "Delete entire message archive (cannot undo):"; +$chat_map[eng] = "Chat map"; +$chat_select[eng] = "Select contact to see chats"; +$chat_m_select[eng] = "Pick a contact:"; +$chat_c_list[eng] = "Contacts list"; +$chat_no_chats[eng] = "There are no chats with selected contact"; +$chat_map_back[eng] = "<<< Back to ChatMap"; +$fav_back[eng] = "<<< Back to Favorites"; +$myl_back[eng] = "<<< Back to MyLinks"; +$sel_language[eng] = "Select prefered language"; +$sel_client[eng] = "Launch Slimster"; +$sel_yes[eng] = "Yes"; +$sel_no[eng] = "No"; +$jump_to_next[eng] = "Go to next conversation with this user"; +$jump_to_prev[eng] = "Go to to previous conversation with this user"; +$show_chats[eng] = "Show chat as"; +$show_chat_stream[eng] = "stream"; +$show_chat_as_map[eng] = "map"; +$tip_next_m[eng] = "Go to next month"; +$tip_prev_m[eng] = "Go to previous month"; +$cal_days[eng]['1'] = "Mon"; +$cal_days[eng]['2'] = "Tue"; +$cal_days[eng]['3'] = "Wed"; +$cal_days[eng]['4'] = "Thu"; +$cal_days[eng]['5'] = "Fri"; +$cal_days[eng]['6'] = "Sat"; +$cal_days[eng]['7'] = "Sun"; +$chat_lines[eng] = "Messages count: "; +$del_time[eng] = "Deleted:"; +$marked_as_d[eng] = "This chat is in trash. If you want to see it - restore it"; +$stats_personal_d[eng] = "Personal statistics"; +$stats_personal[eng] = "Your total number of messages in archiwe:"; +$stats_personal_top[eng] = "Your top 10 chats:"; +$stats_when[eng] = "When"; +$stats_personal_count[eng] = "Messages count"; +$stats_peer[eng] = "Talker"; +$stats_see[eng] = "See this chat"; +$stats_for[eng] = "Stats for: "; +$stats_messages[eng] = "Messages logged by server: "; +$stats_messages_b[eng] = " in "; +$stats_messages_c[eng] = " conversations."; +$stats_graph1[eng] = "Total number of users using message archiving (daily)"; +$stats_graph2[eng] = "Messages logged by server (daily)"; +$stats_graph3[eng] = "Messages logged (hourly)"; +$stats_graph4[eng] = "Messages logged (weekly)"; +$stats_top[eng] = "Longest conversations from last days:"; +$stats_not_eno[eng] = "Not enought data to plot the graphs (30 days needed)"; +$fav_main[eng] = "Favorites"; +$fav_desc[eng] = "List of chats marked as favorites"; +$fav_add[eng] = "Add conversation to favorites"; +$fav_chat[eng] = "Conversation with: "; +$fav_success[eng] = "Conversation has been succesfully added to your Favorites !"; +$fav_discard[eng] = "Discard this message"; +$fav_exist[eng] = "Ooops...This chat is already in your Favorites"; +$fav_favorited[eng] = "This conversation is already added to favorites"; +$fav_contact[eng] = "Conversation with:"; +$fav_when[eng] = "When:"; +$fav_comment[eng] = "Comment:"; +$fav_nocomm[eng] = "No comment"; +$fav_add_comment[eng] = "Add comment"; +$fav_remove[eng] = "delete"; +$fav_removed[eng] = "Conversation has been deleted from Favorites"; +$fav_empty[eng] = "You dont have any Favorites chats saved"; +$fav_error[eng] = "Oooups...There was a problem during processing your request"; +$reset_sort[eng] = "reset sorting"; +$cont_chat[eng] = "chat continues on next day >>>"; +$cont_chat_p[eng] = "<<< this chat is continuation from last day"; +$close_account[eng] = "Close your account:"; +$close_info[eng] = "WARNING: during account removal also account on Google Apps will be removed!"; +$close_warn[eng] = "Do you really want to remove all messages and user account?"; +$close_commit[eng] = "- Close now -"; +$close_failed[eng] = "Close account failed. Please try again later"; +$oper_fail[eng] = "
    Operation failed! Please try again later or contact administrator!
    "; +$go_to_jorge[eng] = "Go to Jorge main page"; +$qlink_l[eng] = "Go to latest conversations"; +$message_type_message[eng] = "Message"; +$message_type_error[eng] = "Message have been marked as faulty, and probably was not delivered."; +$message_type_headline[eng] = "Headline"; +$muc_message[eng] = "System message:"; +$spec_contact_enable[eng] = "Display special contact:"; +$spec_contact_desc[eng] = "This option let you decide if conversations with special contacts like gateways, transports are displayed in your chat list. Most users can say No here."; +$donate[eng] = "Help develop Project Jorge.Read More..."; +$donate_dont[eng] = "Don't show this information anymore..."; +$own_name_desc[eng] = "This option allows to display your name as you wish: f.e. james0021a is replaced with James"; +$own_name_enter[eng] = "Enter your name:"; +$own_name_commit[eng] = "Set your name"; +$own_name_remove[eng] = "If you dont want to use this option, leave it unchanged"; +$stats_vhost_select[eng] = "Select server for viewing its statistics: "; + +?> diff --git a/jorge/lang/pol.php b/jorge/lang/pol.php new file mode 100644 index 0000000..3d96b9b --- /dev/null +++ b/jorge/lang/pol.php @@ -0,0 +1,330 @@ + + UWAGA: System jest w trakcie testów co oznacza że może nie dziaÅ‚ac w ogóle, dziaÅ‚ać wadliwie lub narazić CiÄ™ na utratÄ™ danych. Używasz go na wÅ‚asnÄ… odpowiedzialność!"; +$welcome_1[pol] = "Jorge - archiwa rozmów. Zaloguj siÄ™ do systemu"; +$login_w[pol] = "Login"; +$passwd_w[pol] = "HasÅ‚o"; +$login_act[pol] = "Zaloguj siÄ™"; +$devel_info[pol] = "Wersja BETA"; +$activate_m[pol] = "AKTYWUJ"; +$ch_lan[pol] = "ZmieÅ„ jÄ™zyk na:"; +$ch_lan2[pol] = "Change language to "; +$lang_sw[pol] = "English"; +$lang_sw2[pol] = "English"; +$header_l[pol] = "Archiwa rozmów serwera"; +$menu_item_browser[pol] = "PrzeglÄ…darka"; +$menu_item_map[pol] = "Mapa Rozmów"; +$menu_item_fav[pol] = "Ulubione"; +$menu_item_search[pol] = "Wyszukiwarka"; +$menu_item_links[pol] = "MyLinks"; +$menu_item_panel[pol] = "Panel Sterowania"; +$menu_item_contacts[pol] = "Kontakty"; +$menu_item_logs[pol] = "Logi"; +$menu_item_trash[pol] = "Kosz"; +$filter_form[pol] = "Filtruj listÄ™ kontaktów *"; +$filter_form_tip[pol] = "Wpisz nazwÄ™ kontaktu"; +$filter_tip[pol] = "...a nastÄ™pnie wybierz z listy, lub przeszukaj listÄ™ rÄ™cznie:"; +$ff_notice[pol] = "Ta opcja dziaÅ‚a tylko w przeglÄ…darce Firefox"; +$search_box[pol] = "Szukaj w archiwach"; +$search_tip[pol] = "WyÅ›wietlam"; +$search_why[pol] = " wyników (nie wiÄ™cej niż 100). Dowiedz siÄ™ dlaczego"; +$search_warn[pol] = "Uwaga: Wyszukuje tylko w wybranym przedziale czasu"; +$all_for_u[pol] = "Pokaż wszystkie rozmowy używajÄ…c: "; +$all_for_u_m[pol] = "strumienia"; +$all_for_u_m_d[pol] = "Pokaż wszystkie rozmowy z tÄ… osobÄ… jako strumieÅ„ wiadomoÅ›ci"; +$all_for_u_m2[pol] = "mapy"; +$all_for_u_m2_d[pol] = "Pokaż wszystkie rozmowy z tÄ… osobÄ… jako mapÄ™ rozmów"; +$all_for_u_t[pol] = "Pokaż wszystkie rozmowy z tym użytkownikiem"; +$arch_on[pol] = "WÅ‚Ä…cz archiwizacje"; +$arch_off[pol] = "WyÅ‚Ä…cz archiwizacje"; +$log_out_b[pol] = "Wyloguj"; +$archives_t[pol] = "PrzeglÄ…darka archiwum"; +$main_date[pol] = "Data:"; +$talks[pol] = "Lista rozmów:"; +$thread[pol] = "Treść:"; +$time_t[pol] = "Czas:"; +$user_t[pol] = "Użytkownik:"; +$my_links_save[pol] = "MyLinks"; +$my_links_desc_m[pol] = "MyLinks - Twoje linki"; +$my_links_desc_e[pol] = "Tutaj znajdziesz listÄ™ zapisanych fragmentów rozmów"; +$settings_desc[pol] = "Ustawienia archiwum"; +$settings_desc_detail[pol] = "Panel zawiera opcje pozwalajÄ…ce kontrolować archiwizacje rozmów oraz opcje dotyczÄ…ce konta"; +$api_access_enable[pol] = "WÅ‚Ä…cz publiczne API dla tego konta"; +$api_access_disable[pol] = "WyÅ‚Ä…cz publiczne API dla tego konta"; +$api_access_on[pol] = "API dla tego konta jest wÅ‚Ä…czone"; +$api_access_learn[pol] = "Dowiedz siÄ™ wiÄ™cej na temat publicznego API"; +$print_t[pol] = "drukuj"; +$del_t[pol] = "usuÅ„"; +$resource_only[pol] = "Pokaż rozmowÄ™ tylko z tym zasobem"; +$resource_warn[pol] = "PokazujÄ™ rozmowÄ™ z zasobem: "; +$resource_discard[pol] = "Pokaż "; +$resource_discard2[pol] = "caÅ‚Ä… rozmowÄ™."; +$del_all_conf[pol] = "Czy napewno chcesz usunąć *CAÅE* swoje archiwum wiadomoÅ›ci?\\nUWAGA: Nie bÄ™dzie możliwoÅ›ci przywrócenia archiwum!"; +$deleted_all[pol] = "CaÅ‚e Twoje archiwum zostaÅ‚o usuniÄ™te"; +$delete_nothing[pol] = "Twoje archiwum jest puste. Nic nie usuniÄ™to"; +$delete_error[pol] = "Ooops...WystÄ…piÅ‚y bÅ‚Ä™dy podczas wykonywania polecenia. ProszÄ™ spróbować poźniej"; +$search_w1[pol] = "Wyszukiwany ciÄ…g nie może być krótszy niż 3 i dÅ‚uższy niż 70 znaków..."; +$search_res[pol] = "Wyniki wyszukiwania: "; +$my_links_save_d[pol] = "Zapisuje link. Wprowadź dane"; +$my_links_optional[pol] = "Opis (opcjonalne, max 120 znakow)"; +$my_links_chat[pol] = "Rozmowa z:"; +$my_links_commit[pol] = "zapisz"; +$my_links_cancel[pol] = "anuluj"; +$my_links_link[pol] = "Link z dnia:"; +$my_links_desc[pol] = "Opis:"; +$my_links_added[pol] = "Link zostaÅ‚ zapisany!"; +$my_links_back[pol] = "Wróć do rozmowy"; +$my_links_removed[pol] = "Link zostaÅ‚ usuniÄ™ty z bazy danych"; +$my_links_none[pol] = "Brak opisu"; +$status_msg1[pol] = "Archiwizacja rozmów jest aktualnie wyÅ‚Ä…czona"; +$status_msg2[pol] = "Archiwizacja zostaÅ‚a wÅ‚Ä…czona. (zmiany w profilu widoczne sÄ… po 10 sekundach)"; +$status_msg3[pol] = "Archiwizacja zostaÅ‚a wyÅ‚Ä…czona. (zmiany w profilu widoczne sÄ… po 10 sekundach)"; +$my_links_no_links[pol] = "Nie masz aktualnie zapisanych linków..."; +$quest1[pol] = "ZnalazÅ‚eÅ› bÅ‚Ä…d? ZgÅ‚oÅ› go!"; +$search1[pol] = "Szukaj..."; +$no_result[pol] = "Brak rezultatów wyszukiwania"; +$settings_del[pol] = "UsuÅ„ caÅ‚e archiwum"; +$del_conf[pol] = "Czy na pewno usunąć tÄ… rozmowÄ™?"; +$del_conf_my_link[pol] = "Czy na pewno usunąć ten link?"; +$not_in_r[pol] = "Kontakt specjalny"; +$del_moved[pol] = "Rozmowa zostaÅ‚a przeniesiona do kosza."; +$del_info[pol] = "Rozmowa zostaÅ‚a usuniÄ™ta"; +$undo_info[pol] = "Rozmowa zostaÅ‚a przywrócona"; +$del_my_link[pol] = "usuÅ„"; +$help_but[pol] = "Pomoc"; +$tip_delete[pol] = "UsuÅ„ historiÄ™ rozmowy z tego dnia"; +$tip_export[pol] = "Eksportuj rozmowÄ™ do pliku tekstowego"; +$customize1[pol] = "Dostosuj logowanie"; +$from_u[pol] = "Od: "; +$to_u[pol] = "Do: "; +$search_next[pol] = "NastÄ™pne wyniki..."; +$search_prev[pol] = "Poprzednie wyniki..."; +$change_pass[pol] = "ZmieÅ„ hasÅ‚o"; +$no_contacts[pol] = "Brak kontaktów na liÅ›cie"; +$no_archives[pol] = "W tej chwili nie masz zapisanych żadnych rozmów"; +$con_tab1[pol] = "Lp."; +$con_tab2[pol] = "Nazwa kontaktu"; +$con_tab3[pol] = "JabberID"; +$con_tab4[pol] = "WÅ‚Ä…czyć archiwizacje"; +$con_tab_act_y[pol] = "Tak"; +$con_tab_act_n[pol] = "Nie"; +$con_tab_submit[pol] = "Zapisz zmiany"; +$con_tab6[pol] = "Grupa"; +$con_no_g[pol] = "Brak grupy"; +$map_no_g[pol] = "brak grupy"; +$con_head[pol] = "ZarzÄ…dzanie kontaktami"; +$con_notice[pol] = "Uwaga: wyÅ›wietlane sÄ… tylko kontakty z przypisanÄ… nazwÄ… kontaktu."; +$con_title[pol] = "Kliknij na kontakcie aby zobaczyć archiwum rozmów"; +$con_saved[pol] = "Zmiany zostaÅ‚y zapisane"; +$help_notice[pol] = "Główne zagadnienia"; +$nx_dy[pol] = "Kolejny dzieÅ„"; +$no_more[pol] = "Brak wiÄ™kszej iloÅ›ci wyników"; +$in_min[pol] = "minut"; +$verb_h[pol] = "przerwa w rozmowie trwajÄ…ca ponad godzinÄ™"; +$time_range_w[pol] = "Pole \"Od\" nie może być wiÄ™ksze od pola \"Do\""; +$time_range_from[pol] = "od"; +$time_range_to[pol] = "do"; +$export_link[pol] = "eksportuj"; +$export_head1[pol] = "Historia rozmowy miÄ™dzy TobÄ… a "; +$export_head2[pol] = "przeprowadzona w dniu"; +$help_search_tips[pol] = " +

    +
  • Wyszukiwarka: Podpowiedzi.
  • +
      Przeszukując archiwa można zadawać kilka rodzajów zapytań na przykład:
      + żeby znaleźć wszystkie rozmowy z danym użytkownikiem wpisujemy w oknie wyszukiwania:
      + from:jid@przykład.pl - gdzie jid to nazwa użytkownika, a przykład.pl to serwer na którym wyszukiwana osoba ma konto.
      + aby wyszukać daną frazę w rozmowie z użytkownikiem możemy wykonać następujące zapytanie:
      + from:jid@przykład.pl:co to jest jabber - takie zapytanie przeszuka wszystkie rozmowy z użytkownikem jid z serwera przykład.pl w poszukiwaniu frazy: co to jest jabber
      + Wyszukiwarka obsługuje oczywiście zwykłe wyszukiwanie - we wszystkich przeprowadzonych przez nas rozmowach:
      + co to jest jabber - wyszuka we wszystkich rozmowach frazy \"co to jest jabber\" jak również wyświetli wszystkie linie rozmowy zawierające słowa kluczowe
      + Jeśli nie znamy pełnej nazwy której poszukijemy możemy daną/dane litery zastąpić znakiem: * (gwiazdka) np.:
      + jak* - znajdzie wszystkie słowa zaczynające się na jak czyli np. jaki, jaka + + +
    + +"; +$help_my_links_note[pol] = " +

    +
  • MyLinks: informacje ogólne.
  • +
      MyLinks służy do przechowywania(zapamiętywania) ulubionych fragmentów rozmów. Dzieki opcji MyLinks można w łatwy i szybki sposób odnaleźć poszukiwaną rozmowę.
      +Aby dodać daną rozmowę do MyLinks należy kliknąć po prawej stronie okna z wyszukiwaną rozmową na opcji \"zapisz w mylinks\". Po wprowadzeniu opisu, link zostanie
      +zapisany w zakładce MyLinks. +
    + +"; +$help_advanced_tips[pol] = " +

    +
  • Jak szukać dokÅ‚adnie?
  • +
      Wyszukiwarga Jorge obsługuje zaawansowane tryby wyszukiwania tzw. Boolean mode, co oznacza że znacznie można poprawić rezultaty wyszukiwania.
      + Wyszukiwarka przeszukuje wszystkie Twoje archiwa w poszukiwaniu danej frazy, następnie ocenia tzw. \"score\", sortuje dane i wyświetla najlepiej pasujące 100 wyników
      + Aby ułatwić wyszukiwanie możesz użyć następujących modyfikatorów:
      + + - oznacza że dane słowo musi znaleźć się w wynikach wyszukiwania np. (+abc +def - odszuka wszystkie rozmowy zawierające w danej lini abc oraz def)
      + - - oznacza że dane słowo ma nie występować w wynikach wyszukiwania
      + > oraz < - nadaje dodatkowe punkty wyszukiwanemu słowu w frazie. Np. poszukując linka wiemy że zawieta http i np. słowo planeta. Aby zwiększyć trafność wyników zapytanie powinno wyglądać tak: \"http <planeta\"
      + ( ) - oznacza wykonanie pod-zapytania
      + ~ - dodaje negatywne punkty do danego słowa - ale go nie wyklucza z wyników
      + * - zastępuje ciąg znaków
      + \" - oznacza wyszukiwanie dokładnie pasującej frazy np: \"jak to\" znajdzie tylko rozmowy z dokładnie tą frazą + +
    + +"; +$admin_site_gen[pol] = "Strona została wygenerowana w: "; +$logger_from_day[pol] = " z dnia: "; +$logger_overview[pol] = "Logi aktywności w Jorge"; +$logger_f1[pol] = "Zdarzenie:"; +$logger_f2[pol] = "Data zdarzenia:"; +$logger_f3[pol] = "Poziom zdarzenia:"; +$logger_f4[pol] = "Dodatkowe informacje:"; +$logger_f_ip[pol] = "z adresu IP: "; +$refresh[pol] = "Odśwież"; +$back_t[pol] = "Wróć na góre strony"; +$trash_name[pol] = "Kosz"; +$trash_desc[pol] = "Lista rozmów usuniętych. Wiadomości które przebywają w koszu dłużej niż 30 dni są automatycznie usuwane"; +$trash_undel[pol] = "Przywróć"; +$trash_vit[pol] = "Zobacz przywróconą rozmowę"; +$trash_del[pol] = "Usuń"; +$trash_link[pol] = "Akcja"; +$trash_empty[pol] = "Kosz jest pusty"; +$trash_recovered[pol] = "Rozmowa została przeniesiona do archiwum"; +$cal_head[pol] = "Kalendarz rozmów."; +$cal_notice[pol] = "Kliknij na danym dniu aby zobaczyć rozmowy"; +$change_view[pol] = "Zmień na widok drzewa"; +$change_view_cal[pol] = "Przeglądaj archiwum za pomocą widoku kalendarza."; +$months_names = array("Styczeń","Luty","Marzec","Kwiecień","Maj","Czerwiec","Lipiec","Sierpień","Wrzesień","Październik","Listopad","Grudzień"); +$weekdays = array("Poniedziałek","Wtorek","Środa","Czwartek","Piątek","Sobota","Niedziela"); +$jump_to_l[pol] = "Wybierz miesiąc"; +$chat_list_l[pol] = "Lista rozmów:"; +$select_view[pol] = "Wybierz rodzaj widoku przeglądarki:"; +$view_calendar[pol] = "Widok kalendarza"; +$view_standard[pol] = "Widok drzewa"; +$setting_d1[pol] = "Zmień globalną opcję archiwizacji:"; +$setting_d2[pol] = "Usuń całe archiwum wiadomości (nie można wycofać):"; +$chat_map[pol] = "Mapa rozmów"; +$chat_select[pol] = "Wybierz kontakt aby zobaczyć listę rozmów"; +$chat_m_select[pol] = "Wybierz kontakt:"; +$chat_c_list[pol] = "Lista kontaktów"; +$chat_no_chats[pol] = "Brak rozmów z wybranym kontaktem"; +$chat_map_back[pol] = "<<< Wróć do Mapy Rozmów"; +$fav_back[pol] = "<<< Wróć do Ulubionych"; +$myl_back[pol] = "<<< Wróć do MyLinks"; +$sel_language[pol] = "Wybierz preferowany język"; +$sel_client[pol] = "Uruchom Slimster"; +$sel_yes[pol] = "Tak"; +$sel_no[pol] = "Nie"; +$jump_to_next[pol] = "Przejdź do następnego dnia rozmowy"; +$jump_to_prev[pol] = "Przejdź do poprzedniego dnia rozmowy"; +$show_chats[pol] = "Pokaż rozmowę jako"; +$show_chat_stream[pol] = "strumień"; +$show_chat_as_map[pol] = "mapę"; +$tip_next_m[pol] = "Przejdź do następnego miesiąca"; +$tip_prev_m[pol] = "Przejdź do poprzedniego miesiąca"; +$cal_days[pol]['1'] = "Pon"; +$cal_days[pol]['2'] = "Wto"; +$cal_days[pol]['3'] = "Śro"; +$cal_days[pol]['4'] = "Czw"; +$cal_days[pol]['5'] = "Pią"; +$cal_days[pol]['6'] = "Sob"; +$cal_days[pol]['7'] = "Nie"; +$chat_lines[pol] = "Ilość wiadomości: "; +$del_time[pol] = "Usunięto:"; +$marked_as_d[pol] = "Ta rozmowa znajduje się w koszu. Aby ją przeglądać musisz ją przywrócić"; +$stats_personal_d[pol] = "Statystyki rozmów"; +$stats_personal[pol] = "Twoja całkowita liczba wiadomości w archiwum:"; +$stats_personal_top[pol] = "10 najdłuższych rozmów:"; +$stats_when[pol] = "Kiedy"; +$stats_personal_count[pol] = "Liczba wiadomości"; +$stats_peer[pol] = "Rozmówca"; +$stats_see[pol] = "Zobacz rozmowę"; +$stats_for[pol] = "Statystyki dla: "; +$stats_messages[pol] = "Serwer zalogował "; +$stats_messages_b[pol] = " wiadomości w "; +$stats_messages_c[pol] = " rozmowach."; +$stats_graph1[pol] = "Całkowita liczba użytkowników korzystających z archiwizacji (dziennie)"; +$stats_graph2[pol] = "Ilość wiadomości zalogowanych przez serwer (dziennie)"; +$stats_graph3[pol] = "Zalogowane wiadomości (godzinowo)"; +$stats_graph4[pol] = "Zalogowane wiadomości (tygodniowo)"; +$stats_top[pol] = "Najdłuższe rozmowy z ostatnich dni:"; +$stats_not_eno[pol] = "Brak wystarczających danych do narysowania statystyk (minimum 30 dni)"; +$fav_main[pol] = "Ulubione"; +$fav_desc[pol] = "Lista rozmów oznaczonych jako \"Ulubione\""; +$fav_add[pol] = "Dodaj rozmowę do ulubionych"; +$fav_chat[pol] = "Rozmowa z: "; +$fav_success[pol] = "Rozmowa została dodana do Twoich Ulubionych !"; +$fav_discard[pol] = "Ukryj tą informacje"; +$fav_exist[pol] = "Ooops...Ta rozmowa juz znajduje się w Twoich Ulubionych"; +$fav_favorited[pol] = "Ta rozmowa jest dodana do ulubionych"; +$fav_contact[pol] = "Rozmowa z:"; +$fav_when[pol] = "Kiedy:"; +$fav_comment[pol] = "Komentarz:"; +$fav_nocomm[pol] = "Brak komentarza"; +$fav_add_comment[pol] = "Dodaj komentarz"; +$fav_remove[pol] = "usuń"; +$fav_removed[pol] = "Rozmowa została usnięta z Ulubionych"; +$fav_empty[pol] = "Nie masz aktualnie zapisanych żadnych Ulubionych rozmów"; +$fav_error[pol] = "Oooups...Wystąpił błąd podczas dodawania rozmowy"; +$reset_sort[pol] = "resetuj sortowanie"; +$cont_chat[pol] = "rozmowa kontynuowana jest następnego dnia >>>"; +$cont_chat_p[pol] = "<<< rozmowa jest kontynuacją z dnia poprzedniego"; +$close_account[pol] = "Usuń konto z serwera:"; +$close_info[pol] = "UWAGA: wraz z kontem XMPP zostanie usunięte konto z Google Apps!"; +$close_warn[pol] = "Czy napewno usunąć konto i wszystkie wiadomości?"; +$close_commit[pol] = "- Usuń teraz -"; +$close_failed[pol] = "Usunięcie konta nie powiodło się. Proszę spróbować później"; +$oper_fail[pol] = "
    Operacja nie została wykonana! Proszę spróbować później lub skontaktować się z administratorem!
    "; +$go_to_jorge[pol] = "Idz do strony glownej"; +$qlink_l[pol] = "Przejdź do najnowszych rozmów"; +$message_type_message[pol] = "Wiadomość"; +$message_type_error[pol] = "Wiadomość została oznaczona jako zawierająca błąd i prawdopodobnie nie została dostarczona."; +$message_type_headline[pol] = "Headline"; +$muc_message[pol] = "Wiadomość systemowa:"; +$spec_contact_enable[pol] = "Wyświetlać kontakty specjalne:"; +$spec_contact_desc[pol] = "Pozwala zdecydować czy w liście rozmów mają pojawiać się rozmowy z kontaktami specjalnymi np.: bramkami lub transportami. Większość użytkowników może spokojnie tą opcje wyłączyć"; +$donate[pol] = "Pomóż rozwijać Projekt Jorge.Przeczytaj więcej..."; +$donate_dont[pol] = "Nie pokazuj tej informacji więcej..."; +$own_name_desc[pol] = "Opcja ta umożliwia zmianę wyświetlanej nazwy użytkownika: np. karol002a na Karol"; +$own_name_enter[pol] = "Podaj swoją nazwę:"; +$own_name_commit[pol] = "Ustaw nazwę"; +$own_name_remove[pol] = "Jeśli nie chcesz korzystać z tej opcji, pozostaw pole puste"; +$stats_vhost_select[pol] = "Wybierz serwer dla którego chcesz oglądać statystyki: "; + +?> diff --git a/jorge/lib/dimensions.js b/jorge/lib/dimensions.js new file mode 100644 index 0000000..92b232b --- /dev/null +++ b/jorge/lib/dimensions.js @@ -0,0 +1,320 @@ +/* + * Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) + * and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses. + * + * $LastChangedDate: 2007-03-27 23:29:43 +0200 (Di, 27 Mrz 2007) $ + * $Rev: 1601 $ + */ + +jQuery.fn._height = jQuery.fn.height; +jQuery.fn._width = jQuery.fn.width; + +/** + * If used on document, returns the document's height (innerHeight) + * If used on window, returns the viewport's (window) height + * See core docs on height() to see what happens when used on an element. + * + * @example $("#testdiv").height() + * @result 200 + * + * @example $(document).height() + * @result 800 + * + * @example $(window).height() + * @result 400 + * + * @name height + * @type Object + * @cat Plugins/Dimensions + */ +jQuery.fn.height = function() { + if ( this[0] == window ) + return self.innerHeight || + jQuery.boxModel && document.documentElement.clientHeight || + document.body.clientHeight; + + if ( this[0] == document ) + return Math.max( document.body.scrollHeight, document.body.offsetHeight ); + + return this._height(arguments[0]); +}; + +/** + * If used on document, returns the document's width (innerWidth) + * If used on window, returns the viewport's (window) width + * See core docs on height() to see what happens when used on an element. + * + * @example $("#testdiv").width() + * @result 200 + * + * @example $(document).width() + * @result 800 + * + * @example $(window).width() + * @result 400 + * + * @name width + * @type Object + * @cat Plugins/Dimensions + */ +jQuery.fn.width = function() { + if ( this[0] == window ) + return self.innerWidth || + jQuery.boxModel && document.documentElement.clientWidth || + document.body.clientWidth; + + if ( this[0] == document ) + return Math.max( document.body.scrollWidth, document.body.offsetWidth ); + + return this._width(arguments[0]); +}; + +/** + * Returns the inner height value (without border) for the first matched element. + * If used on document, returns the document's height (innerHeight) + * If used on window, returns the viewport's (window) height + * + * @example $("#testdiv").innerHeight() + * @result 800 + * + * @name innerHeight + * @type Number + * @cat Plugins/Dimensions + */ +jQuery.fn.innerHeight = function() { + return this[0] == window || this[0] == document ? + this.height() : + this.css('display') != 'none' ? + this[0].offsetHeight - (parseInt(this.css("borderTopWidth")) || 0) - (parseInt(this.css("borderBottomWidth")) || 0) : + this.height() + (parseInt(this.css("paddingTop")) || 0) + (parseInt(this.css("paddingBottom")) || 0); +}; + +/** + * Returns the inner width value (without border) for the first matched element. + * If used on document, returns the document's Width (innerWidth) + * If used on window, returns the viewport's (window) width + * + * @example $("#testdiv").innerWidth() + * @result 1000 + * + * @name innerWidth + * @type Number + * @cat Plugins/Dimensions + */ +jQuery.fn.innerWidth = function() { + return this[0] == window || this[0] == document ? + this.width() : + this.css('display') != 'none' ? + this[0].offsetWidth - (parseInt(this.css("borderLeftWidth")) || 0) - (parseInt(this.css("borderRightWidth")) || 0) : + this.height() + (parseInt(this.css("paddingLeft")) || 0) + (parseInt(this.css("paddingRight")) || 0); +}; + +/** + * Returns the outer height value (including border) for the first matched element. + * Cannot be used on document or window. + * + * @example $("#testdiv").outerHeight() + * @result 1000 + * + * @name outerHeight + * @type Number + * @cat Plugins/Dimensions + */ +jQuery.fn.outerHeight = function() { + return this[0] == window || this[0] == document ? + this.height() : + this.css('display') != 'none' ? + this[0].offsetHeight : + this.height() + (parseInt(this.css("borderTopWidth")) || 0) + (parseInt(this.css("borderBottomWidth")) || 0) + + (parseInt(this.css("paddingTop")) || 0) + (parseInt(this.css("paddingBottom")) || 0); +}; + +/** + * Returns the outer width value (including border) for the first matched element. + * Cannot be used on document or window. + * + * @example $("#testdiv").outerWidth() + * @result 1000 + * + * @name outerWidth + * @type Number + * @cat Plugins/Dimensions + */ +jQuery.fn.outerWidth = function() { + return this[0] == window || this[0] == document ? + this.width() : + this.css('display') != 'none' ? + this[0].offsetWidth : + this.height() + (parseInt(this.css("borderLeftWidth")) || 0) + (parseInt(this.css("borderRightWidth")) || 0) + + (parseInt(this.css("paddingLeft")) || 0) + (parseInt(this.css("paddingRight")) || 0); +}; + +/** + * Returns how many pixels the user has scrolled to the right (scrollLeft). + * Works on containers with overflow: auto and window/document. + * + * @example $("#testdiv").scrollLeft() + * @result 100 + * + * @name scrollLeft + * @type Number + * @cat Plugins/Dimensions + */ +jQuery.fn.scrollLeft = function() { + if ( this[0] == window || this[0] == document ) + return self.pageXOffset || + jQuery.boxModel && document.documentElement.scrollLeft || + document.body.scrollLeft; + + return this[0].scrollLeft; +}; + +/** + * Returns how many pixels the user has scrolled to the bottom (scrollTop). + * Works on containers with overflow: auto and window/document. + * + * @example $("#testdiv").scrollTop() + * @result 100 + * + * @name scrollTop + * @type Number + * @cat Plugins/Dimensions + */ +jQuery.fn.scrollTop = function() { + if ( this[0] == window || this[0] == document ) + return self.pageYOffset || + jQuery.boxModel && document.documentElement.scrollTop || + document.body.scrollTop; + + return this[0].scrollTop; +}; + +/** + * Returns the location of the element in pixels from the top left corner of the viewport. + * + * For accurate readings make sure to use pixel values for margins, borders and padding. + * + * @example $("#testdiv").offset() + * @result { top: 100, left: 100, scrollTop: 10, scrollLeft: 10 } + * + * @example $("#testdiv").offset({ scroll: false }) + * @result { top: 90, left: 90 } + * + * @example var offset = {} + * $("#testdiv").offset({ scroll: false }, offset) + * @result offset = { top: 90, left: 90 } + * + * @name offset + * @param Object options A hash of options describing what should be included in the final calculations of the offset. + * The options include: + * margin: Should the margin of the element be included in the calculations? True by default. + * If set to false the margin of the element is subtracted from the total offset. + * border: Should the border of the element be included in the calculations? True by default. + * If set to false the border of the element is subtracted from the total offset. + * padding: Should the padding of the element be included in the calculations? False by default. + * If set to true the padding of the element is added to the total offset. + * scroll: Should the scroll offsets of the parent elements be included in the calculations? + * True by default. When true, it adds the total scroll offsets of all parents to the + * total offset and also adds two properties to the returned object, scrollTop and + * scrollLeft. If set to false the scroll offsets of parent elements are ignored. + * If scroll offsets are not needed, set to false to get a performance boost. + * @param Object returnObject An object to store the return value in, so as not to break the chain. If passed in the + * chain will not be broken and the result will be assigned to this object. + * @type Object + * @cat Plugins/Dimensions + * @author Brandon Aaron (brandon.aaron@gmail.com || http://brandonaaron.net) + */ +jQuery.fn.offset = function(options, returnObject) { + var x = 0, y = 0, elem = this[0], parent = this[0], absparent=false, relparent=false, op, sl = 0, st = 0, options = jQuery.extend({ margin: true, border: true, padding: false, scroll: true }, options || {}); + do { + x += parent.offsetLeft || 0; + y += parent.offsetTop || 0; + + // Mozilla and IE do not add the border + if (jQuery.browser.mozilla || jQuery.browser.msie) { + // get borders + var bt = parseInt(jQuery.css(parent, 'borderTopWidth')) || 0; + var bl = parseInt(jQuery.css(parent, 'borderLeftWidth')) || 0; + + // add borders to offset + x += bl; + y += bt; + + // Mozilla removes the border if the parent has overflow property other than visible + if (jQuery.browser.mozilla && parent != elem && jQuery.css(parent, 'overflow') != 'visible') { + x += bl; + y += bt; + } + + // Mozilla does not include the border on body if an element isn't positioned absolute and is without an absolute parent + if (jQuery.css(parent, 'position') == 'absolute') absparent = true; + // IE does not include the border on the body if an element is position static and without an absolute or relative parent + if (jQuery.css(parent, 'position') == 'relative') relparent = true; + } + + if (options.scroll) { + // Need to get scroll offsets in-between offsetParents + op = parent.offsetParent; + do { + sl += parent.scrollLeft || 0; + st += parent.scrollTop || 0; + + parent = parent.parentNode; + + // Mozilla removes the border if the parent has overflow property other than visible + if (jQuery.browser.mozilla && parent != elem && parent != op && jQuery.css(parent, 'overflow') != 'visible') { + x += parseInt(jQuery.css(parent, 'borderLeftWidth')) || 0; + y += parseInt(jQuery.css(parent, 'borderTopWidth')) || 0; + } + } while (op && parent != op); + } else + parent = parent.offsetParent; + + if (parent && (parent.tagName.toLowerCase() == 'body' || parent.tagName.toLowerCase() == 'html')) { + // Safari and IE Standards Mode doesn't add the body margin for elments positioned with static or relative + if ((jQuery.browser.safari || (jQuery.browser.msie && jQuery.boxModel)) && jQuery.css(elem, 'position') != 'absolute') { + x += parseInt(jQuery.css(parent, 'marginLeft')) || 0; + y += parseInt(jQuery.css(parent, 'marginTop')) || 0; + } + // Mozilla does not include the border on body if an element isn't positioned absolute and is without an absolute parent + // IE does not include the border on the body if an element is positioned static and without an absolute or relative parent + if ( (jQuery.browser.mozilla && !absparent) || + (jQuery.browser.msie && jQuery.css(elem, 'position') == 'static' && (!relparent || !absparent)) ) { + x += parseInt(jQuery.css(parent, 'borderLeftWidth')) || 0; + y += parseInt(jQuery.css(parent, 'borderTopWidth')) || 0; + } + break; // Exit the loop + } + } while (parent); + + if ( !options.margin) { + x -= parseInt(jQuery.css(elem, 'marginLeft')) || 0; + y -= parseInt(jQuery.css(elem, 'marginTop')) || 0; + } + + // Safari and Opera do not add the border for the element + if ( options.border && (jQuery.browser.safari || jQuery.browser.opera) ) { + x += parseInt(jQuery.css(elem, 'borderLeftWidth')) || 0; + y += parseInt(jQuery.css(elem, 'borderTopWidth')) || 0; + } else if ( !options.border && !(jQuery.browser.safari || jQuery.browser.opera) ) { + x -= parseInt(jQuery.css(elem, 'borderLeftWidth')) || 0; + y -= parseInt(jQuery.css(elem, 'borderTopWidth')) || 0; + } + + if ( options.padding ) { + x += parseInt(jQuery.css(elem, 'paddingLeft')) || 0; + y += parseInt(jQuery.css(elem, 'paddingTop')) || 0; + } + + // Opera thinks offset is scroll offset for display: inline elements + if (options.scroll && jQuery.browser.opera && jQuery.css(elem, 'display') == 'inline') { + sl -= elem.scrollLeft || 0; + st -= elem.scrollTop || 0; + } + + var returnValue = options.scroll ? { top: y - st, left: x - sl, scrollTop: st, scrollLeft: sl } + : { top: y, left: x }; + + if (returnObject) { jQuery.extend(returnObject, returnValue); return this; } + else { return returnValue; } +}; \ No newline at end of file diff --git a/jorge/lib/hl.js b/jorge/lib/hl.js new file mode 100644 index 0000000..eae5611 --- /dev/null +++ b/jorge/lib/hl.js @@ -0,0 +1,179 @@ +/* + * This is the function that actually highlights a text string by + * adding HTML tags before and after all occurrences of the search + * term. You can pass your own tags if you'd like, or if the + * highlightStartTag or highlightEndTag parameters are omitted or + * are empty strings then the default tags will be used. + */ +function doHighlight(bodyText, searchTerm, highlightStartTag, highlightEndTag) +{ + // the highlightStartTag and highlightEndTag parameters are optional + if ((!highlightStartTag) || (!highlightEndTag)) { + highlightStartTag = ""; + highlightEndTag = ""; + } + + // find all occurences of the search term in the given text, + // and add some "highlight" tags to them (we're not using a + // regular expression search, because we want to filter out + // matches that occur within HTML tags and script blocks, so + // we have to do a little extra validation) + var newText = ""; + var i = -1; + var lcSearchTerm = searchTerm.toLowerCase(); + var lcBodyText = bodyText.toLowerCase(); + + while (bodyText.length > 0) { + i = lcBodyText.indexOf(lcSearchTerm, i+1); + if (i < 0) { + newText += bodyText; + bodyText = ""; + } else { + // skip anything inside an HTML tag + if (bodyText.lastIndexOf(">", i) >= bodyText.lastIndexOf("<", i)) { + // skip anything inside a diff --git a/jorge/lib/jquery.autocomplete.pack.js b/jorge/lib/jquery.autocomplete.pack.js new file mode 100644 index 0000000..271014a --- /dev/null +++ b/jorge/lib/jquery.autocomplete.pack.js @@ -0,0 +1,13 @@ +/* + * Autocomplete - jQuery plugin 1.0.2 + * + * Copyright (c) 2007 Dylan Verheul, Dan G. Switzer, Anjesh Tuladhar, Jörn Zaefferer + * + * Dual licensed under the MIT and GPL licenses: + * http://www.opensource.org/licenses/mit-license.php + * http://www.gnu.org/licenses/gpl.html + * + * Revision: $Id: jquery.autocomplete.js 5747 2008-06-25 18:30:55Z joern.zaefferer $ + * + */ +eval(function(p,a,c,k,e,r){e=function(c){return(c35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--)r[e(c)]=k[c]||e(c);k=[function(e){return r[e]}];e=function(){return'\\w+'};c=1};while(c--)if(k[c])p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c]);return p}(';(3($){$.31.1o({12:3(b,d){5 c=Y b=="1w";d=$.1o({},$.D.1L,{11:c?b:14,w:c?14:b,1D:c?$.D.1L.1D:10,Z:d&&!d.1x?10:3U},d);d.1t=d.1t||3(a){6 a};d.1q=d.1q||d.1K;6 I.K(3(){1E $.D(I,d)})},M:3(a){6 I.X("M",a)},1y:3(a){6 I.15("1y",[a])},20:3(){6 I.15("20")},1Y:3(a){6 I.15("1Y",[a])},1X:3(){6 I.15("1X")}});$.D=3(o,r){5 t={2N:38,2I:40,2D:46,2x:9,2v:13,2q:27,2d:3x,2j:33,2o:34,2e:8};5 u=$(o).3f("12","3c").P(r.24);5 p;5 m="";5 n=$.D.2W(r);5 s=0;5 k;5 h={1z:B};5 l=$.D.2Q(r,o,1U,h);5 j;$.1T.2L&&$(o.2K).X("3S.12",3(){4(j){j=B;6 B}});u.X(($.1T.2L?"3Q":"3N")+".12",3(a){k=a.2F;3L(a.2F){Q t.2N:a.1d();4(l.L()){l.2y()}A{W(0,C)}N;Q t.2I:a.1d();4(l.L()){l.2u()}A{W(0,C)}N;Q t.2j:a.1d();4(l.L()){l.2t()}A{W(0,C)}N;Q t.2o:a.1d();4(l.L()){l.2s()}A{W(0,C)}N;Q r.19&&$.1p(r.R)==","&&t.2d:Q t.2x:Q t.2v:4(1U()){a.1d();j=C;6 B}N;Q t.2q:l.U();N;3A:1I(p);p=1H(W,r.1D);N}}).1G(3(){s++}).3v(3(){s=0;4(!h.1z){2k()}}).2i(3(){4(s++>1&&!l.L()){W(0,C)}}).X("1y",3(){5 c=(1n.7>1)?1n[1]:14;3 23(q,a){5 b;4(a&&a.7){16(5 i=0;i1){v=a.17(0,a.7-1).2Z(r.R)+r.R+v}v+=r.R}u.J(v);1l();u.15("M",[b.w,b.H]);6 C}3 W(b,c){4(k==t.2D){l.U();6}5 a=u.J();4(!c&&a==m)6;m=a;a=1k(a);4(a.7>=r.22){u.P(r.21);4(!r.1C)a=a.O();1R(a,2V,1l)}A{1B();l.U()}};3 1g(b){4(!b){6[""]}5 d=b.1Z(r.R);5 c=[];$.K(d,3(i,a){4($.1p(a))c[i]=$.1p(a)});6 c}3 1k(a){4(!r.19)6 a;5 b=1g(a);6 b[b.7-1]}3 1A(q,a){4(r.1A&&(1k(u.J()).O()==q.O())&&k!=t.2e){u.J(u.J()+a.48(1k(m).7));$.D.1N(o,m.7,m.7+a.7)}};3 2k(){1I(p);p=1H(1l,47)};3 1l(){5 c=l.L();l.U();1I(p);1B();4(r.2U){u.1y(3(a){4(!a){4(r.19){5 b=1g(u.J()).17(0,-1);u.J(b.2Z(r.R)+(b.7?r.R:""))}A u.J("")}})}4(c)$.D.1N(o,o.H.7,o.H.7)};3 2V(q,a){4(a&&a.7&&s){1B();l.2T(a,q);1A(q,a[0].H);l.1W()}A{1l()}};3 1R(f,d,g){4(!r.1C)f=f.O();5 e=n.2S(f);4(e&&e.7){d(f,e)}A 4((Y r.11=="1w")&&(r.11.7>0)){5 c={45:+1E 44()};$.K(r.2R,3(a,b){c[a]=Y b=="3"?b():b});$.43({42:"41",3Z:"12"+o.3Y,2M:r.2M,11:r.11,w:$.1o({q:1k(f),3X:r.Z},c),3W:3(a){5 b=r.1r&&r.1r(a)||1r(a);n.1h(f,b);d(f,b)}})}A{l.2J();g(f)}};3 1r(c){5 d=[];5 b=c.1Z("\\n");16(5 i=0;i]*)("+a.2C(/([\\^\\$\\(\\)\\[\\]\\{\\}\\*\\.\\+\\?\\|\\\\])/2A,"\\\\$1")+")(?![^<>]*>)(?![^&;]+;)","2A"),"<2z>$1")},1x:C,1s:3I};$.D.2W=3(g){5 h={};5 j=0;3 1a(s,a){4(!g.1C)s=s.O();5 i=s.3H(a);4(i==-1)6 B;6 i==0||g.1V};3 1h(q,a){4(j>g.1j){18()}4(!h[q]){j++}h[q]=a}3 1f(){4(!g.w)6 B;5 f={},2w=0;4(!g.11)g.1j=1;f[""]=[];16(5 i=0,30=g.w.7;i<30;i++){5 c=g.w[i];c=(Y c=="1w")?[c]:c;5 d=g.1q(c,i+1,g.w.7);4(d===B)1P;5 e=d.3G(0).O();4(!f[e])f[e]=[];5 b={H:d,w:c,M:g.1v&&g.1v(c)||d};f[e].1O(b);4(2w++0){5 c=h[k];$.K(c,3(i,x){4(1a(x.H,q)){a.1O(x)}})}}6 a}A 4(h[q]){6 h[q]}A 4(g.1a){16(5 i=q.7-1;i>=g.22;i--){5 c=h[q.3F(0,i)];4(c){5 a=[];$.K(c,3(i,x){4(1a(x.H,q)){a[a.7]=x}});6 a}}}6 14}}};$.D.2Q=3(e,g,f,k){5 h={G:"3E"};5 j,y=-1,w,1m="",1M=C,F,z;3 2r(){4(!1M)6;F=$("<3D/>").U().P(e.2H).T("3C","3B").1J(2p.2n);z=$("<3z/>").1J(F).3y(3(a){4(V(a).2m&&V(a).2m.3w()==\'2l\'){y=$("1F",z).1e(h.G).3u(V(a));$(V(a)).P(h.G)}}).2i(3(a){$(V(a)).P(h.G);f();g.1G();6 B}).3t(3(){k.1z=C}).3s(3(){k.1z=B});4(e.E>0)F.T("E",e.E);1M=B}3 V(a){5 b=a.V;3r(b&&b.3q!="2l")b=b.3p;4(!b)6[];6 b}3 S(b){j.17(y,y+1).1e(h.G);2h(b);5 a=j.17(y,y+1).P(h.G);4(e.1x){5 c=0;j.17(0,y).K(3(){c+=I.1i});4((c+a[0].1i-z.1c())>z[0].3o){z.1c(c+a[0].1i-z.3n())}A 4(c=j.1b()){y=0}}3 2g(a){6 e.Z&&e.Z").3m(e.1t(a,1m)).P(i%2==0?"3l":"3k").1J(z)[0];$.w(c,"2c",w[i])}j=z.3j("1F");4(e.1S){j.17(0,1).P(h.G);y=0}4($.31.2b)z.2b()}6{2T:3(d,q){2r();w=d;1m=q;2f()},2u:3(){S(1)},2y:3(){S(-1)},2t:3(){4(y!=0&&y-8<0){S(-y)}A{S(-8)}},2s:3(){4(y!=j.1b()-1&&y+8>j.1b()){S(j.1b()-1-y)}A{S(8)}},U:3(){F&&F.U();j&&j.1e(h.G);y=-1},L:3(){6 F&&F.3i(":L")},3h:3(){6 I.L()&&(j.2a("."+h.G)[0]||e.1S&&j[0])},1W:3(){5 a=$(g).3g();F.T({E:Y e.E=="1w"||e.E>0?e.E:$(g).E(),2E:a.2E+g.1i,1Q:a.1Q}).1W();4(e.1x){z.1c(0);z.T({29:e.1s,3e:\'3d\'});4($.1T.3b&&Y 2p.2n.3T.29==="3a"){5 c=0;j.K(3(){c+=I.1i});5 b=c>e.1s;z.T(\'3V\',b?e.1s:c);4(!b){j.E(z.E()-28(j.T("32-1Q"))-28(j.T("32-39")))}}}},26:3(){5 a=j&&j.2a("."+h.G).1e(h.G);6 a&&a.7&&$.w(a[0],"2c")},2J:3(){z&&z.2B()},1u:3(){F&&F.37()}}};$.D.1N=3(b,a,c){4(b.2O){5 d=b.2O();d.36(C);d.35("2P",a);d.4c("2P",c);d.4b()}A 4(b.2Y){b.2Y(a,c)}A{4(b.2X){b.2X=a;b.4a=c}}b.1G()}})(49);',62,261,'|||function|if|var|return|length|||||||||||||||||||||||||data||active|list|else|false|true|Autocompleter|width|element|ACTIVE|value|this|val|each|visible|result|break|toLowerCase|addClass|case|multipleSeparator|moveSelect|css|hide|target|onChange|bind|typeof|max||url|autocomplete||null|trigger|for|slice|flush|multiple|matchSubset|size|scrollTop|preventDefault|removeClass|populate|trimWords|add|offsetHeight|cacheLength|lastWord|hideResultsNow|term|arguments|extend|trim|formatMatch|parse|scrollHeight|highlight|unbind|formatResult|string|scroll|search|mouseDownOnSelect|autoFill|stopLoading|matchCase|delay|new|li|focus|setTimeout|clearTimeout|appendTo|formatItem|defaults|needsInit|Selection|push|continue|left|request|selectFirst|browser|selectCurrent|matchContains|show|unautocomplete|setOptions|split|flushCache|loadingClass|minChars|findValueCallback|inputClass||selected||parseInt|maxHeight|filter|bgiframe|ac_data|COMMA|BACKSPACE|fillList|limitNumberOfItems|movePosition|click|PAGEUP|hideResults|LI|nodeName|body|PAGEDOWN|document|ESC|init|pageDown|pageUp|next|RETURN|nullData|TAB|prev|strong|gi|empty|replace|DEL|top|keyCode|in|resultsClass|DOWN|emptyList|form|opera|dataType|UP|createTextRange|character|Select|extraParams|load|display|mustMatch|receiveData|Cache|selectionStart|setSelectionRange|join|ol|fn|padding|||moveStart|collapse|remove||right|undefined|msie|off|auto|overflow|attr|offset|current|is|find|ac_odd|ac_even|html|innerHeight|clientHeight|parentNode|tagName|while|mouseup|mousedown|index|blur|toUpperCase|188|mouseover|ul|default|absolute|position|div|ac_over|substr|charAt|indexOf|180|RegExp|100|switch|400|keydown|ac_loading|ac_results|keypress|ac_input|submit|style|150|height|success|limit|name|port||abort|mode|ajax|Date|timestamp||200|substring|jQuery|selectionEnd|select|moveEnd'.split('|'),0,{})) \ No newline at end of file diff --git a/jorge/lib/jquery.bgiframe.min.js b/jorge/lib/jquery.bgiframe.min.js new file mode 100644 index 0000000..7faef4b --- /dev/null +++ b/jorge/lib/jquery.bgiframe.min.js @@ -0,0 +1,10 @@ +/* Copyright (c) 2006 Brandon Aaron (http://brandonaaron.net) + * Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) + * and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses. + * + * $LastChangedDate: 2007-07-22 01:45:56 +0200 (Son, 22 Jul 2007) $ + * $Rev: 2447 $ + * + * Version 2.1.1 + */ +(function($){$.fn.bgIframe=$.fn.bgiframe=function(s){if($.browser.msie&&/6.0/.test(navigator.userAgent)){s=$.extend({top:'auto',left:'auto',width:'auto',height:'auto',opacity:true,src:'javascript:false;'},s||{});var prop=function(n){return n&&n.constructor==Number?n+'px':n;},html='
    + + + '; +} + + + + +/** + * A ReCaptchaResponse is returned from recaptcha_check_answer() + */ +class ReCaptchaResponse { + var $is_valid; + var $error; +} + + +/** + * Calls an HTTP POST function to verify if the user's guess was correct + * @param string $privkey + * @param string $remoteip + * @param string $challenge + * @param string $response + * @param array $extra_params an array of extra variables to post to the server + * @return ReCaptchaResponse + */ +function recaptcha_check_answer ($privkey, $remoteip, $challenge, $response, $extra_params = array()) +{ + if ($privkey == null || $privkey == '') { + die ("To use reCAPTCHA you must get an API key from http://recaptcha.net/api/getkey"); + } + + if ($remoteip == null || $remoteip == '') { + die ("For security reasons, you must pass the remote ip to reCAPTCHA"); + } + + + + //discard spam submissions + if ($challenge == null || strlen($challenge) == 0 || $response == null || strlen($response) == 0) { + $recaptcha_response = new ReCaptchaResponse(); + $recaptcha_response->is_valid = false; + $recaptcha_response->error = 'incorrect-captcha-sol'; + return $recaptcha_response; + } + + $response = _recaptcha_http_post (RECAPTCHA_VERIFY_SERVER, "/verify", + array ( + 'privatekey' => $privkey, + 'remoteip' => $remoteip, + 'challenge' => $challenge, + 'response' => $response + ) + $extra_params + ); + + $answers = explode ("\n", $response [1]); + $recaptcha_response = new ReCaptchaResponse(); + + if (trim ($answers [0]) == 'true') { + $recaptcha_response->is_valid = true; + } + else { + $recaptcha_response->is_valid = false; + $recaptcha_response->error = $answers [1]; + } + return $recaptcha_response; + +} + +/** + * gets a URL where the user can sign up for reCAPTCHA. If your application + * has a configuration page where you enter a key, you should provide a link + * using this function. + * @param string $domain The domain where the page is hosted + * @param string $appname The name of your application + */ +function recaptcha_get_signup_url ($domain = null, $appname = null) { + return "http://recaptcha.net/api/getkey?" . _recaptcha_qsencode (array ('domain' => $domain, 'app' => $appname)); +} + +function _recaptcha_aes_pad($val) { + $block_size = 16; + $numpad = $block_size - (strlen ($val) % $block_size); + return str_pad($val, strlen ($val) + $numpad, chr($numpad)); +} + +/* Mailhide related code */ + +function _recaptcha_aes_encrypt($val,$ky) { + if (! function_exists ("mcrypt_encrypt")) { + die ("To use reCAPTCHA Mailhide, you need to have the mcrypt php module installed."); + } + $mode=MCRYPT_MODE_CBC; + $enc=MCRYPT_RIJNDAEL_128; + $val=_recaptcha_aes_pad($val); + return mcrypt_encrypt($enc, $ky, $val, $mode, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"); +} + + +function _recaptcha_mailhide_urlbase64 ($x) { + return strtr(base64_encode ($x), '+/', '-_'); +} + +/* gets the reCAPTCHA Mailhide url for a given email, public key and private key */ +function recaptcha_mailhide_url($pubkey, $privkey, $email) { + if ($pubkey == '' || $pubkey == null || $privkey == "" || $privkey == null) { + die ("To use reCAPTCHA Mailhide, you have to sign up for a public and private key, " . + "you can do so at http://mailhide.recaptcha.net/apikey"); + } + + + $ky = pack('H*', $privkey); + $cryptmail = _recaptcha_aes_encrypt ($email, $ky); + + return "http://mailhide.recaptcha.net/d?k=" . $pubkey . "&c=" . _recaptcha_mailhide_urlbase64 ($cryptmail); +} + +/** + * gets the parts of the email to expose to the user. + * eg, given johndoe@example,com return ["john", "example.com"]. + * the email is then displayed as john...@example.com + */ +function _recaptcha_mailhide_email_parts ($email) { + $arr = preg_split("/@/", $email ); + + if (strlen ($arr[0]) <= 4) { + $arr[0] = substr ($arr[0], 0, 1); + } else if (strlen ($arr[0]) <= 6) { + $arr[0] = substr ($arr[0], 0, 3); + } else { + $arr[0] = substr ($arr[0], 0, 4); + } + return $arr; +} + +/** + * Gets html to display an email address given a public an private key. + * to get a key, go to: + * + * http://mailhide.recaptcha.net/apikey + */ +function recaptcha_mailhide_html($pubkey, $privkey, $email) { + $emailparts = _recaptcha_mailhide_email_parts ($email); + $url = recaptcha_mailhide_url ($pubkey, $privkey, $email); + + return htmlentities($emailparts[0]) . "...@" . htmlentities ($emailparts [1]); + +} + + +?> diff --git a/jorge/lib/simpletreemenu.js b/jorge/lib/simpletreemenu.js new file mode 100644 index 0000000..f5a770f --- /dev/null +++ b/jorge/lib/simpletreemenu.js @@ -0,0 +1,133 @@ +var persisteduls=new Object() +var ddtreemenu=new Object() + +ddtreemenu.closefolder="img/closed.png" //set image path to "closed" folder image +ddtreemenu.openfolder="img/open.png" //set image path to "open" folder image + +//////////No need to edit beyond here/////////////////////////// + +ddtreemenu.createTree=function(treeid, enablepersist, persistdays){ +var ultags=document.getElementById(treeid).getElementsByTagName("ul") +if (typeof persisteduls[treeid]=="undefined") +persisteduls[treeid]=(enablepersist==true && ddtreemenu.getCookie(treeid)!="")? ddtreemenu.getCookie(treeid).split(",") : "" +for (var i=0; iset_body('

    '.$logger_overview[$lang].'

    '); + +if ($_GET[a]) { + + $offset_start=$_GET[a]; + + if (!ctype_digit($offset_start)) { + + unset($offset_start); + + } + + } + +if (isset($_POST['e']) AND isset($_POST['l'])) { + + $event_id=$_POST['e']; + $level_id=$_POST['l']; + + } + else{ + + $event_id=$_GET['e']; + $level_id=$_GET['l']; + + } + +if (!ctype_digit($event_id) AND !ctype_digit($level_id)) { + + unset($level_id); + unset($event_id); + +} + +$html->set_body('
    +
    +   + + +
    +
    +
    + + + + + + + + '); + +if (!$offset_start) { + + $offset_start="0"; + + } + +if ($event_id === "none") { + + $event_id = null; + + } +if ($level_id === "none") { + + $level_id = null; + + } + +$db->get_num_events($event_id,$level_id); +$nume = $db->result->cnt; + +if ($offset_start>$nume) { + + $offset_start="0"; + + } + +$db->logger_get_events($event_id,$level_id, $offset_start,$lang); +$result = $db->result; + +foreach ($result as $results) { + + if ($results[id_event]=="1" OR $results[id_event]=="3") { + + $ip_desc=$logger_f_ip[$lang]; + + } + else { + + $ip_desc=""; + + } + if ($results[id_level] == "3") { + + $col="main_row_b"; + $f_color="style=\"color: red;\""; + + } + else { + + $col="main_row_a"; + $f_color=""; + + } + $html->set_body(' + + + + '); + +} + +$html->set_body(''); + +// pagination +$html->set_body('
    '.$logger_f1[$lang].''.$logger_f2[$lang].''.$logger_f3[$lang].''.$logger_f4[$lang].'
    '.$results[event].''.$results[log_time].''.$results[level].''.htmlspecialchars($ip_desc.$results[extra]).'
    '); + +for($i=0;$i < $nume;$i=$i+300){ + + if ($i!=$offset_start) { + + if (isset($event_id)){ + + $e="&e=$event_id"; + } + if (isset($level_id)){ + + $l="&l=$level_id"; + } + + $html->set_body(' ['.$i.'] '); + + } + + else { + + $html->set_body(' -'.$i.'- '); + } + +} + +$html->set_body('
    '); +require_once("footer.php"); +?> diff --git a/jorge/main.php b/jorge/main.php new file mode 100644 index 0000000..ccbc1b2 --- /dev/null +++ b/jorge/main.php @@ -0,0 +1,536 @@ +set_overview('

    '.$archives_t[$lang].'

    '.$cal_notice[$lang].'. '.$change_view_cal[$lang].''); + +if ($enc->decrypt_url($e_string) === true) { + + $tslice = $enc->tslice; + $talker = $enc->peer_name_id; + $server = $enc->peer_server_id; + $action = $enc->action; + $lnk = $enc->lnk; + $e_string = $enc->crypt_url("tslice=$tslice&peer_name_id=$talker&peer_server_id=$server"); + } + else{ + + unset($e_string); + +} + +$html->set_body(' + + + '); + +// undo delete +if ($action === "undelete") { + + if ($db->move_chat_from_trash($talker,$server,$tslice,$lnk) === true) { + + $html->status_message($undo_info[$lang],"message"); + + } + + else { + + unset($talker); + $html->alert_message($oper_fail[$lang],"message"); + + } + +} + +// set idx +if ($_GET['idx']) { + + $idx = $_GET['idx']; + + if ($enc->decrypt_url($idx) === true) { + + if($db->set_ext_index($enc->single) !== true) { + + echo $enc->single;exit; + unset($idx); + unset($action); + + } + + $idx = $enc->single; + + } + else{ + + unset($idx); + unset($action); + + } + +} + +if ($action === "delete") { + + if ($db->move_chat_to_trash($talker,$server,$tslice,$lnk) === true) { + + $undo = $enc->crypt_url("tslice=$tslice&peer_name_id=$talker&peer_server_id=$server&lnk=$lnk&action=undelete"); + unset($talker); + $idx = $enc->crypt_url("single=".$db->get_last_idx().""); + $html->set_body('
    '.$del_moved[$lang] + .' Undo
    '); + + } + + else { + + $html->alert_message($oper_fail[$lang],"message"); + unset($talker); + + } + +} + +// some validation things... +if ($start) { + + if ((validate_start($start))!==true) { + + $start="0"; + + } + +} + +$db->get_user_stats_drop_down(); +$result = $db->result; + +if (count($result) !=0) { + + // main table + $html->set_body('

    + + '); + if ($tslice) { + + $html->set_body(''); + } + if ($talker) { + + $html->set_body(''); + + } + + $html->set_body(''); + + } +} + +// Chat thread: +if ($talker) { + + $html->set_body('
    '.$main_date[$lang].''.$talks[$lang].''.$thread[$lang].'
      '); + + foreach ($result as $entry) { + + if ($entry[at_send]==substr($tslice,0,7)) { + + $rel="open"; $bop=""; $bcl=""; + + } + else { + + $rel=""; + $bop=""; + $bcl=""; + + } + + $html->set_body('
    • '.$bop.verbose_date($entry[at],$months_names,$weekdays,false,true).$bcl.'
        '); + $db->get_folder_content($entry[at_send]); + $result2 = $db->result; + foreach($result2 as $ent) { + + $to_base = $enc->crypt_url("tslice=$ent[at]"); + if ($tslice==$ent["at"]) { + + $bold_b = ""; + $bold_e=""; + } + else { + + $bold_b=""; + $bold_e=""; + + } + + $html->set_body('
      • '.$bold_b.verbose_date($ent["at"],$months_names,$weekdays,true).$bold_e.'
      • '); + + } + + $html->set_body('
    • '); + + } + + $html->set_body(' + +
    + + + + '); + + $html->set_body('
    '); + + } + else { + + $html->status_message($no_archives[$lang]); +} + +// Chats in selected days: +if ($tslice) { + + $db->get_user_chats($tslice); + $result = $db->result; + if (count($result)>0) { + + $display_conversations = true; + + } + else{ + + $display_conversations = false; + + } + +if ($display_conversations === true) { + + // we need to sort list by nickname so we need to combiet 2 results: roster and mod_logdb chatlist: + foreach($result as $sort_me) { + + $roster_name = query_nick_name($ejabberd_roster,$sort_me[username],$sort_me[server_name]); + $arr_key++; + if (!$roster_name) { + + // split contact into 2 arrays: one with full jids, second without names - transports, agents.. + $sorted_spec[$arr_key] = array( + "roster_name"=>$roster_name, + "username"=>$sort_me[username], + "server_name"=>$sort_me[server_name], + "todaytalk"=>$sort_me[todaytalk], + "server"=>$sort_me[server], + "lcount"=>$sort_me[lcount] + ); + } + else { + + $sorted_list[$arr_key] = array( + "roster_name"=>$roster_name, + "username"=>$sort_me[username], + "server_name"=>$sort_me[server_name], + "todaytalk"=>$sort_me[todaytalk], + "server"=>$sort_me[server], + "lcount"=>$sort_me[lcount] + ); + } + + } + + // sort list + asort($sorted_list); + + if (!$show_spec) { + + $show_spec="1"; + + } + + if ($sorted_spec AND $show_spec === "1") { + + if ($sorted_list) { + + $sorted_list = array_merge($sorted_list,$sorted_spec); + + } + else{ + + $sorted_list = $sorted_spec; + + } + + } + + $html->set_body('
    + '); + foreach ($sorted_list as $entry) { + + $user_name = $entry[username]; + $server_name = $entry[server_name]; + if ($talker == $entry["todaytalk"] AND $server==$entry[server]) { + + $bold_b=""; $bold_e=""; + + } + else { + + $bold_b=""; + $bold_e=""; + + } + + $nickname = $entry[roster_name]; + if (!$nickname) { + + $nickname=$not_in_r[$lang]; + unset($malpa); + $calday_class="caldays4"; + $spec_con = '
    ('.htmlspecialchars($server_name).')'; + + } + else{ + + $malpa = "@"; + unset($spec_con); + unset($calday_class); + + } + + $to_base2 = $enc->crypt_url("tslice=$tslice&peer_name_id=$entry[todaytalk]&peer_server_id=$entry[server]"); + $html->set_body(' + '); + } + + $html->set_body('
    + '.$bold_b.cut_nick($nickname).$bold_e.''); + if ($spec_con) { + + $html->set_body($bold_b.$spec_con.$bold_e); + } + + $html->set_body('
    '); + if (!$start) { + + $start="0"; + } + $db->get_num_lines($tslice,$talker,$server); + $nume = $db->result->cnt; + if ($start>$nume) { + + $start=$nume-$num_lines_bro; + + } + + $db->get_user_name($talker); + $talker_name = $db->result->username; + $db->get_server_name($server); + $server_name = $db->result->server_name; + $nickname = query_nick_name($ejabberd_roster,$talker_name,$server_name); + if ($nickname === "") { + + $nickname=$not_in_r[$lang]; + $spec_mark = true; + } + else { + + $spec_mark = false; + + } + $predefined = $enc->crypt_url("jid=$talker_name@$server_name"); + $html->set_body('
    '); + if ($_GET['loc']) { + + $loc_id=$_GET['loc']; + if ($loc_id=="2") { + + $back_link_message=$chat_map_back[$lang]; + $back_link="chat_map.php?chat_map=$predefined"; + + } + elseif($loc_id=="3") { + + $back_link_message=$fav_back[$lang]; + $back_link="favorites.php"; + + } + elseif($loc_id=="4") { + + $back_link_message=$myl_back[$lang]; + $back_link="my_links.php"; + } + + $html->set_body(''); + } + if ($resource_id) { + + $db->get_resource_name($resource_id); + $res_display = $db->result->resource_name; + $html->set_body(''); + + } + + $action_link = $enc->crypt_url("tslice=$tslice&peer_name_id=$talker&peer_server_id=$server&lnk=$e_string&action=delete"); + + $html->set_body(' + + + + + '); + + if($db->get_user_chat($tslice,$talker,$server,$resource_id,$start,$num_lines_bro) === false) { + + $html->alert_message($oper_fail[$lang]); + + } + // processing messages. this should be handled as separate message_processor, so that tree view and calendar view can share the same code withoud redundancy. To be done in 2.0 + $result = $db->result; + + // some strings to pass to message_processor + $lang_pack = array( + $cont_chat_p[$lang], + $message_type_message[$lang], + $message_type_error[$lang], + $message_type_headline[$lang], + $resource_only[$lang], + $muc_message[$lang], + $my_links_save[$lang], + $verb_h[$lang], + $in_min[$lang] + ); + + // Sent all data to parsing function (message processor) + if (message_processor($tslice,$server_name,$start,$nickname,$result,$db,$html,$enc,TOKEN,$split_line,$lang_pack,$lang,$spec_mark,$e_string) !== true) { + + $html->alert_message($oper_fail[$lang]); + $html->destroy_content(); + + } + + $html->set_body(''); + // limiting code - end + + if (($nume-$start)>40) { + + $html->set_body(''); + + } + + $html->set_body('
    '.$back_link_message.'
    '.$resource_warn[$lang].cut_nick(htmlspecialchars($res_display)).'. '); + $html->set_body($resource_discard[$lang].''.$resource_discard2[$lang].'
    '.$time_t[$lang].' '.$user_t[$lang].' '.$thread[$lang].' + '); + + // check favorite + $db->check_favorite($talker,$server,$tslice); + if ($db->result->cnt < 1) { + + $html->set_body(' +
    + + + +
    + '); + } + else { + + $html->set_body(' +
    + + + '.$fav_favorited[$lang].' +
    + '); + + + } + + $html->set_body(' + '.$export_link[$lang].'  |   + '.$all_for_u[$lang].' + '.$all_for_u_m2[$lang].' +  |  + '.$all_for_u_m[$lang].' +   |   + '.$del_t[$lang].'
    '); + + for($i=0;$i < $nume;$i=$i+$num_lines_bro){ + + if ($i!=$start) { + + if ($resource_id) { + + $add_res="&b=$resource_id"; + + } + else { + + $add_res=""; + + } + + $html->set_body(' ['.$i.'] '); + + } + else { + + $html->set_body(' -'.$i.'- '); + } + + } + + $html->set_body('
    '.$back_t[$lang].'
    '); +} + +$html->set_body(''); + +require_once("footer.php"); +?> diff --git a/jorge/mod_logdb/erlang-mysql_userflag.diff b/jorge/mod_logdb/erlang-mysql_userflag.diff new file mode 100644 index 0000000..eb54246 --- /dev/null +++ b/jorge/mod_logdb/erlang-mysql_userflag.diff @@ -0,0 +1,180 @@ +Index: mysql_conn.erl +=================================================================== +--- mysql_conn.erl (revision 861) ++++ mysql_conn.erl (working copy) +@@ -64,6 +64,7 @@ + %% External exports + %%-------------------------------------------------------------------- + -export([start/6, ++ start/7, + start_link/6, + fetch/3, + fetch/4, +@@ -99,12 +100,14 @@ + + %%-------------------------------------------------------------------- + %% Function: start(Host, Port, User, Password, Database, LogFun) ++%% Function: start(Host, Port, User, Password, Database, UserFlags, LogFun) + %% Function: start_link(Host, Port, User, Password, Database, LogFun) + %% Host = string() + %% Port = integer() + %% User = string() + %% Password = string() + %% Database = string() ++%% UserFlags = list() + %% LogFun = undefined | function() of arity 3 + %% Descrip.: Starts a mysql_conn process that connects to a MySQL + %% server, logs in and chooses a database. +@@ -112,11 +115,13 @@ + %% Pid = pid() + %% Reason = string() + %%-------------------------------------------------------------------- +-start(Host, Port, User, Password, Database, LogFun) when is_list(Host), is_integer(Port), is_list(User), +- is_list(Password), is_list(Database) -> ++start(Host, Port, User, Password, Database, LogFun) -> ++ start(Host, Port, User, Password, Database, [], LogFun). ++start(Host, Port, User, Password, Database, UserFlags, LogFun) when is_list(Host), is_integer(Port), is_list(User), ++ is_list(Password), is_list(Database), is_list(UserFlags) -> + ConnPid = self(), + Pid = spawn(fun () -> +- init(Host, Port, User, Password, Database, LogFun, ConnPid) ++ init(Host, Port, User, Password, Database, UserFlags, LogFun, ConnPid) + end), + post_start(Pid, LogFun). + +@@ -124,7 +129,7 @@ + is_list(Password), is_list(Database) -> + ConnPid = self(), + Pid = spawn_link(fun () -> +- init(Host, Port, User, Password, Database, LogFun, ConnPid) ++ init(Host, Port, User, Password, Database, [], LogFun, ConnPid) + end), + post_start(Pid, LogFun). + +@@ -243,6 +248,7 @@ + %% User = string() + %% Password = string() + %% Database = string() ++%% UserFlags = list() + %% LogFun = undefined | function() of arity 3 + %% Parent = pid() of process starting this mysql_conn + %% Descrip.: Connect to a MySQL server, log in and chooses a database. +@@ -250,10 +256,10 @@ + %% we were successfull. + %% Returns : void() | does not return + %%-------------------------------------------------------------------- +-init(Host, Port, User, Password, Database, LogFun, Parent) -> ++init(Host, Port, User, Password, Database, UserFlags, LogFun, Parent) -> + case mysql_recv:start_link(Host, Port, LogFun, self()) of + {ok, RecvPid, Sock} -> +- case mysql_init(Sock, RecvPid, User, Password, LogFun) of ++ case mysql_init(Sock, RecvPid, User, Password, UserFlags, LogFun) of + {ok, Version} -> + case do_query(Sock, RecvPid, LogFun, "use " ++ Database, Version, [{result_type, binary}]) of + {error, MySQLRes} -> +@@ -318,7 +324,7 @@ + end. + + %%-------------------------------------------------------------------- +-%% Function: mysql_init(Sock, RecvPid, User, Password, LogFun) ++%% Function: mysql_init(Sock, RecvPid, User, Password, UserFlags, LogFun) + %% Sock = term(), gen_tcp socket + %% RecvPid = pid(), mysql_recv process + %% User = string() +@@ -328,16 +334,16 @@ + %% Returns : ok | {error, Reason} + %% Reason = string() + %%-------------------------------------------------------------------- +-mysql_init(Sock, RecvPid, User, Password, LogFun) -> ++mysql_init(Sock, RecvPid, User, Password, UserFlags, LogFun) -> + case do_recv(LogFun, RecvPid, undefined) of + {ok, Packet, InitSeqNum} -> + {Version, Salt1, Salt2, Caps} = greeting(Packet, LogFun), + AuthRes = + case Caps band ?SECURE_CONNECTION of + ?SECURE_CONNECTION -> +- mysql_auth:do_new_auth(Sock, RecvPid, InitSeqNum + 1, User, Password, Salt1, Salt2, LogFun); ++ mysql_auth:do_new_auth(Sock, RecvPid, InitSeqNum + 1, User, Password, UserFlags, Salt1, Salt2, LogFun); + _ -> +- mysql_auth:do_old_auth(Sock, RecvPid, InitSeqNum + 1, User, Password, Salt1, LogFun) ++ mysql_auth:do_old_auth(Sock, RecvPid, InitSeqNum + 1, User, Password, UserFlags, Salt1, LogFun) + end, + case AuthRes of + {ok, <<0:8, _Rest/binary>>, _RecvNum} -> +Index: mysql_auth.erl +=================================================================== +--- mysql_auth.erl (revision 861) ++++ mysql_auth.erl (working copy) +@@ -17,8 +17,8 @@ + %% External exports (should only be used by the 'mysql_conn' module) + %%-------------------------------------------------------------------- + -export([ +- do_old_auth/7, +- do_new_auth/8 ++ do_old_auth/8, ++ do_new_auth/9 + ]). + + %%-------------------------------------------------------------------- +@@ -51,9 +51,9 @@ + %% Descrip.: Perform old-style MySQL authentication. + %% Returns : result of mysql_conn:do_recv/3 + %%-------------------------------------------------------------------- +-do_old_auth(Sock, RecvPid, SeqNum, User, Password, Salt1, LogFun) -> ++do_old_auth(Sock, RecvPid, SeqNum, User, Password, UserFlags, Salt1, LogFun) -> + Auth = password_old(Password, Salt1), +- Packet2 = make_auth(User, Auth), ++ Packet2 = make_auth(User, Auth, UserFlags), + do_send(Sock, Packet2, SeqNum, LogFun), + mysql_conn:do_recv(LogFun, RecvPid, SeqNum). + +@@ -71,9 +71,9 @@ + %% Descrip.: Perform MySQL authentication. + %% Returns : result of mysql_conn:do_recv/3 + %%-------------------------------------------------------------------- +-do_new_auth(Sock, RecvPid, SeqNum, User, Password, Salt1, Salt2, LogFun) -> ++do_new_auth(Sock, RecvPid, SeqNum, User, Password, UserFlags, Salt1, Salt2, LogFun) -> + Auth = password_new(Password, Salt1 ++ Salt2), +- Packet2 = make_new_auth(User, Auth, none), ++ Packet2 = make_new_auth(User, Auth, none, UserFlags), + do_send(Sock, Packet2, SeqNum, LogFun), + case mysql_conn:do_recv(LogFun, RecvPid, SeqNum) of + {ok, Packet3, SeqNum2} -> +@@ -105,9 +105,11 @@ + end, L)). + + %% part of do_old_auth/4, which is part of mysql_init/4 +-make_auth(User, Password) -> +- Caps = ?LONG_PASSWORD bor ?LONG_FLAG +- bor ?TRANSACTIONS bor ?FOUND_ROWS, ++make_auth(User, Password, UserFlags) -> ++ Flags = lists:append(UserFlags, [?LONG_PASSWORD, ?LONG_FLAG, ?TRANSACTIONS, ?FOUND_ROWS]), ++ Caps = lists:foldl(fun(Flag, TCaps) when is_integer(Flag) -> ++ TCaps bor Flag ++ end, 0, Flags), + Maxsize = 0, + UserB = list_to_binary(User), + PasswordB = Password, +@@ -115,16 +117,18 @@ + PasswordB/binary>>. + + %% part of do_new_auth/4, which is part of mysql_init/4 +-make_new_auth(User, Password, Database) -> ++make_new_auth(User, Password, Database, UserFlags) -> + DBCaps = case Database of + none -> + 0; + _ -> + ?CONNECT_WITH_DB + end, +- Caps = ?LONG_PASSWORD bor ?LONG_FLAG bor ?TRANSACTIONS bor +- ?PROTOCOL_41 bor ?SECURE_CONNECTION bor DBCaps +- bor ?FOUND_ROWS, ++ ++ Flags = lists:append(UserFlags, [?LONG_PASSWORD, ?LONG_FLAG, ?TRANSACTIONS, ?PROTOCOL_41, ?SECURE_CONNECTION, DBCaps, ?FOUND_ROWS]), ++ Caps = lists:foldl(fun(Flag, TCaps) when is_integer(Flag) -> ++ TCaps bor Flag ++ end, DBCaps, Flags), + Maxsize = ?MAX_PACKET_SIZE, + UserB = list_to_binary(User), + PasswordL = size(Password), diff --git a/jorge/mod_logdb/patch-src-mod_logdb-2.0.3.diff b/jorge/mod_logdb/patch-src-mod_logdb-2.0.3.diff new file mode 100644 index 0000000..24ab753 --- /dev/null +++ b/jorge/mod_logdb/patch-src-mod_logdb-2.0.3.diff @@ -0,0 +1,6647 @@ +--- mod_logdb.erl.orig 2009-02-05 19:21:29.000000000 +0200 ++++ mod_logdb.erl 2009-02-05 19:19:51.000000000 +0200 +@@ -0,0 +1,2094 @@ ++%%%---------------------------------------------------------------------- ++%%% File : mod_logdb.erl ++%%% Author : Oleg Palij (mailto,xmpp:o.palij@gmail.com) ++%%% Purpose : Frontend for log user messages to db ++%%% Version : trunk ++%%% Id : $Id: mod_logdb.erl 1272 2009-02-05 17:16:20Z malik $ ++%%% Url : http://www.dp.uz.gov.ua/o.palij/mod_logdb/ ++%%%---------------------------------------------------------------------- ++ ++-module(mod_logdb). ++-author('o.palij@gmail.com'). ++ ++-behaviour(gen_server). ++-behaviour(gen_mod). ++ ++% supervisor ++-export([start_link/2]). ++% gen_mod ++-export([start/2,stop/1]). ++% gen_server ++-export([code_change/3,handle_call/3,handle_cast/2,handle_info/2,init/1,terminate/2]). ++% hooks ++-export([send_packet/3, receive_packet/4, offline_packet/3, remove_user/2]). ++-export([get_local_identity/5, ++ get_local_features/5, ++ get_local_items/5, ++ adhoc_local_items/4, ++ adhoc_local_commands/4 ++% get_sm_identity/5, ++% get_sm_features/5, ++% get_sm_items/5, ++% adhoc_sm_items/4, ++% adhoc_sm_commands/4]). ++ ]). ++% ejabberdctl ++-export([rebuild_stats/3, ++ copy_messages/1, copy_messages_ctl/3, copy_messages_int_tc/1]). ++% ++-export([get_vhost_stats/1, get_vhost_stats_at/2, ++ get_user_stats/2, get_user_messages_at/3, ++ get_dates/1, ++ sort_stats/1, ++ convert_timestamp/1, convert_timestamp_brief/1, ++ get_user_settings/2, set_user_settings/3, ++ user_messages_at_parse_query/4, user_messages_parse_query/3, ++ vhost_messages_parse_query/2, vhost_messages_at_parse_query/4, ++ list_to_bool/1, bool_to_list/1, ++ list_to_string/1, string_to_list/1, ++ get_module_settings/1, set_module_settings/2, ++ purge_old_records/2]). ++% webadmin hooks ++-export([webadmin_menu/3, ++ webadmin_user/4, ++ webadmin_page/3, ++ user_parse_query/5]). ++% webadmin queries ++-export([vhost_messages_stats/3, ++ vhost_messages_stats_at/4, ++ user_messages_stats/4, ++ user_messages_stats_at/5]). ++ ++-include("mod_logdb.hrl"). ++-include("ejabberd.hrl"). ++-include("mod_roster.hrl"). ++-include("jlib.hrl"). ++-include("ejabberd_ctl.hrl"). ++-include("adhoc.hrl"). ++-include("web/ejabberd_web_admin.hrl"). ++-include("web/ejabberd_http.hrl"). ++ ++-define(PROCNAME, ejabberd_mod_logdb). ++% gen_server call timeout ++-define(CALL_TIMEOUT, 10000). ++ ++-record(state, {vhost, dbmod, backendPid, monref, purgeRef, pollRef, dbopts, dbs, dolog_default, ignore_jids, groupchat, purge_older_days, poll_users_settings, drop_messages_on_user_removal}). ++ ++ets_settings_table(VHost) -> list_to_atom("ets_logdb_settings_" ++ VHost). ++ ++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++% ++% gen_mod/gen_server callbacks ++% ++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++% ejabberd starts module ++start(VHost, Opts) -> ++ ChildSpec = ++ {gen_mod:get_module_proc(VHost, ?PROCNAME), ++ {?MODULE, start_link, [VHost, Opts]}, ++ permanent, ++ 1000, ++ worker, ++ [?MODULE]}, ++ % add child to ejabberd_sup ++ supervisor:start_child(ejabberd_sup, ChildSpec). ++ ++% supervisor starts gen_server ++start_link(VHost, Opts) -> ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ {ok, Pid} = gen_server:start_link({local, Proc}, ?MODULE, [VHost, Opts], []), ++ Pid ! start, ++ {ok, Pid}. ++ ++init([VHost, Opts]) -> ++ process_flag(trap_exit, true), ++ DBs = gen_mod:get_opt(dbs, Opts, [{mnesia, []}]), ++ VHostDB = gen_mod:get_opt(vhosts, Opts, [{VHost, mnesia}]), ++ % 10 is default becouse of using in clustered environment ++ PollUsersSettings = gen_mod:get_opt(poll_users_settings, Opts, 10), ++ ++ {value,{_, DBName}} = lists:keysearch(VHost, 1, VHostDB), ++ {value, {DBName, DBOpts}} = lists:keysearch(DBName, 1, DBs), ++ ++ ?MYDEBUG("Starting mod_logdb for ~p with ~p backend", [VHost, DBName]), ++ ++ DBMod = list_to_atom(atom_to_list(?MODULE) ++ "_" ++ atom_to_list(DBName)), ++ ++ {ok, #state{vhost=VHost, ++ dbmod=DBMod, ++ dbopts=DBOpts, ++ % dbs used for convert messages from one backend to other ++ dbs=DBs, ++ dolog_default=gen_mod:get_opt(dolog_default, Opts, true), ++ drop_messages_on_user_removal=gen_mod:get_opt(drop_messages_on_user_removal, Opts, true), ++ ignore_jids=gen_mod:get_opt(ignore_jids, Opts, []), ++ groupchat=gen_mod:get_opt(groupchat, Opts, none), ++ purge_older_days=gen_mod:get_opt(purge_older_days, Opts, never), ++ poll_users_settings=PollUsersSettings}}. ++ ++cleanup(#state{vhost=VHost} = State) -> ++ ?MYDEBUG("Stopping ~s for ~p", [?MODULE, VHost]), ++ ++ %ets:delete(ets_settings_table(VHost)), ++ ++ ejabberd_hooks:delete(remove_user, VHost, ?MODULE, remove_user, 90), ++ ejabberd_hooks:delete(user_send_packet, VHost, ?MODULE, send_packet, 90), ++ ejabberd_hooks:delete(user_receive_packet, VHost, ?MODULE, receive_packet, 90), ++ ejabberd_hooks:delete(offline_message_hook, VHost, ?MODULE, offline_packet, 10), ++ %ejabberd_hooks:delete(adhoc_sm_commands, VHost, ?MODULE, adhoc_sm_commands, 110), ++ %ejabberd_hooks:delete(adhoc_sm_items, VHost, ?MODULE, adhoc_sm_items, 110), ++ ejabberd_hooks:delete(adhoc_local_commands, VHost, ?MODULE, adhoc_local_commands, 110), ++ ejabberd_hooks:delete(adhoc_local_items, VHost, ?MODULE, adhoc_local_items, 110), ++ %ejabberd_hooks:delete(disco_sm_identity, VHost, ?MODULE, get_sm_identity, 110), ++ %ejabberd_hooks:delete(disco_sm_features, VHost, ?MODULE, get_sm_features, 110), ++ %ejabberd_hooks:delete(disco_sm_items, VHost, ?MODULE, get_sm_items, 110), ++ ejabberd_hooks:delete(disco_local_identity, VHost, ?MODULE, get_local_identity, 110), ++ ejabberd_hooks:delete(disco_local_features, VHost, ?MODULE, get_local_features, 110), ++ ejabberd_hooks:delete(disco_local_items, VHost, ?MODULE, get_local_items, 110), ++ ++ ejabberd_hooks:delete(webadmin_menu_host, VHost, ?MODULE, webadmin_menu, 70), ++ ejabberd_hooks:delete(webadmin_user, VHost, ?MODULE, webadmin_user, 50), ++ ejabberd_hooks:delete(webadmin_page_host, VHost, ?MODULE, webadmin_page, 50), ++ ejabberd_hooks:delete(webadmin_user_parse_query, VHost, ?MODULE, user_parse_query, 50), ++ ++ ?MYDEBUG("Removed hooks for ~p", [VHost]), ++ ++ ejabberd_ctl:unregister_commands(VHost, [{"rebuild_stats", "rebuild mod_logdb module stats for vhost"}], ?MODULE, rebuild_stats), ++ Supported_backends = lists:flatmap(fun({Backend, _Opts}) -> ++ [atom_to_list(Backend), " "] ++ end, State#state.dbs), ++ ejabberd_ctl:unregister_commands( ++ VHost, ++ [{"copy_messages backend", "copy messages from backend to current backend. backends could be: " ++ Supported_backends }], ++ ?MODULE, copy_messages_ctl), ++ ?MYDEBUG("Unregistered commands for ~p", [VHost]). ++ ++stop(VHost) -> ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ %gen_server:call(Proc, {cleanup}), ++ %?MYDEBUG("Cleanup in stop finished!!!!", []), ++ %timer:sleep(10000), ++ ok = supervisor:terminate_child(ejabberd_sup, Proc), ++ ok = supervisor:delete_child(ejabberd_sup, Proc). ++ ++handle_call({cleanup}, _From, State) -> ++ cleanup(State), ++ ?MYDEBUG("Cleanup finished!!!!!", []), ++ {reply, ok, State}; ++handle_call({get_dates}, _From, #state{dbmod=DBMod, vhost=VHost}=State) -> ++ Reply = DBMod:get_dates(VHost), ++ {reply, Reply, State}; ++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++% ejabberd_web_admin callbacks ++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++handle_call({delete_messages_by_user_at, PMsgs, Date}, _From, #state{dbmod=DBMod, vhost=VHost}=State) -> ++ Reply = DBMod:delete_messages_by_user_at(VHost, PMsgs, Date), ++ {reply, Reply, State}; ++handle_call({delete_all_messages_by_user_at, User, Date}, _From, #state{dbmod=DBMod, vhost=VHost}=State) -> ++ Reply = DBMod:delete_all_messages_by_user_at(User, VHost, Date), ++ {reply, Reply, State}; ++handle_call({delete_messages_at, Date}, _From, #state{dbmod=DBMod, vhost=VHost}=State) -> ++ Reply = DBMod:delete_messages_at(VHost, Date), ++ {reply, Reply, State}; ++handle_call({get_vhost_stats}, _From, #state{dbmod=DBMod, vhost=VHost}=State) -> ++ Reply = DBMod:get_vhost_stats(VHost), ++ {reply, Reply, State}; ++handle_call({get_vhost_stats_at, Date}, _From, #state{dbmod=DBMod, vhost=VHost}=State) -> ++ Reply = DBMod:get_vhost_stats_at(VHost, Date), ++ {reply, Reply, State}; ++handle_call({get_user_stats, User}, _From, #state{dbmod=DBMod, vhost=VHost}=State) -> ++ Reply = DBMod:get_user_stats(User, VHost), ++ {reply, Reply, State}; ++handle_call({get_user_messages_at, User, Date}, _From, #state{dbmod=DBMod, vhost=VHost}=State) -> ++ Reply = DBMod:get_user_messages_at(User, VHost, Date), ++ {reply, Reply, State}; ++handle_call({get_user_settings, User}, _From, #state{dbmod=_DBMod, vhost=VHost}=State) -> ++ Reply = case ets:match_object(ets_settings_table(VHost), ++ #user_settings{owner_name=User, _='_'}) of ++ [Set] -> Set; ++ _ -> #user_settings{owner_name=User, ++ dolog_default=State#state.dolog_default, ++ dolog_list=[], ++ donotlog_list=[]} ++ end, ++ {reply, Reply, State}; ++% TODO: remove User ?? ++handle_call({set_user_settings, User, GSet}, _From, #state{dbmod=DBMod, vhost=VHost}=State) -> ++ Set = GSet#user_settings{owner_name=User}, ++ Reply = ++ case ets:match_object(ets_settings_table(VHost), ++ #user_settings{owner_name=User, _='_'}) of ++ [Set] -> ++ ?MYDEBUG("Settings is equal", []), ++ ok; ++ _ -> ++ case DBMod:set_user_settings(User, VHost, Set) of ++ error -> ++ error; ++ ok -> ++ true = ets:insert(ets_settings_table(VHost), Set), ++ ok ++ end ++ end, ++ {reply, Reply, State}; ++handle_call({get_module_settings}, _From, State) -> ++ {reply, State, State}; ++handle_call({set_module_settings, #state{purge_older_days=PurgeDays, ++ poll_users_settings=PollSec} = Settings}, ++ _From, ++ #state{purgeRef=PurgeRefOld, ++ pollRef=PollRefOld, ++ purge_older_days=PurgeDaysOld, ++ poll_users_settings=PollSecOld} = State) -> ++ PurgeRef = if ++ PurgeDays == never, PurgeDaysOld /= never -> ++ {ok, cancel} = timer:cancel(PurgeRefOld), ++ disabled; ++ is_integer(PurgeDays), PurgeDaysOld == never -> ++ set_purge_timer(PurgeDays); ++ true -> ++ PurgeRefOld ++ end, ++ ++ PollRef = if ++ PollSec == PollSecOld -> ++ PollRefOld; ++ PollSec == 0, PollSecOld /= 0 -> ++ {ok, cancel} = timer:cancel(PollRefOld), ++ disabled; ++ is_integer(PollSec), PollSecOld == 0 -> ++ set_poll_timer(PollSec); ++ is_integer(PollSec), PollSecOld /= 0 -> ++ {ok, cancel} = timer:cancel(PollRefOld), ++ set_poll_timer(PollSec) ++ end, ++ ++ NewState = State#state{dolog_default=Settings#state.dolog_default, ++ ignore_jids=Settings#state.ignore_jids, ++ groupchat=Settings#state.groupchat, ++ drop_messages_on_user_removal=Settings#state.drop_messages_on_user_removal, ++ purge_older_days=PurgeDays, ++ poll_users_settings=PollSec, ++ purgeRef=PurgeRef, ++ pollRef=PollRef}, ++ {reply, ok, NewState}; ++handle_call(Msg, _From, State) -> ++ ?INFO_MSG("Got call Msg: ~p, State: ~p", [Msg, State]), ++ {noreply, State}. ++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++% end ejabberd_web_admin callbacks ++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++ ++% ejabberd_hooks call ++handle_cast({addlog, Direction, Owner, Peer, Packet}, #state{dbmod=DBMod, vhost=VHost}=State) -> ++ case filter(Owner, Peer, State) of ++ true -> ++ case catch packet_parse(Owner, Peer, Packet, Direction, State) of ++ ignore -> ++ ok; ++ {'EXIT', Reason} -> ++ ?ERROR_MSG("Failed to parse: ~p", [Reason]); ++ Msg -> ++ DBMod:log_message(VHost, Msg) ++ end; ++ false -> ++ ok ++ end, ++ {noreply, State}; ++handle_cast({remove_user, User}, #state{dbmod=DBMod, vhost=VHost}=State) -> ++ case State#state.drop_messages_on_user_removal of ++ true -> ++ DBMod:drop_user(User, VHost), ++ ?INFO_MSG("Launched ~s@~s removal", [User, VHost]); ++ false -> ++ ?INFO_MSG("Message removing is disabled. Keeping messages for ~s@~s", [User, VHost]) ++ end, ++ {noreply, State}; ++% ejabberdctl rebuild_stats/3 ++handle_cast({rebuild_stats}, #state{dbmod=DBMod, vhost=VHost}=State) -> ++ DBMod:rebuild_stats(VHost), ++ {noreply, State}; ++handle_cast({copy_messages, Backend}, State) -> ++ spawn(?MODULE, copy_messages, [[State, Backend]]), ++ {noreply, State}; ++handle_cast({copy_messages, Backend, Date}, State) -> ++ spawn(?MODULE, copy_messages, [[State, Backend, Date]]), ++ {noreply, State}; ++handle_cast(Msg, State) -> ++ ?INFO_MSG("Got cast Msg:~p, State:~p", [Msg, State]), ++ {noreply, State}. ++ ++% return: disabled | timer reference ++set_purge_timer(PurgeDays) -> ++ case PurgeDays of ++ never -> disabled; ++ Days when is_integer(Days) -> ++ {ok, Ref1} = timer:send_interval(timer:hours(24), scheduled_purging), ++ Ref1 ++ end. ++ ++% return: disabled | timer reference ++set_poll_timer(PollSec) -> ++ if ++ PollSec > 0 -> ++ {ok, Ref2} = timer:send_interval(timer:seconds(PollSec), poll_users_settings), ++ Ref2; ++ % db polling disabled ++ PollSec == 0 -> ++ disabled; ++ true -> ++ {ok, Ref3} = timer:send_interval(timer:seconds(10), poll_users_settings), ++ Ref3 ++ end. ++ ++% actual starting of logging ++% from timer:send_after (in init) ++handle_info(start, #state{dbmod=DBMod, vhost=VHost}=State) -> ++ case DBMod:start(VHost, State#state.dbopts) of ++ {error,{already_started,_}} -> ++ ?MYDEBUG("backend module already started - trying to stop it", []), ++ DBMod:stop(VHost), ++ {stop, already_started, State}; ++ {error, Reason} -> ++ timer:sleep(30000), ++ ?ERROR_MSG("Failed to start: ~p", [Reason]), ++ {stop, db_connection_failed, State}; ++ {ok, SPid} -> ++ ?INFO_MSG("~p connection established", [DBMod]), ++ ++ MonRef = erlang:monitor(process, SPid), ++ ++ ets:new(ets_settings_table(VHost), [named_table,public,set,{keypos, #user_settings.owner_name}]), ++ {ok, DoLog} = DBMod:get_users_settings(VHost), ++ ets:insert(ets_settings_table(VHost), DoLog), ++ ++ TrefPurge = set_purge_timer(State#state.purge_older_days), ++ TrefPoll = set_poll_timer(State#state.poll_users_settings), ++ ++ ejabberd_hooks:add(remove_user, VHost, ?MODULE, remove_user, 90), ++ ejabberd_hooks:add(user_send_packet, VHost, ?MODULE, send_packet, 90), ++ ejabberd_hooks:add(user_receive_packet, VHost, ?MODULE, receive_packet, 90), ++ ejabberd_hooks:add(offline_message_hook, VHost, ?MODULE, offline_packet, 10), ++ ++ ejabberd_hooks:add(disco_local_items, VHost, ?MODULE, get_local_items, 110), ++ ejabberd_hooks:add(disco_local_features, VHost, ?MODULE, get_local_features, 110), ++ ejabberd_hooks:add(disco_local_identity, VHost, ?MODULE, get_local_identity, 110), ++ %ejabberd_hooks:add(disco_sm_items, VHost, ?MODULE, get_sm_items, 110), ++ %ejabberd_hooks:add(disco_sm_features, VHost, ?MODULE, get_sm_features, 110), ++ %ejabberd_hooks:add(disco_sm_identity, VHost, ?MODULE, get_sm_identity, 110), ++ ejabberd_hooks:add(adhoc_local_items, VHost, ?MODULE, adhoc_local_items, 110), ++ ejabberd_hooks:add(adhoc_local_commands, VHost, ?MODULE, adhoc_local_commands, 110), ++ %ejabberd_hooks:add(adhoc_sm_items, VHost, ?MODULE, adhoc_sm_items, 110), ++ %ejabberd_hooks:add(adhoc_sm_commands, VHost, ?MODULE, adhoc_sm_commands, 110), ++ ++ ejabberd_hooks:add(webadmin_menu_host, VHost, ?MODULE, webadmin_menu, 70), ++ ejabberd_hooks:add(webadmin_user, VHost, ?MODULE, webadmin_user, 50), ++ ejabberd_hooks:add(webadmin_page_host, VHost, ?MODULE, webadmin_page, 50), ++ ejabberd_hooks:add(webadmin_user_parse_query, VHost, ?MODULE, user_parse_query, 50), ++ ++ ?MYDEBUG("Added hooks for ~p", [VHost]), ++ ++ ejabberd_ctl:register_commands( ++ VHost, ++ [{"rebuild_stats", "rebuild mod_logdb module stats for vhost"}], ++ ?MODULE, rebuild_stats), ++ Supported_backends = lists:flatmap(fun({Backend, _Opts}) -> ++ [atom_to_list(Backend), " "] ++ end, State#state.dbs), ++ ejabberd_ctl:register_commands( ++ VHost, ++ [{"copy_messages backend", "copy messages from backend to current backend. backends could be: " ++ Supported_backends }], ++ ?MODULE, copy_messages_ctl), ++ ?MYDEBUG("Registered commands for ~p", [VHost]), ++ ++ NewState=State#state{monref = MonRef, backendPid=SPid, purgeRef=TrefPurge, pollRef=TrefPoll}, ++ {noreply, NewState}; ++ Rez -> ++ ?ERROR_MSG("Rez=~p", [Rez]), ++ timer:sleep(30000), ++ {stop, db_connection_failed, State} ++ end; ++% from timer:send_interval/2 (in start handle_info) ++handle_info(scheduled_purging, #state{vhost=VHost, purge_older_days=Days} = State) -> ++ ?MYDEBUG("Starting scheduled purging of old records for ~p", [VHost]), ++ spawn(?MODULE, purge_old_records, [VHost, integer_to_list(Days)]), ++ {noreply, State}; ++% from timer:send_interval/2 (in start handle_info) ++handle_info(poll_users_settings, #state{dbmod=DBMod, vhost=VHost}=State) -> ++ {ok, DoLog} = DBMod:get_users_settings(VHost), ++ ?MYDEBUG("DoLog=~p", [DoLog]), ++ true = ets:delete_all_objects(ets_settings_table(VHost)), ++ ets:insert(ets_settings_table(VHost), DoLog), ++ {noreply, State}; ++handle_info({'DOWN', _MonitorRef, process, _Pid, _Info}, State) -> ++ {stop, db_connection_dropped, State}; ++handle_info({fetch_result, _, _}, State) -> ++ ?MYDEBUG("Got timed out mysql fetch result", []), ++ {noreply, State}; ++handle_info(Info, State) -> ++ ?INFO_MSG("Got Info:~p, State:~p", [Info, State]), ++ {noreply, State}. ++ ++terminate(db_connection_failed, _State) -> ++ ok; ++terminate(db_connection_dropped, State) -> ++ ?MYDEBUG("Got terminate with db_connection_dropped", []), ++ cleanup(State), ++ ok; ++terminate(Reason, #state{monref=undefined} = State) -> ++ ?MYDEBUG("Got terminate with undefined monref.~nReason: ~p", [Reason]), ++ cleanup(State), ++ ok; ++terminate(Reason, #state{dbmod=DBMod, vhost=VHost, monref=MonRef, backendPid=Pid} = State) -> ++ ?INFO_MSG("Reason: ~p", [Reason]), ++ case erlang:is_process_alive(Pid) of ++ true -> ++ erlang:demonitor(MonRef, [flush]), ++ DBMod:stop(VHost); ++ false -> ++ ok ++ end, ++ cleanup(State), ++ ok. ++ ++code_change(_OldVsn, State, _Extra) -> ++ {ok, State}. ++ ++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++% ++% ejabberd_hooks callbacks ++% ++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++% TODO: change to/from to list as sql stores it as list ++send_packet(Owner, Peer, P) -> ++ VHost = Owner#jid.lserver, ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:cast(Proc, {addlog, to, Owner, Peer, P}). ++ ++offline_packet(Peer, Owner, P) -> ++ VHost = Owner#jid.lserver, ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:cast(Proc, {addlog, from, Owner, Peer, P}). ++ ++receive_packet(_JID, Peer, Owner, P) -> ++ VHost = Owner#jid.lserver, ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:cast(Proc, {addlog, from, Owner, Peer, P}). ++ ++remove_user(User, Server) -> ++ LUser = jlib:nodeprep(User), ++ LServer = jlib:nameprep(Server), ++ Proc = gen_mod:get_module_proc(LServer, ?PROCNAME), ++ gen_server:cast(Proc, {remove_user, LUser}). ++ ++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++% ++% ejabberdctl ++% ++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++rebuild_stats(_Val, VHost, ["rebuild_stats"]) -> ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:cast(Proc, {rebuild_stats}), ++ {stop, ?STATUS_SUCCESS}; ++rebuild_stats(Val, _VHost, _Args) -> ++ Val. ++ ++copy_messages_ctl(_Val, VHost, ["copy_messages", Backend]) -> ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:cast(Proc, {copy_messages, Backend}), ++ {stop, ?STATUS_SUCCESS}; ++copy_messages_ctl(_Val, VHost, ["copy_messages", Backend, Date]) -> ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:cast(Proc, {copy_messages, Backend, Date}), ++ {stop, ?STATUS_SUCCESS}; ++copy_messages_ctl(Val, _VHost, _Args) -> ++ Val. ++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++% ++% misc operations ++% ++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++ ++% handle_cast({addlog, E}, _) ++% raw packet -> #msg ++packet_parse(Owner, Peer, Packet, Direction, State) -> ++ case xml:get_subtag(Packet, "body") of ++ false -> ++ ignore; ++ Body_xml -> ++ Message_type = ++ case xml:get_tag_attr_s("type", Packet) of ++ [] -> "normal"; ++ MType -> MType ++ end, ++ ++ case Message_type of ++ "groupchat" when State#state.groupchat == send, Direction == to -> ++ ok; ++ "groupchat" when State#state.groupchat == send, Direction == from -> ++ throw(ignore); ++ "groupchat" when State#state.groupchat == half -> ++ Rooms = ets:match(muc_online_room, '$1'), ++ Ni=lists:foldl(fun([{muc_online_room, {GName, GHost}, Pid}], Names) -> ++ case gen_fsm:sync_send_all_state_event(Pid, {get_jid_nick,Owner}) of ++ [] -> Names; ++ Nick -> ++ lists:append(Names, [jlib:jid_to_string({GName, GHost, Nick})]) ++ end ++ end, [], Rooms), ++ case lists:member(jlib:jid_to_string(Peer), Ni) of ++ true when Direction == from -> ++ throw(ignore); ++ _ -> ++ ok ++ end; ++ "groupchat" when State#state.groupchat == none -> ++ throw(ignore); ++ _ -> ++ ok ++ end, ++ ++ Message_body = xml:get_tag_cdata(Body_xml), ++ Message_subject = ++ case xml:get_subtag(Packet, "subject") of ++ false -> ++ ""; ++ Subject_xml -> ++ xml:get_tag_cdata(Subject_xml) ++ end, ++ ++ OwnerName = stringprep:tolower(Owner#jid.user), ++ PName = stringprep:tolower(Peer#jid.user), ++ PServer = stringprep:tolower(Peer#jid.server), ++ PResource = Peer#jid.resource, ++ ++ #msg{timestamp=get_timestamp(), ++ owner_name=OwnerName, ++ peer_name=PName, ++ peer_server=PServer, ++ peer_resource=PResource, ++ direction=Direction, ++ type=Message_type, ++ subject=Message_subject, ++ body=Message_body} ++ end. ++ ++% called from handle_cast({addlog, _}, _) -> true (log messages) | false (do not log messages) ++filter(Owner, Peer, State) -> ++ OwnerStr = Owner#jid.luser++"@"++Owner#jid.lserver, ++ OwnerServ = "@"++Owner#jid.lserver, ++ PeerStr = Peer#jid.luser++"@"++Peer#jid.lserver, ++ PeerServ = "@"++Peer#jid.lserver, ++ ++ LogTo = case ets:match_object(ets_settings_table(State#state.vhost), ++ #user_settings{owner_name=Owner#jid.luser, _='_'}) of ++ [#user_settings{dolog_default=Default, ++ dolog_list=DLL, ++ donotlog_list=DNLL}] -> ++ A = lists:member(PeerStr, DLL), ++ B = lists:member(PeerStr, DNLL), ++ if ++ A -> true; ++ B -> false; ++ Default == true -> true; ++ Default == false -> false; ++ true -> State#state.dolog_default ++ end; ++ _ -> State#state.dolog_default ++ end, ++ ++ lists:all(fun(O) -> O end, ++ [not lists:member(OwnerStr, State#state.ignore_jids), ++ not lists:member(PeerStr, State#state.ignore_jids), ++ not lists:member(OwnerServ, State#state.ignore_jids), ++ not lists:member(PeerServ, State#state.ignore_jids), ++ LogTo]). ++ ++purge_old_records(VHost, Days) -> ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ ++ Dates = ?MODULE:get_dates(VHost), ++ DateNow = calendar:datetime_to_gregorian_seconds({date(), {0,0,1}}), ++ DateDiff = list_to_integer(Days)*24*60*60, ++ ?MYDEBUG("Purging tables older than ~s days", [Days]), ++ lists:foreach(fun(Date) -> ++ {ok, [Year, Month, Day]} = regexp:split(Date, "[^0-9]+"), ++ DateInSec = calendar:datetime_to_gregorian_seconds({{list_to_integer(Year), list_to_integer(Month), list_to_integer(Day)}, {0,0,1}}), ++ if ++ (DateNow - DateInSec) > DateDiff -> ++ gen_server:call(Proc, {delete_messages_at, Date}); ++ true -> ++ ?MYDEBUG("Skipping messages at ~p", [Date]) ++ end ++ end, Dates). ++ ++% called from get_vhost_stats/2, get_user_stats/3 ++sort_stats(Stats) -> ++ % Stats = [{"2003-4-15",1}, {"2006-8-18",1}, ... ] ++ CFun = fun({TableName, Count}) -> ++ {ok, [Year, Month, Day]} = regexp:split(TableName, "[^0-9]+"), ++ { calendar:datetime_to_gregorian_seconds({{list_to_integer(Year), list_to_integer(Month), list_to_integer(Day)}, {0,0,1}}), Count } ++ end, ++ % convert to [{63364377601,1}, {63360662401,1}, ... ] ++ CStats = lists:map(CFun, Stats), ++ % sort by date ++ SortedStats = lists:reverse(lists:keysort(1, CStats)), ++ % convert to [{"2007-12-9",1}, {"2007-10-27",1}, ... ] sorted list ++ [{mod_logdb:convert_timestamp_brief(TableSec), Count} || {TableSec, Count} <- SortedStats]. ++ ++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++% ++% Date/Time operations ++% ++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++% return float seconds elapsed from "zero hour" as list ++get_timestamp() -> ++ {MegaSec, Sec, MicroSec} = now(), ++ [List] = io_lib:format("~.5f", [MegaSec*1000000 + Sec + MicroSec/1000000]), ++ List. ++ ++% convert float seconds elapsed from "zero hour" to local time "%Y-%m-%d %H:%M:%S" string ++convert_timestamp(Seconds) when is_list(Seconds) -> ++ case string:to_float(Seconds++".0") of ++ {F,_} when is_float(F) -> convert_timestamp(F); ++ _ -> erlang:error(badarg, [Seconds]) ++ end; ++convert_timestamp(Seconds) when is_float(Seconds) -> ++ GregSec = trunc(Seconds + 719528*86400), ++ UnivDT = calendar:gregorian_seconds_to_datetime(GregSec), ++ {{Year, Month, Day},{Hour, Minute, Sec}} = calendar:universal_time_to_local_time(UnivDT), ++ integer_to_list(Year) ++ "-" ++ integer_to_list(Month) ++ "-" ++ integer_to_list(Day) ++ " " ++ integer_to_list(Hour) ++ ":" ++ integer_to_list(Minute) ++ ":" ++ integer_to_list(Sec). ++ ++% convert float seconds elapsed from "zero hour" to local time "%Y-%m-%d" string ++convert_timestamp_brief(Seconds) when is_list(Seconds) -> ++ convert_timestamp_brief(list_to_float(Seconds)); ++convert_timestamp_brief(Seconds) when is_float(Seconds) -> ++ GregSec = trunc(Seconds + 719528*86400), ++ UnivDT = calendar:gregorian_seconds_to_datetime(GregSec), ++ {{Year, Month, Day},{_Hour, _Minute, _Sec}} = calendar:universal_time_to_local_time(UnivDT), ++ integer_to_list(Year) ++ "-" ++ integer_to_list(Month) ++ "-" ++ integer_to_list(Day); ++convert_timestamp_brief(Seconds) when is_integer(Seconds) -> ++ {{Year, Month, Day},{_Hour, _Minute, _Sec}} = calendar:gregorian_seconds_to_datetime(Seconds), ++ integer_to_list(Year) ++ "-" ++ integer_to_list(Month) ++ "-" ++ integer_to_list(Day). ++ ++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++% ++% DB operations (get) ++% ++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++get_vhost_stats(VHost) -> ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:call(Proc, {get_vhost_stats}, ?CALL_TIMEOUT). ++ ++get_vhost_stats_at(VHost, Date) -> ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:call(Proc, {get_vhost_stats_at, Date}, ?CALL_TIMEOUT). ++ ++get_user_stats(User, VHost) -> ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:call(Proc, {get_user_stats, User}, ?CALL_TIMEOUT). ++ ++get_user_messages_at(User, VHost, Date) -> ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:call(Proc, {get_user_messages_at, User, Date}, ?CALL_TIMEOUT). ++ ++get_dates(VHost) -> ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:call(Proc, {get_dates}, ?CALL_TIMEOUT). ++ ++get_user_settings(User, VHost) -> ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:call(Proc, {get_user_settings, User}, ?CALL_TIMEOUT). ++ ++set_user_settings(User, VHost, Set) -> ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:call(Proc, {set_user_settings, User, Set}). ++ ++get_module_settings(VHost) -> ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:call(Proc, {get_module_settings}). ++ ++set_module_settings(VHost, Settings) -> ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:call(Proc, {set_module_settings, Settings}). ++ ++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++% ++% Web admin callbacks (delete) ++% ++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++user_messages_at_parse_query(VHost, Date, Msgs, Query) -> ++ case lists:keysearch("delete", 1, Query) of ++ {value, _} -> ++ PMsgs = lists:filter( ++ fun(Msg) -> ++ ID = jlib:encode_base64(binary_to_list(term_to_binary(Msg#msg.timestamp))), ++ lists:member({"selected", ID}, Query) ++ end, Msgs), ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:call(Proc, {delete_messages_by_user_at, PMsgs, Date}, ?CALL_TIMEOUT); ++ false -> ++ nothing ++ end. ++ ++user_messages_parse_query(User, VHost, Query) -> ++ case lists:keysearch("delete", 1, Query) of ++ {value, _} -> ++ Dates = get_dates(VHost), ++ PDates = lists:filter( ++ fun(Date) -> ++ ID = jlib:encode_base64(binary_to_list(term_to_binary(User++Date))), ++ lists:member({"selected", ID}, Query) ++ end, Dates), ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ Rez = lists:foldl( ++ fun(Date, Acc) -> ++ lists:append(Acc, ++ [gen_server:call(Proc, ++ {delete_all_messages_by_user_at, User, Date}, ++ ?CALL_TIMEOUT)]) ++ end, [], PDates), ++ case lists:member(error, Rez) of ++ true -> ++ error; ++ false -> ++ nothing ++ end; ++ false -> ++ nothing ++ end. ++ ++vhost_messages_parse_query(VHost, Query) -> ++ case lists:keysearch("delete", 1, Query) of ++ {value, _} -> ++ Dates = get_dates(VHost), ++ PDates = lists:filter( ++ fun(Date) -> ++ ID = jlib:encode_base64(binary_to_list(term_to_binary(VHost++Date))), ++ lists:member({"selected", ID}, Query) ++ end, Dates), ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ Rez = lists:foldl(fun(Date, Acc) -> ++ lists:append(Acc, [gen_server:call(Proc, ++ {delete_messages_at, Date}, ++ ?CALL_TIMEOUT)]) ++ end, [], PDates), ++ case lists:member(error, Rez) of ++ true -> ++ error; ++ false -> ++ nothing ++ end; ++ false -> ++ nothing ++ end. ++ ++vhost_messages_at_parse_query(VHost, Date, Stats, Query) -> ++ case lists:keysearch("delete", 1, Query) of ++ {value, _} -> ++ PStats = lists:filter( ++ fun({User, _Count}) -> ++ ID = jlib:encode_base64(binary_to_list(term_to_binary(User++VHost))), ++ lists:member({"selected", ID}, Query) ++ end, Stats), ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ Rez = lists:foldl(fun({User, _Count}, Acc) -> ++ lists:append(Acc, [gen_server:call(Proc, ++ {delete_all_messages_by_user_at, ++ User, Date}, ++ ?CALL_TIMEOUT)]) ++ end, [], PStats), ++ case lists:member(error, Rez) of ++ true -> ++ error; ++ false -> ++ ok ++ end; ++ false -> ++ nothing ++ end. ++ ++copy_messages([#state{vhost=VHost}=State, From]) -> ++ ?INFO_MSG("Going to copy messages from ~p for ~p", [From, VHost]), ++ ++ {FromDBName, FromDBOpts} = ++ case lists:keysearch(list_to_atom(From), 1, State#state.dbs) of ++ {value, {FN, FO}} -> ++ {FN, FO}; ++ false -> ++ ?ERROR_MSG("Failed to find record for ~p in dbs", [From]), ++ throw(error) ++ end, ++ ++ FromDBMod = list_to_atom(atom_to_list(?MODULE) ++ "_" ++ atom_to_list(FromDBName)), ++ ++ {ok, _FromPid} = FromDBMod:start(VHost, FromDBOpts), ++ ++ Dates = FromDBMod:get_dates(VHost), ++ DatesLength = length(Dates), ++ ++ lists:foldl(fun(Date, Acc) -> ++ case copy_messages_int([FromDBMod, State#state.dbmod, VHost, Date]) of ++ ok -> ++ ?INFO_MSG("Copied messages at ~p (~p/~p)", [Date, Acc, DatesLength]); ++ Value -> ++ ?ERROR_MSG("Failed to copy messages at ~p (~p/~p): ~p", [Date, Acc, DatesLength, Value]), ++ FromDBMod:stop(VHost), ++ throw(error) ++ end, ++ Acc + 1 ++ end, 1, Dates), ++ ?INFO_MSG("Copied messages from ~p", [From]), ++ FromDBMod:stop(VHost); ++copy_messages([#state{vhost=VHost}=State, From, Date]) -> ++ {value, {FromDBName, FromDBOpts}} = lists:keysearch(list_to_atom(From), 1, State#state.dbs), ++ FromDBMod = list_to_atom(atom_to_list(?MODULE) ++ "_" ++ atom_to_list(FromDBName)), ++ {ok, _FromPid} = FromDBMod:start(VHost, FromDBOpts), ++ case catch copy_messages_int([FromDBMod, State#state.dbmod, VHost, Date]) of ++ {'exit', Reason} -> ++ ?ERROR_MSG("Failed to copy messages at ~p: ~p", [Date, Reason]); ++ ok -> ++ ?INFO_MSG("Copied messages at ~p", [Date]); ++ Value -> ++ ?ERROR_MSG("Failed to copy messages at ~p: ~p", [Date, Value]) ++ end, ++ FromDBMod:stop(VHost). ++ ++copy_messages_int([FromDBMod, ToDBMod, VHost, Date]) -> ++ ets:new(mod_logdb_temp, [named_table, set, public]), ++ {Time, Value} = timer:tc(?MODULE, copy_messages_int_tc, [[FromDBMod, ToDBMod, VHost, Date]]), ++ ets:delete_all_objects(mod_logdb_temp), ++ ets:delete(mod_logdb_temp), ++ ?INFO_MSG("copy_messages at ~p elapsed ~p sec", [Date, Time/1000000]), ++ Value. ++ ++copy_messages_int_tc([FromDBMod, ToDBMod, VHost, Date]) -> ++ ?INFO_MSG("Going to copy messages from ~p for ~p at ~p", [FromDBMod, VHost, Date]), ++ ++ ok = FromDBMod:rebuild_stats_at(VHost, Date), ++ catch mod_logdb:rebuild_stats_at(VHost, Date), ++ {ok, FromStats} = FromDBMod:get_vhost_stats_at(VHost, Date), ++ ToStats = case mod_logdb:get_vhost_stats_at(VHost, Date) of ++ {ok, Stats} -> Stats; ++ {error, _} -> [] ++ end, ++ ++ FromStatsS = lists:keysort(1, FromStats), ++ ToStatsS = lists:keysort(1, ToStats), ++ ++ StatsLength = length(FromStats), ++ ++ CopyFun = if ++ % destination table is empty ++ FromDBMod /= mod_logdb_mnesia_old, ToStats == [] -> ++ fun({User, _Count}, Acc) -> ++ {ok, Msgs} = FromDBMod:get_user_messages_at(User, VHost, Date), ++ MAcc = ++ lists:foldl(fun(Msg, MFAcc) -> ++ ok = ToDBMod:log_message(VHost, Msg), ++ MFAcc + 1 ++ end, 0, Msgs), ++ NewAcc = Acc + 1, ++ ?INFO_MSG("Copied ~p messages for ~p (~p/~p) at ~p", [MAcc, User, NewAcc, StatsLength, Date]), ++ %timer:sleep(100), ++ NewAcc ++ end; ++ % destination table is not empty ++ FromDBMod /= mod_logdb_mnesia_old, ToStats /= [] -> ++ fun({User, _Count}, Acc) -> ++ {ok, ToMsgs} = ToDBMod:get_user_messages_at(User, VHost, Date), ++ lists:foreach(fun(#msg{timestamp=Tst}) when length(Tst) == 16 -> ++ ets:insert(mod_logdb_temp, {Tst}); ++ % mysql, pgsql removes final zeros after decimal point ++ (#msg{timestamp=Tst}) when length(Tst) < 16 -> ++ {F, _} = string:to_float(Tst++".0"), ++ [T] = io_lib:format("~.5f", [F]), ++ ets:insert(mod_logdb_temp, {T}) ++ end, ToMsgs), ++ {ok, Msgs} = FromDBMod:get_user_messages_at(User, VHost, Date), ++ MAcc = ++ lists:foldl(fun(#msg{timestamp=ToTimestamp} = Msg, MFAcc) -> ++ case ets:member(mod_logdb_temp, ToTimestamp) of ++ false -> ++ ok = ToDBMod:log_message(VHost, Msg), ++ ets:insert(mod_logdb_temp, {ToTimestamp}), ++ MFAcc + 1; ++ true -> ++ MFAcc ++ end ++ end, 0, Msgs), ++ NewAcc = Acc + 1, ++ ets:delete_all_objects(mod_logdb_temp), ++ ?INFO_MSG("Copied ~p messages for ~p (~p/~p) at ~p", [MAcc, User, NewAcc, StatsLength, Date]), ++ %timer:sleep(100), ++ NewAcc ++ end; ++ % copying from mod_logmnesia ++ true -> ++ fun({User, _Count}, Acc) -> ++ ToStats = ++ case ToDBMod:get_user_messages_at(User, VHost, Date) of ++ {ok, []} -> ++ ok; ++ {ok, ToMsgs} -> ++ lists:foreach(fun(#msg{timestamp=Tst}) when length(Tst) == 16 -> ++ ets:insert(mod_logdb_temp, {Tst}); ++ % mysql, pgsql removes final zeros after decimal point ++ (#msg{timestamp=Tst}) when length(Tst) < 15 -> ++ {F, _} = string:to_float(Tst++".0"), ++ [T] = io_lib:format("~.5f", [F]), ++ ets:insert(mod_logdb_temp, {T}) ++ end, ToMsgs); ++ {error, _} -> ++ ok ++ end, ++ {ok, Msgs} = FromDBMod:get_user_messages_at(User, VHost, Date), ++ ++ MAcc = ++ lists:foldl( ++ fun({msg, TU, TS, TR, FU, FS, FR, Type, Subj, Body, Timest}, ++ MFAcc) -> ++ [Timestamp] = if is_float(Timest) == true -> ++ io_lib:format("~.5f", [Timest]); ++ % early versions of mod_logmnesia ++ is_integer(Timest) == true -> ++ io_lib:format("~.5f", [Timest-719528*86400.0]); ++ true -> ++ ?ERROR_MSG("Incorrect timestamp ~p", [Timest]), ++ throw(error) ++ end, ++ case ets:member(mod_logdb_temp, Timestamp) of ++ false -> ++ if ++ % from ++ TS == VHost -> ++ TMsg = #msg{timestamp=Timestamp, ++ owner_name=TU, ++ peer_name=FU, peer_server=FS, peer_resource=FR, ++ direction=from, ++ type=Type, ++ subject=Subj, body=Body}, ++ ok = ToDBMod:log_message(VHost, TMsg); ++ true -> ok ++ end, ++ if ++ % to ++ FS == VHost -> ++ FMsg = #msg{timestamp=Timestamp, ++ owner_name=FU, ++ peer_name=TU, peer_server=TS, peer_resource=TR, ++ direction=to, ++ type=Type, ++ subject=Subj, body=Body}, ++ ok = ToDBMod:log_message(VHost, FMsg); ++ true -> ok ++ end, ++ ets:insert(mod_logdb_temp, {Timestamp}), ++ MFAcc + 1; ++ true -> % not ets:member ++ MFAcc ++ end % case ++ end, 0, Msgs), % foldl ++ NewAcc = Acc + 1, ++ ?INFO_MSG("Copied ~p messages for ~p (~p/~p) at ~p", [MAcc, User, NewAcc, StatsLength, Date]), ++ %timer:sleep(100), ++ NewAcc ++ end % fun ++ end, % if FromDBMod /= mod_logdb_mnesia_old ++ ++ if ++ FromStats == [] -> ++ ?INFO_MSG("No messages were found at ~p", [Date]); ++ FromStatsS == ToStatsS -> ++ ?INFO_MSG("Stats are equal at ~p", [Date]); ++ FromStatsS /= ToStatsS -> ++ lists:foldl(CopyFun, 0, FromStats), ++ ok = ToDBMod:rebuild_stats_at(VHost, Date) ++ %timer:sleep(1000) ++ end, ++ ++ ok. ++ ++list_to_bool(Num) -> ++ case lists:member(Num, ["t", "true", "y", "yes", "1"]) of ++ true -> ++ true; ++ false -> ++ case lists:member(Num, ["f", "false", "n", "no", "0"]) of ++ true -> ++ false; ++ false -> ++ error ++ end ++ end. ++ ++bool_to_list(true) -> ++ "TRUE"; ++bool_to_list(false) -> ++ "FALSE". ++ ++list_to_string([]) -> ++ ""; ++list_to_string(List) when is_list(List) -> ++ Str = lists:flatmap(fun(Elm) -> Elm ++ "\n" end, List), ++ lists:sublist(Str, length(Str)-1). ++ ++string_to_list(null) -> ++ []; ++string_to_list([]) -> ++ []; ++string_to_list(String) -> ++ {ok, List} = regexp:split(String, "\n"), ++ List. ++ ++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++% ++% ad-hoc (copy/pasted from mod_configure.erl) ++% ++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++-define(ITEMS_RESULT(Allow, LNode, Fallback), ++ case Allow of ++ deny -> ++ Fallback; ++ allow -> ++ case get_local_items(LServer, LNode, ++ jlib:jid_to_string(To), Lang) of ++ {result, Res} -> ++ {result, Res}; ++ {error, Error} -> ++ {error, Error} ++ end ++ end). ++ ++get_local_items(Acc, From, #jid{lserver = LServer} = To, "", Lang) -> ++ case gen_mod:is_loaded(LServer, mod_adhoc) of ++ false -> ++ Acc; ++ _ -> ++ Items = case Acc of ++ {result, Its} -> Its; ++ empty -> [] ++ end, ++ AllowUser = acl:match_rule(LServer, mod_logdb, From), ++ AllowAdmin = acl:match_rule(LServer, mod_logdb_admin, From), ++ if ++ AllowUser == allow; AllowAdmin == allow -> ++ case get_local_items(LServer, [], ++ jlib:jid_to_string(To), Lang) of ++ {result, Res} -> ++ {result, Items ++ Res}; ++ {error, _Error} -> ++ {result, Items} ++ end; ++ true -> ++ {result, Items} ++ end ++ end; ++get_local_items(Acc, From, #jid{lserver = LServer} = To, Node, Lang) -> ++ case gen_mod:is_loaded(LServer, mod_adhoc) of ++ false -> ++ Acc; ++ _ -> ++ LNode = string:tokens(Node, "/"), ++ AllowAdmin = acl:match_rule(LServer, mod_logdb_admin, From), ++ case LNode of ++ ["mod_logdb"] -> ++ ?ITEMS_RESULT(AllowAdmin, LNode, {error, ?ERR_FORBIDDEN}); ++ ["mod_logdb_users"] -> ++ ?ITEMS_RESULT(AllowAdmin, LNode, {error, ?ERR_FORBIDDEN}); ++ ["mod_logdb_users", [$@ | _]] -> ++ ?ITEMS_RESULT(AllowAdmin, LNode, {error, ?ERR_FORBIDDEN}); ++ ["mod_logdb_users", _User] -> ++ ?ITEMS_RESULT(AllowAdmin, LNode, {error, ?ERR_FORBIDDEN}); ++ ["mod_logdb_settings"] -> ++ ?ITEMS_RESULT(AllowAdmin, LNode, {error, ?ERR_FORBIDDEN}); ++ _ -> ++ Acc ++ end ++ end. ++ ++-define(NODE(Name, Node), ++ {xmlelement, "item", ++ [{"jid", Server}, ++ {"name", translate:translate(Lang, Name)}, ++ {"node", Node}], []}). ++ ++get_local_items(_Host, [], Server, Lang) -> ++ {result, ++ [?NODE("Messages logging engine", "mod_logdb")] ++ }; ++get_local_items(_Host, ["mod_logdb"], Server, Lang) -> ++ {result, ++ [?NODE("Messages logging engine users", "mod_logdb_users"), ++ ?NODE("Messages logging engine settings", "mod_logdb_settings")] ++ }; ++get_local_items(Host, ["mod_logdb_users"], Server, Lang) -> ++ {result, get_all_vh_users(Host, Server, Lang)}; ++get_local_items(_Host, ["mod_logdb_users", [$@ | Diap]], Server, Lang) -> ++ case catch ejabberd_auth:dirty_get_registered_users() of ++ {'EXIT', _Reason} -> ++ ?ERR_INTERNAL_SERVER_ERROR; ++ Users -> ++ SUsers = lists:sort([{S, U} || {U, S} <- Users]), ++ case catch begin ++ {ok, [S1, S2]} = regexp:split(Diap, "-"), ++ N1 = list_to_integer(S1), ++ N2 = list_to_integer(S2), ++ Sub = lists:sublist(SUsers, N1, N2 - N1 + 1), ++ lists:map(fun({S, U}) -> ++ ?NODE(U ++ "@" ++ S, "mod_logdb_users/" ++ U ++ "@" ++ S) ++ end, Sub) ++ end of ++ {'EXIT', _Reason} -> ++ ?ERR_NOT_ACCEPTABLE; ++ Res -> ++ {result, Res} ++ end ++ end; ++get_local_items(_Host, ["mod_logdb_users", _User], _Server, _Lang) -> ++ {result, []}; ++get_local_items(_Host, ["mod_logdb_settings"], _Server, _Lang) -> ++ {result, []}; ++get_local_items(_Host, Item, _Server, _Lang) -> ++ ?MYDEBUG("asked for items in ~p", [Item]), ++ {error, ?ERR_ITEM_NOT_FOUND}. ++ ++-define(INFO_RESULT(Allow, Feats), ++ case Allow of ++ deny -> ++ {error, ?ERR_FORBIDDEN}; ++ allow -> ++ {result, Feats} ++ end). ++ ++get_local_features(Acc, From, #jid{lserver = LServer} = _To, Node, _Lang) -> ++ case gen_mod:is_loaded(LServer, mod_adhoc) of ++ false -> ++ Acc; ++ _ -> ++ LNode = string:tokens(Node, "/"), ++ AllowUser = acl:match_rule(LServer, mod_logdb, From), ++ AllowAdmin = acl:match_rule(LServer, mod_logdb_admin, From), ++ case LNode of ++ ["mod_logdb"] when AllowUser == allow; AllowAdmin == allow -> ++ ?INFO_RESULT(allow, [?NS_COMMANDS]); ++ ["mod_logdb"] -> ++ ?INFO_RESULT(deny, [?NS_COMMANDS]); ++ ["mod_logdb_users"] -> ++ ?INFO_RESULT(AllowAdmin, []); ++ ["mod_logdb_users", [$@ | _]] -> ++ ?INFO_RESULT(AllowAdmin, []); ++ ["mod_logdb_users", _User] -> ++ ?INFO_RESULT(AllowAdmin, [?NS_COMMANDS]); ++ ["mod_logdb_settings"] -> ++ ?INFO_RESULT(AllowAdmin, [?NS_COMMANDS]); ++ [] -> ++ Acc; ++ _ -> ++ %?MYDEBUG("asked for ~p features: ~p", [LNode, Allow]), ++ Acc ++ end ++ end. ++ ++-define(INFO_IDENTITY(Category, Type, Name, Lang), ++ [{xmlelement, "identity", ++ [{"category", Category}, ++ {"type", Type}, ++ {"name", translate:translate(Lang, Name)}], []}]). ++ ++-define(INFO_COMMAND(Name, Lang), ++ ?INFO_IDENTITY("automation", "command-node", Name, Lang)). ++ ++get_local_identity(Acc, _From, _To, Node, Lang) -> ++ LNode = string:tokens(Node, "/"), ++ case LNode of ++ ["mod_logdb"] -> ++ ?INFO_COMMAND("Messages logging engine", Lang); ++ ["mod_logdb_users"] -> ++ ?INFO_COMMAND("Messages logging engine users", Lang); ++ ["mod_logdb_users", [$@ | _]] -> ++ Acc; ++ ["mod_logdb_users", User] -> ++ ?INFO_COMMAND(User, Lang); ++ ["mod_logdb_settings"] -> ++ ?INFO_COMMAND("Messages logging engine settings", Lang); ++ [] -> ++ Acc; ++ _ -> ++ Acc ++ end. ++ ++%get_sm_items(Acc, From, To, Node, Lang) -> ++% ?MYDEBUG("get_sm_items Acc=~p From=~p To=~p Node=~p Lang=~p", [Acc, From, To, Node, Lang]), ++% Acc. ++ ++%get_sm_features(Acc, From, To, Node, Lang) -> ++% ?MYDEBUG("get_sm_features Acc=~p From=~p To=~p Node=~p Lang=~p", [Acc, From, To, Node, Lang]), ++% Acc. ++ ++%get_sm_identity(Acc, From, To, Node, Lang) -> ++% ?MYDEBUG("get_sm_identity Acc=~p From=~p To=~p Node=~p Lang=~p", [Acc, From, To, Node, Lang]), ++% Acc. ++ ++adhoc_local_items(Acc, From, #jid{lserver = LServer, server = Server} = To, ++ Lang) -> ++ Items = case Acc of ++ {result, Its} -> Its; ++ empty -> [] ++ end, ++ Nodes = recursively_get_local_items(LServer, "", Server, Lang), ++ Nodes1 = lists:filter( ++ fun(N) -> ++ Nd = xml:get_tag_attr_s("node", N), ++ F = get_local_features([], From, To, Nd, Lang), ++ case F of ++ {result, [?NS_COMMANDS]} -> ++ true; ++ _ -> ++ false ++ end ++ end, Nodes), ++ {result, Items ++ Nodes1}. ++ ++recursively_get_local_items(_LServer, "mod_logdb_users", _Server, _Lang) -> ++ []; ++recursively_get_local_items(LServer, Node, Server, Lang) -> ++ LNode = string:tokens(Node, "/"), ++ Items = case get_local_items(LServer, LNode, Server, Lang) of ++ {result, Res} -> ++ Res; ++ {error, _Error} -> ++ [] ++ end, ++ Nodes = lists:flatten( ++ lists:map( ++ fun(N) -> ++ S = xml:get_tag_attr_s("jid", N), ++ Nd = xml:get_tag_attr_s("node", N), ++ if (S /= Server) or (Nd == "") -> ++ []; ++ true -> ++ [N, recursively_get_local_items( ++ LServer, Nd, Server, Lang)] ++ end ++ end, Items)), ++ Nodes. ++ ++-define(COMMANDS_RESULT(Allow, From, To, Request), ++ case Allow of ++ deny -> ++ {error, ?ERR_FORBIDDEN}; ++ allow -> ++ adhoc_local_commands(From, To, Request) ++ end). ++ ++adhoc_local_commands(Acc, From, #jid{lserver = LServer} = To, ++ #adhoc_request{node = Node} = Request) -> ++ LNode = string:tokens(Node, "/"), ++ AllowUser = acl:match_rule(LServer, mod_logdb, From), ++ AllowAdmin = acl:match_rule(LServer, mod_logdb_admin, From), ++ case LNode of ++ ["mod_logdb"] when AllowUser == allow; AllowAdmin == allow -> ++ ?COMMANDS_RESULT(allow, From, To, Request); ++ ["mod_logdb_users", _User] when AllowAdmin == allow -> ++ ?COMMANDS_RESULT(allow, From, To, Request); ++ ["mod_logdb_settings"] when AllowAdmin == allow -> ++ ?COMMANDS_RESULT(allow, From, To, Request); ++ _ -> ++ Acc ++ end. ++ ++adhoc_local_commands(From, #jid{lserver = LServer} = _To, ++ #adhoc_request{lang = Lang, ++ node = Node, ++ sessionid = SessionID, ++ action = Action, ++ xdata = XData} = Request) -> ++ LNode = string:tokens(Node, "/"), ++ %% If the "action" attribute is not present, it is ++ %% understood as "execute". If there was no ++ %% element in the first response (which there isn't in our ++ %% case), "execute" and "complete" are equivalent. ++ ActionIsExecute = lists:member(Action, ++ ["", "execute", "complete"]), ++ if Action == "cancel" -> ++ %% User cancels request ++ adhoc:produce_response( ++ Request, ++ #adhoc_response{status = canceled}); ++ XData == false, ActionIsExecute -> ++ %% User requests form ++ case get_form(LServer, LNode, From, Lang) of ++ {result, Form} -> ++ adhoc:produce_response( ++ Request, ++ #adhoc_response{status = executing, ++ elements = Form}); ++ {error, Error} -> ++ {error, Error} ++ end; ++ XData /= false, ActionIsExecute -> ++ %% User returns form. ++ case jlib:parse_xdata_submit(XData) of ++ invalid -> ++ {error, ?ERR_BAD_REQUEST}; ++ Fields -> ++ case set_form(From, LServer, LNode, Lang, Fields) of ++ {result, _Res} -> ++ adhoc:produce_response( ++ #adhoc_response{lang = Lang, ++ node = Node, ++ sessionid = SessionID, ++ status = completed}); ++ {error, Error} -> ++ {error, Error} ++ end ++ end; ++ true -> ++ {error, ?ERR_BAD_REQUEST} ++ end. ++ ++-define(LISTLINE(Label, Value), ++ {xmlelement, "option", [{"label", Label}], ++ [{xmlelement, "value", [], [{xmlcdata, Value}]}]}). ++-define(DEFVAL(Value), {xmlelement, "value", [], [{xmlcdata, Value}]}). ++ ++get_user_form(LUser, LServer, Lang) -> ++ %From = jlib:jid_to_string(jlib:jid_remove_resource(Jid)), ++ #user_settings{dolog_default=DLD, ++ dolog_list=DLL, ++ donotlog_list=DNLL} = get_user_settings(LUser, LServer), ++ {result, [{xmlelement, "x", [{"xmlns", ?NS_XDATA}], ++ [{xmlelement, "title", [], ++ [{xmlcdata, ++ translate:translate( ++ Lang, "Messages logging engine settings")}]}, ++ {xmlelement, "instructions", [], ++ [{xmlcdata, ++ translate:translate( ++ Lang, "Set logging preferences")++ ": " ++ LUser ++ "@" ++ LServer}]}, ++ % default to log ++ {xmlelement, "field", [{"type", "list-single"}, ++ {"label", ++ translate:translate(Lang, "Default")}, ++ {"var", "dolog_default"}], ++ [?DEFVAL(atom_to_list(DLD)), ++ ?LISTLINE(translate:translate(Lang, "Log Messages"), "true"), ++ ?LISTLINE(translate:translate(Lang, "Do Not Log Messages"), "false") ++ ]}, ++ % do log list ++ {xmlelement, "field", [{"type", "text-multi"}, ++ {"label", ++ translate:translate( ++ Lang, "Log Messages")}, ++ {"var", "dolog_list"}], ++ [{xmlelement, "value", [], [{xmlcdata, list_to_string(DLL)}]}]}, ++ % do not log list ++ {xmlelement, "field", [{"type", "text-multi"}, ++ {"label", ++ translate:translate( ++ Lang, "Do Not Log Messages")}, ++ {"var", "donotlog_list"}], ++ [{xmlelement, "value", [], [{xmlcdata, list_to_string(DNLL)}]}]} ++ ]}]}. ++ ++get_settings_form(Host, Lang) -> ++ #state{dbmod=DBMod, ++ dbs=DBs, ++ dolog_default=DLD, ++ ignore_jids=IgnoreJids, ++ groupchat=GroupChat, ++ purge_older_days=PurgeDaysT, ++ drop_messages_on_user_removal=MRemoval, ++ poll_users_settings=PollTime} = mod_logdb:get_module_settings(Host), ++ ++ Backends = lists:map(fun({Backend, _Opts}) -> ++ ?LISTLINE(atom_to_list(Backend), atom_to_list(Backend)) ++ end, DBs), ++ DB = lists:sublist(atom_to_list(DBMod), length(atom_to_list(?MODULE)) + 2, length(atom_to_list(DBMod))), ++ DBsL = lists:append([?DEFVAL(DB)], Backends), ++ ++ PurgeDays = ++ case PurgeDaysT of ++ never -> "never"; ++ Num when is_integer(Num) -> integer_to_list(Num); ++ _ -> "unknown" ++ end, ++ {result, [{xmlelement, "x", [{"xmlns", ?NS_XDATA}], ++ [{xmlelement, "title", [], ++ [{xmlcdata, ++ translate:translate( ++ Lang, "Messages logging engine settings") ++ " (run-time)"}]}, ++ {xmlelement, "instructions", [], ++ [{xmlcdata, ++ translate:translate( ++ Lang, "Set run-time settings")}]}, ++ % backends ++ {xmlelement, "field", [{"type", "list-single"}, ++ {"label", ++ translate:translate(Lang, "Backend")}, ++ {"var", "backend"}], ++ DBsL}, ++ % dbs ++ {xmlelement, "field", [{"type", "text-multi"}, ++ {"label", ++ translate:translate( ++ Lang, "dbs")}, ++ {"var", "dbs"}], ++ [{xmlelement, "value", [], [{xmlcdata, lists:flatten(io_lib:format("~p.",[DBs]))}]}]}, ++ % default to log ++ {xmlelement, "field", [{"type", "list-single"}, ++ {"label", ++ translate:translate(Lang, "Default")}, ++ {"var", "dolog_default"}], ++ [?DEFVAL(atom_to_list(DLD)), ++ ?LISTLINE(translate:translate(Lang, "Log Messages"), "true"), ++ ?LISTLINE(translate:translate(Lang, "Do Not Log Messages"), "false") ++ ]}, ++ % drop_messages_on_user_removal ++ {xmlelement, "field", [{"type", "list-single"}, ++ {"label", ++ translate:translate(Lang, "Drop messages on user removal")}, ++ {"var", "drop_messages_on_user_removal"}], ++ [?DEFVAL(atom_to_list(MRemoval)), ++ ?LISTLINE(translate:translate(Lang, "Drop"), "true"), ++ ?LISTLINE(translate:translate(Lang, "Do not drop"), "false") ++ ]}, ++ % groupchat ++ {xmlelement, "field", [{"type", "list-single"}, ++ {"label", ++ translate:translate(Lang, "Groupchat messages logging")}, ++ {"var", "groupchat"}], ++ [?DEFVAL(atom_to_list(GroupChat)), ++ ?LISTLINE("all", "all"), ++ ?LISTLINE("none", "none"), ++ ?LISTLINE("send", "send"), ++ ?LISTLINE("half", "half") ++ ]}, ++ % ignore_jids ++ {xmlelement, "field", [{"type", "text-multi"}, ++ {"label", ++ translate:translate( ++ Lang, "Jids/Domains to ignore")}, ++ {"var", "ignore_list"}], ++ [{xmlelement, "value", [], [{xmlcdata, list_to_string(IgnoreJids)}]}]}, ++ % purge older days ++ {xmlelement, "field", [{"type", "text-single"}, ++ {"label", ++ translate:translate( ++ Lang, "Purge messages older than (days)")}, ++ {"var", "purge_older_days"}], ++ [{xmlelement, "value", [], [{xmlcdata, PurgeDays}]}]}, ++ % poll users settings ++ {xmlelement, "field", [{"type", "text-single"}, ++ {"label", ++ translate:translate( ++ Lang, "Poll users settings (seconds)")}, ++ {"var", "poll_users_settings"}], ++ [{xmlelement, "value", [], [{xmlcdata, integer_to_list(PollTime)}]}]} ++ ]}]}. ++ ++get_form(_Host, ["mod_logdb"], #jid{luser = LUser, lserver = LServer} = _Jid, Lang) -> ++ get_user_form(LUser, LServer, Lang); ++get_form(_Host, ["mod_logdb_users", User], _JidFrom, Lang) -> ++ #jid{luser=LUser, lserver=LServer} = jlib:string_to_jid(User), ++ get_user_form(LUser, LServer, Lang); ++get_form(Host, ["mod_logdb_settings"], _JidFrom, Lang) -> ++ get_settings_form(Host, Lang); ++get_form(_Host, Command, _, _Lang) -> ++ ?MYDEBUG("asked for form ~p", [Command]), ++ {error, ?ERR_SERVICE_UNAVAILABLE}. ++ ++check_log_list([Head | Tail]) -> ++ case lists:member($@, Head) of ++ true -> ok; ++ false -> throw(error) ++ end, ++ % this check for Head to be valid jid ++ case jlib:string_to_jid(Head) of ++ error -> ++ throw(error); ++ _ -> ++ check_log_list(Tail) ++ end; ++check_log_list([]) -> ++ ok. ++ ++check_ignore_list([Head | Tail]) -> ++ case lists:member($@, Head) of ++ true -> ok; ++ false -> throw(error) ++ end, ++ % this check for Head to be valid jid ++ case jlib:string_to_jid(Head) of ++ error -> ++ % this check for Head to be valid domain "@domain.org" ++ case lists:nth(1, Head) of ++ $@ -> ++ % TODO: this allows spaces and special characters in Head. May be change to nodeprep? ++ case jlib:nameprep(lists:delete($@, Head)) of ++ error -> throw(error); ++ _ -> check_log_list(Tail) ++ end; ++ _ -> throw(error) ++ end; ++ _ -> ++ check_ignore_list(Tail) ++ end; ++check_ignore_list([]) -> ++ ok. ++ ++parse_users_settings(XData) -> ++ DLD = case lists:keysearch("dolog_default", 1, XData) of ++ {value, {_, [String]}} when String == "true"; String == "false" -> ++ list_to_bool(String); ++ _ -> ++ throw(bad_request) ++ end, ++ DLL = case lists:keysearch("dolog_list", 1, XData) of ++ false -> ++ throw(bad_request); ++ {value, {_, [[]]}} -> ++ []; ++ {value, {_, List1}} -> ++ case catch check_log_list(List1) of ++ error -> ++ throw(bad_request); ++ ok -> ++ List1 ++ end ++ end, ++ DNLL = case lists:keysearch("donotlog_list", 1, XData) of ++ false -> ++ throw(bad_request); ++ {value, {_, [[]]}} -> ++ []; ++ {value, {_, List2}} -> ++ case catch check_log_list(List2) of ++ error -> ++ throw(bad_request); ++ ok -> ++ List2 ++ end ++ end, ++ #user_settings{dolog_default=DLD, ++ dolog_list=DLL, ++ donotlog_list=DNLL}. ++ ++parse_module_settings(XData) -> ++ DLD = case lists:keysearch("dolog_default", 1, XData) of ++ {value, {_, [Str1]}} when Str1 == "true"; Str1 == "false" -> ++ list_to_bool(Str1); ++ _ -> ++ throw(bad_request) ++ end, ++ MRemoval = case lists:keysearch("drop_messages_on_user_removal", 1, XData) of ++ {value, {_, [Str5]}} when Str5 == "true"; Str5 == "false" -> ++ list_to_bool(Str5); ++ _ -> ++ throw(bad_request) ++ end, ++ GroupChat = case lists:keysearch("groupchat", 1, XData) of ++ {value, {_, [Str2]}} when Str2 == "none"; ++ Str2 == "all"; ++ Str2 == "send"; ++ Str2 == "half" -> ++ list_to_atom(Str2); ++ _ -> ++ throw(bad_request) ++ end, ++ Ignore = case lists:keysearch("ignore_list", 1, XData) of ++ {value, {_, List}} -> ++ case catch check_ignore_list(List) of ++ ok -> ++ List; ++ error -> ++ throw(bad_request) ++ end; ++ _ -> ++ throw(bad_request) ++ end, ++ Purge = case lists:keysearch("purge_older_days", 1, XData) of ++ {value, {_, ["never"]}} -> ++ never; ++ {value, {_, [Str3]}} -> ++ case catch list_to_integer(Str3) of ++ {'EXIT', {badarg, _}} -> throw(bad_request); ++ Int1 -> Int1 ++ end; ++ _ -> ++ throw(bad_request) ++ end, ++ Poll = case lists:keysearch("poll_users_settings", 1, XData) of ++ {value, {_, [Str4]}} -> ++ case catch list_to_integer(Str4) of ++ {'EXIT', {badarg, _}} -> throw(bad_request); ++ Int2 -> Int2 ++ end; ++ _ -> ++ throw(bad_request) ++ end, ++ #state{dolog_default=DLD, ++ groupchat=GroupChat, ++ ignore_jids=Ignore, ++ purge_older_days=Purge, ++ drop_messages_on_user_removal=MRemoval, ++ poll_users_settings=Poll}. ++ ++set_form(From, _Host, ["mod_logdb"], _Lang, XData) -> ++ #jid{luser=LUser, lserver=LServer} = From, ++ case catch parse_users_settings(XData) of ++ bad_request -> ++ {error, ?ERR_BAD_REQUEST}; ++ UserSettings -> ++ case mod_logdb:set_user_settings(LUser, LServer, UserSettings) of ++ ok -> ++ {result, []}; ++ error -> ++ {error, ?ERR_INTERNAL_SERVER_ERROR} ++ end ++ end; ++set_form(_From, _Host, ["mod_logdb_users", User], _Lang, XData) -> ++ #jid{luser=LUser, lserver=LServer} = jlib:string_to_jid(User), ++ case catch parse_users_settings(XData) of ++ bad_request -> {error, ?ERR_BAD_REQUEST}; ++ UserSettings -> ++ case mod_logdb:set_user_settings(LUser, LServer, UserSettings) of ++ ok -> ++ {result, []}; ++ error -> ++ {error, ?ERR_INTERNAL_SERVER_ERROR} ++ end ++ end; ++set_form(_From, Host, ["mod_logdb_settings"], _Lang, XData) -> ++ case catch parse_module_settings(XData) of ++ bad_request -> {error, ?ERR_BAD_REQUEST}; ++ Settings -> ++ case mod_logdb:set_module_settings(Host, Settings) of ++ ok -> ++ {result, []}; ++ error -> ++ {error, ?ERR_INTERNAL_SERVER_ERROR} ++ end ++ end; ++set_form(From, _Host, Node, _Lang, XData) -> ++ User = jlib:jid_to_string(jlib:jid_remove_resource(From)), ++ ?MYDEBUG("set form for ~p at ~p XData=~p", [User, Node, XData]), ++ {error, ?ERR_SERVICE_UNAVAILABLE}. ++ ++%adhoc_sm_items(Acc, From, To, Request) -> ++% ?MYDEBUG("adhoc_sm_items Acc=~p From=~p To=~p Request=~p", [Acc, From, To, Request]), ++% Acc. ++ ++%adhoc_sm_commands(Acc, From, To, Request) -> ++% ?MYDEBUG("adhoc_sm_commands Acc=~p From=~p To=~p Request=~p", [Acc, From, To, Request]), ++% Acc. ++ ++get_all_vh_users(Host, Server, Lang) -> ++ case catch ejabberd_auth:get_vh_registered_users(Host) of ++ {'EXIT', _Reason} -> ++ []; ++ Users -> ++ SUsers = lists:sort([{S, U} || {U, S} <- Users]), ++ case length(SUsers) of ++ N when N =< 100 -> ++ lists:map(fun({S, U}) -> ++ ?NODE(U ++ "@" ++ S, "mod_logdb_users/" ++ U ++ "@" ++ S) ++ end, SUsers); ++ N -> ++ NParts = trunc(math:sqrt(N * 0.618)) + 1, ++ M = trunc(N / NParts) + 1, ++ lists:map(fun(K) -> ++ L = K + M - 1, ++ Node = ++ "@" ++ integer_to_list(K) ++ ++ "-" ++ integer_to_list(L), ++ {FS, FU} = lists:nth(K, SUsers), ++ {LS, LU} = ++ if L < N -> lists:nth(L, SUsers); ++ true -> lists:last(SUsers) ++ end, ++ Name = ++ FU ++ "@" ++ FS ++ ++ " -- " ++ ++ LU ++ "@" ++ LS, ++ ?NODE(Name, "mod_logdb_users/" ++ Node) ++ end, lists:seq(1, N, M)) ++ end ++ end. ++ ++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++% ++% webadmin hooks ++% ++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++webadmin_menu(Acc, _Host, Lang) -> ++ [{"messages", ?T("Users Messages")} | Acc]. ++ ++webadmin_user(Acc, User, Server, Lang) -> ++ Sett = get_user_settings(User, Server), ++ Log = ++ case Sett#user_settings.dolog_default of ++ false -> ++ ?INPUTT("submit", "dolog", "Log Messages"); ++ true -> ++ ?INPUTT("submit", "donotlog", "Do Not Log Messages"); ++ _ -> [] ++ end, ++ Acc ++ [?XE("h3", [?ACT("messages/", "Messages"), ?C(" "), Log])]. ++ ++webadmin_page(_, Host, ++ #request{path = ["messages"], ++ q = Query, ++ lang = Lang}) when is_list(Host) -> ++ Res = vhost_messages_stats(Host, Query, Lang), ++ {stop, Res}; ++webadmin_page(_, Host, ++ #request{path = ["messages", Date], ++ q = Query, ++ lang = Lang}) when is_list(Host) -> ++ Res = vhost_messages_stats_at(Host, Query, Lang, Date), ++ {stop, Res}; ++webadmin_page(_, Host, ++ #request{path = ["user", U, "messages"], ++ q = Query, ++ lang = Lang}) -> ++ Res = user_messages_stats(U, Host, Query, Lang), ++ {stop, Res}; ++webadmin_page(_, Host, ++ #request{path = ["user", U, "messages", Date], ++ q = Query, ++ lang = Lang}) -> ++ Res = mod_logdb:user_messages_stats_at(U, Host, Query, Lang, Date), ++ {stop, Res}; ++webadmin_page(Acc, _, _) -> Acc. ++ ++user_parse_query(_, "dolog", User, Server, _Query) -> ++ Sett = get_user_settings(User, Server), ++ % TODO: check returned value ++ set_user_settings(User, Server, Sett#user_settings{dolog_default=true}), ++ {stop, ok}; ++user_parse_query(_, "donotlog", User, Server, _Query) -> ++ Sett = get_user_settings(User, Server), ++ % TODO: check returned value ++ set_user_settings(User, Server, Sett#user_settings{dolog_default=false}), ++ {stop, ok}; ++user_parse_query(Acc, _Action, _User, _Server, _Query) -> ++ Acc. ++ ++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++% ++% webadmin funcs ++% ++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++vhost_messages_stats(Server, Query, Lang) -> ++ Res = case catch vhost_messages_parse_query(Server, Query) of ++ {'EXIT', Reason} -> ++ ?ERROR_MSG("~p", [Reason]), ++ error; ++ VResult -> VResult ++ end, ++ {Time, Value} = timer:tc(mod_logdb, get_vhost_stats, [Server]), ++ ?INFO_MSG("get_vhost_stats(~p) elapsed ~p sec", [Server, Time/1000000]), ++ %case get_vhost_stats(Server) of ++ case Value of ++ {'EXIT', CReason} -> ++ ?ERROR_MSG("Failed to get_vhost_stats: ~p", [CReason]), ++ [?XC("h1", ?T("Error occupied while fetching list"))]; ++ {error, GReason} -> ++ ?ERROR_MSG("Failed to get_vhost_stats: ~p", [GReason]), ++ [?XC("h1", ?T("Error occupied while fetching list"))]; ++ {ok, []} -> ++ [?XC("h1", ?T("No logged messages for ") ++ Server)]; ++ {ok, Dates} -> ++ Fun = fun({Date, Count}) -> ++ ID = jlib:encode_base64(binary_to_list(term_to_binary(Server++Date))), ++ ?XE("tr", ++ [?XE("td", [?INPUT("checkbox", "selected", ID)]), ++ ?XE("td", [?AC(Date, Date)]), ++ ?XC("td", integer_to_list(Count)) ++ ]) ++ end, ++ [?XC("h1", ?T("Logged messages for ") ++ Server)] ++ ++ case Res of ++ ok -> [?CT("Submitted"), ?P]; ++ error -> [?CT("Bad format"), ?P]; ++ nothing -> [] ++ end ++ ++ [?XAE("form", [{"action", ""}, {"method", "post"}], ++ [?XE("table", ++ [?XE("thead", ++ [?XE("tr", ++ [?X("td"), ++ ?XCT("td", "Date"), ++ ?XCT("td", "Count") ++ ])]), ++ ?XE("tbody", ++ lists:map(Fun, Dates) ++ )]), ++ ?BR, ++ ?INPUTT("submit", "delete", "Delete Selected") ++ ])] ++ end. ++ ++vhost_messages_stats_at(Server, Query, Lang, Date) -> ++ {Time, Value} = timer:tc(mod_logdb, get_vhost_stats_at, [Server, Date]), ++ ?INFO_MSG("get_vhost_stats_at(~p,~p) elapsed ~p sec", [Server, Date, Time/1000000]), ++ %case get_vhost_stats_at(Server, Date) of ++ case Value of ++ {'EXIT', CReason} -> ++ ?ERROR_MSG("Failed to get_vhost_stats_at: ~p", [CReason]), ++ [?XC("h1", ?T("Error occupied while fetching list"))]; ++ {error, GReason} -> ++ ?ERROR_MSG("Failed to get_vhost_stats_at: ~p", [GReason]), ++ [?XC("h1", ?T("Error occupied while fetching list"))]; ++ {ok, []} -> ++ [?XC("h1", ?T("No logged messages for ") ++ Server ++ ?T(" at ") ++ Date)]; ++ {ok, Users} -> ++ Res = case catch vhost_messages_at_parse_query(Server, Date, Users, Query) of ++ {'EXIT', Reason} -> ++ ?ERROR_MSG("~p", [Reason]), ++ error; ++ VResult -> VResult ++ end, ++ Fun = fun({User, Count}) -> ++ ID = jlib:encode_base64(binary_to_list(term_to_binary(User++Server))), ++ ?XE("tr", ++ [?XE("td", [?INPUT("checkbox", "selected", ID)]), ++ ?XE("td", [?AC("../user/"++User++"/messages/"++Date, User)]), ++ ?XC("td", integer_to_list(Count)) ++ ]) ++ end, ++ [?XC("h1", ?T("Logged messages for ") ++ Server ++ ?T(" at ") ++ Date)] ++ ++ case Res of ++ ok -> [?CT("Submitted"), ?P]; ++ error -> [?CT("Bad format"), ?P]; ++ nothing -> [] ++ end ++ ++ [?XAE("form", [{"action", ""}, {"method", "post"}], ++ [?XE("table", ++ [?XE("thead", ++ [?XE("tr", ++ [?X("td"), ++ ?XCT("td", "User"), ++ ?XCT("td", "Count") ++ ])]), ++ ?XE("tbody", ++ lists:map(Fun, Users) ++ )]), ++ ?BR, ++ ?INPUTT("submit", "delete", "Delete Selected") ++ ])] ++ end. ++ ++user_messages_stats(User, Server, Query, Lang) -> ++ Jid = jlib:jid_to_string({User, Server, ""}), ++ ++ Res = case catch user_messages_parse_query(User, Server, Query) of ++ {'EXIT', Reason} -> ++ ?ERROR_MSG("~p", [Reason]), ++ error; ++ VResult -> VResult ++ end, ++ ++ {Time, Value} = timer:tc(mod_logdb, get_user_stats, [User, Server]), ++ ?INFO_MSG("get_user_stats(~p,~p) elapsed ~p sec", [User, Server, Time/1000000]), ++ ++ case Value of ++ {'EXIT', CReason} -> ++ ?ERROR_MSG("Failed to get_user_stats: ~p", [CReason]), ++ [?XC("h1", ?T("Error occupied while fetching days"))]; ++ {error, GReason} -> ++ ?ERROR_MSG("Failed to get_user_stats: ~p", [GReason]), ++ [?XC("h1", ?T("Error occupied while fetching days"))]; ++ {ok, []} -> ++ [?XC("h1", ?T("No logged messages for ") ++ Jid)]; ++ {ok, Dates} -> ++ Fun = fun({Date, Count}) -> ++ ID = jlib:encode_base64(binary_to_list(term_to_binary(User++Date))), ++ ?XE("tr", ++ [?XE("td", [?INPUT("checkbox", "selected", ID)]), ++ ?XE("td", [?AC(Date, Date)]), ++ ?XC("td", integer_to_list(Count)) ++ ]) ++ %[?AC(Date, Date ++ " (" ++ integer_to_list(Count) ++ ")"), ?BR] ++ end, ++ [?XC("h1", ?T("Logged messages for ") ++ Jid)] ++ ++ case Res of ++ ok -> [?CT("Submitted"), ?P]; ++ error -> [?CT("Bad format"), ?P]; ++ nothing -> [] ++ end ++ ++ [?XAE("form", [{"action", ""}, {"method", "post"}], ++ [?XE("table", ++ [?XE("thead", ++ [?XE("tr", ++ [?X("td"), ++ ?XCT("td", "Date"), ++ ?XCT("td", "Count") ++ ])]), ++ ?XE("tbody", ++ lists:map(Fun, Dates) ++ )]), ++ ?BR, ++ ?INPUTT("submit", "delete", "Delete Selected") ++ ])] ++ end. ++ ++search_user_nick(User, List) -> ++ case lists:keysearch(User, 1, List) of ++ {value,{User, []}} -> ++ nothing; ++ {value,{User, Nick}} -> ++ Nick; ++ false -> ++ nothing ++ end. ++ ++user_messages_stats_at(User, Server, Query, Lang, Date) -> ++ Jid = jlib:jid_to_string({User, Server, ""}), ++ ++ {Time, Value} = timer:tc(mod_logdb, get_user_messages_at, [User, Server, Date]), ++ ?INFO_MSG("get_user_messages_at(~p,~p,~p) elapsed ~p sec", [User, Server, Date, Time/1000000]), ++ case Value of ++ {'EXIT', CReason} -> ++ ?ERROR_MSG("Failed to get_user_messages_at: ~p", [CReason]), ++ [?XC("h1", ?T("Error occupied while fetching messages"))]; ++ {error, GReason} -> ++ ?ERROR_MSG("Failed to get_user_messages_at: ~p", [GReason]), ++ [?XC("h1", ?T("Error occupied while fetching messages"))]; ++ {ok, []} -> ++ [?XC("h1", ?T("No logged messages for ") ++ Jid ++ ?T(" at ") ++ Date)]; ++ {ok, User_messages} -> ++ Res = case catch user_messages_at_parse_query(Server, ++ Date, ++ User_messages, ++ Query) of ++ {'EXIT', Reason} -> ++ ?ERROR_MSG("~p", [Reason]), ++ error; ++ VResult -> VResult ++ end, ++ ++ UR = ejabberd_hooks:run_fold(roster_get, Server, [], [{User, Server}]), ++ UserRoster = ++ lists:map(fun(Item) -> ++ {jlib:jid_to_string(Item#roster.jid), Item#roster.name} ++ end, UR), ++ ++ UniqUsers = lists:foldl(fun(#msg{peer_name=PName, peer_server=PServer}, List) -> ++ ToAdd = PName++"@"++PServer, ++ case lists:member(ToAdd, List) of ++ true -> List; ++ false -> lists:append([ToAdd], List) ++ end ++ end, [], User_messages), ++ ++ % Users to filter (sublist of UniqUsers) ++ CheckedUsers = case lists:keysearch("filter", 1, Query) of ++ {value, _} -> ++ lists:filter(fun(UFUser) -> ++ ID = jlib:encode_base64(binary_to_list(term_to_binary(UFUser))), ++ lists:member({"selected", ID}, Query) ++ end, UniqUsers); ++ false -> [] ++ end, ++ ++ % UniqUsers in html (noone selected -> everyone selected) ++ Users = lists:map(fun(UHUser) -> ++ ID = jlib:encode_base64(binary_to_list(term_to_binary(UHUser))), ++ Input = case lists:member(UHUser, CheckedUsers) of ++ true -> [?INPUTC("checkbox", "selected", ID)]; ++ false when CheckedUsers == [] -> [?INPUTC("checkbox", "selected", ID)]; ++ false -> [?INPUT("checkbox", "selected", ID)] ++ end, ++ Nick = ++ case search_user_nick(UHUser, UserRoster) of ++ nothing -> ""; ++ N -> " ("++ N ++")" ++ end, ++ ?XE("tr", ++ [?XE("td", Input), ++ ?XC("td", UHUser++Nick)]) ++ end, lists:sort(UniqUsers)), ++ % Messages to show (based on Users) ++ User_messages_filtered = case CheckedUsers of ++ [] -> User_messages; ++ _ -> lists:filter(fun(#msg{peer_name=PName, peer_server=PServer}) -> ++ lists:member(PName++"@"++PServer, CheckedUsers) ++ end, User_messages) ++ end, ++ ++ Msgs_Fun = fun(#msg{timestamp=Timestamp, ++ subject=Subject, ++ direction=Direction, ++ peer_name=PName, peer_server=PServer, peer_resource=PRes, ++ type=Type, ++ body=Body}) -> ++ TextRaw = case Subject of ++ "" -> Body; ++ _ -> [?T("Subject"),": ",Subject,"
    ", Body] ++ end, ++ ID = jlib:encode_base64(binary_to_list(term_to_binary(Timestamp))), ++ % replace \n with
    ++ Text = lists:map(fun(10) -> "
    "; ++ (A) -> A ++ end, TextRaw), ++ Resource = case PRes of ++ [] -> []; ++ undefined -> []; ++ R -> "/" ++ R ++ end, ++ UserNick = ++ case search_user_nick(PName++"@"++PServer, UserRoster) of ++ nothing when PServer == Server -> ++ PName; ++ nothing when Type == "groupchat", Direction == from -> ++ PName++"@"++PServer++Resource; ++ nothing -> ++ PName++"@"++PServer; ++ N -> N ++ end, ++ ?XE("tr", ++ [?XE("td", [?INPUT("checkbox", "selected", ID)]), ++ ?XC("td", convert_timestamp(Timestamp)), ++ ?XC("td", atom_to_list(Direction)++": "++UserNick), ++ ?XC("td", Text)]) ++ end, ++ % Filtered user messages in html ++ Msgs = lists:map(Msgs_Fun, lists:sort(User_messages_filtered)), ++ ++ [?XC("h1", ?T("Logged messages for ") ++ Jid ++ ?T(" at ") ++ Date)] ++ ++ case Res of ++ ok -> [?CT("Submitted"), ?P]; ++ error -> [?CT("Bad format"), ?P]; ++ nothing -> [] ++ end ++ ++ [?XAE("form", [{"action", ""}, {"method", "post"}], ++ [?XE("table", ++ [?XE("thead", ++ [?X("td"), ++ ?XCT("td", "User") ++ ] ++ ), ++ ?XE("tbody", ++ Users ++ )]), ++ ?INPUTT("submit", "filter", "Filter Selected") ++ ] ++ ++ [?XE("table", ++ [?XE("thead", ++ [?XE("tr", ++ [?X("td"), ++ ?XCT("td", "Date, Time"), ++ ?XCT("td", "Direction: Jid"), ++ ?XCT("td", "Body") ++ ])]), ++ ?XE("tbody", ++ Msgs ++ )]), ++ ?INPUTT("submit", "delete", "Delete Selected"), ++ ?BR ++ ] ++ )] ++ end. +--- mod_logdb.hrl.orig 2009-02-05 19:21:29.000000000 +0200 ++++ mod_logdb.hrl 2009-02-05 19:21:02.000000000 +0200 +@@ -0,0 +1,35 @@ ++%%%---------------------------------------------------------------------- ++%%% File : mod_logdb.hrl ++%%% Author : Oleg Palij (mailto,xmpp:o.palij@gmail.com) ++%%% Purpose : ++%%% Version : trunk ++%%% Id : $Id: mod_logdb.hrl 1163 2008-09-15 11:12:08Z malik $ ++%%% Url : http://www.dp.uz.gov.ua/o.palij/mod_logdb/ ++%%%---------------------------------------------------------------------- ++ ++-define(logdb_debug, true). ++ ++-ifdef(logdb_debug). ++-define(MYDEBUG(Format, Args), io:format("D(~p:~p:~p) : "++Format++"~n", ++ [calendar:local_time(),?MODULE,?LINE]++Args)). ++-else. ++-define(MYDEBUG(_F,_A),[]). ++-endif. ++ ++-record(msg, {timestamp, ++ owner_name, ++ peer_name, peer_server, peer_resource, ++ direction, ++ type, subject, ++ body}). ++ ++-record(user_settings, {owner_name, ++ dolog_default, ++ dolog_list=[], ++ donotlog_list=[]}). ++ ++-define(INPUTC(Type, Name, Value), ++ ?XA("input", [{"type", Type}, ++ {"name", Name}, ++ {"value", Value}, ++ {"checked", "true"}])). +--- mod_logdb_mnesia.erl.orig 2009-02-05 19:21:29.000000000 +0200 ++++ mod_logdb_mnesia.erl 2009-02-05 19:19:59.000000000 +0200 +@@ -0,0 +1,546 @@ ++%%%---------------------------------------------------------------------- ++%%% File : mod_logdb_mnesia.erl ++%%% Author : Oleg Palij (mailto,xmpp:o.palij@gmail.com) ++%%% Purpose : mnesia backend for mod_logdb ++%%% Version : trunk ++%%% Id : $Id: mod_logdb_mnesia.erl 1169 2008-09-16 12:14:36Z malik $ ++%%% Url : http://www.dp.uz.gov.ua/o.palij/mod_logdb/ ++%%%---------------------------------------------------------------------- ++ ++-module(mod_logdb_mnesia). ++-author('o.palij@gmail.com'). ++ ++-include("mod_logdb.hrl"). ++-include("ejabberd.hrl"). ++-include("jlib.hrl"). ++ ++-behaviour(gen_logdb). ++-behaviour(gen_server). ++ ++% gen_server ++-export([code_change/3,handle_call/3,handle_cast/2,handle_info/2,init/1,terminate/2]). ++% gen_mod ++-export([start/2, stop/1]). ++% gen_logdb ++-export([log_message/2, ++ rebuild_stats/1, ++ rebuild_stats_at/2, ++ delete_messages_by_user_at/3, delete_all_messages_by_user_at/3, delete_messages_at/2, ++ get_vhost_stats/1, get_vhost_stats_at/2, get_user_stats/2, get_user_messages_at/3, ++ get_dates/1, ++ get_users_settings/1, get_user_settings/2, set_user_settings/3, ++ drop_user/2]). ++ ++-define(PROCNAME, mod_logdb_mnesia). ++-define(CALL_TIMEOUT, 10000). ++ ++-record(state, {vhost}). ++ ++-record(stats, {user, at, count}). ++ ++prefix() -> ++ "logdb_". ++ ++suffix(VHost) -> ++ "_" ++ VHost. ++ ++stats_table(VHost) -> ++ list_to_atom(prefix() ++ "stats" ++ suffix(VHost)). ++ ++table_name(VHost, Date) -> ++ list_to_atom(prefix() ++ "messages_" ++ Date ++ suffix(VHost)). ++ ++settings_table(VHost) -> ++ list_to_atom(prefix() ++ "settings" ++ suffix(VHost)). ++ ++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++% ++% gen_mod callbacks ++% ++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++start(VHost, Opts) -> ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:start({local, Proc}, ?MODULE, [VHost, Opts], []). ++ ++stop(VHost) -> ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:call(Proc, {stop}, ?CALL_TIMEOUT). ++ ++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++% ++% gen_server callbacks ++% ++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++init([VHost, _Opts]) -> ++ case mnesia:system_info(is_running) of ++ yes -> ++ ok = create_stats_table(VHost), ++ ok = create_settings_table(VHost), ++ {ok, #state{vhost=VHost}}; ++ no -> ++ ?ERROR_MSG("Mnesia not running", []), ++ {stop, db_connection_failed}; ++ Status -> ++ ?ERROR_MSG("Mnesia status: ~p", [Status]), ++ {stop, db_connection_failed} ++ end. ++ ++handle_call({log_message, Msg}, _From, #state{vhost=VHost}=State) -> ++ {reply, log_message_int(VHost, Msg), State}; ++handle_call({rebuild_stats}, _From, #state{vhost=VHost}=State) -> ++ {atomic, ok} = delete_nonexistent_stats(VHost), ++ Reply = ++ lists:foreach(fun(Date) -> ++ rebuild_stats_at_int(VHost, Date) ++ end, get_dates_int(VHost)), ++ {reply, Reply, State}; ++handle_call({rebuild_stats_at, Date}, _From, #state{vhost=VHost}=State) -> ++ Reply = rebuild_stats_at_int(VHost, Date), ++ {reply, Reply, State}; ++handle_call({delete_messages_by_user_at, Msgs, Date}, _From, #state{vhost=VHost}=State) -> ++ Table = table_name(VHost, Date), ++ Fun = fun() -> ++ lists:foreach( ++ fun(Msg) -> ++ mnesia:write_lock_table(stats_table(VHost)), ++ mnesia:write_lock_table(Table), ++ mnesia:delete_object(Table, Msg, write) ++ end, Msgs) ++ end, ++ DRez = case mnesia:transaction(Fun) of ++ {aborted, Reason} -> ++ ?ERROR_MSG("Failed to delete_messages_by_user_at at ~p for ~p: ~p", [Date, VHost, Reason]), ++ error; ++ _ -> ++ ok ++ end, ++ Reply = ++ case rebuild_stats_at_int(VHost, Date) of ++ error -> ++ error; ++ ok -> ++ DRez ++ end, ++ {reply, Reply, State}; ++handle_call({delete_all_messages_by_user_at, User, Date}, _From, #state{vhost=VHost}=State) -> ++ {reply, delete_all_messages_by_user_at_int(User, VHost, Date), State}; ++handle_call({delete_messages_at, Date}, _From, #state{vhost=VHost}=State) -> ++ Reply = ++ case mnesia:delete_table(table_name(VHost, Date)) of ++ {atomic, ok} -> ++ delete_stats_by_vhost_at_int(VHost, Date); ++ {aborted, Reason} -> ++ ?ERROR_MSG("Failed to delete_messages_at for ~p at ~p", [VHost, Date, Reason]), ++ error ++ end, ++ {reply, Reply, State}; ++handle_call({get_vhost_stats}, _From, #state{vhost=VHost}=State) -> ++ Fun = fun(#stats{at=Date, count=Count}, Stats) -> ++ case lists:keysearch(Date, 1, Stats) of ++ false -> ++ lists:append(Stats, [{Date, Count}]); ++ {value, {_, TempCount}} -> ++ lists:keyreplace(Date, 1, Stats, {Date, TempCount+Count}) ++ end ++ end, ++ Reply = ++ case mnesia:transaction(fun() -> ++ mnesia:foldl(Fun, [], stats_table(VHost)) ++ end) of ++ {atomic, Result} -> {ok, mod_logdb:sort_stats(Result)}; ++ {aborted, Reason} -> {error, Reason} ++ end, ++ {reply, Reply, State}; ++handle_call({get_vhost_stats_at, Date}, _From, #state{vhost=VHost}=State) -> ++ Fun = fun() -> ++ Pat = #stats{user='$1', at=Date, count='$2'}, ++ mnesia:select(stats_table(VHost), [{Pat, [], [['$1', '$2']]}]) ++ end, ++ Reply = ++ case mnesia:transaction(Fun) of ++ {atomic, Result} -> ++ {ok, lists:reverse(lists:keysort(2, [{User, Count} || [User, Count] <- Result]))}; ++ {aborted, Reason} -> ++ {error, Reason} ++ end, ++ {reply, Reply, State}; ++handle_call({get_user_stats, User}, _From, #state{vhost=VHost}=State) -> ++ {reply, get_user_stats_int(User, VHost), State}; ++handle_call({get_user_messages_at, User, Date}, _From, #state{vhost=VHost}=State) -> ++ Reply = ++ case mnesia:transaction(fun() -> ++ Pat = #msg{owner_name=User, _='_'}, ++ mnesia:select(table_name(VHost, Date), ++ [{Pat, [], ['$_']}]) ++ end) of ++ {atomic, Result} -> {ok, Result}; ++ {aborted, Reason} -> ++ {error, Reason} ++ end, ++ {reply, Reply, State}; ++handle_call({get_dates}, _From, #state{vhost=VHost}=State) -> ++ {reply, get_dates_int(VHost), State}; ++handle_call({get_users_settings}, _From, #state{vhost=VHost}=State) -> ++ Reply = mnesia:dirty_match_object(settings_table(VHost), #user_settings{_='_'}), ++ {reply, {ok, Reply}, State}; ++handle_call({get_user_settings, User}, _From, #state{vhost=VHost}=State) -> ++ Reply = ++ case mnesia:dirty_match_object(settings_table(VHost), #user_settings{owner_name=User, _='_'}) of ++ [] -> []; ++ [Setting] -> ++ Setting ++ end, ++ {reply, Reply, State}; ++handle_call({set_user_settings, _User, Set}, _From, #state{vhost=VHost}=State) -> ++ ?MYDEBUG("~p~n~p", [settings_table(VHost), Set]), ++ Reply = mnesia:dirty_write(settings_table(VHost), Set), ++ ?MYDEBUG("~p", [Reply]), ++ {reply, Reply, State}; ++handle_call({drop_user, User}, _From, #state{vhost=VHost}=State) -> ++ {ok, Dates} = get_user_stats_int(User, VHost), ++ MDResult = lists:map(fun({Date, _}) -> ++ delete_all_messages_by_user_at_int(User, VHost, Date) ++ end, Dates), ++ SDResult = delete_user_settings_int(User, VHost), ++ Reply = ++ case lists:all(fun(Result) when Result == ok -> ++ true; ++ (Result) when Result == error -> ++ false ++ end, lists:append(MDResult, [SDResult])) of ++ true -> ++ ok; ++ false -> ++ error ++ end, ++ {reply, Reply, State}; ++handle_call({stop}, _From, State) -> ++ {stop, normal, ok, State}; ++handle_call(Msg, _From, State) -> ++ ?INFO_MSG("Got call Msg: ~p, State: ~p", [Msg, State]), ++ {noreply, State}. ++ ++handle_cast(Msg, State) -> ++ ?INFO_MSG("Got cast Msg:~p, State:~p", [Msg, State]), ++ {noreply, State}. ++ ++handle_info(Info, State) -> ++ ?INFO_MSG("Got Info:~p, State:~p", [Info, State]), ++ {noreply, State}. ++ ++terminate(_Reason, _State) -> ++ ok. ++ ++code_change(_OldVsn, State, _Extra) -> ++ {ok, State}. ++ ++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++% ++% gen_logdb callbacks ++% ++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++log_message(VHost, Msg) -> ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:call(Proc, {log_message, Msg}, ?CALL_TIMEOUT). ++rebuild_stats(VHost) -> ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:call(Proc, {rebuild_stats}, ?CALL_TIMEOUT). ++rebuild_stats_at(VHost, Date) -> ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:call(Proc, {rebuild_stats_at, Date}, ?CALL_TIMEOUT). ++delete_messages_by_user_at(VHost, Msgs, Date) -> ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:call(Proc, {delete_messages_by_user_at, Msgs, Date}, ?CALL_TIMEOUT). ++delete_all_messages_by_user_at(User, VHost, Date) -> ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:call(Proc, {delete_all_messages_by_user_at, User, Date}, ?CALL_TIMEOUT). ++delete_messages_at(VHost, Date) -> ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:call(Proc, {delete_messages_at, Date}, ?CALL_TIMEOUT). ++get_vhost_stats(VHost) -> ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:call(Proc, {get_vhost_stats}, ?CALL_TIMEOUT). ++get_vhost_stats_at(VHost, Date) -> ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:call(Proc, {get_vhost_stats_at, Date}, ?CALL_TIMEOUT). ++get_user_stats(User, VHost) -> ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:call(Proc, {get_user_stats, User}, ?CALL_TIMEOUT). ++get_user_messages_at(User, VHost, Date) -> ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:call(Proc, {get_user_messages_at, User, Date}, ?CALL_TIMEOUT). ++get_dates(VHost) -> ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:call(Proc, {get_dates}, ?CALL_TIMEOUT). ++get_user_settings(User, VHost) -> ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:call(Proc, {get_user_settings, User}, ?CALL_TIMEOUT). ++get_users_settings(VHost) -> ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:call(Proc, {get_users_settings}, ?CALL_TIMEOUT). ++set_user_settings(User, VHost, Set) -> ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:call(Proc, {set_user_settings, User, Set}, ?CALL_TIMEOUT). ++drop_user(User, VHost) -> ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:call(Proc, {drop_user, User}, ?CALL_TIMEOUT). ++ ++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++% ++% internals ++% ++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++log_message_int(VHost, #msg{timestamp=Timestamp}=Msg) -> ++ Date = mod_logdb:convert_timestamp_brief(Timestamp), ++ ++ ATable = table_name(VHost, Date), ++ Fun = fun() -> ++ mnesia:write_lock_table(ATable), ++ mnesia:write(ATable, Msg, write) ++ end, ++ % log message, increment stats for both users ++ case mnesia:transaction(Fun) of ++ % if table does not exists - create it and try to log message again ++ {aborted,{no_exists, _Table}} -> ++ case create_msg_table(VHost, Date) of ++ {aborted, CReason} -> ++ ?ERROR_MSG("Failed to log message: ~p", [CReason]), ++ error; ++ {atomic, ok} -> ++ ?MYDEBUG("Created msg table for ~p at ~p", [VHost, Date]), ++ log_message_int(VHost, Msg) ++ end; ++ {aborted, TReason} -> ++ ?ERROR_MSG("Failed to log message: ~p", [TReason]), ++ error; ++ {atomic, _} -> ++ ?MYDEBUG("Logged ok for ~p, peer: ~p", [Msg#msg.owner_name++"@"++VHost, ++ Msg#msg.peer_name++"@"++Msg#msg.peer_server]), ++ increment_user_stats(Msg#msg.owner_name, VHost, Date) ++ end. ++ ++increment_user_stats(Owner, VHost, Date) -> ++ Fun = fun() -> ++ Pat = #stats{user=Owner, at=Date, count='$1'}, ++ mnesia:write_lock_table(stats_table(VHost)), ++ case mnesia:select(stats_table(VHost), [{Pat, [], ['$_']}]) of ++ [] -> ++ mnesia:write(stats_table(VHost), ++ #stats{user=Owner, ++ at=Date, ++ count=1}, ++ write); ++ [Stats] -> ++ mnesia:delete_object(stats_table(VHost), ++ #stats{user=Owner, ++ at=Date, ++ count=Stats#stats.count}, ++ write), ++ New = Stats#stats{count = Stats#stats.count+1}, ++ if ++ New#stats.count > 0 -> mnesia:write(stats_table(VHost), ++ New, ++ write); ++ true -> ok ++ end ++ end ++ end, ++ case mnesia:transaction(Fun) of ++ {aborted, Reason} -> ++ ?ERROR_MSG("Failed to update stats for ~s@~s: ~p", [Owner, VHost, Reason]), ++ error; ++ {atomic, _} -> ++ ?MYDEBUG("Updated stats for ~s@~s", [Owner, VHost]), ++ ok ++ end. ++ ++get_dates_int(VHost) -> ++ Tables = mnesia:system_info(tables), ++ lists:foldl(fun(ATable, Dates) -> ++ Table = atom_to_list(ATable), ++ case regexp:match(Table, VHost++"$") of ++ {match, _, _} -> ++ case regexp:match(Table,"_[0-9]+-[0-9]+-[0-9]+_") of ++ {match, S, E} -> ++ lists:append(Dates, [lists:sublist(Table,S+1,E-2)]); ++ nomatch -> ++ Dates ++ end; ++ nomatch -> ++ Dates ++ end ++ end, [], Tables). ++ ++rebuild_stats_at_int(VHost, Date) -> ++ Table = table_name(VHost, Date), ++ STable = stats_table(VHost), ++ CFun = fun(Msg, Stats) -> ++ Owner = Msg#msg.owner_name, ++ case lists:keysearch(Owner, 1, Stats) of ++ {value, {_, Count}} -> ++ lists:keyreplace(Owner, 1, Stats, {Owner, Count + 1}); ++ false -> ++ lists:append(Stats, [{Owner, 1}]) ++ end ++ end, ++ DFun = fun(#stats{at=SDate} = Stat, _Acc) ++ when SDate == Date -> ++ mnesia:delete_object(stats_table(VHost), Stat, write); ++ (_Stat, _Acc) -> ok ++ end, ++ % TODO: Maybe unregister hooks ? ++ case mnesia:transaction(fun() -> ++ mnesia:write_lock_table(Table), ++ mnesia:write_lock_table(STable), ++ % Calc stats for VHost at Date ++ case mnesia:foldl(CFun, [], Table) of ++ [] -> empty; ++ AStats -> ++ % Delete all stats for VHost at Date ++ mnesia:foldl(DFun, [], STable), ++ % Write new calc'ed stats ++ lists:foreach(fun({Owner, Count}) -> ++ WStat = #stats{user=Owner, at=Date, count=Count}, ++ mnesia:write(stats_table(VHost), WStat, write) ++ end, AStats), ++ ok ++ end ++ end) of ++ {aborted, Reason} -> ++ ?ERROR_MSG("Failed to rebuild_stats_at for ~p at ~p: ~p", [VHost, Date, Reason]), ++ error; ++ {atomic, ok} -> ++ ok; ++ {atomic, empty} -> ++ {atomic,ok} = mnesia:delete_table(Table), ++ ?MYDEBUG("Dropped table at ~p", [Date]), ++ ok ++ end. ++ ++delete_nonexistent_stats(VHost) -> ++ Dates = get_dates_int(VHost), ++ mnesia:transaction(fun() -> ++ mnesia:foldl(fun(#stats{at=Date} = Stat, _Acc) -> ++ case lists:member(Date, Dates) of ++ false -> mnesia:delete_object(Stat); ++ true -> ok ++ end ++ end, ok, stats_table(VHost)) ++ end). ++ ++delete_stats_by_vhost_at_int(VHost, Date) -> ++ StatsDelete = fun(#stats{at=SDate} = Stat, _Acc) ++ when SDate == Date -> ++ mnesia:delete_object(stats_table(VHost), Stat, write), ++ ok; ++ (_Msg, _Acc) -> ok ++ end, ++ case mnesia:transaction(fun() -> ++ mnesia:write_lock_table(stats_table(VHost)), ++ mnesia:foldl(StatsDelete, ok, stats_table(VHost)) ++ end) of ++ {aborted, Reason} -> ++ ?ERROR_MSG("Failed to update stats at ~p for ~p: ~p", [Date, VHost, Reason]), ++ rebuild_stats_at_int(VHost, Date); ++ _ -> ++ ?INFO_MSG("Updated stats at ~p for ~p", [Date, VHost]), ++ ok ++ end. ++ ++get_user_stats_int(User, VHost) -> ++ case mnesia:transaction(fun() -> ++ Pat = #stats{user=User, at='$1', count='$2'}, ++ mnesia:select(stats_table(VHost), [{Pat, [], [['$1', '$2']]}]) ++ end) of ++ {atomic, Result} -> ++ {ok, mod_logdb:sort_stats([{Date, Count} || [Date, Count] <- Result])}; ++ {aborted, Reason} -> ++ {error, Reason} ++ end. ++ ++delete_all_messages_by_user_at_int(User, VHost, Date) -> ++ Table = table_name(VHost, Date), ++ MsgDelete = fun(#msg{owner_name=Owner} = Msg, _Acc) ++ when Owner == User -> ++ mnesia:delete_object(Table, Msg, write), ++ ok; ++ (_Msg, _Acc) -> ok ++ end, ++ DRez = case mnesia:transaction(fun() -> ++ mnesia:foldl(MsgDelete, ok, Table) ++ end) of ++ {aborted, Reason} -> ++ ?ERROR_MSG("Failed to delete_all_messages_by_user_at for ~p@~p at ~p: ~p", [User, VHost, Date, Reason]), ++ error; ++ _ -> ++ ok ++ end, ++ case rebuild_stats_at_int(VHost, Date) of ++ error -> ++ error; ++ ok -> ++ DRez ++ end. ++ ++delete_user_settings_int(User, VHost) -> ++ STable = settings_table(VHost), ++ case mnesia:dirty_match_object(STable, #user_settings{owner_name=User, _='_'}) of ++ [] -> ++ ok; ++ [UserSettings] -> ++ mnesia:dirty_delete_object(STable, UserSettings) ++ end. ++ ++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++% ++% tables internals ++% ++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++create_stats_table(VHost) -> ++ SName = stats_table(VHost), ++ case mnesia:create_table(SName, ++ [{disc_only_copies, [node()]}, ++ {type, bag}, ++ {attributes, record_info(fields, stats)}, ++ {record_name, stats} ++ ]) of ++ {atomic, ok} -> ++ ?MYDEBUG("Created stats table for ~p", [VHost]), ++ lists:foreach(fun(Date) -> ++ rebuild_stats_at_int(VHost, Date) ++ end, get_dates_int(VHost)), ++ ok; ++ {aborted, {already_exists, _}} -> ++ ?MYDEBUG("Stats table for ~p already exists", [VHost]), ++ ok; ++ {aborted, Reason} -> ++ ?ERROR_MSG("Failed to create stats table: ~p", [Reason]), ++ error ++ end. ++ ++create_settings_table(VHost) -> ++ SName = settings_table(VHost), ++ case mnesia:create_table(SName, ++ [{disc_copies, [node()]}, ++ {type, set}, ++ {attributes, record_info(fields, user_settings)}, ++ {record_name, user_settings} ++ ]) of ++ {atomic, ok} -> ++ ?MYDEBUG("Created settings table for ~p", [VHost]), ++ ok; ++ {aborted, {already_exists, _}} -> ++ ?MYDEBUG("Settings table for ~p already exists", [VHost]), ++ ok; ++ {aborted, Reason} -> ++ ?ERROR_MSG("Failed to create settings table: ~p", [Reason]), ++ error ++ end. ++ ++create_msg_table(VHost, Date) -> ++ mnesia:create_table( ++ table_name(VHost, Date), ++ [{disc_only_copies, [node()]}, ++ {type, bag}, ++ {attributes, record_info(fields, msg)}, ++ {record_name, msg}]). +--- mod_logdb_mysql.erl.orig 2009-02-05 19:21:29.000000000 +0200 ++++ mod_logdb_mysql.erl 2009-02-05 19:20:23.000000000 +0200 +@@ -0,0 +1,1052 @@ ++%%%---------------------------------------------------------------------- ++%%% File : mod_logdb_mysql.erl ++%%% Author : Oleg Palij (mailto,xmpp:o.palij@gmail.com) ++%%% Purpose : MySQL backend for mod_logdb ++%%% Version : trunk ++%%% Id : $Id: mod_logdb_mysql.erl 1253 2009-02-02 11:10:03Z malik $ ++%%% Url : http://www.dp.uz.gov.ua/o.palij/mod_logdb/ ++%%%---------------------------------------------------------------------- ++ ++-module(mod_logdb_mysql). ++-author('o.palij@gmail.com'). ++ ++-include("mod_logdb.hrl"). ++-include("ejabberd.hrl"). ++-include("jlib.hrl"). ++ ++-behaviour(gen_logdb). ++-behaviour(gen_server). ++ ++% gen_server ++-export([code_change/3,handle_call/3,handle_cast/2,handle_info/2,init/1,terminate/2]). ++% gen_mod ++-export([start/2, stop/1]). ++% gen_logdb ++-export([log_message/2, ++ rebuild_stats/1, ++ rebuild_stats_at/2, ++ delete_messages_by_user_at/3, delete_all_messages_by_user_at/3, delete_messages_at/2, ++ get_vhost_stats/1, get_vhost_stats_at/2, get_user_stats/2, get_user_messages_at/3, ++ get_dates/1, ++ get_users_settings/1, get_user_settings/2, set_user_settings/3, ++ drop_user/2]). ++ ++% gen_server call timeout ++-define(CALL_TIMEOUT, 30000). ++-define(MYSQL_TIMEOUT, 60000). ++-define(INDEX_SIZE, integer_to_list(170)). ++-define(PROCNAME, mod_logdb_mysql). ++ ++-import(mod_logdb, [list_to_bool/1, bool_to_list/1, ++ list_to_string/1, string_to_list/1, ++ convert_timestamp_brief/1]). ++ ++-record(state, {dbref, vhost, server, port, db, user, password}). ++ ++% replace "." with "_" ++escape_vhost(VHost) -> lists:map(fun(46) -> 95; ++ (A) -> A ++ end, VHost). ++prefix() -> ++ "`logdb_". ++ ++suffix(VHost) -> ++ "_" ++ escape_vhost(VHost) ++ "`". ++ ++messages_table(VHost, Date) -> ++ prefix() ++ "messages_" ++ Date ++ suffix(VHost). ++ ++stats_table(VHost) -> ++ prefix() ++ "stats" ++ suffix(VHost). ++ ++temp_table(VHost) -> ++ prefix() ++ "temp" ++ suffix(VHost). ++ ++settings_table(VHost) -> ++ prefix() ++ "settings" ++ suffix(VHost). ++ ++users_table(VHost) -> ++ prefix() ++ "users" ++ suffix(VHost). ++servers_table(VHost) -> ++ prefix() ++ "servers" ++ suffix(VHost). ++resources_table(VHost) -> ++ prefix() ++ "resources" ++ suffix(VHost). ++ ++ets_users_table(VHost) -> list_to_atom("logdb_users_" ++ VHost). ++ets_servers_table(VHost) -> list_to_atom("logdb_servers_" ++ VHost). ++ets_resources_table(VHost) -> list_to_atom("logdb_resources_" ++ VHost). ++ ++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++% ++% gen_mod callbacks ++% ++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++start(VHost, Opts) -> ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:start({local, Proc}, ?MODULE, [VHost, Opts], []). ++ ++stop(VHost) -> ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:call(Proc, {stop}, ?CALL_TIMEOUT). ++ ++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++% ++% gen_server callbacks ++% ++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++init([VHost, Opts]) -> ++ crypto:start(), ++ ++ Server = gen_mod:get_opt(server, Opts, "localhost"), ++ Port = gen_mod:get_opt(port, Opts, 3306), ++ DB = gen_mod:get_opt(db, Opts, "logdb"), ++ User = gen_mod:get_opt(user, Opts, "root"), ++ Password = gen_mod:get_opt(password, Opts, ""), ++ ++ St = #state{vhost=VHost, ++ server=Server, port=Port, db=DB, ++ user=User, password=Password}, ++ ++ case open_mysql_connection(St) of ++ {ok, DBRef} -> ++ State = St#state{dbref=DBRef}, ++ ok = create_stats_table(State), ++ ok = create_settings_table(State), ++ ok = create_users_table(State), ++ % clear ets cache every ... ++ timer:send_interval(timer:hours(12), clear_ets_tables), ++ ok = create_servers_table(State), ++ ok = create_resources_table(State), ++ erlang:monitor(process, DBRef), ++ {ok, State}; ++ {error, Reason} -> ++ ?ERROR_MSG("MySQL connection failed: ~p~n", [Reason]), ++ {stop, db_connection_failed} ++ end. ++ ++open_mysql_connection(#state{server=Server, port=Port, db=DB, ++ user=DBUser, password=Password} = _State) -> ++ LogFun = fun(debug, _Format, _Argument) -> ++ %?MYDEBUG(Format, Argument); ++ ok; ++ (error, Format, Argument) -> ++ ?ERROR_MSG(Format, Argument); ++ (Level, Format, Argument) -> ++ ?MYDEBUG("MySQL (~p)~n", [Level]), ++ ?MYDEBUG(Format, Argument) ++ end, ++ mysql_conn:start(Server, Port, DBUser, Password, DB, LogFun). ++ ++close_mysql_connection(DBRef) -> ++ ?MYDEBUG("Closing ~p mysql connection", [DBRef]), ++ mysql_conn:stop(DBRef). ++ ++handle_call({log_message, Msg}, _From, #state{dbref=DBRef, vhost=VHost}=State) -> ++ Date = convert_timestamp_brief(Msg#msg.timestamp), ++ ++ Table = messages_table(VHost, Date), ++ Owner_id = get_user_id(DBRef, VHost, Msg#msg.owner_name), ++ Peer_name_id = get_user_id(DBRef, VHost, Msg#msg.peer_name), ++ Peer_server_id = get_server_id(DBRef, VHost, Msg#msg.peer_server), ++ Peer_resource_id = get_resource_id(DBRef, VHost, Msg#msg.peer_resource), ++ ++ Query = ["INSERT INTO ",Table," ", ++ "(owner_id,", ++ "peer_name_id,", ++ "peer_server_id,", ++ "peer_resource_id,", ++ "direction,", ++ "type,", ++ "subject,", ++ "body,", ++ "timestamp) ", ++ "VALUES ", ++ "('", Owner_id, "',", ++ "'", Peer_name_id, "',", ++ "'", Peer_server_id, "',", ++ "'", Peer_resource_id, "',", ++ "'", atom_to_list(Msg#msg.direction), "',", ++ "'", Msg#msg.type, "',", ++ "'", ejabberd_odbc:escape(Msg#msg.subject), "',", ++ "'", ejabberd_odbc:escape(Msg#msg.body), "',", ++ "'", Msg#msg.timestamp, "');"], ++ ++ Reply = ++ case sql_query_internal_silent(DBRef, Query) of ++ {updated, _} -> ++ ?MYDEBUG("Logged ok for ~p, peer: ~p", [Msg#msg.owner_name++"@"++VHost, ++ Msg#msg.peer_name++"@"++Msg#msg.peer_server]), ++ increment_user_stats(DBRef, Msg#msg.owner_name, Owner_id, VHost, Peer_name_id, Peer_server_id, Date); ++ {error, Reason} -> ++ case regexp:match(Reason, "#42S02") of ++ % Table doesn't exist ++ {match, _, _} -> ++ case create_msg_table(DBRef, VHost, Date) of ++ error -> ++ error; ++ ok -> ++ {updated, _} = sql_query_internal(DBRef, Query), ++ increment_user_stats(DBRef, Msg#msg.owner_name, Owner_id, VHost, Peer_name_id, Peer_server_id, Date) ++ end; ++ _ -> ++ ?ERROR_MSG("Failed to log message: ~p", [Reason]), ++ error ++ end ++ end, ++ {reply, Reply, State}; ++handle_call({rebuild_stats_at, Date}, _From, #state{dbref=DBRef, vhost=VHost}=State) -> ++ Reply = rebuild_stats_at_int(DBRef, VHost, Date), ++ {reply, Reply, State}; ++handle_call({delete_messages_by_user_at, [], _Date}, _From, State) -> ++ {reply, error, State}; ++handle_call({delete_messages_by_user_at, Msgs, Date}, _From, #state{dbref=DBRef, vhost=VHost}=State) -> ++ Temp = lists:flatmap(fun(#msg{timestamp=Timestamp} = _Msg) -> ++ ["\"",Timestamp,"\"",","] ++ end, Msgs), ++ ++ Temp1 = lists:append([lists:sublist(Temp, length(Temp)-1), ");"]), ++ ++ Query = ["DELETE FROM ",messages_table(VHost, Date)," ", ++ "WHERE timestamp IN (", Temp1], ++ ++ Reply = ++ case sql_query_internal(DBRef, Query) of ++ {updated, Aff} -> ++ ?MYDEBUG("Aff=~p", [Aff]), ++ rebuild_stats_at_int(DBRef, VHost, Date); ++ {error, _} -> ++ error ++ end, ++ {reply, Reply, State}; ++handle_call({delete_all_messages_by_user_at, User, Date}, _From, #state{dbref=DBRef, vhost=VHost}=State) -> ++ ok = delete_all_messages_by_user_at_int(DBRef, User, VHost, Date), ++ ok = delete_stats_by_user_at_int(DBRef, User, VHost, Date), ++ {reply, ok, State}; ++handle_call({delete_messages_at, Date}, _From, #state{dbref=DBRef, vhost=VHost}=State) -> ++ Reply = ++ case sql_query_internal(DBRef, ["DROP TABLE ",messages_table(VHost, Date),";"]) of ++ {updated, _} -> ++ Query = ["DELETE FROM ",stats_table(VHost)," " ++ "WHERE at=\"",Date,"\";"], ++ case sql_query_internal(DBRef, Query) of ++ {updated, _} -> ++ ok; ++ {error, _} -> ++ error ++ end; ++ {error, _} -> ++ error ++ end, ++ {reply, Reply, State}; ++handle_call({get_vhost_stats}, _From, #state{dbref=DBRef, vhost=VHost}=State) -> ++ SName = stats_table(VHost), ++ Query = ["SELECT at, sum(count) ", ++ "FROM ",SName," ", ++ "GROUP BY at ", ++ "ORDER BY DATE(at) DESC;" ++ ], ++ Reply = ++ case sql_query_internal(DBRef, Query) of ++ {data, Result} -> ++ {ok, [ {Date, list_to_integer(Count)} || [Date, Count] <- Result ]}; ++ {error, Reason} -> ++ % TODO: Duplicate error message ? ++ {error, Reason} ++ end, ++ {reply, Reply, State}; ++handle_call({get_vhost_stats_at, Date}, _From, #state{dbref=DBRef, vhost=VHost}=State) -> ++ SName = stats_table(VHost), ++ Query = ["SELECT username, sum(count) AS allcount ", ++ "FROM ",SName," ", ++ "JOIN ",users_table(VHost)," ON owner_id=user_id " ++ "WHERE at=\"",Date,"\" " ++ "GROUP BY username ", ++ "ORDER BY allcount DESC;" ++ ], ++ Reply = ++ case sql_query_internal(DBRef, Query) of ++ {data, Result} -> ++ {ok, lists:reverse( ++ lists:keysort(2, ++ [ {User, list_to_integer(Count)} || [User, Count] <- Result]))}; ++ {error, Reason} -> ++ % TODO: ++ {error, Reason} ++ end, ++ {reply, Reply, State}; ++handle_call({get_user_stats, User}, _From, #state{dbref=DBRef, vhost=VHost}=State) -> ++ {reply, get_user_stats_int(DBRef, User, VHost), State}; ++handle_call({get_user_messages_at, User, Date}, _From, #state{dbref=DBRef, vhost=VHost}=State) -> ++ TName = messages_table(VHost, Date), ++ UName = users_table(VHost), ++ SName = servers_table(VHost), ++ RName = resources_table(VHost), ++ Query = ["SELECT users.username,", ++ "servers.server,", ++ "resources.resource,", ++ "messages.direction," ++ "messages.type," ++ "messages.subject," ++ "messages.body," ++ "messages.timestamp " ++ "FROM ",TName," AS messages " ++ "JOIN ",UName," AS users ON peer_name_id=user_id ", ++ "JOIN ",SName," AS servers ON peer_server_id=server_id ", ++ "JOIN ",RName," AS resources ON peer_resource_id=resource_id ", ++ "WHERE owner_id=\"",get_user_id(DBRef, VHost, User),"\" ", ++ "ORDER BY timestamp ASC;"], ++ Reply = ++ case sql_query_internal(DBRef, Query) of ++ {data, Result} -> ++ Fun = fun([Peer_name, Peer_server, Peer_resource, ++ Direction, ++ Type, ++ Subject, Body, ++ Timestamp]) -> ++ #msg{peer_name=Peer_name, peer_server=Peer_server, peer_resource=Peer_resource, ++ direction=list_to_atom(Direction), ++ type=Type, ++ subject=Subject, body=Body, ++ timestamp=Timestamp} ++ end, ++ {ok, lists:map(Fun, Result)}; ++ {error, Reason} -> ++ {error, Reason} ++ end, ++ {reply, Reply, State}; ++handle_call({get_dates}, _From, #state{dbref=DBRef, vhost=VHost}=State) -> ++ SName = stats_table(VHost), ++ Query = ["SELECT at ", ++ "FROM ",SName," ", ++ "GROUP BY at ", ++ "ORDER BY DATE(at) DESC;" ++ ], ++ Reply = ++ case sql_query_internal(DBRef, Query) of ++ {data, Result} -> ++ [ Date || [Date] <- Result ]; ++ {error, Reason} -> ++ {error, Reason} ++ end, ++ {reply, Reply, State}; ++handle_call({get_users_settings}, _From, #state{dbref=DBRef, vhost=VHost}=State) -> ++ Query = ["SELECT username,dolog_default,dolog_list,donotlog_list ", ++ "FROM ",settings_table(VHost)," ", ++ "JOIN ",users_table(VHost)," ON user_id=owner_id;"], ++ Reply = ++ case sql_query_internal(DBRef, Query) of ++ {data, Result} -> ++ {ok, lists:map(fun([Owner, DoLogDef, DoLogL, DoNotLogL]) -> ++ #user_settings{owner_name=Owner, ++ dolog_default=list_to_bool(DoLogDef), ++ dolog_list=string_to_list(DoLogL), ++ donotlog_list=string_to_list(DoNotLogL) ++ } ++ end, Result)}; ++ {error, _} -> ++ error ++ end, ++ {reply, Reply, State}; ++handle_call({get_user_settings, User}, _From, #state{dbref=DBRef, vhost=VHost}=State) -> ++ Query = ["SELECT dolog_default,dolog_list,donotlog_list FROM ",settings_table(VHost)," ", ++ "WHERE owner_id=\"",get_user_id(DBRef, VHost, User),"\";"], ++ Reply = ++ case sql_query_internal(DBRef, Query) of ++ {data, []} -> ++ {ok, []}; ++ {data, [[Owner, DoLogDef, DoLogL, DoNotLogL]]} -> ++ {ok, #user_settings{owner_name=Owner, ++ dolog_default=list_to_bool(DoLogDef), ++ dolog_list=string_to_list(DoLogL), ++ donotlog_list=string_to_list(DoNotLogL)}}; ++ {error, _} -> ++ error ++ end, ++ {reply, Reply, State}; ++handle_call({set_user_settings, User, #user_settings{dolog_default=DoLogDef, ++ dolog_list=DoLogL, ++ donotlog_list=DoNotLogL}}, ++ _From, #state{dbref=DBRef, vhost=VHost} = State) -> ++ User_id = get_user_id(DBRef, VHost, User), ++ ++ Query = ["UPDATE ",settings_table(VHost)," ", ++ "SET dolog_default=",bool_to_list(DoLogDef),", ", ++ "dolog_list='",list_to_string(DoLogL),"', ", ++ "donotlog_list='",list_to_string(DoNotLogL),"' ", ++ "WHERE owner_id=\"",User_id,"\";"], ++ ++ Reply = ++ case sql_query_internal(DBRef, Query) of ++ {updated, 0} -> ++ IQuery = ["INSERT INTO ",settings_table(VHost)," ", ++ "(owner_id, dolog_default, dolog_list, donotlog_list) ", ++ "VALUES ", ++ "('",User_id,"', ",bool_to_list(DoLogDef),",'",list_to_string(DoLogL),"','",list_to_string(DoNotLogL),"');"], ++ case sql_query_internal_silent(DBRef, IQuery) of ++ {updated, _} -> ++ ?MYDEBUG("New settings for ~s@~s", [User, VHost]), ++ ok; ++ {error, Reason} -> ++ case regexp:match(Reason, "#23000") of ++ % Already exists ++ {match, _, _} -> ++ ok; ++ _ -> ++ ?ERROR_MSG("Failed setup user ~p@~p: ~p", [User, VHost, Reason]), ++ error ++ end ++ end; ++ {updated, 1} -> ++ ?MYDEBUG("Updated settings for ~s@~s", [User, VHost]), ++ ok; ++ {error, _} -> ++ error ++ end, ++ {reply, Reply, State}; ++handle_call({stop}, _From, #state{vhost=VHost}=State) -> ++ ets:delete(ets_users_table(VHost)), ++ ets:delete(ets_servers_table(VHost)), ++ ?MYDEBUG("Stoping mysql backend for ~p", [VHost]), ++ {stop, normal, ok, State}; ++handle_call(Msg, _From, State) -> ++ ?INFO_MSG("Got call Msg: ~p, State: ~p", [Msg, State]), ++ {noreply, State}. ++ ++handle_cast({rebuild_stats}, State) -> ++ rebuild_all_stats_int(State), ++ {noreply, State}; ++handle_cast({drop_user, User}, #state{vhost=VHost} = State) -> ++ Fun = fun() -> ++ {ok, DBRef} = open_mysql_connection(State), ++ {ok, Dates} = get_user_stats_int(DBRef, User, VHost), ++ MDResult = lists:map(fun({Date, _}) -> ++ delete_all_messages_by_user_at_int(DBRef, User, VHost, Date) ++ end, Dates), ++ StDResult = delete_all_stats_by_user_int(DBRef, User, VHost), ++ SDResult = delete_user_settings_int(DBRef, User, VHost), ++ case lists:all(fun(Result) when Result == ok -> ++ true; ++ (Result) when Result == error -> ++ false ++ end, lists:append([MDResult, [StDResult], [SDResult]])) of ++ true -> ++ ?INFO_MSG("Removed ~s@~s", [User, VHost]); ++ false -> ++ ?ERROR_MSG("Failed to remove ~s@~s", [User, VHost]) ++ end, ++ close_mysql_connection(DBRef) ++ end, ++ spawn(Fun), ++ {noreply, State}; ++handle_cast(Msg, State) -> ++ ?INFO_MSG("Got cast Msg:~p, State:~p", [Msg, State]), ++ {noreply, State}. ++ ++handle_info(clear_ets_tables, State) -> ++ ets:delete_all_objects(ets_users_table(State#state.vhost)), ++ ets:delete_all_objects(ets_resources_table(State#state.vhost)), ++ {noreply, State}; ++handle_info({'DOWN', _MonitorRef, process, _Pid, _Info}, State) -> ++ {stop, connection_dropped, State}; ++handle_info(Info, State) -> ++ ?INFO_MSG("Got Info:~p, State:~p", [Info, State]), ++ {noreply, State}. ++ ++terminate(_Reason, #state{dbref=DBRef}=_State) -> ++ close_mysql_connection(DBRef), ++ ok. ++ ++code_change(_OldVsn, State, _Extra) -> ++ {ok, State}. ++ ++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++% ++% gen_logdb callbacks ++% ++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++log_message(VHost, Msg) -> ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:call(Proc, {log_message, Msg}, ?CALL_TIMEOUT). ++rebuild_stats(VHost) -> ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:cast(Proc, {rebuild_stats}). ++rebuild_stats_at(VHost, Date) -> ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:call(Proc, {rebuild_stats_at, Date}, ?CALL_TIMEOUT). ++delete_messages_by_user_at(VHost, Msgs, Date) -> ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:call(Proc, {delete_messages_by_user_at, Msgs, Date}, ?CALL_TIMEOUT). ++delete_all_messages_by_user_at(User, VHost, Date) -> ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:call(Proc, {delete_all_messages_by_user_at, User, Date}, ?CALL_TIMEOUT). ++delete_messages_at(VHost, Date) -> ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:call(Proc, {delete_messages_at, Date}, ?CALL_TIMEOUT). ++get_vhost_stats(VHost) -> ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:call(Proc, {get_vhost_stats}, ?CALL_TIMEOUT). ++get_vhost_stats_at(VHost, Date) -> ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:call(Proc, {get_vhost_stats_at, Date}, ?CALL_TIMEOUT). ++get_user_stats(User, VHost) -> ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:call(Proc, {get_user_stats, User}, ?CALL_TIMEOUT). ++get_user_messages_at(User, VHost, Date) -> ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:call(Proc, {get_user_messages_at, User, Date}, ?CALL_TIMEOUT). ++get_dates(VHost) -> ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:call(Proc, {get_dates}, ?CALL_TIMEOUT). ++get_users_settings(VHost) -> ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:call(Proc, {get_users_settings}, ?CALL_TIMEOUT). ++get_user_settings(User, VHost) -> ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:call(Proc, {get_user_settings, User}, ?CALL_TIMEOUT). ++set_user_settings(User, VHost, Set) -> ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:call(Proc, {set_user_settings, User, Set}, ?CALL_TIMEOUT). ++drop_user(User, VHost) -> ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:cast(Proc, {drop_user, User}). ++ ++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++% ++% internals ++% ++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++increment_user_stats(DBRef, User_name, User_id, VHost, PNameID, PServerID, Date) -> ++ SName = stats_table(VHost), ++ UQuery = ["UPDATE ",SName," ", ++ "SET count=count+1 ", ++ "WHERE owner_id=\"",User_id,"\" AND peer_name_id=\"",PNameID,"\" AND peer_server_id=\"",PServerID,"\" AND at=\"",Date,"\";"], ++ ++ case sql_query_internal(DBRef, UQuery) of ++ {updated, 0} -> ++ IQuery = ["INSERT INTO ",SName," ", ++ "(owner_id, peer_name_id, peer_server_id, at, count) ", ++ "VALUES ", ++ "('",User_id,"', '",PNameID,"', '",PServerID,"', '",Date,"', '1');"], ++ case sql_query_internal(DBRef, IQuery) of ++ {updated, _} -> ++ ?MYDEBUG("New stats for ~s@~s at ~s", [User_name, VHost, Date]), ++ ok; ++ {error, _} -> ++ error ++ end; ++ {updated, _} -> ++ ?MYDEBUG("Updated stats for ~s@~s at ~s", [User_name, VHost, Date]), ++ ok; ++ {error, _} -> ++ error ++ end. ++ ++get_dates_int(DBRef, VHost) -> ++ case sql_query_internal(DBRef, ["SHOW TABLES"]) of ++ {data, Tables} -> ++ lists:foldl(fun([Table], Dates) -> ++ Reg = lists:sublist(prefix(),2,length(prefix())) ++ ".*" ++ escape_vhost(VHost), ++ case regexp:match(Table, Reg) of ++ {match, 1, _} -> ++ ?MYDEBUG("matched ~p against ~p", [Table, Reg]), ++ case regexp:match(Table,"[0-9]+-[0-9]+-[0-9]+") of ++ {match, S, E} -> ++ lists:append(Dates, [lists:sublist(Table,S,E)]); ++ nomatch -> ++ Dates ++ end; ++ ++ _ -> ++ Dates ++ end ++ end, [], Tables); ++ {error, _} -> ++ [] ++ end. ++ ++rebuild_all_stats_int(#state{vhost=VHost}=State) -> ++ Fun = fun() -> ++ {ok, DBRef} = open_mysql_connection(State), ++ ok = delete_nonexistent_stats(DBRef, VHost), ++ case lists:filter(fun(Date) -> ++ case catch rebuild_stats_at_int(DBRef, VHost, Date) of ++ ok -> false; ++ error -> true; ++ {'EXIT', _} -> true ++ end ++ end, get_dates_int(DBRef, VHost)) of ++ [] -> ok; ++ FTables -> ++ ?ERROR_MSG("Failed to rebuild stats for ~p dates", [FTables]), ++ error ++ end, ++ close_mysql_connection(DBRef) ++ end, ++ spawn(Fun). ++ ++rebuild_stats_at_int(DBRef, VHost, Date) -> ++ TempTable = temp_table(VHost), ++ Fun = fun() -> ++ Table = messages_table(VHost, Date), ++ STable = stats_table(VHost), ++ ++ DQuery = [ "DELETE FROM ",STable," ", ++ "WHERE at='",Date,"';"], ++ ++ ok = create_temp_table(DBRef, TempTable), ++ {updated, _} = sql_query_internal(DBRef, ["LOCK TABLE ",Table," WRITE, ",TempTable," WRITE;"]), ++ SQuery = ["INSERT INTO ",TempTable," ", ++ "(owner_id,peer_name_id,peer_server_id,at,count) ", ++ "SELECT owner_id,peer_name_id,peer_server_id,\"",Date,"\",count(*) ", ++ "FROM ",Table," GROUP BY owner_id,peer_name_id,peer_server_id;"], ++ case sql_query_internal(DBRef, SQuery) of ++ {updated, 0} -> ++ Count = sql_query_internal(DBRef, ["SELECT count(*) FROM ",Table,";"]), ++ case Count of ++ {data, [["0"]]} -> ++ {updated, _} = sql_query_internal(DBRef, ["DROP TABLE ",Table,";"]), ++ {updated, _} = sql_query_internal(DBRef, ["LOCK TABLE ",STable," WRITE;"]), ++ {updated, _} = sql_query_internal(DBRef, DQuery), ++ ok; ++ _ -> ++ ?ERROR_MSG("Failed to calculate stats for ~s table! Count was ~p.", [Date, Count]), ++ error ++ end; ++ {updated, _} -> ++ {updated, _} = sql_query_internal(DBRef, ["LOCK TABLE ",STable," WRITE, ",TempTable," WRITE;"]), ++ {updated, _} = sql_query_internal(DBRef, DQuery), ++ SQuery1 = ["INSERT INTO ",STable," ", ++ "(owner_id,peer_name_id,peer_server_id,at,count) ", ++ "SELECT owner_id,peer_name_id,peer_server_id,at,count ", ++ "FROM ",TempTable,";"], ++ case sql_query_internal(DBRef, SQuery1) of ++ {updated, _} -> ok; ++ {error, _} -> error ++ end; ++ {error, _} -> error ++ end ++ end, ++ ++ case catch apply(Fun, []) of ++ ok -> ++ ?INFO_MSG("Rebuilded stats for ~p at ~p", [VHost, Date]), ++ ok; ++ error -> ++ error; ++ {'EXIT', Reason} -> ++ ?ERROR_MSG("Failed to rebuild stats for ~s table: ~p.", [Date, Reason]), ++ error ++ end, ++ sql_query_internal(DBRef, ["UNLOCK TABLES;"]), ++ sql_query_internal(DBRef, ["DROP TABLE ",TempTable,";"]), ++ ok. ++ ++ ++delete_nonexistent_stats(DBRef, VHost) -> ++ Dates = get_dates_int(DBRef, VHost), ++ STable = stats_table(VHost), ++ ++ Temp = lists:flatmap(fun(Date) -> ++ ["\"",Date,"\"",","] ++ end, Dates), ++ ++ case Temp of ++ [] -> ++ ok; ++ _ -> ++ % replace last "," with ");" ++ Temp1 = lists:append([lists:sublist(Temp, length(Temp)-1), ");"]), ++ Query = ["DELETE FROM ",STable," ", ++ "WHERE at NOT IN (", Temp1], ++ case sql_query_internal(DBRef, Query) of ++ {updated, _} -> ++ ok; ++ {error, _} -> ++ error ++ end ++ end. ++ ++get_user_stats_int(DBRef, User, VHost) -> ++ SName = stats_table(VHost), ++ Query = ["SELECT at, sum(count) as allcount ", ++ "FROM ",SName," ", ++ "WHERE owner_id=\"",get_user_id(DBRef, VHost, User),"\" ", ++ "GROUP BY at " ++ "ORDER BY DATE(at) DESC;" ++ ], ++ case sql_query_internal(DBRef, Query) of ++ {data, Result} -> ++ {ok, [ {Date, list_to_integer(Count)} || [Date, Count] <- Result]}; ++ {error, Result} -> ++ {error, Result} ++ end. ++ ++delete_all_messages_by_user_at_int(DBRef, User, VHost, Date) -> ++ DQuery = ["DELETE FROM ",messages_table(VHost, Date)," ", ++ "WHERE owner_id=(SELECT user_id FROM ",users_table(VHost)," WHERE username=\"",User,"\");"], ++ case sql_query_internal(DBRef, DQuery) of ++ {updated, _} -> ++ ?INFO_MSG("Dropped messages for ~s@~s at ~s", [User, VHost, Date]), ++ ok; ++ {error, _} -> ++ error ++ end. ++ ++delete_all_stats_by_user_int(DBRef, User, VHost) -> ++ SQuery = ["DELETE FROM ",stats_table(VHost)," ", ++ "WHERE owner_id=(SELECT user_id FROM ",users_table(VHost)," WHERE username=\"",User,"\");"], ++ case sql_query_internal(DBRef, SQuery) of ++ {updated, _} -> ++ ?INFO_MSG("Dropped all stats for ~s@~s", [User, VHost]), ++ ok; ++ {error, _} -> error ++ end. ++ ++delete_stats_by_user_at_int(DBRef, User, VHost, Date) -> ++ SQuery = ["DELETE FROM ",stats_table(VHost)," ", ++ "WHERE owner_id=(SELECT user_id FROM ",users_table(VHost)," WHERE username=\"",User,"\") ", ++ "AND at=\"",Date,"\";"], ++ case sql_query_internal(DBRef, SQuery) of ++ {updated, _} -> ++ ?INFO_MSG("Dropped stats for ~s@~s at ~s", [User, VHost, Date]), ++ ok; ++ {error, _} -> error ++ end. ++ ++delete_user_settings_int(DBRef, User, VHost) -> ++ Query = ["DELETE FROM ",settings_table(VHost)," ", ++ "WHERE owner_id=(SELECT user_id FROM ",users_table(VHost)," WHERE username=\"",User,"\");"], ++ case sql_query_internal(DBRef, Query) of ++ {updated, _} -> ++ ?INFO_MSG("Dropped ~s@~s settings", [User, VHost]), ++ ok; ++ {error, Reason} -> ++ ?ERROR_MSG("Failed to drop ~s@~s settings: ~p", [User, VHost, Reason]), ++ error ++ end. ++ ++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++% ++% tables internals ++% ++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++create_temp_table(DBRef, Name) -> ++ Query = ["CREATE TABLE ",Name," (", ++ "owner_id MEDIUMINT UNSIGNED, ", ++ "peer_name_id MEDIUMINT UNSIGNED, ", ++ "peer_server_id MEDIUMINT UNSIGNED, ", ++ "at VARCHAR(11), ", ++ "count INT(11) ", ++ ") ENGINE=MyISAM CHARACTER SET utf8;" ++ ], ++ case sql_query_internal(DBRef, Query) of ++ {updated, _} -> ok; ++ {error, _Reason} -> error ++ end. ++ ++create_stats_table(#state{dbref=DBRef, vhost=VHost}=State) -> ++ SName = stats_table(VHost), ++ Query = ["CREATE TABLE ",SName," (", ++ "owner_id MEDIUMINT UNSIGNED, ", ++ "peer_name_id MEDIUMINT UNSIGNED, ", ++ "peer_server_id MEDIUMINT UNSIGNED, ", ++ "at varchar(20), ", ++ "count int(11), ", ++ "INDEX(owner_id, peer_name_id, peer_server_id), ", ++ "INDEX(at)" ++ ") ENGINE=InnoDB CHARACTER SET utf8;" ++ ], ++ case sql_query_internal_silent(DBRef, Query) of ++ {updated, _} -> ++ ?INFO_MSG("Created stats table for ~p", [VHost]), ++ rebuild_all_stats_int(State), ++ ok; ++ {error, Reason} -> ++ case regexp:match(Reason, "#42S01") of ++ {match, _, _} -> ++ ?MYDEBUG("Stats table for ~p already exists", [VHost]), ++ CheckQuery = ["SHOW COLUMNS FROM ",SName," LIKE 'peer_%_id';"], ++ case sql_query_internal(DBRef, CheckQuery) of ++ {data, Elems} when length(Elems) == 2 -> ++ ?MYDEBUG("Stats table structure is ok", []), ++ ok; ++ _ -> ++ ?INFO_MSG("It seems like stats table structure is invalid. I will drop it and recreate", []), ++ case sql_query_internal(DBRef, ["DROP TABLE ",SName,";"]) of ++ {updated, _} -> ++ ?INFO_MSG("Successfully dropped ~p", [SName]); ++ _ -> ++ ?ERROR_MSG("Failed to drop ~p. You should drop it and restart module", [SName]) ++ end, ++ error ++ end; ++ _ -> ++ ?ERROR_MSG("Failed to create stats table for ~p: ~p", [VHost, Reason]), ++ error ++ end ++ end. ++ ++create_settings_table(#state{dbref=DBRef, vhost=VHost}) -> ++ SName = settings_table(VHost), ++ Query = ["CREATE TABLE IF NOT EXISTS ",SName," (", ++ "owner_id MEDIUMINT UNSIGNED PRIMARY KEY, ", ++ "dolog_default TINYINT(1) NOT NULL DEFAULT 1, ", ++ "dolog_list TEXT, ", ++ "donotlog_list TEXT ", ++ ") ENGINE=InnoDB CHARACTER SET utf8;" ++ ], ++ case sql_query_internal(DBRef, Query) of ++ {updated, _} -> ++ ?MYDEBUG("Created settings table for ~p", [VHost]), ++ ok; ++ {error, _} -> ++ error ++ end. ++ ++create_users_table(#state{dbref=DBRef, vhost=VHost}) -> ++ SName = users_table(VHost), ++ Query = ["CREATE TABLE IF NOT EXISTS ",SName," (", ++ "username TEXT NOT NULL, ", ++ "user_id MEDIUMINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE, ", ++ "UNIQUE INDEX(username(",?INDEX_SIZE,")) ", ++ ") ENGINE=InnoDB CHARACTER SET utf8;" ++ ], ++ case sql_query_internal(DBRef, Query) of ++ {updated, _} -> ++ ?MYDEBUG("Created users table for ~p", [VHost]), ++ ets:new(ets_users_table(VHost), [named_table, set, public]), ++ %update_users_from_db(DBRef, VHost), ++ ok; ++ {error, _} -> ++ error ++ end. ++ ++create_servers_table(#state{dbref=DBRef, vhost=VHost}) -> ++ SName = servers_table(VHost), ++ Query = ["CREATE TABLE IF NOT EXISTS ",SName," (", ++ "server TEXT NOT NULL, ", ++ "server_id MEDIUMINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE, ", ++ "UNIQUE INDEX(server(",?INDEX_SIZE,")) ", ++ ") ENGINE=InnoDB CHARACTER SET utf8;" ++ ], ++ case sql_query_internal(DBRef, Query) of ++ {updated, _} -> ++ ?MYDEBUG("Created servers table for ~p", [VHost]), ++ ets:new(ets_servers_table(VHost), [named_table, set, public]), ++ update_servers_from_db(DBRef, VHost), ++ ok; ++ {error, _} -> ++ error ++ end. ++ ++create_resources_table(#state{dbref=DBRef, vhost=VHost}) -> ++ RName = resources_table(VHost), ++ Query = ["CREATE TABLE IF NOT EXISTS ",RName," (", ++ "resource TEXT NOT NULL, ", ++ "resource_id MEDIUMINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE, ", ++ "UNIQUE INDEX(resource(",?INDEX_SIZE,")) ", ++ ") ENGINE=InnoDB CHARACTER SET utf8;" ++ ], ++ case sql_query_internal(DBRef, Query) of ++ {updated, _} -> ++ ?MYDEBUG("Created resources table for ~p", [VHost]), ++ ets:new(ets_resources_table(VHost), [named_table, set, public]), ++ ok; ++ {error, _} -> ++ error ++ end. ++ ++create_msg_table(DBRef, VHost, Date) -> ++ TName = messages_table(VHost, Date), ++ Query = ["CREATE TABLE ",TName," (", ++ "owner_id MEDIUMINT UNSIGNED, ", ++ "peer_name_id MEDIUMINT UNSIGNED, ", ++ "peer_server_id MEDIUMINT UNSIGNED, ", ++ "peer_resource_id MEDIUMINT(8) UNSIGNED, ", ++ "direction ENUM('to', 'from'), ", ++ "type ENUM('chat','error','groupchat','headline','normal') NOT NULL, ", ++ "subject TEXT, ", ++ "body TEXT, ", ++ "timestamp DOUBLE, ", ++ "INDEX search_i (owner_id, peer_name_id, peer_server_id, peer_resource_id), ", ++ "FULLTEXT (body) " ++ ") ENGINE=MyISAM CHARACTER SET utf8;" ++ ], ++ case sql_query_internal(DBRef, Query) of ++ {updated, _MySQLRes} -> ++ ?MYDEBUG("Created msg table for ~p at ~p", [VHost, Date]), ++ ok; ++ {error, _} -> ++ error ++ end. ++ ++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++% ++% internal ets cache (users, servers, resources) ++% ++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++update_servers_from_db(DBRef, VHost) -> ++ ?INFO_MSG("Reading servers from db for ~p", [VHost]), ++ SQuery = ["SELECT server, server_id FROM ",servers_table(VHost),";"], ++ {data, Result} = sql_query_internal(DBRef, SQuery), ++ true = ets:delete_all_objects(ets_servers_table(VHost)), ++ true = ets:insert(ets_servers_table(VHost), [ {Server, Server_id} || [Server, Server_id] <- Result]). ++ ++%update_users_from_db(DBRef, VHost) -> ++% ?INFO_MSG("Reading users from db for ~p", [VHost]), ++% SQuery = ["SELECT username, user_id FROM ",users_table(VHost),";"], ++% {data, Result} = sql_query_internal(DBRef, SQuery), ++% true = ets:delete_all_objects(ets_users_table(VHost)), ++% true = ets:insert(ets_users_table(VHost), [ {Username, User_id} || [Username, User_id] <- Result]). ++ ++%get_user_name(DBRef, VHost, User_id) -> ++% case ets:match(ets_users_table(VHost), {'$1', User_id}) of ++% [[User]] -> User; ++% % this can be in clustered environment ++% [] -> ++% %update_users_from_db(DBRef, VHost), ++% SQuery = ["SELECT username FROM ",users_table(VHost)," ", ++% "WHERE user_id=\"",User_id,"\";"], ++% {data, [[Name]]} = sql_query_internal(DBRef, SQuery), ++% % cache {user, id} pair ++% ets:insert(ets_users_table(VHost), {Name, User_id}), ++% Name ++% end. ++ ++%get_server_name(DBRef, VHost, Server_id) -> ++% case ets:match(ets_servers_table(VHost), {'$1', Server_id}) of ++% [[Server]] -> Server; ++ % this can be in clustered environment ++% [] -> ++% update_servers_from_db(DBRef, VHost), ++% [[Server1]] = ets:match(ets_servers_table(VHost), {'$1', Server_id}), ++% Server1 ++% end. ++ ++get_user_id_from_db(DBRef, VHost, User) -> ++ SQuery = ["SELECT user_id FROM ",users_table(VHost)," ", ++ "WHERE username=\"",User,"\";"], ++ case sql_query_internal(DBRef, SQuery) of ++ % no such user in db ++ {data, []} -> ++ {ok, []}; ++ {data, [[DBId]]} -> ++ % cache {user, id} pair ++ ets:insert(ets_users_table(VHost), {User, DBId}), ++ {ok, DBId} ++ end. ++get_user_id(DBRef, VHost, User) -> ++ % Look at ets ++ case ets:match(ets_users_table(VHost), {User, '$1'}) of ++ [] -> ++ % Look at db ++ case get_user_id_from_db(DBRef, VHost, User) of ++ % no such user in db ++ {ok, []} -> ++ IQuery = ["INSERT INTO ",users_table(VHost)," ", ++ "SET username=\"",User,"\";"], ++ case sql_query_internal_silent(DBRef, IQuery) of ++ {updated, _} -> ++ {ok, NewId} = get_user_id_from_db(DBRef, VHost, User), ++ NewId; ++ {error, Reason} -> ++ % this can be in clustered environment ++ {match, _, _} = regexp:match(Reason, "#23000"), ++ ?ERROR_MSG("Duplicate key name for ~p", [User]), ++ {ok, ClID} = get_user_id_from_db(DBRef, VHost, User), ++ ClID ++ end; ++ {ok, DBId} -> ++ DBId ++ end; ++ [[EtsId]] -> EtsId ++ end. ++ ++get_server_id(DBRef, VHost, Server) -> ++ case ets:match(ets_servers_table(VHost), {Server, '$1'}) of ++ [] -> ++ IQuery = ["INSERT INTO ",servers_table(VHost)," ", ++ "SET server=\"",Server,"\";"], ++ case sql_query_internal_silent(DBRef, IQuery) of ++ {updated, _} -> ++ SQuery = ["SELECT server_id FROM ",servers_table(VHost)," ", ++ "WHERE server=\"",Server,"\";"], ++ {data, [[Id]]} = sql_query_internal(DBRef, SQuery), ++ ets:insert(ets_servers_table(VHost), {Server, Id}), ++ Id; ++ {error, Reason} -> ++ % this can be in clustered environment ++ {match, _, _} = regexp:match(Reason, "#23000"), ++ ?ERROR_MSG("Duplicate key name for ~p", [Server]), ++ update_servers_from_db(DBRef, VHost), ++ [[Id1]] = ets:match(ets_servers_table(VHost), {Server, '$1'}), ++ Id1 ++ end; ++ [[Id]] -> Id ++ end. ++ ++get_resource_id_from_db(DBRef, VHost, Resource) -> ++ SQuery = ["SELECT resource_id FROM ",resources_table(VHost)," ", ++ "WHERE resource=\"",ejabberd_odbc:escape(Resource),"\";"], ++ case sql_query_internal(DBRef, SQuery) of ++ % no such resource in db ++ {data, []} -> ++ {ok, []}; ++ {data, [[DBId]]} -> ++ % cache {resource, id} pair ++ ets:insert(ets_resources_table(VHost), {Resource, DBId}), ++ {ok, DBId} ++ end. ++get_resource_id(DBRef, VHost, Resource) -> ++ % Look at ets ++ case ets:match(ets_resources_table(VHost), {Resource, '$1'}) of ++ [] -> ++ % Look at db ++ case get_resource_id_from_db(DBRef, VHost, Resource) of ++ % no such resource in db ++ {ok, []} -> ++ IQuery = ["INSERT INTO ",resources_table(VHost)," ", ++ "SET resource=\"",ejabberd_odbc:escape(Resource),"\";"], ++ case sql_query_internal_silent(DBRef, IQuery) of ++ {updated, _} -> ++ {ok, NewId} = get_resource_id_from_db(DBRef, VHost, Resource), ++ NewId; ++ {error, Reason} -> ++ % this can be in clustered environment ++ {match, _, _} = regexp:match(Reason, "#23000"), ++ ?ERROR_MSG("Duplicate key name for ~p", [Resource]), ++ {ok, ClID} = get_resource_id_from_db(DBRef, VHost, Resource), ++ ClID ++ end; ++ {ok, DBId} -> ++ DBId ++ end; ++ [[EtsId]] -> EtsId ++ end. ++ ++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++% ++% SQL internals ++% ++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++sql_query_internal(DBRef, Query) -> ++ case sql_query_internal_silent(DBRef, Query) of ++ {error, Reason} -> ++ ?ERROR_MSG("~p while ~p", [Reason, lists:append(Query)]), ++ {error, Reason}; ++ Rez -> Rez ++ end. ++ ++sql_query_internal_silent(DBRef, Query) -> ++ ?MYDEBUG("DOING: \"~s\"", [lists:append(Query)]), ++ get_result(mysql_conn:fetch(DBRef, Query, self(), ?MYSQL_TIMEOUT)). ++ ++get_result({updated, MySQLRes}) -> ++ {updated, mysql:get_result_affected_rows(MySQLRes)}; ++get_result({data, MySQLRes}) -> ++ {data, mysql:get_result_rows(MySQLRes)}; ++get_result({error, "query timed out"}) -> ++ {error, "query timed out"}; ++get_result({error, MySQLRes}) -> ++ Reason = mysql:get_result_reason(MySQLRes), ++ {error, Reason}. +--- mod_logdb_mysql5.erl.orig 2009-02-05 19:21:29.000000000 +0200 ++++ mod_logdb_mysql5.erl 2009-02-05 19:20:14.000000000 +0200 +@@ -0,0 +1,978 @@ ++%%%---------------------------------------------------------------------- ++%%% File : mod_logdb_mysql5.erl ++%%% Author : Oleg Palij (mailto,xmpp:o.palij@gmail.com) ++%%% Purpose : MySQL 5 backend for mod_logdb ++%%% Version : trunk ++%%% Id : $Id: mod_logdb_mysql5.erl 1253 2009-02-02 11:10:03Z malik $ ++%%% Url : http://www.dp.uz.gov.ua/o.palij/mod_logdb/ ++%%%---------------------------------------------------------------------- ++ ++-module(mod_logdb_mysql5). ++-author('o.palij@gmail.com'). ++ ++-include("mod_logdb.hrl"). ++-include("ejabberd.hrl"). ++-include("jlib.hrl"). ++ ++-behaviour(gen_logdb). ++-behaviour(gen_server). ++ ++% gen_server ++-export([code_change/3,handle_call/3,handle_cast/2,handle_info/2,init/1,terminate/2]). ++% gen_mod ++-export([start/2, stop/1]). ++% gen_logdb ++-export([log_message/2, ++ rebuild_stats/1, ++ rebuild_stats_at/2, ++ delete_messages_by_user_at/3, delete_all_messages_by_user_at/3, delete_messages_at/2, ++ get_vhost_stats/1, get_vhost_stats_at/2, get_user_stats/2, get_user_messages_at/3, ++ get_dates/1, ++ get_users_settings/1, get_user_settings/2, set_user_settings/3, ++ drop_user/2]). ++ ++% gen_server call timeout ++-define(CALL_TIMEOUT, 30000). ++-define(MYSQL_TIMEOUT, 60000). ++-define(INDEX_SIZE, integer_to_list(170)). ++-define(PROCNAME, mod_logdb_mysql5). ++ ++-import(mod_logdb, [list_to_bool/1, bool_to_list/1, ++ list_to_string/1, string_to_list/1, ++ convert_timestamp_brief/1]). ++ ++-record(state, {dbref, vhost, server, port, db, user, password}). ++ ++% replace "." with "_" ++escape_vhost(VHost) -> lists:map(fun(46) -> 95; ++ (A) -> A ++ end, VHost). ++prefix() -> ++ "`logdb_". ++ ++suffix(VHost) -> ++ "_" ++ escape_vhost(VHost) ++ "`". ++ ++messages_table(VHost, Date) -> ++ prefix() ++ "messages_" ++ Date ++ suffix(VHost). ++ ++% TODO: this needs to be redone to unify view name in stored procedure and in delete_messages_at/2 ++view_table(VHost, Date) -> ++ Table = messages_table(VHost, Date), ++ TablewoQ = lists:sublist(Table, 2, length(Table) - 2), ++ lists:append(["`v_", TablewoQ, "`"]). ++ ++stats_table(VHost) -> ++ prefix() ++ "stats" ++ suffix(VHost). ++ ++temp_table(VHost) -> ++ prefix() ++ "temp" ++ suffix(VHost). ++ ++settings_table(VHost) -> ++ prefix() ++ "settings" ++ suffix(VHost). ++ ++users_table(VHost) -> ++ prefix() ++ "users" ++ suffix(VHost). ++servers_table(VHost) -> ++ prefix() ++ "servers" ++ suffix(VHost). ++resources_table(VHost) -> ++ prefix() ++ "resources" ++ suffix(VHost). ++ ++logmessage_name(VHost) -> ++ prefix() ++ "logmessage" ++ suffix(VHost). ++ ++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++% ++% gen_mod callbacks ++% ++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++start(VHost, Opts) -> ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:start({local, Proc}, ?MODULE, [VHost, Opts], []). ++ ++stop(VHost) -> ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:call(Proc, {stop}, ?CALL_TIMEOUT). ++ ++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++% ++% gen_server callbacks ++% ++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++init([VHost, Opts]) -> ++ crypto:start(), ++ ++ Server = gen_mod:get_opt(server, Opts, "localhost"), ++ Port = gen_mod:get_opt(port, Opts, 3306), ++ DB = gen_mod:get_opt(db, Opts, "logdb"), ++ User = gen_mod:get_opt(user, Opts, "root"), ++ Password = gen_mod:get_opt(password, Opts, ""), ++ ++ St = #state{vhost=VHost, ++ server=Server, port=Port, db=DB, ++ user=User, password=Password}, ++ ++ case open_mysql_connection(St) of ++ {ok, DBRef} -> ++ State = St#state{dbref=DBRef}, ++ ok = create_internals(State), ++ ok = create_stats_table(State), ++ ok = create_settings_table(State), ++ ok = create_users_table(State), ++ ok = create_servers_table(State), ++ ok = create_resources_table(State), ++ erlang:monitor(process, DBRef), ++ {ok, State}; ++ {error, Reason} -> ++ ?ERROR_MSG("MySQL connection failed: ~p~n", [Reason]), ++ {stop, db_connection_failed} ++ end. ++ ++open_mysql_connection(#state{server=Server, port=Port, db=DB, ++ user=DBUser, password=Password} = _State) -> ++ LogFun = fun(debug, _Format, _Argument) -> ++ %?MYDEBUG(Format, Argument); ++ ok; ++ (error, Format, Argument) -> ++ ?ERROR_MSG(Format, Argument); ++ (Level, Format, Argument) -> ++ ?MYDEBUG("MySQL (~p)~n", [Level]), ++ ?MYDEBUG(Format, Argument) ++ end, ++ mysql_conn:start(Server, Port, DBUser, Password, DB, [65536, 131072], LogFun). ++ ++close_mysql_connection(DBRef) -> ++ ?MYDEBUG("Closing ~p mysql connection", [DBRef]), ++ mysql_conn:stop(DBRef). ++ ++handle_call({rebuild_stats_at, Date}, _From, #state{dbref=DBRef, vhost=VHost}=State) -> ++ Reply = rebuild_stats_at_int(DBRef, VHost, Date), ++ {reply, Reply, State}; ++handle_call({delete_messages_by_user_at, [], _Date}, _From, State) -> ++ {reply, error, State}; ++handle_call({delete_messages_by_user_at, Msgs, Date}, _From, #state{dbref=DBRef, vhost=VHost}=State) -> ++ Temp = lists:flatmap(fun(#msg{timestamp=Timestamp} = _Msg) -> ++ ["\"",Timestamp,"\"",","] ++ end, Msgs), ++ ++ Temp1 = lists:append([lists:sublist(Temp, length(Temp)-1), ");"]), ++ ++ Query = ["DELETE FROM ",messages_table(VHost, Date)," ", ++ "WHERE timestamp IN (", Temp1], ++ ++ Reply = ++ case sql_query_internal(DBRef, Query) of ++ {updated, Aff} -> ++ ?MYDEBUG("Aff=~p", [Aff]), ++ rebuild_stats_at_int(DBRef, VHost, Date); ++ {error, _} -> ++ error ++ end, ++ {reply, Reply, State}; ++handle_call({delete_all_messages_by_user_at, User, Date}, _From, #state{dbref=DBRef, vhost=VHost}=State) -> ++ ok = delete_all_messages_by_user_at_int(DBRef, User, VHost, Date), ++ ok = delete_stats_by_user_at_int(DBRef, User, VHost, Date), ++ {reply, ok, State}; ++handle_call({delete_messages_at, Date}, _From, #state{dbref=DBRef, vhost=VHost}=State) -> ++ Fun = fun() -> ++ {updated, _} = sql_query_internal(DBRef, ["DROP TABLE ",messages_table(VHost, Date),";"]), ++ TQuery = ["DELETE FROM ",stats_table(VHost)," " ++ "WHERE at=\"",Date,"\";"], ++ {updated, _} = sql_query_internal(DBRef, TQuery), ++ VQuery = ["DROP VIEW IF EXISTS ",view_table(VHost,Date),";"], ++ {updated, _} = sql_query_internal(DBRef, VQuery), ++ ok ++ end, ++ Reply = ++ case catch apply(Fun, []) of ++ ok -> ++ ok; ++ {'EXIT', _} -> ++ error ++ end, ++ {reply, Reply, State}; ++handle_call({get_vhost_stats}, _From, #state{dbref=DBRef, vhost=VHost}=State) -> ++ SName = stats_table(VHost), ++ Query = ["SELECT at, sum(count) ", ++ "FROM ",SName," ", ++ "GROUP BY at ", ++ "ORDER BY DATE(at) DESC;" ++ ], ++ Reply = ++ case sql_query_internal(DBRef, Query) of ++ {data, Result} -> ++ {ok, [ {Date, list_to_integer(Count)} || [Date, Count] <- Result ]}; ++ {error, Reason} -> ++ % TODO: Duplicate error message ? ++ {error, Reason} ++ end, ++ {reply, Reply, State}; ++handle_call({get_vhost_stats_at, Date}, _From, #state{dbref=DBRef, vhost=VHost}=State) -> ++ SName = stats_table(VHost), ++ Query = ["SELECT username, sum(count) as allcount ", ++ "FROM ",SName," ", ++ "JOIN ",users_table(VHost)," ON owner_id=user_id " ++ "WHERE at=\"",Date,"\" ", ++ "GROUP BY username ", ++ "ORDER BY allcount DESC;" ++ ], ++ Reply = ++ case sql_query_internal(DBRef, Query) of ++ {data, Result} -> ++ {ok, [ {User, list_to_integer(Count)} || [User, Count] <- Result ]}; ++ {error, Reason} -> ++ {error, Reason} ++ end, ++ {reply, Reply, State}; ++handle_call({get_user_stats, User}, _From, #state{dbref=DBRef, vhost=VHost}=State) -> ++ {reply, get_user_stats_int(DBRef, User, VHost), State}; ++handle_call({get_user_messages_at, User, Date}, _From, #state{dbref=DBRef, vhost=VHost}=State) -> ++ Query = ["SELECT peer_name,", ++ "peer_server,", ++ "peer_resource,", ++ "direction," ++ "type," ++ "subject," ++ "body," ++ "timestamp " ++ "FROM ",view_table(VHost, Date)," " ++ "WHERE owner_name=\"",User,"\";"], ++ Reply = ++ case sql_query_internal(DBRef, Query) of ++ {data, Result} -> ++ Fun = fun([Peer_name, Peer_server, Peer_resource, ++ Direction, ++ Type, ++ Subject, Body, ++ Timestamp]) -> ++ #msg{peer_name=Peer_name, peer_server=Peer_server, peer_resource=Peer_resource, ++ direction=list_to_atom(Direction), ++ type=Type, ++ subject=Subject, body=Body, ++ timestamp=Timestamp} ++ end, ++ {ok, lists:map(Fun, Result)}; ++ {error, Reason} -> ++ {error, Reason} ++ end, ++ {reply, Reply, State}; ++handle_call({get_dates}, _From, #state{dbref=DBRef, vhost=VHost}=State) -> ++ SName = stats_table(VHost), ++ Query = ["SELECT at ", ++ "FROM ",SName," ", ++ "GROUP BY at ", ++ "ORDER BY DATE(at) DESC;" ++ ], ++ Reply = ++ case sql_query_internal(DBRef, Query) of ++ {data, Result} -> ++ [ Date || [Date] <- Result ]; ++ {error, Reason} -> ++ {error, Reason} ++ end, ++ {reply, Reply, State}; ++handle_call({get_users_settings}, _From, #state{dbref=DBRef, vhost=VHost}=State) -> ++ Query = ["SELECT username,dolog_default,dolog_list,donotlog_list ", ++ "FROM ",settings_table(VHost)," ", ++ "JOIN ",users_table(VHost)," ON user_id=owner_id;"], ++ Reply = ++ case sql_query_internal(DBRef, Query) of ++ {data, Result} -> ++ {ok, lists:map(fun([Owner, DoLogDef, DoLogL, DoNotLogL]) -> ++ #user_settings{owner_name=Owner, ++ dolog_default=list_to_bool(DoLogDef), ++ dolog_list=string_to_list(DoLogL), ++ donotlog_list=string_to_list(DoNotLogL) ++ } ++ end, Result)}; ++ {error, _} -> ++ error ++ end, ++ {reply, Reply, State}; ++handle_call({get_user_settings, User}, _From, #state{dbref=DBRef, vhost=VHost}=State) -> ++ Query = ["SELECT dolog_default,dolog_list,donotlog_list FROM ",settings_table(VHost)," ", ++ "WHERE owner_id=(SELECT user_id FROM ",users_table(VHost)," WHERE username=\"",User,"\");"], ++ Reply = ++ case sql_query_internal(DBRef, Query) of ++ {data, []} -> ++ {ok, []}; ++ {data, [[Owner, DoLogDef, DoLogL, DoNotLogL]]} -> ++ {ok, #user_settings{owner_name=Owner, ++ dolog_default=list_to_bool(DoLogDef), ++ dolog_list=string_to_list(DoLogL), ++ donotlog_list=string_to_list(DoNotLogL)}}; ++ {error, _} -> ++ error ++ end, ++ {reply, Reply, State}; ++handle_call({set_user_settings, User, #user_settings{dolog_default=DoLogDef, ++ dolog_list=DoLogL, ++ donotlog_list=DoNotLogL}}, ++ _From, #state{dbref=DBRef, vhost=VHost} = State) -> ++ User_id = get_user_id(DBRef, VHost, User), ++ Query = ["UPDATE ",settings_table(VHost)," ", ++ "SET dolog_default=",bool_to_list(DoLogDef),", ", ++ "dolog_list='",list_to_string(DoLogL),"', ", ++ "donotlog_list='",list_to_string(DoNotLogL),"' ", ++ "WHERE owner_id=",User_id,";"], ++ ++ Reply = ++ case sql_query_internal(DBRef, Query) of ++ {updated, 0} -> ++ IQuery = ["INSERT INTO ",settings_table(VHost)," ", ++ "(owner_id, dolog_default, dolog_list, donotlog_list) ", ++ "VALUES ", ++ "(",User_id,",",bool_to_list(DoLogDef),",'",list_to_string(DoLogL),"','",list_to_string(DoNotLogL),"');"], ++ case sql_query_internal_silent(DBRef, IQuery) of ++ {updated, _} -> ++ ?MYDEBUG("New settings for ~s@~s", [User, VHost]), ++ ok; ++ {error, Reason} -> ++ case regexp:match(Reason, "#23000") of ++ % Already exists ++ {match, _, _} -> ++ ok; ++ _ -> ++ ?ERROR_MSG("Failed setup user ~p@~p: ~p", [User, VHost, Reason]), ++ error ++ end ++ end; ++ {updated, 1} -> ++ ?MYDEBUG("Updated settings for ~s@~s", [User, VHost]), ++ ok; ++ {error, _} -> ++ error ++ end, ++ {reply, Reply, State}; ++handle_call({stop}, _From, #state{vhost=VHost}=State) -> ++ ?MYDEBUG("Stoping mysql5 backend for ~p", [VHost]), ++ {stop, normal, ok, State}; ++handle_call(Msg, _From, State) -> ++ ?INFO_MSG("Got call Msg: ~p, State: ~p", [Msg, State]), ++ {noreply, State}. ++ ++handle_cast({log_message, Msg}, #state{dbref=DBRef, vhost=VHost}=State) -> ++ Fun = fun() -> ++ Date = convert_timestamp_brief(Msg#msg.timestamp), ++ TableName = messages_table(VHost, Date), ++ ++ Query = [ "CALL ",logmessage_name(VHost)," " ++ "('", TableName, "',", ++ "'", Date, "',", ++ "'", Msg#msg.owner_name, "',", ++ "'", Msg#msg.peer_name, "',", ++ "'", Msg#msg.peer_server, "',", ++ "'", ejabberd_odbc:escape(Msg#msg.peer_resource), "',", ++ "'", atom_to_list(Msg#msg.direction), "',", ++ "'", Msg#msg.type, "',", ++ "'", ejabberd_odbc:escape(Msg#msg.subject), "',", ++ "'", ejabberd_odbc:escape(Msg#msg.body), "',", ++ "'", Msg#msg.timestamp, "');"], ++ ++ case sql_query_internal(DBRef, Query) of ++ {updated, _} -> ++ ?MYDEBUG("Logged ok for ~p, peer: ~p", [Msg#msg.owner_name++"@"++VHost, ++ Msg#msg.peer_name++"@"++Msg#msg.peer_server]), ++ ok; ++ {error, _Reason} -> ++ error ++ end ++ end, ++ spawn(Fun), ++ {noreply, State}; ++handle_cast({rebuild_stats}, State) -> ++ rebuild_all_stats_int(State), ++ {noreply, State}; ++handle_cast({drop_user, User}, #state{vhost=VHost} = State) -> ++ Fun = fun() -> ++ {ok, DBRef} = open_mysql_connection(State), ++ {ok, Dates} = get_user_stats_int(DBRef, User, VHost), ++ MDResult = lists:map(fun({Date, _}) -> ++ delete_all_messages_by_user_at_int(DBRef, User, VHost, Date) ++ end, Dates), ++ StDResult = delete_all_stats_by_user_int(DBRef, User, VHost), ++ SDResult = delete_user_settings_int(DBRef, User, VHost), ++ case lists:all(fun(Result) when Result == ok -> ++ true; ++ (Result) when Result == error -> ++ false ++ end, lists:append([MDResult, [StDResult], [SDResult]])) of ++ true -> ++ ?INFO_MSG("Removed ~s@~s", [User, VHost]); ++ false -> ++ ?ERROR_MSG("Failed to remove ~s@~s", [User, VHost]) ++ end, ++ close_mysql_connection(DBRef) ++ end, ++ spawn(Fun), ++ {noreply, State}; ++handle_cast(Msg, State) -> ++ ?INFO_MSG("Got cast Msg:~p, State:~p", [Msg, State]), ++ {noreply, State}. ++ ++handle_info({'DOWN', _MonitorRef, process, _Pid, _Info}, State) -> ++ {stop, connection_dropped, State}; ++handle_info(Info, State) -> ++ ?INFO_MSG("Got Info:~p, State:~p", [Info, State]), ++ {noreply, State}. ++ ++terminate(_Reason, #state{dbref=DBRef}=_State) -> ++ close_mysql_connection(DBRef), ++ ok. ++ ++code_change(_OldVsn, State, _Extra) -> ++ {ok, State}. ++ ++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++% ++% gen_logdb callbacks ++% ++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++log_message(VHost, Msg) -> ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:cast(Proc, {log_message, Msg}). ++rebuild_stats(VHost) -> ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:cast(Proc, {rebuild_stats}). ++rebuild_stats_at(VHost, Date) -> ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:call(Proc, {rebuild_stats_at, Date}, ?CALL_TIMEOUT). ++delete_messages_by_user_at(VHost, Msgs, Date) -> ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:call(Proc, {delete_messages_by_user_at, Msgs, Date}, ?CALL_TIMEOUT). ++delete_all_messages_by_user_at(User, VHost, Date) -> ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:call(Proc, {delete_all_messages_by_user_at, User, Date}, ?CALL_TIMEOUT). ++delete_messages_at(VHost, Date) -> ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:call(Proc, {delete_messages_at, Date}, ?CALL_TIMEOUT). ++get_vhost_stats(VHost) -> ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:call(Proc, {get_vhost_stats}, ?CALL_TIMEOUT). ++get_vhost_stats_at(VHost, Date) -> ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:call(Proc, {get_vhost_stats_at, Date}, ?CALL_TIMEOUT). ++get_user_stats(User, VHost) -> ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:call(Proc, {get_user_stats, User}, ?CALL_TIMEOUT). ++get_user_messages_at(User, VHost, Date) -> ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:call(Proc, {get_user_messages_at, User, Date}, ?CALL_TIMEOUT). ++get_dates(VHost) -> ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:call(Proc, {get_dates}, ?CALL_TIMEOUT). ++get_users_settings(VHost) -> ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:call(Proc, {get_users_settings}, ?CALL_TIMEOUT). ++get_user_settings(User, VHost) -> ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:call(Proc, {get_user_settings, User}, ?CALL_TIMEOUT). ++set_user_settings(User, VHost, Set) -> ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:call(Proc, {set_user_settings, User, Set}, ?CALL_TIMEOUT). ++drop_user(User, VHost) -> ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:cast(Proc, {drop_user, User}). ++ ++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++% ++% internals ++% ++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++get_dates_int(DBRef, VHost) -> ++ case sql_query_internal(DBRef, ["SHOW TABLES"]) of ++ {data, Tables} -> ++ lists:foldl(fun([Table], Dates) -> ++ Reg = lists:sublist(prefix(),2,length(prefix())) ++ ".*" ++ escape_vhost(VHost), ++ case regexp:match(Table, Reg) of ++ {match, 1, _} -> ++ case regexp:match(Table,"[0-9]+-[0-9]+-[0-9]+") of ++ {match, S, E} -> ++ lists:append(Dates, [lists:sublist(Table,S,E)]); ++ nomatch -> ++ Dates ++ end; ++ _ -> ++ Dates ++ end ++ end, [], Tables); ++ {error, _} -> ++ [] ++ end. ++ ++rebuild_all_stats_int(#state{vhost=VHost}=State) -> ++ Fun = fun() -> ++ {ok, DBRef} = open_mysql_connection(State), ++ ok = delete_nonexistent_stats(DBRef, VHost), ++ case lists:filter(fun(Date) -> ++ case catch rebuild_stats_at_int(DBRef, VHost, Date) of ++ ok -> false; ++ error -> true; ++ {'EXIT', _} -> true ++ end ++ end, get_dates_int(DBRef, VHost)) of ++ [] -> ok; ++ FTables -> ++ ?ERROR_MSG("Failed to rebuild stats for ~p dates", [FTables]), ++ error ++ end, ++ close_mysql_connection(DBRef) ++ end, ++ spawn(Fun). ++ ++rebuild_stats_at_int(DBRef, VHost, Date) -> ++ TempTable = temp_table(VHost), ++ Fun = fun() -> ++ Table = messages_table(VHost, Date), ++ STable = stats_table(VHost), ++ ++ DQuery = [ "DELETE FROM ",STable," ", ++ "WHERE at='",Date,"';"], ++ ++ ok = create_temp_table(DBRef, TempTable), ++ {updated, _} = sql_query_internal(DBRef, ["LOCK TABLE ",Table," WRITE, ",TempTable," WRITE;"]), ++ SQuery = ["INSERT INTO ",TempTable," ", ++ "(owner_id,peer_name_id,peer_server_id,at,count) ", ++ "SELECT owner_id,peer_name_id,peer_server_id,\"",Date,"\",count(*) ", ++ "FROM ",Table," WHERE ext is NULL GROUP BY owner_id,peer_name_id,peer_server_id;"], ++ case sql_query_internal(DBRef, SQuery) of ++ {updated, 0} -> ++ Count = sql_query_internal(DBRef, ["SELECT count(*) FROM ",Table,";"]), ++ case Count of ++ {data, [["0"]]} -> ++ {updated, _} = sql_query_internal(DBRef, ["DROP VIEW IF EXISTS ",view_table(VHost,Date),";"]), ++ {updated, _} = sql_query_internal(DBRef, ["DROP TABLE ",Table,";"]), ++ {updated, _} = sql_query_internal(DBRef, ["LOCK TABLE ",STable," WRITE;"]), ++ {updated, _} = sql_query_internal(DBRef, DQuery), ++ ok; ++ _ -> ++ ?ERROR_MSG("Failed to calculate stats for ~s table! Count was ~p.", [Date, Count]), ++ error ++ end; ++ {updated, _} -> ++ {updated, _} = sql_query_internal(DBRef, ["LOCK TABLE ",STable," WRITE, ",TempTable," WRITE;"]), ++ {updated, _} = sql_query_internal(DBRef, DQuery), ++ SQuery1 = ["INSERT INTO ",STable," ", ++ "(owner_id,peer_name_id,peer_server_id,at,count) ", ++ "SELECT owner_id,peer_name_id,peer_server_id,at,count ", ++ "FROM ",TempTable,";"], ++ case sql_query_internal(DBRef, SQuery1) of ++ {updated, _} -> ok; ++ {error, _} -> error ++ end; ++ {error, _} -> error ++ end ++ end, ++ ++ case catch apply(Fun, []) of ++ ok -> ++ ?INFO_MSG("Rebuilded stats for ~p at ~p", [VHost, Date]), ++ ok; ++ error -> ++ error; ++ {'EXIT', Reason} -> ++ ?ERROR_MSG("Failed to rebuild stats for ~s table: ~p.", [Date, Reason]), ++ error ++ end, ++ sql_query_internal(DBRef, ["UNLOCK TABLES;"]), ++ sql_query_internal(DBRef, ["DROP TABLE ",TempTable,";"]), ++ ok. ++ ++delete_nonexistent_stats(DBRef, VHost) -> ++ Dates = get_dates_int(DBRef, VHost), ++ STable = stats_table(VHost), ++ ++ Temp = lists:flatmap(fun(Date) -> ++ ["\"",Date,"\"",","] ++ end, Dates), ++ case Temp of ++ [] -> ++ ok; ++ _ -> ++ % replace last "," with ");" ++ Temp1 = lists:append([lists:sublist(Temp, length(Temp)-1), ");"]), ++ Query = ["DELETE FROM ",STable," ", ++ "WHERE at NOT IN (", Temp1], ++ case sql_query_internal(DBRef, Query) of ++ {updated, _} -> ++ ok; ++ {error, _} -> ++ error ++ end ++ end. ++ ++get_user_stats_int(DBRef, User, VHost) -> ++ SName = stats_table(VHost), ++ UName = users_table(VHost), ++ Query = ["SELECT stats.at, sum(stats.count) ", ++ "FROM ",UName," AS users ", ++ "JOIN ",SName," AS stats ON owner_id=user_id " ++ "WHERE users.username=\"",User,"\" ", ++ "GROUP BY stats.at " ++ "ORDER BY DATE(stats.at) DESC;" ++ ], ++ case sql_query_internal(DBRef, Query) of ++ {data, Result} -> ++ {ok, [ {Date, list_to_integer(Count)} || [Date, Count] <- Result ]}; ++ {error, Result} -> ++ {error, Result} ++ end. ++ ++delete_all_messages_by_user_at_int(DBRef, User, VHost, Date) -> ++ DQuery = ["DELETE FROM ",messages_table(VHost, Date)," ", ++ "WHERE owner_id=(SELECT user_id FROM ",users_table(VHost)," WHERE username=\"",User,"\");"], ++ case sql_query_internal(DBRef, DQuery) of ++ {updated, _} -> ++ ?INFO_MSG("Dropped messages for ~s@~s at ~s", [User, VHost, Date]), ++ ok; ++ {error, _} -> ++ error ++ end. ++ ++delete_all_stats_by_user_int(DBRef, User, VHost) -> ++ SQuery = ["DELETE FROM ",stats_table(VHost)," ", ++ "WHERE owner_id=(SELECT user_id FROM ",users_table(VHost)," WHERE username=\"",User,"\");"], ++ case sql_query_internal(DBRef, SQuery) of ++ {updated, _} -> ++ ?INFO_MSG("Dropped all stats for ~s@~s", [User, VHost]), ++ ok; ++ {error, _} -> error ++ end. ++ ++delete_stats_by_user_at_int(DBRef, User, VHost, Date) -> ++ SQuery = ["DELETE FROM ",stats_table(VHost)," ", ++ "WHERE owner_id=(SELECT user_id FROM ",users_table(VHost)," WHERE username=\"",User,"\") ", ++ "AND at=\"",Date,"\";"], ++ case sql_query_internal(DBRef, SQuery) of ++ {updated, _} -> ++ ?INFO_MSG("Dropped stats for ~s@~s at ~s", [User, VHost, Date]), ++ ok; ++ {error, _} -> error ++ end. ++ ++delete_user_settings_int(DBRef, User, VHost) -> ++ Query = ["DELETE FROM ",settings_table(VHost)," ", ++ "WHERE owner_id=(SELECT user_id FROM ",users_table(VHost)," WHERE username=\"",User,"\");"], ++ case sql_query_internal(DBRef, Query) of ++ {updated, _} -> ++ ?INFO_MSG("Dropped ~s@~s settings", [User, VHost]), ++ ok; ++ {error, Reason} -> ++ ?ERROR_MSG("Failed to drop ~s@~s settings: ~p", [User, VHost, Reason]), ++ error ++ end. ++ ++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++% ++% tables internals ++% ++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++create_temp_table(DBRef, Name) -> ++ Query = ["CREATE TABLE ",Name," (", ++ "owner_id MEDIUMINT UNSIGNED, ", ++ "peer_name_id MEDIUMINT UNSIGNED, ", ++ "peer_server_id MEDIUMINT UNSIGNED, ", ++ "at VARCHAR(11), ", ++ "count INT(11) ", ++ ") ENGINE=MyISAM CHARACTER SET utf8;" ++ ], ++ case sql_query_internal(DBRef, Query) of ++ {updated, _} -> ok; ++ {error, _Reason} -> error ++ end. ++ ++create_stats_table(#state{dbref=DBRef, vhost=VHost}=State) -> ++ SName = stats_table(VHost), ++ Query = ["CREATE TABLE ",SName," (", ++ "owner_id MEDIUMINT UNSIGNED, ", ++ "peer_name_id MEDIUMINT UNSIGNED, ", ++ "peer_server_id MEDIUMINT UNSIGNED, ", ++ "at VARCHAR(11), ", ++ "count INT(11), ", ++ "ext INTEGER DEFAULT NULL, " ++ "INDEX ext_i (ext), " ++ "INDEX(owner_id,peer_name_id,peer_server_id), ", ++ "INDEX(at) ", ++ ") ENGINE=MyISAM CHARACTER SET utf8;" ++ ], ++ case sql_query_internal_silent(DBRef, Query) of ++ {updated, _} -> ++ ?MYDEBUG("Created stats table for ~p", [VHost]), ++ rebuild_all_stats_int(State), ++ ok; ++ {error, Reason} -> ++ case regexp:match(Reason, "#42S01") of ++ {match, _, _} -> ++ ?MYDEBUG("Stats table for ~p already exists", [VHost]), ++ CheckQuery = ["SHOW COLUMNS FROM ",SName," LIKE 'peer_%_id';"], ++ case sql_query_internal(DBRef, CheckQuery) of ++ {data, Elems} when length(Elems) == 2 -> ++ ?MYDEBUG("Stats table structure is ok", []), ++ ok; ++ _ -> ++ ?INFO_MSG("It seems like stats table structure is invalid. I will drop it and recreate", []), ++ case sql_query_internal(DBRef, ["DROP TABLE ",SName,";"]) of ++ {updated, _} -> ++ ?INFO_MSG("Successfully dropped ~p", [SName]); ++ _ -> ++ ?ERROR_MSG("Failed to drop ~p. You should drop it and restart module", [SName]) ++ end, ++ error ++ end; ++ _ -> ++ ?ERROR_MSG("Failed to create stats table for ~p: ~p", [VHost, Reason]), ++ error ++ end ++ end. ++ ++create_settings_table(#state{dbref=DBRef, vhost=VHost}) -> ++ SName = settings_table(VHost), ++ Query = ["CREATE TABLE IF NOT EXISTS ",SName," (", ++ "owner_id MEDIUMINT UNSIGNED PRIMARY KEY, ", ++ "dolog_default TINYINT(1) NOT NULL DEFAULT 1, ", ++ "dolog_list TEXT, ", ++ "donotlog_list TEXT ", ++ ") ENGINE=InnoDB CHARACTER SET utf8;" ++ ], ++ case sql_query_internal(DBRef, Query) of ++ {updated, _} -> ++ ?MYDEBUG("Created settings table for ~p", [VHost]), ++ ok; ++ {error, _} -> ++ error ++ end. ++ ++create_users_table(#state{dbref=DBRef, vhost=VHost}) -> ++ SName = users_table(VHost), ++ Query = ["CREATE TABLE IF NOT EXISTS ",SName," (", ++ "username TEXT NOT NULL, ", ++ "user_id MEDIUMINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE, ", ++ "UNIQUE INDEX(username(",?INDEX_SIZE,")) ", ++ ") ENGINE=InnoDB CHARACTER SET utf8;" ++ ], ++ case sql_query_internal(DBRef, Query) of ++ {updated, _} -> ++ ?MYDEBUG("Created users table for ~p", [VHost]), ++ ok; ++ {error, _} -> ++ error ++ end. ++ ++create_servers_table(#state{dbref=DBRef, vhost=VHost}) -> ++ SName = servers_table(VHost), ++ Query = ["CREATE TABLE IF NOT EXISTS ",SName," (", ++ "server TEXT NOT NULL, ", ++ "server_id MEDIUMINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE, ", ++ "UNIQUE INDEX(server(",?INDEX_SIZE,")) ", ++ ") ENGINE=InnoDB CHARACTER SET utf8;" ++ ], ++ case sql_query_internal(DBRef, Query) of ++ {updated, _} -> ++ ?MYDEBUG("Created servers table for ~p", [VHost]), ++ ok; ++ {error, _} -> ++ error ++ end. ++ ++create_resources_table(#state{dbref=DBRef, vhost=VHost}) -> ++ RName = resources_table(VHost), ++ Query = ["CREATE TABLE IF NOT EXISTS ",RName," (", ++ "resource TEXT NOT NULL, ", ++ "resource_id MEDIUMINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE, ", ++ "UNIQUE INDEX(resource(",?INDEX_SIZE,")) ", ++ ") ENGINE=InnoDB CHARACTER SET utf8;" ++ ], ++ case sql_query_internal(DBRef, Query) of ++ {updated, _} -> ++ ?MYDEBUG("Created resources table for ~p", [VHost]), ++ ok; ++ {error, _} -> ++ error ++ end. ++ ++create_internals(#state{dbref=DBRef, vhost=VHost}) -> ++ sql_query_internal(DBRef, ["DROP PROCEDURE IF EXISTS ",logmessage_name(VHost),";"]), ++ case sql_query_internal(DBRef, [get_logmessage(VHost)]) of ++ {updated, _} -> ++ ?MYDEBUG("Created logmessage for ~p", [VHost]), ++ ok; ++ {error, _} -> ++ error ++ end. ++ ++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++% ++% SQL internals ++% ++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++sql_query_internal(DBRef, Query) -> ++ case sql_query_internal_silent(DBRef, Query) of ++ {error, Reason} -> ++ ?ERROR_MSG("~p while ~p", [Reason, lists:append(Query)]), ++ {error, Reason}; ++ Rez -> Rez ++ end. ++ ++sql_query_internal_silent(DBRef, Query) -> ++ ?MYDEBUG("DOING: \"~s\"", [lists:append(Query)]), ++ get_result(mysql_conn:fetch(DBRef, Query, self(), ?MYSQL_TIMEOUT)). ++ ++get_result({updated, MySQLRes}) -> ++ {updated, mysql:get_result_affected_rows(MySQLRes)}; ++get_result({data, MySQLRes}) -> ++ {data, mysql:get_result_rows(MySQLRes)}; ++get_result({error, "query timed out"}) -> ++ {error, "query timed out"}; ++get_result({error, MySQLRes}) -> ++ Reason = mysql:get_result_reason(MySQLRes), ++ {error, Reason}. ++ ++get_user_id(DBRef, VHost, User) -> ++ SQuery = ["SELECT user_id FROM ",users_table(VHost)," ", ++ "WHERE username=\"",User,"\";"], ++ case sql_query_internal(DBRef, SQuery) of ++ {data, []} -> ++ IQuery = ["INSERT INTO ",users_table(VHost)," ", ++ "SET username=\"",User,"\";"], ++ case sql_query_internal_silent(DBRef, IQuery) of ++ {updated, _} -> ++ {data, [[DBIdNew]]} = sql_query_internal(DBRef, SQuery), ++ DBIdNew; ++ {error, Reason} -> ++ % this can be in clustered environment ++ {match, _, _} = regexp:match(Reason, "#23000"), ++ ?ERROR_MSG("Duplicate key name for ~p", [User]), ++ {data, [[ClID]]} = sql_query_internal(DBRef, SQuery), ++ ClID ++ end; ++ {data, [[DBId]]} -> ++ DBId ++ end. ++ ++get_logmessage(VHost) -> ++ UName = users_table(VHost), ++ SName = servers_table(VHost), ++ RName = resources_table(VHost), ++ StName = stats_table(VHost), ++ io_lib:format(" ++CREATE PROCEDURE ~s(tablename TEXT, atdate TEXT, owner TEXT, peer_name TEXT, peer_server TEXT, peer_resource TEXT, mdirection VARCHAR(4), mtype VARCHAR(10), msubject TEXT, mbody TEXT, mtimestamp DOUBLE) ++BEGIN ++ DECLARE ownerID MEDIUMINT UNSIGNED; ++ DECLARE peer_nameID MEDIUMINT UNSIGNED; ++ DECLARE peer_serverID MEDIUMINT UNSIGNED; ++ DECLARE peer_resourceID MEDIUMINT UNSIGNED; ++ DECLARE Vmtype VARCHAR(10); ++ DECLARE Vmtimestamp DOUBLE; ++ DECLARE Vmdirection VARCHAR(4); ++ DECLARE Vmbody TEXT; ++ DECLARE Vmsubject TEXT; ++ DECLARE iq TEXT; ++ DECLARE cq TEXT; ++ DECLARE viewname TEXT; ++ DECLARE notable INT; ++ DECLARE CONTINUE HANDLER FOR SQLSTATE '42S02' SET @notable = 1; ++ ++ SET @notable = 0; ++ SET @ownerID = NULL; ++ SET @peer_nameID = NULL; ++ SET @peer_serverID = NULL; ++ SET @peer_resourceID = NULL; ++ ++ SET @Vmtype = mtype; ++ SET @Vmtimestamp = mtimestamp; ++ SET @Vmdirection = mdirection; ++ SET @Vmbody = mbody; ++ SET @Vmsubject = msubject; ++ ++ SELECT user_id INTO @ownerID FROM ~s WHERE username=owner; ++ IF @ownerID IS NULL THEN ++ INSERT INTO ~s SET username=owner; ++ SET @ownerID = LAST_INSERT_ID(); ++ END IF; ++ ++ SELECT user_id INTO @peer_nameID FROM ~s WHERE username=peer_name; ++ IF @peer_nameID IS NULL THEN ++ INSERT INTO ~s SET username=peer_name; ++ SET @peer_nameID = LAST_INSERT_ID(); ++ END IF; ++ ++ SELECT server_id INTO @peer_serverID FROM ~s WHERE server=peer_server; ++ IF @peer_serverID IS NULL THEN ++ INSERT INTO ~s SET server=peer_server; ++ SET @peer_serverID = LAST_INSERT_ID(); ++ END IF; ++ ++ SELECT resource_id INTO @peer_resourceID FROM ~s WHERE resource=peer_resource; ++ IF @peer_resourceID IS NULL THEN ++ INSERT INTO ~s SET resource=peer_resource; ++ SET @peer_resourceID = LAST_INSERT_ID(); ++ END IF; ++ ++ SET @iq = CONCAT(\"INSERT INTO \",tablename,\" (owner_id, peer_name_id, peer_server_id, peer_resource_id, direction, type, subject, body, timestamp) VALUES (@ownerID,@peer_nameID,@peer_serverID,@peer_resourceID,@Vmdirection,@Vmtype,@Vmsubject,@Vmbody,@Vmtimestamp);\"); ++ PREPARE insertmsg FROM @iq; ++ ++ IF @notable = 1 THEN ++ SET @cq = CONCAT(\"CREATE TABLE \",tablename,\" ( ++ owner_id MEDIUMINT UNSIGNED NOT NULL, ++ peer_name_id MEDIUMINT UNSIGNED NOT NULL, ++ peer_server_id MEDIUMINT UNSIGNED NOT NULL, ++ peer_resource_id MEDIUMINT(8) UNSIGNED NOT NULL, ++ direction ENUM('to', 'from') NOT NULL, ++ type ENUM('chat','error','groupchat','headline','normal') NOT NULL, ++ subject TEXT, ++ body TEXT, ++ timestamp DOUBLE NOT NULL, ++ ext INTEGER DEFAULT NULL, ++ INDEX search_i (owner_id, peer_name_id, peer_server_id, peer_resource_id), ++ INDEX ext_i (ext), ++ FULLTEXT (body) ++ ) ENGINE=MyISAM ++ PACK_KEYS=1 ++ CHARACTER SET utf8;\"); ++ PREPARE createtable FROM @cq; ++ EXECUTE createtable; ++ DEALLOCATE PREPARE createtable; ++ ++ SET @viewname = CONCAT(\"`v_\", TRIM(BOTH '`' FROM tablename), \"`\"); ++ SET @cq = CONCAT(\"CREATE OR REPLACE VIEW \",@viewname,\" AS ++ SELECT owner.username AS owner_name, ++ peer.username AS peer_name, ++ servers.server AS peer_server, ++ resources.resource AS peer_resource, ++ messages.direction, ++ messages.type, ++ messages.subject, ++ messages.body, ++ messages.timestamp ++ FROM ++ ~s owner, ++ ~s peer, ++ ~s servers, ++ ~s resources, ++ \", tablename,\" messages ++ WHERE ++ owner.user_id=messages.owner_id and ++ peer.user_id=messages.peer_name_id and ++ servers.server_id=messages.peer_server_id and ++ resources.resource_id=messages.peer_resource_id ++ ORDER BY messages.timestamp;\"); ++ PREPARE createview FROM @cq; ++ EXECUTE createview; ++ DEALLOCATE PREPARE createview; ++ ++ SET @notable = 0; ++ PREPARE insertmsg FROM @iq; ++ EXECUTE insertmsg; ++ ELSEIF @notable = 0 THEN ++ EXECUTE insertmsg; ++ END IF; ++ ++ DEALLOCATE PREPARE insertmsg; ++ ++ IF @notable = 0 THEN ++ UPDATE ~s SET count=count+1 WHERE owner_id=@ownerID AND peer_name_id=@peer_nameID AND peer_server_id=@peer_serverID AND at=atdate; ++ IF ROW_COUNT() = 0 THEN ++ INSERT INTO ~s (owner_id, peer_name_id, peer_server_id, at, count) VALUES (@ownerID, @peer_nameID, @peer_serverID, atdate, 1); ++ END IF; ++ END IF; ++END;", [logmessage_name(VHost),UName,UName,UName,UName,SName,SName,RName,RName,UName,UName,SName,RName,StName,StName]). +--- mod_logdb_pgsql.erl.orig 2009-02-05 19:21:29.000000000 +0200 ++++ mod_logdb_pgsql.erl 2009-02-05 19:20:29.000000000 +0200 +@@ -0,0 +1,1078 @@ ++%%%---------------------------------------------------------------------- ++%%% File : mod_logdb_pgsql.erl ++%%% Author : Oleg Palij (mailto,xmpp:o.palij@gmail.com) ++%%% Purpose : Posgresql backend for mod_logdb ++%%% Version : trunk ++%%% Id : $Id: mod_logdb_pgsql.erl 1253 2009-02-02 11:10:03Z malik $ ++%%% Url : http://www.dp.uz.gov.ua/o.palij/mod_logdb/ ++%%%---------------------------------------------------------------------- ++ ++-module(mod_logdb_pgsql). ++-author('o.palij@gmail.com'). ++ ++-include("mod_logdb.hrl"). ++-include("ejabberd.hrl"). ++-include("jlib.hrl"). ++ ++-behaviour(gen_logdb). ++-behaviour(gen_server). ++ ++% gen_server ++-export([code_change/3,handle_call/3,handle_cast/2,handle_info/2,init/1,terminate/2]). ++% gen_mod ++-export([start/2, stop/1]). ++% gen_logdb ++-export([log_message/2, ++ rebuild_stats/1, ++ rebuild_stats_at/2, ++ delete_messages_by_user_at/3, delete_all_messages_by_user_at/3, delete_messages_at/2, ++ get_vhost_stats/1, get_vhost_stats_at/2, get_user_stats/2, get_user_messages_at/3, ++ get_dates/1, ++ get_users_settings/1, get_user_settings/2, set_user_settings/3, ++ drop_user/2]). ++ ++-export([view_table/3]). ++ ++% gen_server call timeout ++-define(CALL_TIMEOUT, 30000). ++-define(PGSQL_TIMEOUT, 60000). ++-define(PROCNAME, mod_logdb_pgsql). ++ ++-import(mod_logdb, [list_to_bool/1, bool_to_list/1, ++ list_to_string/1, string_to_list/1, ++ convert_timestamp_brief/1]). ++ ++-record(state, {dbref, vhost, server, port, db, user, password, schema}). ++ ++% replace "." with "_" ++escape_vhost(VHost) -> lists:map(fun(46) -> 95; ++ (A) -> A ++ end, VHost). ++ ++prefix(Schema) -> ++ Schema ++ ".\"" ++ "logdb_". ++ ++suffix(VHost) -> ++ "_" ++ escape_vhost(VHost) ++ "\"". ++ ++messages_table(VHost, Schema, Date) -> ++ prefix(Schema) ++ "messages_" ++ Date ++ suffix(VHost). ++ ++view_table(VHost, Schema, Date) -> ++ Table = messages_table(VHost, Schema, Date), ++ TablewoS = lists:sublist(Table, length(Schema) + 3, length(Table) - length(Schema) - 3), ++ lists:append([Schema, ".\"v_", TablewoS, "\""]). ++ ++stats_table(VHost, Schema) -> ++ prefix(Schema) ++ "stats" ++ suffix(VHost). ++ ++temp_table(VHost, Schema) -> ++ prefix(Schema) ++ "temp" ++ suffix(VHost). ++ ++settings_table(VHost, Schema) -> ++ prefix(Schema) ++ "settings" ++ suffix(VHost). ++ ++users_table(VHost, Schema) -> ++ prefix(Schema) ++ "users" ++ suffix(VHost). ++servers_table(VHost, Schema) -> ++ prefix(Schema) ++ "servers" ++ suffix(VHost). ++resources_table(VHost, Schema) -> ++ prefix(Schema) ++ "resources" ++ suffix(VHost). ++ ++logmessage_name(VHost, Schema) -> ++ prefix(Schema) ++ "logmessage" ++ suffix(VHost). ++ ++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++% ++% gen_mod callbacks ++% ++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++start(VHost, Opts) -> ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:start({local, Proc}, ?MODULE, [VHost, Opts], []). ++ ++stop(VHost) -> ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:call(Proc, {stop}, ?CALL_TIMEOUT). ++ ++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++% ++% gen_server callbacks ++% ++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++init([VHost, Opts]) -> ++ Server = gen_mod:get_opt(server, Opts, "localhost"), ++ DB = gen_mod:get_opt(db, Opts, "ejabberd_logdb"), ++ User = gen_mod:get_opt(user, Opts, "root"), ++ Port = gen_mod:get_opt(port, Opts, 5432), ++ Password = gen_mod:get_opt(password, Opts, ""), ++ Schema = gen_mod:get_opt(schema, Opts, "public"), ++ ++ ?MYDEBUG("Starting pgsql backend for ~p", [VHost]), ++ ++ St = #state{vhost=VHost, ++ server=Server, port=Port, db=DB, ++ user=User, password=Password, ++ schema=Schema}, ++ ++ case open_pgsql_connection(St) of ++ {ok, DBRef} -> ++ State = St#state{dbref=DBRef}, ++ ok = create_internals(State), ++ ok = create_stats_table(State), ++ ok = create_settings_table(State), ++ ok = create_users_table(State), ++ ok = create_servers_table(State), ++ ok = create_resources_table(State), ++ erlang:monitor(process, DBRef), ++ {ok, State}; ++ % this does not work ++ {error, Reason} -> ++ ?ERROR_MSG("PgSQL connection failed: ~p~n", [Reason]), ++ {stop, db_connection_failed}; ++ % and this too, becouse pgsql_conn do exit() which can not be catched ++ {'EXIT', Rez} -> ++ ?ERROR_MSG("Rez: ~p~n", [Rez]), ++ {stop, db_connection_failed} ++ end. ++ ++open_pgsql_connection(#state{server=Server, port=Port, db=DB, schema=Schema, ++ user=User, password=Password} = _State) -> ++ {ok, DBRef} = pgsql:connect(Server, DB, User, Password, Port), ++ {updated, _} = sql_query_internal(DBRef, ["SET SEARCH_PATH TO ",Schema,";"]), ++ {ok, DBRef}. ++ ++close_pgsql_connection(DBRef) -> ++ ?MYDEBUG("Closing ~p pgsql connection", [DBRef]), ++ pgsql:terminate(DBRef). ++ ++handle_call({log_message, Msg}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) -> ++ Date = convert_timestamp_brief(Msg#msg.timestamp), ++ TableName = messages_table(VHost, Schema, Date), ++ ViewName = view_table(VHost, Schema, Date), ++ ++ Query = [ "SELECT ", logmessage_name(VHost, Schema)," " ++ "('", TableName, "',", ++ "'", ViewName, "',", ++ "'", Date, "',", ++ "'", Msg#msg.owner_name, "',", ++ "'", Msg#msg.peer_name, "',", ++ "'", Msg#msg.peer_server, "',", ++ "'", ejabberd_odbc:escape(Msg#msg.peer_resource), "',", ++ "'", atom_to_list(Msg#msg.direction), "',", ++ "'", Msg#msg.type, "',", ++ "'", ejabberd_odbc:escape(Msg#msg.subject), "',", ++ "'", ejabberd_odbc:escape(Msg#msg.body), "',", ++ "'", Msg#msg.timestamp, "');"], ++ ++ case sql_query_internal_silent(DBRef, Query) of ++ % TODO: change this ++ {data, [{"0"}]} -> ++ ?MYDEBUG("Logged ok for ~p, peer: ~p", [Msg#msg.owner_name++"@"++VHost, ++ Msg#msg.peer_name++"@"++Msg#msg.peer_server]), ++ ok; ++ {error, _Reason} -> ++ error ++ end, ++ {reply, ok, State}; ++handle_call({rebuild_stats_at, Date}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) -> ++ Reply = rebuild_stats_at_int(DBRef, VHost, Schema, Date), ++ {reply, Reply, State}; ++handle_call({delete_messages_by_user_at, [], _Date}, _From, State) -> ++ {reply, error, State}; ++handle_call({delete_messages_by_user_at, Msgs, Date}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) -> ++ Temp = lists:flatmap(fun(#msg{timestamp=Timestamp} = _Msg) -> ++ ["'",Timestamp,"'",","] ++ end, Msgs), ++ ++ Temp1 = lists:append([lists:sublist(Temp, length(Temp)-1), ");"]), ++ ++ Query = ["DELETE FROM ",messages_table(VHost, Schema, Date)," ", ++ "WHERE timestamp IN (", Temp1], ++ ++ Reply = ++ case sql_query_internal(DBRef, Query) of ++ {updated, _} -> ++ rebuild_stats_at_int(DBRef, VHost, Schema, Date); ++ {error, _} -> ++ error ++ end, ++ {reply, Reply, State}; ++handle_call({delete_all_messages_by_user_at, User, Date}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) -> ++ ok = delete_all_messages_by_user_at_int(DBRef, Schema, User, VHost, Date), ++ ok = delete_stats_by_user_at_int(DBRef, Schema, User, VHost, Date), ++ {reply, ok, State}; ++handle_call({delete_messages_at, Date}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) -> ++ {updated, _} = sql_query_internal(DBRef, ["DROP VIEW ",view_table(VHost, Schema, Date),";"]), ++ Reply = ++ case sql_query_internal(DBRef, ["DROP TABLE ",messages_table(VHost, Schema, Date)," CASCADE;"]) of ++ {updated, _} -> ++ Query = ["DELETE FROM ",stats_table(VHost, Schema)," " ++ "WHERE at='",Date,"';"], ++ case sql_query_internal(DBRef, Query) of ++ {updated, _} -> ++ ok; ++ {error, _} -> ++ error ++ end; ++ {error, _} -> ++ error ++ end, ++ {reply, Reply, State}; ++handle_call({get_vhost_stats}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) -> ++ SName = stats_table(VHost, Schema), ++ Query = ["SELECT at, sum(count) ", ++ "FROM ",SName," ", ++ "GROUP BY at ", ++ "ORDER BY DATE(at) DESC;" ++ ], ++ Reply = ++ case sql_query_internal(DBRef, Query) of ++ {data, Recs} -> ++ {ok, [ {Date, list_to_integer(Count)} || {Date, Count} <- Recs]}; ++ {error, Reason} -> ++ % TODO: Duplicate error message ? ++ {error, Reason} ++ end, ++ {reply, Reply, State}; ++handle_call({get_vhost_stats_at, Date}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) -> ++ SName = stats_table(VHost, Schema), ++ Query = ["SELECT username, sum(count) AS allcount ", ++ "FROM ",SName," ", ++ "JOIN ",users_table(VHost, Schema)," ON owner_id=user_id ", ++ "WHERE at='",Date,"' ", ++ "GROUP BY username ", ++ "ORDER BY allcount DESC;" ++ ], ++ Reply = ++ case sql_query_internal(DBRef, Query) of ++ {data, Recs} -> ++ RFun = fun({User, Count}) -> ++ {User, list_to_integer(Count)} ++ end, ++ {ok, lists:reverse(lists:keysort(2, lists:map(RFun, Recs)))}; ++ {error, Reason} -> ++ % TODO: ++ {error, Reason} ++ end, ++ {reply, Reply, State}; ++handle_call({get_user_stats, User}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) -> ++ {reply, get_user_stats_int(DBRef, Schema, User, VHost), State}; ++handle_call({get_user_messages_at, User, Date}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) -> ++ Query = ["SELECT peer_name,", ++ "peer_server,", ++ "peer_resource,", ++ "direction," ++ "type," ++ "subject," ++ "body," ++ "timestamp " ++ "FROM ",view_table(VHost, Schema, Date)," " ++ "WHERE owner_name='",User,"';"], ++ Reply = ++ case sql_query_internal(DBRef, Query) of ++ {data, Recs} -> ++ Fun = fun({Peer_name, Peer_server, Peer_resource, ++ Direction, ++ Type, ++ Subject, Body, ++ Timestamp}) -> ++ #msg{peer_name=Peer_name, peer_server=Peer_server, peer_resource=Peer_resource, ++ direction=list_to_atom(Direction), ++ type=Type, ++ subject=Subject, body=Body, ++ timestamp=Timestamp} ++ end, ++ {ok, lists:map(Fun, Recs)}; ++ {error, Reason} -> ++ {error, Reason} ++ end, ++ {reply, Reply, State}; ++handle_call({get_dates}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) -> ++ SName = stats_table(VHost, Schema), ++ Query = ["SELECT at ", ++ "FROM ",SName," ", ++ "GROUP BY at ", ++ "ORDER BY at DESC;" ++ ], ++ Reply = ++ case sql_query_internal(DBRef, Query) of ++ {data, Result} -> ++ [ Date || {Date} <- Result ]; ++ {error, Reason} -> ++ {error, Reason} ++ end, ++ {reply, Reply, State}; ++handle_call({get_users_settings}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) -> ++ Query = ["SELECT username,dolog_default,dolog_list,donotlog_list ", ++ "FROM ",settings_table(VHost, Schema)," ", ++ "JOIN ",users_table(VHost, Schema)," ON user_id=owner_id;"], ++ Reply = ++ case sql_query_internal(DBRef, Query) of ++ {data, Recs} -> ++ {ok, [#user_settings{owner_name=Owner, ++ dolog_default=list_to_bool(DoLogDef), ++ dolog_list=string_to_list(DoLogL), ++ donotlog_list=string_to_list(DoNotLogL) ++ } || {Owner, DoLogDef, DoLogL, DoNotLogL} <- Recs]}; ++ {error, Reason} -> ++ {error, Reason} ++ end, ++ {reply, Reply, State}; ++handle_call({get_user_settings, User}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) -> ++ Query = ["SELECT dolog_default,dolog_list,donotlog_list ", ++ "FROM ",settings_table(VHost, Schema)," ", ++ "WHERE owner_id=(SELECT user_id FROM ",users_table(VHost, Schema)," WHERE username='",User,"');"], ++ Reply = ++ case sql_query_internal_silent(DBRef, Query) of ++ {data, []} -> ++ {ok, []}; ++ {data, [{DoLogDef, DoLogL, DoNotLogL}]} -> ++ {ok, #user_settings{owner_name=User, ++ dolog_default=list_to_bool(DoLogDef), ++ dolog_list=string_to_list(DoLogL), ++ donotlog_list=string_to_list(DoNotLogL)}}; ++ {error, Reason} -> ++ ?ERROR_MSG("Failed to get_user_settings for ~p@~p: ~p", [User, VHost, Reason]), ++ error ++ end, ++ {reply, Reply, State}; ++handle_call({set_user_settings, User, #user_settings{dolog_default=DoLogDef, ++ dolog_list=DoLogL, ++ donotlog_list=DoNotLogL}}, ++ _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) -> ++ User_id = get_user_id(DBRef, VHost, Schema, User), ++ Query = ["UPDATE ",settings_table(VHost, Schema)," ", ++ "SET dolog_default=",bool_to_list(DoLogDef),", ", ++ "dolog_list='",list_to_string(DoLogL),"', ", ++ "donotlog_list='",list_to_string(DoNotLogL),"' ", ++ "WHERE owner_id=",User_id,";"], ++ ++ Reply = ++ case sql_query_internal(DBRef, Query) of ++ {updated, 0} -> ++ IQuery = ["INSERT INTO ",settings_table(VHost, Schema)," ", ++ "(owner_id, dolog_default, dolog_list, donotlog_list) ", ++ "VALUES ", ++ "(",User_id,", ",bool_to_list(DoLogDef),",'",list_to_string(DoLogL),"','",list_to_string(DoNotLogL),"');"], ++ case sql_query_internal(DBRef, IQuery) of ++ {updated, 1} -> ++ ?MYDEBUG("New settings for ~s@~s", [User, VHost]), ++ ok; ++ {error, _} -> ++ error ++ end; ++ {updated, 1} -> ++ ?MYDEBUG("Updated settings for ~s@~s", [User, VHost]), ++ ok; ++ {error, _} -> ++ error ++ end, ++ {reply, Reply, State}; ++handle_call({stop}, _From, State) -> ++ ?MYDEBUG("Stoping pgsql backend for ~p", [State#state.vhost]), ++ {stop, normal, ok, State}; ++handle_call(Msg, _From, State) -> ++ ?INFO_MSG("Got call Msg: ~p, State: ~p", [Msg, State]), ++ {noreply, State}. ++ ++ ++handle_cast({rebuild_stats}, State) -> ++ rebuild_all_stats_int(State), ++ {noreply, State}; ++handle_cast({drop_user, User}, #state{vhost=VHost, schema=Schema}=State) -> ++ Fun = fun() -> ++ {ok, DBRef} = open_pgsql_connection(State), ++ {ok, Dates} = get_user_stats_int(DBRef, Schema, User, VHost), ++ MDResult = lists:map(fun({Date, _}) -> ++ delete_all_messages_by_user_at_int(DBRef, Schema, User, VHost, Date) ++ end, Dates), ++ StDResult = delete_all_stats_by_user_int(DBRef, Schema, User, VHost), ++ SDResult = delete_user_settings_int(DBRef, Schema, User, VHost), ++ case lists:all(fun(Result) when Result == ok -> ++ true; ++ (Result) when Result == error -> ++ false ++ end, lists:append([MDResult, [StDResult], [SDResult]])) of ++ true -> ++ ?INFO_MSG("Removed ~s@~s", [User, VHost]); ++ false -> ++ ?ERROR_MSG("Failed to remove ~s@~s", [User, VHost]) ++ end, ++ close_pgsql_connection(DBRef) ++ end, ++ spawn(Fun), ++ {noreply, State}; ++handle_cast(Msg, State) -> ++ ?INFO_MSG("Got cast Msg:~p, State:~p", [Msg, State]), ++ {noreply, State}. ++ ++handle_info({'DOWN', _MonitorRef, process, _Pid, _Info}, State) -> ++ {stop, connection_dropped, State}; ++handle_info(Info, State) -> ++ ?INFO_MSG("Got Info:~p, State:~p", [Info, State]), ++ {noreply, State}. ++ ++terminate(_Reason, #state{dbref=DBRef}=_State) -> ++ close_pgsql_connection(DBRef), ++ ok. ++ ++code_change(_OldVsn, State, _Extra) -> ++ {ok, State}. ++ ++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++% ++% gen_logdb callbacks ++% ++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++log_message(VHost, Msg) -> ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:call(Proc, {log_message, Msg}, ?CALL_TIMEOUT). ++rebuild_stats(VHost) -> ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:cast(Proc, {rebuild_stats}). ++rebuild_stats_at(VHost, Date) -> ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:call(Proc, {rebuild_stats_at, Date}, ?CALL_TIMEOUT). ++delete_messages_by_user_at(VHost, Msgs, Date) -> ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:call(Proc, {delete_messages_by_user_at, Msgs, Date}, ?CALL_TIMEOUT). ++delete_all_messages_by_user_at(User, VHost, Date) -> ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:call(Proc, {delete_all_messages_by_user_at, User, Date}, ?CALL_TIMEOUT). ++delete_messages_at(VHost, Date) -> ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:call(Proc, {delete_messages_at, Date}, ?CALL_TIMEOUT). ++get_vhost_stats(VHost) -> ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:call(Proc, {get_vhost_stats}, ?CALL_TIMEOUT). ++get_vhost_stats_at(VHost, Date) -> ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:call(Proc, {get_vhost_stats_at, Date}, ?CALL_TIMEOUT). ++get_user_stats(User, VHost) -> ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:call(Proc, {get_user_stats, User}, ?CALL_TIMEOUT). ++get_user_messages_at(User, VHost, Date) -> ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:call(Proc, {get_user_messages_at, User, Date}, ?CALL_TIMEOUT). ++get_dates(VHost) -> ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:call(Proc, {get_dates}, ?CALL_TIMEOUT). ++get_users_settings(VHost) -> ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:call(Proc, {get_users_settings}, ?CALL_TIMEOUT). ++get_user_settings(User, VHost) -> ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:call(Proc, {get_user_settings, User}, ?CALL_TIMEOUT). ++set_user_settings(User, VHost, Set) -> ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:call(Proc, {set_user_settings, User, Set}, ?CALL_TIMEOUT). ++drop_user(User, VHost) -> ++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME), ++ gen_server:cast(Proc, {drop_user, User}). ++ ++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++% ++% internals ++% ++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++get_dates_int(DBRef, VHost) -> ++ Query = ["SELECT n.nspname as \"Schema\", ++ c.relname as \"Name\", ++ CASE c.relkind WHEN 'r' THEN 'table' WHEN 'v' THEN 'view' WHEN 'i' THEN 'index' WHEN 'S' THEN 'sequence' WHEN 's' THEN 'special' END as \"Type\", ++ r.rolname as \"Owner\" ++ FROM pg_catalog.pg_class c ++ JOIN pg_catalog.pg_roles r ON r.oid = c.relowner ++ LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace ++ WHERE c.relkind IN ('r','') ++ AND n.nspname NOT IN ('pg_catalog', 'pg_toast') ++ AND c.relname ~ '^(.*",escape_vhost(VHost),".*)$' ++ AND pg_catalog.pg_table_is_visible(c.oid) ++ ORDER BY 1,2;"], ++ case sql_query_internal(DBRef, Query) of ++ {data, Recs} -> ++ lists:foldl(fun({_Schema, Table, _Type, _Owner}, Dates) -> ++ case regexp:match(Table,"[0-9]+-[0-9]+-[0-9]+") of ++ {match, S, E} -> ++ lists:append(Dates, [lists:sublist(Table,S,E)]); ++ nomatch -> ++ Dates ++ end ++ end, [], Recs); ++ {error, _} -> ++ [] ++ end. ++ ++rebuild_all_stats_int(#state{vhost=VHost, schema=Schema}=State) -> ++ Fun = fun() -> ++ {ok, DBRef} = open_pgsql_connection(State), ++ ok = delete_nonexistent_stats(DBRef, Schema, VHost), ++ case lists:filter(fun(Date) -> ++ case catch rebuild_stats_at_int(DBRef, VHost, Schema, Date) of ++ ok -> false; ++ error -> true; ++ {'EXIT', _} -> true ++ end ++ end, get_dates_int(DBRef, VHost)) of ++ [] -> ok; ++ FTables -> ++ ?ERROR_MSG("Failed to rebuild stats for ~p dates", [FTables]), ++ error ++ end, ++ close_pgsql_connection(DBRef) ++ end, ++ spawn(Fun). ++ ++rebuild_stats_at_int(DBRef, VHost, Schema, Date) -> ++ TempTable = temp_table(VHost, Schema), ++ Fun = ++ fun() -> ++ Table = messages_table(VHost, Schema, Date), ++ STable = stats_table(VHost, Schema), ++ ++ DQuery = [ "DELETE FROM ",STable," ", ++ "WHERE at='",Date,"';"], ++ ++ ok = create_temp_table(DBRef, VHost, Schema), ++ {updated, _} = sql_query_internal(DBRef, ["LOCK TABLE ",Table," IN ACCESS EXCLUSIVE MODE;"]), ++ {updated, _} = sql_query_internal(DBRef, ["LOCK TABLE ",TempTable," IN ACCESS EXCLUSIVE MODE;"]), ++ SQuery = ["INSERT INTO ",TempTable," ", ++ "(owner_id,peer_name_id,peer_server_id,at,count) ", ++ "SELECT owner_id,peer_name_id,peer_server_id,'",Date,"'",",count(*) ", ++ "FROM ",Table," GROUP BY owner_id,peer_name_id,peer_server_id;"], ++ case sql_query_internal(DBRef, SQuery) of ++ {updated, 0} -> ++ Count = sql_query_internal(DBRef, ["SELECT count(*) FROM ",Table,";"]), ++ case Count of ++ {data, [{"0"}]} -> ++ {updated, _} = sql_query_internal(DBRef, ["DROP VIEW ",view_table(VHost, Schema, Date),";"]), ++ {updated, _} = sql_query_internal(DBRef, ["DROP TABLE ",Table," CASCADE;"]), ++ {updated, _} = sql_query_internal(DBRef, ["LOCK TABLE ",STable," IN ACCESS EXCLUSIVE MODE;"]), ++ {updated, _} = sql_query_internal(DBRef, DQuery), ++ ok; ++ _ -> ++ ?ERROR_MSG("Failed to calculate stats for ~s table! Count was ~p.", [Date, Count]), ++ error ++ end; ++ {updated, _} -> ++ {updated, _} = sql_query_internal(DBRef, ["LOCK TABLE ",STable," IN ACCESS EXCLUSIVE MODE;"]), ++ {updated, _} = sql_query_internal(DBRef, ["LOCK TABLE ",TempTable," IN ACCESS EXCLUSIVE MODE;"]), ++ {updated, _} = sql_query_internal(DBRef, DQuery), ++ SQuery1 = ["INSERT INTO ",STable," ", ++ "(owner_id,peer_name_id,peer_server_id,at,count) ", ++ "SELECT owner_id,peer_name_id,peer_server_id,at,count ", ++ "FROM ",TempTable,";"], ++ case sql_query_internal(DBRef, SQuery1) of ++ {updated, _} -> ok; ++ {error, _} -> error ++ end; ++ {error, _} -> error ++ end ++ end, % fun ++ ++ case sql_transaction_internal(DBRef, Fun) of ++ {atomic, _} -> ++ ?INFO_MSG("Rebuilded stats for ~p at ~p", [VHost, Date]), ++ ok; ++ {aborted, Reason} -> ++ ?ERROR_MSG("Failed to rebuild stats for ~s table: ~p.", [Date, Reason]), ++ error ++ end, ++ sql_query_internal(DBRef, ["DROP TABLE ",TempTable,";"]), ++ ok. ++ ++delete_nonexistent_stats(DBRef, Schema, VHost) -> ++ Dates = get_dates_int(DBRef, VHost), ++ STable = stats_table(VHost, Schema), ++ ++ Temp = lists:flatmap(fun(Date) -> ++ ["'",Date,"'",","] ++ end, Dates), ++ ++ case Temp of ++ [] -> ++ ok; ++ _ -> ++ % replace last "," with ");" ++ Temp1 = lists:append([lists:sublist(Temp, length(Temp)-1), ");"]), ++ Query = ["DELETE FROM ",STable," ", ++ "WHERE at NOT IN (", Temp1], ++ case sql_query_internal(DBRef, Query) of ++ {updated, _} -> ++ ok; ++ {error, _} -> ++ error ++ end ++ end. ++ ++get_user_stats_int(DBRef, Schema, User, VHost) -> ++ SName = stats_table(VHost, Schema), ++ UName = users_table(VHost, Schema), ++ Query = ["SELECT stats.at, sum(stats.count) ", ++ "FROM ",UName," AS users ", ++ "JOIN ",SName," AS stats ON owner_id=user_id " ++ "WHERE users.username='",User,"' ", ++ "GROUP BY stats.at " ++ "ORDER BY DATE(at) DESC;" ++ ], ++ case sql_query_internal(DBRef, Query) of ++ {data, Recs} -> ++ {ok, [ {Date, list_to_integer(Count)} || {Date, Count} <- Recs ]}; ++ {error, Result} -> ++ {error, Result} ++ end. ++ ++delete_all_messages_by_user_at_int(DBRef, Schema, User, VHost, Date) -> ++ DQuery = ["DELETE FROM ",messages_table(VHost, Schema, Date)," ", ++ "WHERE owner_id=(SELECT user_id FROM ",users_table(VHost, Schema)," WHERE username='",User,"');"], ++ case sql_query_internal(DBRef, DQuery) of ++ {updated, _} -> ++ ?INFO_MSG("Dropped messages for ~s@~s at ~s", [User, VHost, Date]), ++ ok; ++ {error, _} -> ++ error ++ end. ++ ++delete_all_stats_by_user_int(DBRef, Schema, User, VHost) -> ++ SQuery = ["DELETE FROM ",stats_table(VHost, Schema)," ", ++ "WHERE owner_id=(SELECT user_id FROM ",users_table(VHost, Schema)," WHERE username='",User,"');"], ++ case sql_query_internal(DBRef, SQuery) of ++ {updated, _} -> ++ ?INFO_MSG("Dropped all stats for ~s@~s", [User, VHost]), ++ ok; ++ {error, _} -> error ++ end. ++ ++delete_stats_by_user_at_int(DBRef, Schema, User, VHost, Date) -> ++ SQuery = ["DELETE FROM ",stats_table(VHost, Schema)," ", ++ "WHERE owner_id=(SELECT user_id FROM ",users_table(VHost, Schema)," WHERE username='",User,"') ", ++ "AND at='",Date,"';"], ++ case sql_query_internal(DBRef, SQuery) of ++ {updated, _} -> ++ ?INFO_MSG("Dropped stats for ~s@~s at ~s", [User, VHost, Date]), ++ ok; ++ {error, _} -> error ++ end. ++ ++delete_user_settings_int(DBRef, Schema, User, VHost) -> ++ Query = ["DELETE FROM ",settings_table(VHost, Schema)," ", ++ "WHERE owner_id=(SELECT user_id FROM ",users_table(VHost, Schema)," WHERE username='",User,"');"], ++ case sql_query_internal(DBRef, Query) of ++ {updated, _} -> ++ ?INFO_MSG("Dropped ~s@~s settings", [User, VHost]), ++ ok; ++ {error, Reason} -> ++ ?ERROR_MSG("Failed to drop ~s@~s settings: ~p", [User, VHost, Reason]), ++ error ++ end. ++ ++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++% ++% tables internals ++% ++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++create_temp_table(DBRef, VHost, Schema) -> ++ TName = temp_table(VHost, Schema), ++ Query = ["CREATE TABLE ",TName," (", ++ "owner_id INTEGER, ", ++ "peer_name_id INTEGER, ", ++ "peer_server_id INTEGER, ", ++ "at VARCHAR(20), ", ++ "count INTEGER ", ++ ");" ++ ], ++ case sql_query_internal(DBRef, Query) of ++ {updated, _} -> ok; ++ {error, _Reason} -> error ++ end. ++ ++create_stats_table(#state{dbref=DBRef, vhost=VHost, schema=Schema}=State) -> ++ SName = stats_table(VHost, Schema), ++ ++ Fun = ++ fun() -> ++ Query = ["CREATE TABLE ",SName," (", ++ "owner_id INTEGER, ", ++ "peer_name_id INTEGER, ", ++ "peer_server_id INTEGER, ", ++ "at VARCHAR(20), ", ++ "count integer", ++ ");" ++ ], ++ case sql_query_internal_silent(DBRef, Query) of ++ {updated, _} -> ++ {updated, _} = sql_query_internal(DBRef, ["CREATE INDEX \"s_search_i_",Schema,"_",escape_vhost(VHost),"\" ON ",SName," (owner_id, peer_name_id, peer_server_id);"]), ++ {updated, _} = sql_query_internal(DBRef, ["CREATE INDEX \"s_at_i_",Schema,"_",escape_vhost(VHost),"\" ON ",SName," (at);"]), ++ created; ++ {error, Reason} -> ++ case lists:keysearch(code, 1, Reason) of ++ {value, {code, "42P07"}} -> ++ exists; ++ _ -> ++ ?ERROR_MSG("Failed to create stats table for ~p: ~p", [VHost, Reason]), ++ error ++ end ++ end ++ end, ++ case sql_transaction_internal(DBRef, Fun) of ++ {atomic, created} -> ++ ?MYDEBUG("Created stats table for ~p", [VHost]), ++ rebuild_all_stats_int(State), ++ ok; ++ {atomic, exists} -> ++ ?MYDEBUG("Stats table for ~p already exists", [VHost]), ++ {match, F, L} = regexp:match(SName, "\".*\""), ++ QTable = lists:sublist(SName, F+1, L-2), ++ OIDQuery = ["SELECT c.oid FROM pg_catalog.pg_class c LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE c.relname='",QTable,"' AND pg_catalog.pg_table_is_visible(c.oid);"], ++ {data,[{OID}]} = sql_query_internal(DBRef, OIDQuery), ++ CheckQuery = ["SELECT a.attname FROM pg_catalog.pg_attribute a WHERE a.attrelid = '",OID,"' AND a.attnum > 0 AND NOT a.attisdropped AND a.attname ~ '^peer_.*_id$';"], ++ case sql_query_internal(DBRef, CheckQuery) of ++ {data, Elems} when length(Elems) == 2 -> ++ ?MYDEBUG("Stats table structure is ok", []), ++ ok; ++ _ -> ++ ?INFO_MSG("It seems like stats table structure is invalid. I will drop it and recreate", []), ++ case sql_query_internal(DBRef, ["DROP TABLE ",SName,";"]) of ++ {updated, _} -> ++ ?INFO_MSG("Successfully dropped ~p", [SName]); ++ _ -> ++ ?ERROR_MSG("Failed to drop ~p. You should drop it and restart module", [SName]) ++ end, ++ error ++ end; ++ {error, _} -> error ++ end. ++ ++create_settings_table(#state{dbref=DBRef, vhost=VHost, schema=Schema}) -> ++ SName = settings_table(VHost, Schema), ++ Query = ["CREATE TABLE ",SName," (", ++ "owner_id INTEGER PRIMARY KEY, ", ++ "dolog_default BOOLEAN, ", ++ "dolog_list TEXT DEFAULT '', ", ++ "donotlog_list TEXT DEFAULT ''", ++ ");" ++ ], ++ case sql_query_internal_silent(DBRef, Query) of ++ {updated, _} -> ++ ?MYDEBUG("Created settings table for ~p", [VHost]), ++ ok; ++ {error, Reason} -> ++ case lists:keysearch(code, 1, Reason) of ++ {value, {code, "42P07"}} -> ++ ?MYDEBUG("Settings table for ~p already exists", [VHost]), ++ ok; ++ _ -> ++ ?ERROR_MSG("Failed to create settings table for ~p: ~p", [VHost, Reason]), ++ error ++ end ++ end. ++ ++create_users_table(#state{dbref=DBRef, vhost=VHost, schema=Schema}) -> ++ SName = users_table(VHost, Schema), ++ ++ Fun = ++ fun() -> ++ Query = ["CREATE TABLE ",SName," (", ++ "username TEXT UNIQUE, ", ++ "user_id SERIAL PRIMARY KEY", ++ ");" ++ ], ++ case sql_query_internal_silent(DBRef, Query) of ++ {updated, _} -> ++ {updated, _} = sql_query_internal(DBRef, ["CREATE INDEX \"username_i_",Schema,"_",escape_vhost(VHost),"\" ON ",SName," (username);"]), ++ created; ++ {error, Reason} -> ++ case lists:keysearch(code, 1, Reason) of ++ {value, {code, "42P07"}} -> ++ exists; ++ _ -> ++ ?ERROR_MSG("Failed to create users table for ~p: ~p", [VHost, Reason]), ++ error ++ end ++ end ++ end, ++ case sql_transaction_internal(DBRef, Fun) of ++ {atomic, created} -> ++ ?MYDEBUG("Created users table for ~p", [VHost]), ++ ok; ++ {atomic, exists} -> ++ ?MYDEBUG("Users table for ~p already exists", [VHost]), ++ ok; ++ {aborted, _} -> error ++ end. ++ ++create_servers_table(#state{dbref=DBRef, vhost=VHost, schema=Schema}) -> ++ SName = servers_table(VHost, Schema), ++ Fun = ++ fun() -> ++ Query = ["CREATE TABLE ",SName," (", ++ "server TEXT UNIQUE, ", ++ "server_id SERIAL PRIMARY KEY", ++ ");" ++ ], ++ case sql_query_internal_silent(DBRef, Query) of ++ {updated, _} -> ++ {updated, _} = sql_query_internal(DBRef, ["CREATE INDEX \"server_i_",Schema,"_",escape_vhost(VHost),"\" ON ",SName," (server);"]), ++ created; ++ {error, Reason} -> ++ case lists:keysearch(code, 1, Reason) of ++ {value, {code, "42P07"}} -> ++ exists; ++ _ -> ++ ?ERROR_MSG("Failed to create servers table for ~p: ~p", [VHost, Reason]), ++ error ++ end ++ end ++ end, ++ case sql_transaction_internal(DBRef, Fun) of ++ {atomic, created} -> ++ ?MYDEBUG("Created servers table for ~p", [VHost]), ++ ok; ++ {atomic, exists} -> ++ ?MYDEBUG("Servers table for ~p already exists", [VHost]), ++ ok; ++ {aborted, _} -> error ++ end. ++ ++create_resources_table(#state{dbref=DBRef, vhost=VHost, schema=Schema}) -> ++ RName = resources_table(VHost, Schema), ++ Fun = fun() -> ++ Query = ["CREATE TABLE ",RName," (", ++ "resource TEXT UNIQUE, ", ++ "resource_id SERIAL PRIMARY KEY", ++ ");" ++ ], ++ case sql_query_internal_silent(DBRef, Query) of ++ {updated, _} -> ++ {updated, _} = sql_query_internal(DBRef, ["CREATE INDEX \"resource_i_",Schema,"_",escape_vhost(VHost),"\" ON ",RName," (resource);"]), ++ created; ++ {error, Reason} -> ++ case lists:keysearch(code, 1, Reason) of ++ {value, {code, "42P07"}} -> ++ exists; ++ _ -> ++ ?ERROR_MSG("Failed to create users table for ~p: ~p", [VHost, Reason]), ++ error ++ end ++ end ++ end, ++ case sql_transaction_internal(DBRef, Fun) of ++ {atomic, created} -> ++ ?MYDEBUG("Created resources table for ~p", [VHost]), ++ ok; ++ {atomic, exists} -> ++ ?MYDEBUG("Resources table for ~p already exists", [VHost]), ++ ok; ++ {aborted, _} -> error ++ end. ++ ++create_internals(#state{dbref=DBRef, vhost=VHost, schema=Schema}) -> ++ sql_query_internal(DBRef, ["DROP FUNCTION IF EXISTS ",logmessage_name(VHost,Schema)," (tbname TEXT, atdt TEXT, owner TEXT, peer_name TEXT, peer_server TEXT, peer_resource TEXT, mdirection VARCHAR(4), mtype VARCHAR(9), msubj TEXT, mbody TEXT, mtimestamp DOUBLE PRECISION);"]), ++ case sql_query_internal(DBRef, [get_logmessage(VHost, Schema)]) of ++ {updated, _} -> ++ ?MYDEBUG("Created logmessage for ~p", [VHost]), ++ ok; ++ {error, _} -> ++ error ++ end. ++ ++get_user_id(DBRef, VHost, Schema, User) -> ++ SQuery = ["SELECT user_id FROM ",users_table(VHost, Schema)," ", ++ "WHERE username='",User,"';"], ++ case sql_query_internal(DBRef, SQuery) of ++ {data, []} -> ++ IQuery = ["INSERT INTO ",users_table(VHost, Schema)," ", ++ "VALUES ('",User,"');"], ++ case sql_query_internal_silent(DBRef, IQuery) of ++ {updated, _} -> ++ {data, [{DBIdNew}]} = sql_query_internal(DBRef, SQuery), ++ DBIdNew; ++ {error, Reason} -> ++ % this can be in clustered environment ++ {value, {code, "23505"}} = lists:keysearch(code, 1, Reason), ++ ?ERROR_MSG("Duplicate key name for ~p", [User]), ++ {data, [{ClID}]} = sql_query_internal(DBRef, SQuery), ++ ClID ++ end; ++ {data, [{DBId}]} -> ++ DBId ++ end. ++ ++get_logmessage(VHost,Schema) -> ++ UName = users_table(VHost,Schema), ++ SName = servers_table(VHost,Schema), ++ RName = resources_table(VHost,Schema), ++ StName = stats_table(VHost,Schema), ++ io_lib:format("CREATE OR REPLACE FUNCTION ~s (tbname TEXT, vname TEXT, atdt TEXT, owner TEXT, peer_name TEXT, peer_server TEXT, peer_resource TEXT, mdirection VARCHAR(4), mtype VARCHAR(9), msubj TEXT, mbody TEXT, mtimestamp DOUBLE PRECISION) RETURNS INTEGER AS $$ ++DECLARE ++ ownerID INTEGER; ++ peer_nameID INTEGER; ++ peer_serverID INTEGER; ++ peer_resourceID INTEGER; ++ tablename ALIAS for $1; ++ viewname ALIAS for $2; ++ atdate ALIAS for $3; ++BEGIN ++ SELECT INTO ownerID user_id FROM ~s WHERE username = owner; ++ IF NOT FOUND THEN ++ INSERT INTO ~s (username) VALUES (owner); ++ ownerID := lastval(); ++ END IF; ++ ++ SELECT INTO peer_nameID user_id FROM ~s WHERE username = peer_name; ++ IF NOT FOUND THEN ++ INSERT INTO ~s (username) VALUES (peer_name); ++ peer_nameID := lastval(); ++ END IF; ++ ++ SELECT INTO peer_serverID server_id FROM ~s WHERE server = peer_server; ++ IF NOT FOUND THEN ++ INSERT INTO ~s (server) VALUES (peer_server); ++ peer_serverID := lastval(); ++ END IF; ++ ++ SELECT INTO peer_resourceID resource_id FROM ~s WHERE resource = peer_resource; ++ IF NOT FOUND THEN ++ INSERT INTO ~s (resource) VALUES (peer_resource); ++ peer_resourceID := lastval(); ++ END IF; ++ ++ BEGIN ++ EXECUTE 'INSERT INTO ' || tablename || ' (owner_id, peer_name_id, peer_server_id, peer_resource_id, direction, type, subject, body, timestamp) VALUES (' || ownerID || ',' || peer_nameID || ',' || peer_serverID || ',' || peer_resourceID || ',''' || mdirection || ''',''' || mtype || ''',' || quote_literal(msubj) || ',' || quote_literal(mbody) || ',' || mtimestamp || ')'; ++ EXCEPTION WHEN undefined_table THEN ++ EXECUTE 'CREATE TABLE ' || tablename || ' (' || ++ 'owner_id INTEGER, ' || ++ 'peer_name_id INTEGER, ' || ++ 'peer_server_id INTEGER, ' || ++ 'peer_resource_id INTEGER, ' || ++ 'direction VARCHAR(4) CHECK (direction IN (''to'',''from'')), ' || ++ 'type VARCHAR(9) CHECK (type IN (''chat'',''error'',''groupchat'',''headline'',''normal'')), ' || ++ 'subject TEXT, ' || ++ 'body TEXT, ' || ++ 'timestamp DOUBLE PRECISION)'; ++ EXECUTE 'CREATE INDEX \"search_i_' || '~s' || '_' || atdate || '_' || '~s' || '\"' || ' ON ' || tablename || ' (owner_id, peer_name_id, peer_server_id, peer_resource_id)'; ++ ++ EXECUTE 'CREATE OR REPLACE VIEW ' || viewname || ' AS ' || ++ 'SELECT owner.username AS owner_name, ' || ++ 'peer.username AS peer_name, ' || ++ 'servers.server AS peer_server, ' || ++ 'resources.resource AS peer_resource, ' || ++ 'messages.direction, ' || ++ 'messages.type, ' || ++ 'messages.subject, ' || ++ 'messages.body, ' || ++ 'messages.timestamp ' || ++ 'FROM ' || ++ '~s owner, ' || ++ '~s peer, ' || ++ '~s servers, ' || ++ '~s resources, ' || ++ tablename || ' messages ' || ++ 'WHERE ' || ++ 'owner.user_id=messages.owner_id and ' || ++ 'peer.user_id=messages.peer_name_id and ' || ++ 'servers.server_id=messages.peer_server_id and ' || ++ 'resources.resource_id=messages.peer_resource_id ' || ++ 'ORDER BY messages.timestamp'; ++ ++ EXECUTE 'INSERT INTO ' || tablename || ' (owner_id, peer_name_id, peer_server_id, peer_resource_id, direction, type, subject, body, timestamp) VALUES (' || ownerID || ',' || peer_nameID || ',' || peer_serverID || ',' || peer_resourceID || ',''' || mdirection || ''',''' || mtype || ''',' || quote_literal(msubj) || ',' || quote_literal(mbody) || ',' || mtimestamp || ')'; ++ END; ++ ++ UPDATE ~s SET count=count+1 where at=atdate and owner_id=ownerID and peer_name_id=peer_nameID and peer_server_id=peer_serverID; ++ IF NOT FOUND THEN ++ INSERT INTO ~s (owner_id, peer_name_id, peer_server_id, at, count) VALUES (ownerID, peer_nameID, peer_serverID, atdate, 1); ++ END IF; ++ RETURN 0; ++END; ++$$ LANGUAGE plpgsql; ++", [logmessage_name(VHost,Schema),UName,UName,UName,UName,SName,SName,RName,RName,Schema,escape_vhost(VHost),UName,UName,SName,RName,StName,StName]). ++ ++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++% ++% SQL internals ++% ++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++% like do_transaction/2 in mysql_conn.erl (changeset by Yariv Sadan ) ++sql_transaction_internal(DBRef, Fun) -> ++ case sql_query_internal(DBRef, ["BEGIN;"]) of ++ {updated, _} -> ++ case catch Fun() of ++ error = Err -> ++ rollback_internal(DBRef, Err); ++ {error, _} = Err -> ++ rollback_internal(DBRef, Err); ++ {'EXIT', _} = Err -> ++ rollback_internal(DBRef, Err); ++ Res -> ++ case sql_query_internal(DBRef, ["COMMIT;"]) of ++ {error, _} -> rollback_internal(DBRef, {commit_error}); ++ {updated, _} -> ++ case Res of ++ {atomic, _} -> Res; ++ _ -> {atomic, Res} ++ end ++ end ++ end; ++ {error, _} -> ++ {aborted, {begin_error}} ++ end. ++ ++% like rollback/2 in mysql_conn.erl (changeset by Yariv Sadan ) ++rollback_internal(DBRef, Reason) -> ++ Res = sql_query_internal(DBRef, ["ROLLBACK;"]), ++ {aborted, {Reason, {rollback_result, Res}}}. ++ ++sql_query_internal(DBRef, Query) -> ++ case sql_query_internal_silent(DBRef, Query) of ++ {error, undefined, Rez} -> ++ ?ERROR_MSG("Got undefined result: ~p while ~p", [Rez, lists:append(Query)]), ++ {error, undefined}; ++ {error, Error} -> ++ ?ERROR_MSG("Failed: ~p while ~p", [Error, lists:append(Query)]), ++ {error, Error}; ++ Rez -> Rez ++ end. ++ ++sql_query_internal_silent(DBRef, Query) -> ++ ?MYDEBUG("DOING: \"~s\"", [lists:append(Query)]), ++ % TODO: use pquery? ++ get_result(pgsql:squery(DBRef, Query)). ++ ++get_result({ok, ["CREATE TABLE"]}) -> ++ {updated, 1}; ++get_result({ok, ["DROP TABLE"]}) -> ++ {updated, 1}; ++get_result({ok, ["ALTER TABLE"]}) -> ++ {updated, 1}; ++get_result({ok,["DROP VIEW"]}) -> ++ {updated, 1}; ++get_result({ok,["DROP FUNCTION"]}) -> ++ {updated, 1}; ++get_result({ok, ["CREATE INDEX"]}) -> ++ {updated, 1}; ++get_result({ok, ["CREATE FUNCTION"]}) -> ++ {updated, 1}; ++get_result({ok, [{"SELECT", _Rows, Recs}]}) -> ++ {data, [list_to_tuple(Rec) || Rec <- Recs]}; ++get_result({ok, ["INSERT " ++ OIDN]}) -> ++ [_OID, N] = string:tokens(OIDN, " "), ++ {updated, list_to_integer(N)}; ++get_result({ok, ["DELETE " ++ N]}) -> ++ {updated, list_to_integer(N)}; ++get_result({ok, ["UPDATE " ++ N]}) -> ++ {updated, list_to_integer(N)}; ++get_result({ok, ["BEGIN"]}) -> ++ {updated, 1}; ++get_result({ok, ["LOCK TABLE"]}) -> ++ {updated, 1}; ++get_result({ok, ["ROLLBACK"]}) -> ++ {updated, 1}; ++get_result({ok, ["COMMIT"]}) -> ++ {updated, 1}; ++get_result({ok, ["SET"]}) -> ++ {updated, 1}; ++get_result({ok, [{error, Error}]}) -> ++ {error, Error}; ++get_result(Rez) -> ++ {error, undefined, Rez}. ++ +--- mod_logdb_mnesia_old.erl.orig 2009-02-05 19:21:29.000000000 +0200 ++++ mod_logdb_mnesia_old.erl 2009-02-05 19:20:07.000000000 +0200 +@@ -0,0 +1,258 @@ ++%%%---------------------------------------------------------------------- ++%%% File : mod_logdb_mnesia_old.erl ++%%% Author : Oleg Palij (mailto,xmpp:o.palij@gmail.com) ++%%% Purpose : mod_logmnesia backend for mod_logdb (should be used only for copy_tables functionality) ++%%% Version : trunk ++%%% Id : $Id: mod_logdb_mnesia_old.erl 1169 2008-09-16 12:14:36Z malik $ ++%%% Url : http://www.dp.uz.gov.ua/o.palij/mod_logdb/ ++%%%---------------------------------------------------------------------- ++ ++-module(mod_logdb_mnesia_old). ++-author('o.palij@gmail.com'). ++ ++-include("ejabberd.hrl"). ++-include("jlib.hrl"). ++ ++-behaviour(gen_logdb). ++ ++-export([start/2, stop/1, ++ log_message/2, ++ rebuild_stats/1, ++ rebuild_stats_at/2, ++ rebuild_stats_at1/2, ++ delete_messages_by_user_at/3, delete_all_messages_by_user_at/3, delete_messages_at/2, ++ get_vhost_stats/1, get_vhost_stats_at/2, get_user_stats/2, get_user_messages_at/3, ++ get_dates/1, ++ get_users_settings/1, get_user_settings/2, set_user_settings/3, ++ drop_user/2]). ++ ++-record(stats, {user, server, table, count}). ++-record(msg, {to_user, to_server, to_resource, from_user, from_server, from_resource, id, type, subject, body, timestamp}). ++ ++tables_prefix() -> "messages_". ++% stats_table should not start with tables_prefix(VHost) ! ++% i.e. lists:prefix(tables_prefix(VHost), atom_to_list(stats_table())) must be /= true ++stats_table() -> list_to_atom("messages-stats"). ++% table name as atom from Date ++-define(ATABLE(Date), list_to_atom(tables_prefix() ++ Date)). ++-define(LTABLE(Date), tables_prefix() ++ Date). ++ ++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++% ++% gen_logdb callbacks ++% ++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++start(_Opts, _VHost) -> ++ case mnesia:system_info(is_running) of ++ yes -> ++ ok = create_stats_table(), ++ {ok, ok}; ++ no -> ++ ?ERROR_MSG("Mnesia not running", []), ++ error; ++ Status -> ++ ?ERROR_MSG("Mnesia status: ~p", [Status]), ++ error ++ end. ++ ++stop(_VHost) -> ++ ok. ++ ++log_message(_VHost, _Msg) -> ++ error. ++ ++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++% ++% gen_logdb callbacks (maintaince) ++% ++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++rebuild_stats(_VHost) -> ++ ok. ++ ++rebuild_stats_at(VHost, Date) -> ++ Table = ?LTABLE(Date), ++ {Time, Value}=timer:tc(?MODULE, rebuild_stats_at1, [VHost, Table]), ++ ?INFO_MSG("rebuild_stats_at ~p elapsed ~p sec: ~p~n", [Date, Time/1000000, Value]), ++ Value. ++rebuild_stats_at1(VHost, Table) -> ++ CFun = fun(Msg, Stats) -> ++ To = Msg#msg.to_user ++ "@" ++ Msg#msg.to_server, ++ Stats_to = if ++ Msg#msg.to_server == VHost -> ++ case lists:keysearch(To, 1, Stats) of ++ {value, {Who_to, Count_to}} -> ++ lists:keyreplace(To, 1, Stats, {Who_to, Count_to + 1}); ++ false -> ++ lists:append(Stats, [{To, 1}]) ++ end; ++ true -> ++ Stats ++ end, ++ From = Msg#msg.from_user ++ "@" ++ Msg#msg.from_server, ++ Stats_from = if ++ Msg#msg.from_server == VHost -> ++ case lists:keysearch(From, 1, Stats_to) of ++ {value, {Who_from, Count_from}} -> ++ lists:keyreplace(From, 1, Stats_to, {Who_from, Count_from + 1}); ++ false -> ++ lists:append(Stats_to, [{From, 1}]) ++ end; ++ true -> ++ Stats_to ++ end, ++ Stats_from ++ end, ++ DFun = fun(#stats{table=STable, server=Server} = Stat, _Acc) ++ when STable == Table, Server == VHost -> ++ mnesia:delete_object(stats_table(), Stat, write); ++ (_Stat, _Acc) -> ok ++ end, ++ case mnesia:transaction(fun() -> ++ mnesia:write_lock_table(list_to_atom(Table)), ++ mnesia:write_lock_table(stats_table()), ++ % Calc stats for VHost at Date ++ AStats = mnesia:foldl(CFun, [], list_to_atom(Table)), ++ % Delete all stats for VHost at Date ++ mnesia:foldl(DFun, [], stats_table()), ++ % Write new calc'ed stats ++ lists:foreach(fun({Who, Count}) -> ++ Jid = jlib:string_to_jid(Who), ++ JUser = Jid#jid.user, ++ WStat = #stats{user=JUser, server=VHost, table=Table, count=Count}, ++ mnesia:write(stats_table(), WStat, write) ++ end, AStats) ++ end) of ++ {aborted, Reason} -> ++ ?ERROR_MSG("Failed to rebuild_stats_at for ~p at ~p: ~p", [VHost, Table, Reason]), ++ error; ++ {atomic, _} -> ++ ok ++ end. ++ ++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++% ++% gen_logdb callbacks (delete) ++% ++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++delete_messages_by_user_at(_VHost, _Msgs, _Date) -> ++ error. ++ ++delete_all_messages_by_user_at(_User, _VHost, _Date) -> ++ error. ++ ++delete_messages_at(VHost, Date) -> ++ Table = list_to_atom(tables_prefix() ++ Date), ++ ++ DFun = fun(#msg{to_server=To_server, from_server=From_server}=Msg, _Acc) ++ when To_server == VHost; From_server == VHost -> ++ mnesia:delete_object(Table, Msg, write); ++ (_Msg, _Acc) -> ok ++ end, ++ ++ case mnesia:transaction(fun() -> ++ mnesia:foldl(DFun, [], Table) ++ end) of ++ {aborted, Reason} -> ++ ?ERROR_MSG("Failed to delete_messages_at for ~p at ~p: ~p", [VHost, Date, Reason]), ++ error; ++ {atomic, _} -> ++ ok ++ end. ++ ++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++% ++% gen_logdb callbacks (get) ++% ++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++get_vhost_stats(_VHost) -> ++ {error, "does not emplemented"}. ++ ++get_vhost_stats_at(VHost, Date) -> ++ Fun = fun() -> ++ Pat = #stats{user='$1', server=VHost, table=tables_prefix()++Date, count = '$2'}, ++ mnesia:select(stats_table(), [{Pat, [], [['$1', '$2']]}]) ++ end, ++ case mnesia:transaction(Fun) of ++ {atomic, Result} -> ++ RFun = fun([User, Count]) -> ++ {User, Count} ++ end, ++ {ok, lists:reverse(lists:keysort(2, lists:map(RFun, Result)))}; ++ {aborted, Reason} -> {error, Reason} ++ end. ++ ++get_user_stats(_User, _VHost) -> ++ {error, "does not emplemented"}. ++ ++get_user_messages_at(User, VHost, Date) -> ++ Table_name = tables_prefix() ++ Date, ++ case mnesia:transaction(fun() -> ++ Pat_to = #msg{to_user=User, to_server=VHost, _='_'}, ++ Pat_from = #msg{from_user=User, from_server=VHost, _='_'}, ++ mnesia:select(list_to_atom(Table_name), ++ [{Pat_to, [], ['$_']}, ++ {Pat_from, [], ['$_']}]) ++ end) of ++ {atomic, Result} -> ++ Msgs = lists:map(fun(#msg{to_user=To_user, to_server=To_server, to_resource=To_res, ++ from_user=From_user, from_server=From_server, from_resource=From_res, ++ type=Type, ++ subject=Subj, ++ body=Body, timestamp=Timestamp} = _Msg) -> ++ Subject = case Subj of ++ "None" -> ""; ++ _ -> Subj ++ end, ++ {msg, To_user, To_server, To_res, From_user, From_server, From_res, Type, Subject, Body, Timestamp} ++ end, Result), ++ {ok, Msgs}; ++ {aborted, Reason} -> ++ {error, Reason} ++ end. ++ ++get_dates(_VHost) -> ++ Tables = mnesia:system_info(tables), ++ MessagesTables = ++ lists:filter(fun(Table) -> ++ lists:prefix(tables_prefix(), atom_to_list(Table)) ++ end, ++ Tables), ++ lists:map(fun(Table) -> ++ lists:sublist(atom_to_list(Table), ++ length(tables_prefix())+1, ++ length(atom_to_list(Table))) ++ end, ++ MessagesTables). ++ ++get_users_settings(_VHost) -> ++ {ok, []}. ++get_user_settings(_User, _VHost) -> ++ {ok, []}. ++set_user_settings(_User, _VHost, _Set) -> ++ ok. ++drop_user(_User, _VHost) -> ++ ok. ++ ++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++% ++% internal ++% ++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++% called from db_logon/2 ++create_stats_table() -> ++ SName = stats_table(), ++ case mnesia:create_table(SName, ++ [{disc_only_copies, [node()]}, ++ {type, bag}, ++ {attributes, record_info(fields, stats)}, ++ {record_name, stats} ++ ]) of ++ {atomic, ok} -> ++ ?INFO_MSG("Created stats table", []), ++ ok; ++ {aborted, {already_exists, _}} -> ++ ok; ++ {aborted, Reason} -> ++ ?ERROR_MSG("Failed to create stats table: ~p", [Reason]), ++ error ++ end. +--- gen_logdb.erl.orig 2009-02-05 19:21:29.000000000 +0200 ++++ gen_logdb.erl 2009-02-05 19:19:39.000000000 +0200 +@@ -0,0 +1,164 @@ ++%%%---------------------------------------------------------------------- ++%%% File : gen_logdb.erl ++%%% Author : Oleg Palij (mailto,xmpp:o.palij@gmail.com) ++%%% Purpose : Describes generic behaviour for mod_logdb backends. ++%%% Version : trunk ++%%% Id : $Id: gen_logdb.erl 1169 2008-09-16 12:14:36Z malik $ ++%%% Url : http://www.dp.uz.gov.ua/o.palij/mod_logdb/ ++%%%---------------------------------------------------------------------- ++ ++-module(gen_logdb). ++-author('o.palij@gmail.com'). ++ ++-export([behaviour_info/1]). ++ ++behaviour_info(callbacks) -> ++ [ ++ % called from handle_info(start, _) ++ % it should logon database and return reference to started instance ++ % start(VHost, Opts) -> {ok, SPid} | error ++ % Options - list of options to connect to db ++ % Types: Options = list() -> [] | ++ % [{user, "logdb"}, ++ % {pass, "1234"}, ++ % {db, "logdb"}] | ... ++ % VHost = list() -> "jabber.example.org" ++ {start, 2}, ++ ++ % called from cleanup/1 ++ % it should logoff database and do cleanup ++ % stop(VHost) ++ % Types: VHost = list() -> "jabber.example.org" ++ {stop, 1}, ++ ++ % called from handle_call({addlog, _}, _, _) ++ % it should log messages to database ++ % log_message(VHost, Msg) -> ok | error ++ % Types: ++ % VHost = list() -> "jabber.example.org" ++ % Msg = record() -> #msg ++ {log_message, 2}, ++ ++ % called from ejabberdctl rebuild_stats ++ % it should rebuild stats table (if used) for vhost ++ % rebuild_stats(VHost) ++ % Types: ++ % VHost = list() -> "jabber.example.org" ++ {rebuild_stats, 1}, ++ ++ % it should rebuild stats table (if used) for vhost at Date ++ % rebuild_stats_at(VHost, Date) ++ % Types: ++ % VHost = list() -> "jabber.example.org" ++ % Date = list() -> "2007-02-12" ++ {rebuild_stats_at, 2}, ++ ++ % called from user_messages_at_parse_query/5 ++ % it should delete selected user messages at date ++ % delete_messages_by_user_at(VHost, Msgs, Date) -> ok | error ++ % Types: ++ % VHost = list() -> "jabber.example.org" ++ % Msgs = list() -> [ #msg1, msg2, ... ] ++ % Date = list() -> "2007-02-12" ++ {delete_messages_by_user_at, 3}, ++ ++ % called from user_messages_parse_query/4 | vhost_messages_at_parse_query/4 ++ % it should delete all user messages at date ++ % delete_all_messages_by_user_at(User, VHost, Date) -> ok | error ++ % Types: ++ % User = list() -> "admin" ++ % VHost = list() -> "jabber.example.org" ++ % Date = list() -> "2007-02-12" ++ {delete_all_messages_by_user_at, 3}, ++ ++ % called from vhost_messages_parse_query/3 ++ % it should delete messages for vhost at date and update stats ++ % delete_messages_at(VHost, Date) -> ok | error ++ % Types: ++ % VHost = list() -> "jabber.example.org" ++ % Date = list() -> "2007-02-12" ++ {delete_messages_at, 2}, ++ ++ % called from ejabberd_web_admin:vhost_messages_stats/3 ++ % it should return sorted list of count of messages by dates for vhost ++ % get_vhost_stats(VHost) -> {ok, [{Date1, Msgs_count1}, {Date2, Msgs_count2}, ... ]} | ++ % {error, Reason} ++ % Types: ++ % VHost = list() -> "jabber.example.org" ++ % DateN = list() -> "2007-02-12" ++ % Msgs_countN = number() -> 241 ++ {get_vhost_stats, 1}, ++ ++ % called from ejabberd_web_admin:vhost_messages_stats_at/4 ++ % it should return sorted list of count of messages by users at date for vhost ++ % get_vhost_stats_at(VHost, Date) -> {ok, [{User1, Msgs_count1}, {User2, Msgs_count2}, ....]} | ++ % {error, Reason} ++ % Types: ++ % VHost = list() -> "jabber.example.org" ++ % Date = list() -> "2007-02-12" ++ % UserN = list() -> "admin" ++ % Msgs_countN = number() -> 241 ++ {get_vhost_stats_at, 2}, ++ ++ % called from ejabberd_web_admin:user_messages_stats/4 ++ % it should return sorted list of count of messages by date for user at vhost ++ % get_user_stats(User, VHost) -> {ok, [{Date1, Msgs_count1}, {Date2, Msgs_count2}, ...]} | ++ % {error, Reason} ++ % Types: ++ % User = list() -> "admin" ++ % VHost = list() -> "jabber.example.org" ++ % DateN = list() -> "2007-02-12" ++ % Msgs_countN = number() -> 241 ++ {get_user_stats, 2}, ++ ++ % called from ejabberd_web_admin:user_messages_stats_at/5 ++ % it should return all user messages at date ++ % get_user_messages_at(User, VHost, Date) -> {ok, Msgs} | {error, Reason} ++ % Types: ++ % User = list() -> "admin" ++ % VHost = list() -> "jabber.example.org" ++ % Date = list() -> "2007-02-12" ++ % Msgs = list() -> [ #msg1, msg2, ... ] ++ {get_user_messages_at, 3}, ++ ++ % called from many places ++ % it should return list of dates for vhost ++ % get_dates(VHost) -> [Date1, Date2, ... ] ++ % Types: ++ % VHost = list() -> "jabber.example.org" ++ % DateN = list() -> "2007-02-12" ++ {get_dates, 1}, ++ ++ % called from start ++ % it should return list with users settings for VHost in db ++ % get_users_settings(VHost) -> [#user_settings1, #user_settings2, ... ] | error ++ % Types: ++ % VHost = list() -> "jabber.example.org" ++ {get_users_settings, 1}, ++ ++ % called from many places ++ % it should return User settings at VHost from db ++ % get_user_settings(User, VHost) -> error | {ok, #user_settings} ++ % Types: ++ % User = list() -> "admin" ++ % VHost = list() -> "jabber.example.org" ++ {get_user_settings, 2}, ++ ++ % called from web admin ++ % it should set User settings at VHost ++ % set_user_settings(User, VHost, #user_settings) -> ok | error ++ % Types: ++ % User = list() -> "admin" ++ % VHost = list() -> "jabber.example.org" ++ {set_user_settings, 3}, ++ ++ % called from remove_user (ejabberd hook) ++ % it should remove user messages and settings at VHost ++ % drop_user(User, VHost) -> ok | error ++ % Types: ++ % User = list() -> "admin" ++ % VHost = list() -> "jabber.example.org" ++ {drop_user, 2} ++ ]; ++behaviour_info(_) -> ++ undefined. +--- web/ejabberd_web_admin-2.0.3.erl 2009-02-03 08:27:39.000000000 +0200 ++++ web/ejabberd_web_admin.erl 2009-02-03 08:40:57.000000000 +0200 +@@ -1514,25 +1514,31 @@ + + + user_parse_query(User, Server, Query) -> +- case lists:keysearch("chpassword", 1, Query) of +- {value, _} -> +- case lists:keysearch("password", 1, Query) of +- {value, {_, undefined}} -> +- error; +- {value, {_, Password}} -> +- ejabberd_auth:set_password(User, Server, Password), +- ok; +- _ -> +- error +- end; +- _ -> +- case lists:keysearch("removeuser", 1, Query) of +- {value, _} -> +- ejabberd_auth:remove_user(User, Server), +- ok; +- false -> +- nothing +- end ++ lists:foldl(fun({Action, Value}, Acc) when Acc == nothing -> ++ user_parse_query1(Action, User, Server, Query); ++ ({Action, Value}, Acc) -> ++ Acc ++ end, nothing, Query). ++ ++user_parse_query1("password", User, Server, Query) -> ++ nothing; ++user_parse_query1("chpassword", User, Server, Query) -> ++ case lists:keysearch("password", 1, Query) of ++ {value, {_, undefined}} -> ++ error; ++ {value, {_, Password}} -> ++ ejabberd_auth:set_password(User, Server, Password), ++ ok; ++ _ -> ++ error ++ end; ++user_parse_query1("removeuser", User, Server, Query) -> ++ ejabberd_auth:remove_user(User, Server), ++ ok; ++user_parse_query1(Action, User, Server, Query) -> ++ case ejabberd_hooks:run_fold(webadmin_user_parse_query, Server, [], [Action, User, Server, Query]) of ++ [] -> nothing; ++ Res -> Res + end. + + +--- mod_muc/mod_muc_room-2.0.3.erl 2009-02-03 08:27:59.000000000 +0200 ++++ mod_muc/mod_muc_room.erl 2009-02-03 08:37:26.000000000 +0200 +@@ -695,6 +695,12 @@ + handle_sync_event({change_config, Config}, _From, StateName, StateData) -> + {result, [], NSD} = change_config(Config, StateData), + {reply, {ok, NSD#state.config}, StateName, NSD}; ++handle_sync_event({get_jid_nick, Jid}, _From, StateName, StateData) -> ++ R = case ?DICT:find(jlib:jid_tolower(Jid), StateData#state.users) of ++ error -> []; ++ {ok, {user, _, Nick, _, _}} -> Nick ++ end, ++ {reply, R, StateName, StateData}; + handle_sync_event(_Event, _From, StateName, StateData) -> + Reply = ok, + {reply, Reply, StateName, StateData}. +--- msgs/uk-2.0.3.msg 2009-01-14 11:54:15.000000000 +0200 ++++ msgs/uk.msg 2009-02-03 08:26:20.000000000 +0200 +@@ -388,6 +388,35 @@ + % mod_offline_odbc.erl + {"Your contact offline message queue is full. The message has been discarded.", "Черга повідомлень, що не були доÑтавлені, переповнена. ÐŸÐ¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ Ð½Ðµ було збережено."}. + ++% mod_logdb ++{"Users Messages", "ÐŸÐ¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ ÐºÐ¾Ñ€Ð¸Ñтувачів"}. ++{"Date", "Дата"}. ++{"Count", "КількіÑÑ‚ÑŒ"}. ++{"Logged messages for ", "Збережені Ð¿Ð¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ Ð´Ð»Ñ "}. ++{" at ", " за "}. ++{"No logged messages for ", "ВідÑутні Ð¿Ð¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ Ð´Ð»Ñ "}. ++{"Date, Time", "Дата, ЧаÑ"}. ++{"Direction: Jid", "ÐапрÑмок: Jid"}. ++{"Subject", "Тема"}. ++{"Body", "ТекÑÑ‚"}. ++{"Messages", "ПовідомленнÑ"}. ++{"Filter Selected", "Відфільтрувати виділені"}. ++{"Do Not Log Messages", "Ðе зберігати повідомленнÑ"}. ++{"Log Messages", "Зберігати повідомленнÑ"}. ++{"Messages logging engine", "СиÑтема Ð·Ð±ÐµÑ€ÐµÐ¶ÐµÐ½Ð½Ñ Ð¿Ð¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½ÑŒ"}. ++{"Default", "За замовчуваннÑм"}. ++{"Set logging preferences", "Вкажіть Ð½Ð°Ð»Ð°Ð³Ð¾Ð´Ð¶ÑƒÐ²Ð°Ð½Ð½Ñ Ð·Ð±ÐµÑ€ÐµÐ¶ÐµÐ½Ð½Ñ Ð¿Ð¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½ÑŒ"}. ++{"Messages logging engine users", "КориÑтувачі ÑиÑтеми Ð·Ð±ÐµÑ€ÐµÐ¶ÐµÐ½Ð½Ñ Ð¿Ð¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½ÑŒ"}. ++{"Messages logging engine settings", "ÐÐ°Ð»Ð°Ð³Ð¾Ð´Ð¶ÑƒÐ²Ð°Ð½Ð½Ñ ÑиÑтеми Ð·Ð±ÐµÑ€ÐµÐ¶ÐµÐ½Ð½Ñ Ð¿Ð¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½ÑŒ"}. ++{"Set run-time settings", "Вкажіть поточні налагоджуваннÑ"}. ++{"Groupchat messages logging", "Ð—Ð±ÐµÑ€ÐµÐ¶ÐµÐ½Ð½Ñ Ð¿Ð¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½ÑŒ типу groupchat"}. ++{"Jids/Domains to ignore", "Ігнорувати наÑтупні jids/домени"}. ++{"Purge messages older than (days)", "ВидалÑти Ð¿Ð¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ Ñтаріші ніж (дні)"}. ++{"Poll users settings (seconds)", "Оновлювати Ð½Ð°Ð»Ð°Ð³Ð¾Ð´Ð¶ÑƒÐ²Ð°Ð½Ð½Ñ ÐºÐ¾Ñ€Ð¸Ñтувачів кожні (Ñекунд)"}. ++{"Drop", "ВидалÑти"}. ++{"Do not drop", "Ðе видалÑти"}. ++{"Drop messages on user removal", "ВидалÑти Ð¿Ð¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ Ð¿Ñ–Ð´ Ñ‡Ð°Ñ Ð²Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ ÐºÐ¾Ñ€Ð¸Ñтувача"}. ++ + % Local Variables: + % mode: erlang + % End: +--- msgs/ru-2.0.3.msg 2009-01-14 11:54:15.000000000 +0200 ++++ msgs/ru.msg 2009-02-03 08:25:31.000000000 +0200 +@@ -388,6 +388,35 @@ + % mod_offline_odbc.erl + {"Your contact offline message queue is full. The message has been discarded.", "Очередь недоÑтавленных Ñообщений Вашего адреÑата переполнена. Сообщение не было Ñохранено."}. + ++% mod_logdb.erl ++{"Users Messages", "Ð¡Ð¾Ð¾Ð±Ñ‰ÐµÐ½Ð¸Ñ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»ÐµÐ¹"}. ++{"Date", "Дата"}. ++{"Count", "КоличеÑтво"}. ++{"Logged messages for ", "Сохранённые cÐ¾Ð¾Ð±Ñ‰ÐµÐ½Ð¸Ñ Ð´Ð»Ñ "}. ++{" at ", " за "}. ++{"No logged messages for ", "ОтÑутÑтвуют ÑÐ¾Ð¾Ð±Ñ‰ÐµÐ½Ð¸Ñ Ð´Ð»Ñ "}. ++{"Date, Time", "Дата, ВремÑ"}. ++{"Direction: Jid", "Ðаправление: Jid"}. ++{"Subject", "Тема"}. ++{"Body", "ТекÑÑ‚"}. ++{"Messages", "СообщениÑ"}. ++{"Filter Selected", "Отфильтровать выделенные"}. ++{"Do Not Log Messages", "Ðе ÑохранÑÑ‚ÑŒ ÑообщениÑ"}. ++{"Log Messages", "СохранÑÑ‚ÑŒ ÑообщениÑ"}. ++{"Messages logging engine", "СиÑтема Ð»Ð¾Ð³Ð¸Ñ€Ð¾Ð²Ð°Ð½Ð¸Ñ Ñообщений"}. ++{"Default", "По умолчанию"}. ++{"Set logging preferences", "Задайте наÑтройки логированиÑ"}. ++{"Messages logging engine users", "Пользователи ÑиÑтемы Ð»Ð¾Ð³Ð¸Ñ€Ð¾Ð²Ð°Ð½Ð¸Ñ Ñообщений"}. ++{"Messages logging engine settings", "ÐаÑтройки ÑиÑтемы Ð»Ð¾Ð³Ð¸Ñ€Ð¾Ð²Ð°Ð½Ð¸Ñ Ñообщений"}. ++{"Set run-time settings", "Задайте текущие наÑтройки"}. ++{"Groupchat messages logging", "Логирование Ñообщений типа groupchat"}. ++{"Jids/Domains to ignore", "Игнорировать Ñледующие jids/домены"}. ++{"Purge messages older than (days)", "УдалÑÑ‚ÑŒ ÑÐ¾Ð¾Ð±Ñ‰ÐµÐ½Ð¸Ñ Ñтарее чем (дни)"}. ++{"Poll users settings (seconds)", "ОбновлÑÑ‚ÑŒ наÑтройки пользователей через (Ñекунд)"}. ++{"Drop", "УдалÑÑ‚ÑŒ"}. ++{"Do not drop", "Ðе удалÑÑ‚ÑŒ"}. ++{"Drop messages on user removal", "УдалÑÑ‚ÑŒ ÑÐ¾Ð¾Ð±Ñ‰ÐµÐ½Ð¸Ñ Ð¿Ñ€Ð¸ удалении пользователÑ"}. ++ + % Local Variables: + % mode: erlang + % End: +--- msgs/pl-2.0.3.msg 2009-01-14 11:54:15.000000000 +0200 ++++ msgs/pl.msg 2009-02-03 08:24:33.000000000 +0200 +@@ -408,6 +408,31 @@ + % mod_offline.erl + {"Your contact offline message queue is full. The message has been discarded.", "Twoja kolejka wiadomoci offline jest peÅ‚na. Wiadomoć zostaÅ‚a odrzucona."}. + ++% mod_logdb ++{"Users Messages", "WiadomoÅ›ci użytkownika"}. ++{"Date", "Data"}. ++{"Count", "Liczba"}. ++{"Logged messages for ", "Zapisane wiadomoÅ›ci dla "}. ++{" at ", " o "}. ++{"No logged messages for ", "Brak zapisanych wiadomoÅ›ci dla "}. ++{"Date, Time", "Data, Godzina"}. ++{"Direction: Jid", "Kierunek: Jid"}. ++{"Subject", "Temat"}. ++{"Body", "Treść"}. ++{"Messages","WiadomoÅ›ci"}. ++{"Filter Selected", "Odfiltruj zaznaczone"}. ++{"Do Not Log Messages", "Nie zapisuj wiadomoÅ›ci"}. ++{"Log Messages", "Zapisuj wiadomoÅ›ci"}. ++{"Messages logging engine", "System zapisywania historii rozmów"}. ++{"Default", "DomyÅ›lne"}. ++{"Set logging preferences", "Ustaw preferencje zapisywania"}. ++{"Messages logging engine settings", "Ustawienia systemu logowania"}. ++{"Set run-time settings", "Zapisz ustawienia systemu logowania"}. ++{"Groupchat messages logging", "Zapisywanie rozmów z konferencji"}. ++{"Jids/Domains to ignore", "JID/Domena która ma być ignorowana"}. ++{"Purge messages older than (days)", "UsuÅ„ wiadomoÅ›ci starsze niż (w dniach)"}. ++{"Poll users settings (seconds)", "Czas aktualizacji preferencji użytkowników (sekundy)"}. ++ + % Local Variables: + % mode: erlang + % End: +--- msgs/nl-2.0.3.msg 2009-01-14 11:54:15.000000000 +0200 ++++ msgs/nl.msg 1970-01-01 03:00:00.000000000 +0300 +@@ -379,6 +379,19 @@ + % mod_proxy65/mod_proxy65_service.erl + {"ejabberd SOCKS5 Bytestreams module", "ejabberd SOCKS5 Bytestreams module"}. + ++% mod_logdb ++{"Users Messages", "Gebruikersberichten"}. ++{"Date", "Datum"}. ++{"Count", "Aantal"}. ++{"Logged messages for ", "Gelogde berichten van "}. ++{" at ", " op "}. ++{"No logged messages for ", "Geen gelogde berichten van "}. ++{"Date, Time", "Datum en tijd"}. ++{"Direction: Jid", "Richting: Jabber ID"}. ++{"Subject", "Onderwerp"}. ++{"Body", "Berichtveld"}. ++{"Messages", "Berichten"}. ++ + % Local Variables: + % mode: erlang + % End: +--- ./mod_roster-2.0.3.erl 2009-02-03 08:28:12.000000000 +0200 ++++ mod_roster.erl 2009-02-03 08:32:14.000000000 +0200 +@@ -48,7 +48,7 @@ + -include("mod_roster.hrl"). + -include("web/ejabberd_http.hrl"). + -include("web/ejabberd_web_admin.hrl"). +- ++-include("mod_logdb.hrl"). + + start(Host, Opts) -> + IQDisc = gen_mod:get_opt(iqdisc, Opts, one_queue), +@@ -829,6 +829,14 @@ + Res = user_roster_parse_query(User, Server, Items1, Query), + Items = mnesia:dirty_index_read(roster, US, #roster.us), + SItems = lists:sort(Items), ++ ++ Settings = case gen_mod:is_loaded(Server, mod_logdb) of ++ true -> ++ mod_logdb:get_user_settings(User, Server); ++ false -> ++ [] ++ end, ++ + FItems = + case SItems of + [] -> +@@ -876,7 +884,33 @@ + [?INPUTT("submit", + "remove" ++ + ejabberd_web_admin:term_to_id(R#roster.jid), +- "Remove")])]) ++ "Remove")]), ++ case gen_mod:is_loaded(Server, mod_logdb) of ++ true -> ++ Peer = jlib:jid_to_string(R#roster.jid), ++ A = lists:member(Peer, Settings#user_settings.dolog_list), ++ B = lists:member(Peer, Settings#user_settings.donotlog_list), ++ {Name, Value} = ++ if ++ A -> ++ {"donotlog", "Do Not Log Messages"}; ++ B -> ++ {"dolog", "Log Messages"}; ++ Settings#user_settings.dolog_default == true -> ++ {"donotlog", "Do Not Log Messages"}; ++ Settings#user_settings.dolog_default == false -> ++ {"dolog", "Log Messages"} ++ end, ++ ++ ?XAE("td", [{"class", "valign"}], ++ [?INPUTT("submit", ++ Name ++ ++ ejabberd_web_admin:term_to_id(R#roster.jid), ++ Value)]); ++ false -> ++ ?X([]) ++ end ++ ]) + end, SItems))])] + end, + [?XC("h1", ?T("Roster of ") ++ us_to_list(US))] ++ +@@ -958,11 +992,42 @@ + {"subscription", "remove"}], + []}]}}), + throw(submitted); +- false -> +- ok +- end +- +- end ++ false -> ++ case lists:keysearch( ++ "donotlog" ++ ejabberd_web_admin:term_to_id(JID), 1, Query) of ++ {value, _} -> ++ Peer = jlib:jid_to_string(JID), ++ Settings = mod_logdb:get_user_settings(User, Server), ++ DNLL = case lists:member(Peer, Settings#user_settings.donotlog_list) of ++ false -> lists:append(Settings#user_settings.donotlog_list, [Peer]); ++ true -> Settings#user_settings.donotlog_list ++ end, ++ DLL = lists:delete(jlib:jid_to_string(JID), Settings#user_settings.dolog_list), ++ Sett = Settings#user_settings{donotlog_list=DNLL, dolog_list=DLL}, ++ % TODO: check returned value ++ ok = mod_logdb:set_user_settings(User, Server, Sett), ++ throw(nothing); ++ false -> ++ case lists:keysearch( ++ "dolog" ++ ejabberd_web_admin:term_to_id(JID), 1, Query) of ++ {value, _} -> ++ Peer = jlib:jid_to_string(JID), ++ Settings = mod_logdb:get_user_settings(User, Server), ++ DLL = case lists:member(Peer, Settings#user_settings.dolog_list) of ++ false -> lists:append(Settings#user_settings.dolog_list, [Peer]); ++ true -> Settings#user_settings.dolog_list ++ end, ++ DNLL = lists:delete(jlib:jid_to_string(JID), Settings#user_settings.donotlog_list), ++ Sett = Settings#user_settings{donotlog_list=DNLL, dolog_list=DLL}, ++ % TODO: check returned value ++ ok = mod_logdb:set_user_settings(User, Server, Sett), ++ throw(nothing); ++ false -> ++ ok ++ end % dolog ++ end % donotlog ++ end % remove ++ end % validate + end, Items), + nothing. + +--- ./mod_roster_odbc-2.0.3.erl 2009-02-03 08:28:26.000000000 +0200 ++++ mod_roster_odbc.erl 2009-02-03 08:47:04.000000000 +0200 +@@ -48,7 +48,7 @@ + -include("mod_roster.hrl"). + -include("web/ejabberd_http.hrl"). + -include("web/ejabberd_web_admin.hrl"). +- ++-include("mod_logdb.hrl"). + + start(Host, Opts) -> + IQDisc = gen_mod:get_opt(iqdisc, Opts, one_queue), +@@ -937,6 +937,14 @@ + Res = user_roster_parse_query(User, Server, Items1, Query), + Items = get_roster(LUser, LServer), + SItems = lists:sort(Items), ++ ++ Settings = case gen_mod:is_loaded(Server, mod_logdb) of ++ true -> ++ mod_logdb:get_user_settings(User, Server); ++ false -> ++ [] ++ end, ++ + FItems = + case SItems of + [] -> +@@ -984,7 +992,33 @@ + [?INPUTT("submit", + "remove" ++ + ejabberd_web_admin:term_to_id(R#roster.jid), +- "Remove")])]) ++ "Remove")]), ++ case gen_mod:is_loaded(Server, mod_logdb) of ++ true -> ++ Peer = jlib:jid_to_string(R#roster.jid), ++ A = lists:member(Peer, Settings#user_settings.dolog_list), ++ B = lists:member(Peer, Settings#user_settings.donotlog_list), ++ {Name, Value} = ++ if ++ A -> ++ {"donotlog", "Do Not Log Messages"}; ++ B -> ++ {"dolog", "Log Messages"}; ++ Settings#user_settings.dolog_default == true -> ++ {"donotlog", "Do Not Log Messages"}; ++ Settings#user_settings.dolog_default == false -> ++ {"dolog", "Log Messages"} ++ end, ++ ++ ?XAE("td", [{"class", "valign"}], ++ [?INPUTT("submit", ++ Name ++ ++ ejabberd_web_admin:term_to_id(R#roster.jid), ++ Value)]); ++ false -> ++ ?X([]) ++ end ++ ]) + end, SItems))])] + end, + [?XC("h1", ?T("Roster of ") ++ us_to_list(US))] ++ +@@ -1066,11 +1100,42 @@ + {"subscription", "remove"}], + []}]}}), + throw(submitted); +- false -> +- ok +- end +- +- end ++ false -> ++ case lists:keysearch( ++ "donotlog" ++ ejabberd_web_admin:term_to_id(JID), 1, Query) of ++ {value, _} -> ++ Peer = jlib:jid_to_string(JID), ++ Settings = mod_logdb:get_user_settings(User, Server), ++ DNLL = case lists:member(Peer, Settings#user_settings.donotlog_list) of ++ false -> lists:append(Settings#user_settings.donotlog_list, [Peer]); ++ true -> Settings#user_settings.donotlog_list ++ end, ++ DLL = lists:delete(jlib:jid_to_string(JID), Settings#user_settings.dolog_list), ++ Sett = Settings#user_settings{donotlog_list=DNLL, dolog_list=DLL}, ++ % TODO: check returned value ++ ok = mod_logdb:set_user_settings(User, Server, Sett), ++ throw(nothing); ++ false -> ++ case lists:keysearch( ++ "dolog" ++ ejabberd_web_admin:term_to_id(JID), 1, Query) of ++ {value, _} -> ++ Peer = jlib:jid_to_string(JID), ++ Settings = mod_logdb:get_user_settings(User, Server), ++ DLL = case lists:member(Peer, Settings#user_settings.dolog_list) of ++ false -> lists:append(Settings#user_settings.dolog_list, [Peer]); ++ true -> Settings#user_settings.dolog_list ++ end, ++ DNLL = lists:delete(jlib:jid_to_string(JID), Settings#user_settings.donotlog_list), ++ Sett = Settings#user_settings{donotlog_list=DNLL, dolog_list=DLL}, ++ % TODO: check returned value ++ ok = mod_logdb:set_user_settings(User, Server, Sett), ++ throw(nothing); ++ false -> ++ ok ++ end % dolog ++ end % donotlog ++ end % remove ++ end % validate + end, Items), + nothing. + diff --git a/jorge/mod_logdb/patch-src-mod_logdb-disable_ejabberd_ctl.diff b/jorge/mod_logdb/patch-src-mod_logdb-disable_ejabberd_ctl.diff new file mode 100644 index 0000000..7b9b902 --- /dev/null +++ b/jorge/mod_logdb/patch-src-mod_logdb-disable_ejabberd_ctl.diff @@ -0,0 +1,65 @@ +Index: mod_logdb.erl +=================================================================== +--- mod_logdb.erl (revision 1273) ++++ mod_logdb.erl (working copy) +@@ -126,7 +126,7 @@ + purge_older_days=gen_mod:get_opt(purge_older_days, Opts, never), + poll_users_settings=PollUsersSettings}}. + +-cleanup(#state{vhost=VHost} = State) -> ++cleanup(#state{vhost=VHost} = _State) -> + ?MYDEBUG("Stopping ~s for ~p", [?MODULE, VHost]), + + %ets:delete(ets_settings_table(VHost)), +@@ -153,14 +153,14 @@ + + ?MYDEBUG("Removed hooks for ~p", [VHost]), + +- ejabberd_ctl:unregister_commands(VHost, [{"rebuild_stats", "rebuild mod_logdb module stats for vhost"}], ?MODULE, rebuild_stats), +- Supported_backends = lists:flatmap(fun({Backend, _Opts}) -> +- [atom_to_list(Backend), " "] +- end, State#state.dbs), +- ejabberd_ctl:unregister_commands( +- VHost, +- [{"copy_messages backend", "copy messages from backend to current backend. backends could be: " ++ Supported_backends }], +- ?MODULE, copy_messages_ctl), ++ %ejabberd_ctl:unregister_commands(VHost, [{"rebuild_stats", "rebuild mod_logdb module stats for vhost"}], ?MODULE, rebuild_stats), ++ %Supported_backends = lists:flatmap(fun({Backend, _Opts}) -> ++ % [atom_to_list(Backend), " "] ++ % end, State#state.dbs), ++ %ejabberd_ctl:unregister_commands( ++ % VHost, ++ % [{"copy_messages backend", "copy messages from backend to current backend. backends could be: " ++ Supported_backends }], ++ % ?MODULE, copy_messages_ctl), + ?MYDEBUG("Unregistered commands for ~p", [VHost]). + + stop(VHost) -> +@@ -388,17 +388,17 @@ + + ?MYDEBUG("Added hooks for ~p", [VHost]), + +- ejabberd_ctl:register_commands( +- VHost, +- [{"rebuild_stats", "rebuild mod_logdb module stats for vhost"}], +- ?MODULE, rebuild_stats), +- Supported_backends = lists:flatmap(fun({Backend, _Opts}) -> +- [atom_to_list(Backend), " "] +- end, State#state.dbs), +- ejabberd_ctl:register_commands( +- VHost, +- [{"copy_messages backend", "copy messages from backend to current backend. backends could be: " ++ Supported_backends }], +- ?MODULE, copy_messages_ctl), ++ %ejabberd_ctl:register_commands( ++ % VHost, ++ % [{"rebuild_stats", "rebuild mod_logdb module stats for vhost"}], ++ % ?MODULE, rebuild_stats), ++ %Supported_backends = lists:flatmap(fun({Backend, _Opts}) -> ++ % [atom_to_list(Backend), " "] ++ % end, State#state.dbs), ++ %ejabberd_ctl:register_commands( ++ % VHost, ++ % [{"copy_messages backend", "copy messages from backend to current backend. backends could be: " ++ Supported_backends }], ++ % ?MODULE, copy_messages_ctl), + ?MYDEBUG("Registered commands for ~p", [VHost]), + + NewState=State#state{monref = MonRef, backendPid=SPid, purgeRef=TrefPurge, pollRef=TrefPoll}, diff --git a/jorge/mod_logdb/readme.txt b/jorge/mod_logdb/readme.txt new file mode 100644 index 0000000..5bcc318 --- /dev/null +++ b/jorge/mod_logdb/readme.txt @@ -0,0 +1,48 @@ +mod_logdb by Oleg Palij +----------------------- + +*NOTE* - Jorge is compatible only with the distributed mod_logdb. Please follow instruction below carefully. + +Instalation instruction: + +1) Grab ejabberd from SVN (Tested with Revision: 1868, but probably will work with any newer) or ejabberd 2.x +2) Patch mysql erlang driver with userflags patch +3) Patch sources of ejabberd using "patch" tool + (f.e: patch -p0 < patch-src-mod_logdb_svn) +4) Setup mysql5 database (dbname, username, etc...). + Db schema will be automaticaly setup during mod_logdb startup. +5) Edit config of your ejabberd server by adding following lines into modules section: +6) If you are using mysql5 backend with procedural language *you must patch* mysql driver with provided patch: userflags.diff, and add user priviledges for creating views (CREATE VIEW) + +{modules, [ + ... + {mod_logdb, + [{vhosts, [{"your_xmpp_server", mysql5}]}, + {dbs, [{mysql5, [{user, "db_username"}, + {password, "db_password"}, + {server, "ip_of_the_db_server"}, + {port, 3306}, + {db, "db_name"} + ] + }]}, + {groupchat, none}, + {purge_older_days, never}, + {ignore_jids, ["example@jid.pl", "example2@jid.pl"]}, + {dolog_default, false} + ] + }, + ... +]}. + +And for ad-hoc commands, add on the top of the config file: + +{access, mod_logdb, [{allow, all}]}. +{access, mod_logdb_admin, [{allow, admin}]}. + +7) Restart the server +8) Have fun + +For further info consult mod_logdb manual. + +Note for admins who use clustered setup: you need to install mod_logdb on each ejabberd node. +Multiple mod_logdb sessions can share database access without any problems. diff --git a/jorge/my_links.php b/jorge/my_links.php new file mode 100644 index 0000000..ed10187 --- /dev/null +++ b/jorge/my_links.php @@ -0,0 +1,193 @@ +decrypt_url($_GET[a]) === true) { + + $variables[tslice] = $enc->tslice; + $variables[peer_name_id] = $enc->peer_name_id; + $variables[peer_server_id] = $enc->peer_server_id; + $variables[lnk] = $enc->lnk; + $variables[linktag] = $enc->linktag; + $variables[strt] = $enc->strt; + $variables[ismylink] = $enc->ismylink; + + } + else { + unset($variables); + + } + +} + +if ($del === "t") { + + if ($db->del_mylink($link_id) === true) { + + $html->status_message($my_links_removed[$lang]); + + // recount number of links + $db->get_mylinks_count(); + $my_links_count = $db->result->cnt; + + } + + else { + + $html->alert_message($oper_fail[$lang]); + + } + +} + + +if ($tigger === $my_links_commit[$lang]) { + + if ($enc->decrypt_url($_POST[hidden_field]) === true) { + + $peer_name_id = $enc->peer_name_id; + $peer_server_id = $enc->peer_server_id; + $datat = $enc->tslice; + $lnk = $enc->lnk; + $strt = $enc->strt; + $linktag = $enc->linktag; + $link = $lnk."&start=$strt#$linktag"; + if ($desc === $my_links_optional[$lang]) { + + $desc = $my_links_none[$lang]; + + } + + $desc = substr($desc,0,120); + if($db->add_mylink($peer_name_id,$peer_server_id,$datat,$link,$desc) === true) { + + $html->status_message($my_links_added[$lang].' +
    '.$my_links_back[$lang].''); + + // recount number of links + $db->get_mylinks_count(); + $my_links_count = $db->result->cnt; + + } + else{ + + $html->alert_message($oper_fail[$lang]); + + } + + } + +} + +if ($variables[ismylink] === "1") { + + $db->get_server_name($enc->peer_server_id); + $sname = $db->result->server_name; + $db->get_user_name($enc->peer_name_id); + $uname = $db->result->username; + $nickname=query_nick_name($ejabberd_roster,$uname,$sname); + $jid=''.$uname.'@'.$sname.''; + $hidden_fields = $enc->crypt_url("tslice=$enc->tslice&peer_name_id=$variables[peer_name_id]&peer_server_id=$variables[peer_server_id]&lnk=$variables[lnk]&strt=$variables[strt]&linktag=$variables[linktag]"); + if (!$nickname) { + + $nickname = $not_in_r[$lang]; + $jid = $sname; + + } + $html->set_body(' +
    + '.$my_links_save_d[$lang].'
    + + + + + + + +
    '.$my_links_chat[$lang].'   + '.cut_nick($nickname).' ('.htmlspecialchars($jid).')
          +
    +



    + '); + +} + +$html->set_overview('

    '.$my_links_desc_m[$lang].'

    '.$my_links_desc_e[$lang].''); + +if ($my_links_count === "0") { + + $html->status_message($my_links_no_links[$lang]); + + } + + else { + + $html->set_body(' +
    + + + + '); + $db->get_mylink(); + $result = $db->result; + foreach ($result as $entry) { + + $db->get_user_name($entry[peer_name_id]); + $peer_name = $db->result->username; + $db->get_server_name($entry[peer_server_id]); + $peer_server = $db->result->server_name; + $nickname=query_nick_name($ejabberd_roster,$peer_name,$peer_server); + $desc = htmlspecialchars($entry[description]); + $jid = $peer_name.'@'.$peer_server; + if (!$nickname) { + + $nickname = $not_in_r[$lang]; + $jid = $peer_server; + + } + $html->set_body(' + + + + + + + '); + } + + $html->set_body(' +
    '.$my_links_link[$lang].''.$my_links_chat[$lang].''.$my_links_desc[$lang].'
    '.verbose_date($entry['datat'],$months_names).' '.cut_nick(htmlspecialchars($nickname)).' ('.htmlspecialchars($jid).')  '.$desc.' '.$del_my_link[$lang].' 
    + '); + +} +require_once("footer.php"); + +?> diff --git a/jorge/not_enabled.php b/jorge/not_enabled.php new file mode 100644 index 0000000..7a86b55 --- /dev/null +++ b/jorge/not_enabled.php @@ -0,0 +1,105 @@ +get('log_status')!== null) { + + header("Location: index.php"); + +} + +$action = $_POST[activate]; +$user_name = $sess->get('uid_l'); + +if ($action === $activate_m[$lang]) { + + + if ($db->get_user_id($user_name) === true) { + + if (!$db->result->user_id) { + + $db->insert_user_id($user_name); + + } + + if ($db->insert_new_settings($user_name) === true) { + + $html->set_body('
    '.$act_su[$lang].'
    '.$act_su2[$lang].'
    +
    + '); + + if ($db->get_user_id($user_name) === true) { + + $user_id = $db->result->user_id; + $db->set_user_id($user_id); + // non critical error + if ($db->set_jorge_pref("1","2") === false OR $db->set_jorge_pref("2","1") === false) { + + $html->alert_message('Ooops something goes wrong...its still beta...please contact system admin with this message'); + + } + + $sess->set('log_status',true); + $sess->set('view_type','2'); + $db->set_logger("7","1",$rem_adre); + + } + else { + + $html->alert_message('Ooops something goes wrong...its still beta...'); + + } + + } + else { + + $html->alert_message('Ooops something goes wrong...its still beta...'); + } + + } + else { + + $html->alert_message('Ooops something goes wrong...its still beta...'); + } + } + else { + + $user_name=htmlspecialchars($user_name); + $html->set_body($act_info[$lang].''.$user_name.' ('.$user_name.'@'.XMPP_HOST.')


    +
    +
    + +
    +

    '.$warning1[$lang].'
    '.$devel_info[$lang].'


    +
    +
    + + +
    +
    + '); +} + +require_once("footer.php"); +?> + diff --git a/jorge/search_v2.php b/jorge/search_v2.php new file mode 100644 index 0000000..18ddf13 --- /dev/null +++ b/jorge/search_v2.php @@ -0,0 +1,593 @@ +decrypt_url($predefined) === true) { + + $search_phase = "from:".$enc->jid; + + } + else { + + unset($search_phase); + unset($predefined); + } + +} + +if ($next_link) { + + if($enc->decrypt_url($next_link) === true) { + + $tslice_next = $enc->tslice; + $search_phase = $enc->search_phase; + $offset_arch = $enc->offset_arch; + $offset_day = $enc->offset_day; + $tag_count = $enc->tag_count; + + } + else { + + unset ($next_link); + unset ($search_phase); + } + +} + +if ($tag_count=="t") { + + $start_from = $offset_day; + + } + else{ + + $start_from = null; + } + +$plain_phase = $search_phase; // fix me + +require_once("upper.php"); + +//need to initialize counter here +$r=0; + +// Check if user have set up OwnName +$db->get_own_name(); +if ($db->result->own_name) { + + $own_name = $db->result->own_name; + + } + else{ + + $own_name = false; + +} + +// we need to rewrite this part internaly... +if ($search_phase!="") { + + // check if we are using parametrized search or not + $qquery = is_query_from($search_phase); + + // parametric search + if ($qquery[from] == "t") { + + $user_chat_search = "1"; // temp hack + unset($search_phase); + list($user_name, $server) = split("@", $qquery[talker]); + $db->get_user_id($user_name); + $user_name = $db->result->user_id; + $db->get_server_id($server); + $server = $db->result->server_id; + $search_phase=$qquery[query]; + + } + + + if ($search_phase) { + + if ($db->create_search_results_table() === false) { + + $html->alert_message($oper_fail[$lang]); + } + + else { + + $score="score"; + } + } + + + //main table + if ($time2_start AND $time2_end) { + + $html->status_message($search_warn[$lang].': '.$time2_start.' - '.$time2_end); + + } + else { + + $time2_start = null; + $time2_end = null; + } + + $html->set_overview('

    '.$search_res[$lang].'

    + + ' + ); + + if ($offset_arch) { + + $type_p="6"; + + } + else { + + $type_p="1"; + + } + + // run optimized query in specyfic conditions + if ($qquery[words]=="t" AND $qquery[from]=="t") { + + $type_p="8"; + + } + + if ($type_p==="1" OR $type_p==="8") { + + $db->get_uniq_chat_dates($time2_start, $time2_end, false, $offset_arch, $user_name,$server); + + } + elseif($type_p==="6") { + + $db->get_uniq_chat_dates($time2_start, $time2_end, true, $offset_arch, $user_name,$server); + + } + + debug(DEBUG,"Selected search type: $type_p (first stage)"); + + $result = $db->result; + + // query offsets init for calculations: + $arch_table = count($result); + $external=0; + $internal = $offset_day; + + // set string for searching + if ($search_phase) { + + $db->set_user_query($search_phase); + + } + + foreach($result as $entry) { + + $external++; + $time_slice = $entry["at"]; + + // sub query + + if ($search_phase) { + + $type="4"; + + } + + if ($user_chat_search) { + + if ($qquery['words']=="t") { + + $type="5"; + + } + + elseif($qquery['words'] == "f") { + + $type="7"; + } + + } + + $a++; + + if ($type==="4") { + + $db->search_query($time_slice); + + } + elseif($type==="5") { + + $db->search_query_in_user_chat($user_name,$server,$time_slice,$start_from); + + } + elseif($type==="7") { + + $db-> search_query_chat_stream($user_name,$server,$time_slice,$start_from); + + } + + debug(DEBUG,"Selected search type: $type (second stage)"); + + $search_result = $db->result; + $num_rows = count($search_result); + + $day_mark=0; + if ($num_rows!="0") { + + foreach ($search_result as $results) { + + // if there is no "from:" clausule perform normal search + if ($type!="7") { + + $body = base64_encode($results[body]); + + if ($db->insert_data_to_result_table ( + $results[ts], + $time_slice, + $results[peer_name_id], + $results[peer_server_id], + $results[direction], + $body, + $results[score], + $results[ext] + ) === false ) { + + + $html->alert_message($oper_fail[$lang]); + + } + + } + + else { + + $internal++; + $day_mark++; + + // we like colors dont we? + if ($results["direction"] == "to") { + + $col="e0e9f7"; + } + + else { + + $col="e8eef7"; + + } + + $to_user = $results["peer_name_id"]; + $to_server=$results["peer_server_id"]; + + // let's make a link + $to_base = $enc->crypt_url("tslice=$time_slice&peer_name_id=$to_user&peer_server_id=$to_server"); + + // time calc + $pass_to_next = $results["ts"]; + $new_d = $results["ts"]; + $time_diff = abs((strtotime("$old_d") - strtotime(date("$new_d")))); + $old_d = $pass_to_next; + + // split line + if ($time_diff>$split_line AND $day_mark>1 AND $type!="4") { + + $in_minutes = round(($time_diff/60),0); + $html->set_body(' + '); + } + + // talker and server names + $db->get_user_name($results[peer_name_id]); + $talk = $db->result->username; + $db->get_server_name($results[peer_server_id]); + $sname = $db->result->server_name; + // cleaning username + $jid = htmlspecialchars($talk); + + $html->set_body(' + + '); + + // username from user roster + $talk = query_nick_name($ejabberd_roster,$talk,$sname); + + // if there is no user in roster - advise that + if ($talk=="f") { + + $talk=$not_in_r[$lang]; + + } + + // threaded view + if ($results["direction"] == "from") { + + $out=$talk; + $tt=$tt+1; + $aa=0; + } + else { + $out = TOKEN; + $aa=$aa+1; + $tt=0; + + } + + if ($aa<2 AND $tt<2) { + + $html->set_body(''); + + } + else { + + $html->set_body(''); + + } + + // end threaded view + + // message body + $body_message=wordwrap(str_replace("\n","
    ",htmlspecialchars($results["body"])),107,"
    ",true); + $html->set_body(''); + + // run pagination code only if search contains from: clausule + /* + The pagination code havent been changed after upgrade to search_engine_v2 - it work well so if one want to improve it + f.e. by adding "back" button be my guest...current code is nightmare :/ + */ + $r=$r+1; + debug(DEBUG,"all: $r, num_r: $num_rows, internal: $internal"); + if ($r==$num_search_results) { + + if ($num_rows>$internal) { + + debug(DEBUG,"-->more results in this day...$entry[at] offset: $internal"); + $tag_count="t"; + } + + $next_r=$external+$offset_arch; + debug(DEBUG,"before cutdown: $next_r"); + // back to one day and continue with offset + if ($tag_count=="t") { + + $next_r=$next_r-1; + + } + + debug(DEBUG,"after cutdown: $next_r"); + debug(DEBUG,"Internal: $internal, offset: $offset_day, is_tag: $s_variables[tag_count]"); + // if the same day - we increase offset + if ($internal==$offset_day AND $s_variables[tag_count] == "t") { + + $internal=$internal+$offset_day; + debug(DEBUG,"Increasing offset..."); + + } + + // hack + if ($qquery[from] == "t") { + + $plain_phase=str_replace("@","//",$plain_phase); + + } + + $trange = $enc->crypt_url("time_start=$time2_start&time_end=$time2_end"); + $lnk_n = $enc->crypt_url("tslice=$entry[at]&offset_arch=$next_r&offset_day=$internal&search_phase=$plain_phase&tag_count=$tag_count"); + + $html->set_body(' + + + + '); + break 2; + + } + + + + } + + + } + + } + + else{ + + // if we haven't found anything increase counter by one... + $b++; + } + + $start_from = null; // reset... + $internal=0; + $day_mark=1; + if ($num_rows!=0 AND $type=="7") { + + if ($arch_table == $external) { + + $html->set_body(''); + } + + elseif($type=="7") { + + $html->set_body(''); + + // initialize thread + $aa="0"; + $tt="0"; + } + } + + // end of main loop + } + + // if normal search: + if ($type!="7" AND $type!==NULL) { + + $db->get_search_results(); + $result = $db->result; + $num_results = count($result); + $html->set_body(' + '); + + foreach ($result as $dat) { + + //building link: + $to_base = $enc->crypt_url("tslice=$dat[time_slice]&peer_name_id=$dat[peer_name_id]&peer_server_id=$dat[peer_server_id]"); + + // get the name of user that we was talking to + $db->get_user_name($dat[peer_name_id]); + $talk = $db->result->username; + + // get it's server name + $db->get_server_name($dat[peer_server_id]); + $sname = $db->result->server_name; + + // cleanup jid + $jid = htmlspecialchars($talk); + + // color every second line... + if ($col=="e0e9f7") { + + $col="e8eef7"; + + } + else { + + $col="e0e9f7"; + + } + + // get username from user roster: + $talk = query_nick_name($ejabberd_roster,$talk,$sname); + + // if user is not in list, advise about that + if ($talk === "" ) { + + $talk=$not_in_r[$lang]; + + } + + // now we want to know who was talking to who... + if ($dat["direction"] == "to") { + + $fr=$to_u[$lang]; + + } + else { + + $fr=$from_u[$lang]; + + } + + // ... and what was talking, and format that ... + $body_talk = wordwrap(str_replace("\n","
    ",htmlspecialchars(base64_decode($dat["body"]))),107,"
    ",true); + + // advise user if chat is deleted. Extension=1 stands for "Chat temporary deleted" or "Chat awaiting deletion" + if ($dat[ext] == 1) { + + $html->set_body(''); + + } + + // opening line + if ($dat[ext]!=1) { + + $html->set_body(''); + + } + else { + + $html->set_body(''); + + } + + // content + $html->set_body(' + + + + ' + ); + + } + + $html->set_body(''); + + } + + if($a==$b) { + + $html->set_body(''); + + } + + $html->set_body('
    '.$time_t[$lang].''.$talks[$lang].''.$thread[$lang].''.$score.'
    '.verbose_split_line($in_minutes,$verb_h[$lang],$in_min[$lang]).'
    '.$results["ts"].' '); + + if ($out === TOKEN) { + + if ($own_name !== false) { + + $html->set_body(cut_nick(htmlspecialchars($own_name))); + + } + else{ + + $html->set_body(cut_nick(htmlspecialchars($out))); + + } + } + else { + + $html->set_body(cut_nick(htmlspecialchars($out))); + + } + + $html->set_body('   -'.$body_message.'
    + + '.$search_next[$lang].'
    '.$no_more[$lang].'
    '.$nx_dy[$lang].'
    '.$search_tip[$lang].' '.$num_results.''.$search_why[$lang].'
    '.$marked_as_d[$lang].'
    '.$dat["ts"].''.$fr.'  '.cut_nick($talk).'  '.$body_talk.''.round($dat[score],2).'
    '.$no_result[$lang].'
    '); + + } + + else { + + // if user input is empty: + $html->set_body('

    '.$search1[$lang].'




    '); + +} + +require_once("footer.php"); +?> diff --git a/jorge/settings.php b/jorge/settings.php new file mode 100644 index 0000000..8b92482 --- /dev/null +++ b/jorge/settings.php @@ -0,0 +1,330 @@ +get('validate_number') !== $_POST['validate_form']) { + + debug(DEBUG," - Invalid control number, destroying POST data. Control should be: ".$sess->get('validate_number')); + unset($_POST); + $html->alert_message($oper_fail[$lang]); + + } + else{ + + debug(DEBUG," - POST data seems to be ok."); + + } + +} + +// Generate new control data for forms +$set_control = md5(rand(10000,10000000)); +$sess->set("validate_number",$set_control); +debug(DEBUG,"Setting new control data: $set_control"); + +// toggle message saving +if ($tgle) { + + debug(DEBUG,"Trying to change archiving option"); + if ($tgle === $arch_on[$lang]) { + + if($db->set_log(true) === true) { + + $sess->set('log_status',true); + $db->set_logger("7","1"); + $html->status_message($status_msg2[$lang]); + + } + else{ + + $html->alert_message($oper_fail[$lang]); + + } + + } + elseif($tgle === $arch_off[$lang]) { + + if($db->set_log(false) === true) { + + $sess->set('log_status',false); + $db->set_logger("6","1"); + $html->status_message($status_msg3[$lang]); + $html->system_message($status_msg1[$lang]); + + } + else{ + + $html->alert_message($oper_fail[$lang]); + + } + + } +} + +// Control diplaying of special contacs +if ($vspec) { + + if ($db->set_jorge_pref("3",$vspec) === true) { + + $html->status_message($con_saved[$lang]); + + } + else{ + + $html->alert_message($oper_fail[$lang]); + + } + +} + +// delete entire archive +if ($_POST['erase_confirm'] === "true") { + + if ($_POST['del_all'] === $settings_del[$lang]) { + + debug(DEBUG," - Trying to erase all message archives"); + if ($db->erase_all() === true) { + + $html->status_message($deleted_all[$lang]); + $db->set_logger("9","2"); + debug(DEBUG," - DONE"); + + } + else{ + + $html->alert_message($delete_error[$lang]); + debug(DEBUG," - FAILED"); + + } + + } + +} + +// set own name +if ($_POST['own_name_value']) { + + // Validation is done by class, so we pass values there... + if ($db->set_own_name($_POST['own_name_value']) === true) { + + $html->status_message($con_saved[$lang]); + + }else{ + + $html->alert_message($oper_fail[$lang]); + + } + +} + +$html->set_overview('

    '.$settings_desc[$lang].'

    '.$settings_desc_detail[$lang].''); +$html->set_body('
    + '); + +$html->set_body(' + + + + + '); + +if ($db->get_jorge_pref("3") === false) { + + $html->alert_message($oper_fail[$lang]); + +} + +$special_select = $db->result->pref_value; + +if ($special_select === "2") { + + $n_is_sel = "selected"; + + } + else{ + + $y_is_sel = "selected"; + +} + +$html->set_body(' + + + + + +'); + +$html->set_body(' + + + +'); + +$html->set_body(' + + +'); + +$html->set_body(' + + + '); + +$html->set_body('
    '.$setting_d1[$lang].'
    '.$setting_d2[$lang].' + + + +
    '.$spec_contact_enable[$lang].'(?) +
    '.$select_view[$lang].' + + +
    '.$sel_language[$lang].' + + +
    '.$own_name_enter[$lang].'(?) + + +


    '.$stats_personal_d[$lang].''); + +$db->get_personal_sum(); +$total_messages = number_format($db->result->cnt); + +$html->set_body('

    '.$stats_personal[$lang].' '.$total_messages.'

    '.$stats_personal_top[$lang].'

    '); + +$db->get_personal_top(); +$results = $db->result; + +if (count($results)!=0) { + + $html->set_body(' + + + '); + + foreach ($results as $result) { + + $db->get_user_name($result[peer_name_id]); + $user_name = $db->result->username; + $db->get_server_name($result[peer_server_id]); + $server_name = $db->result->server_name; + $nickname=query_nick_name($ejabberd_roster,$user_name,$server_name); + $to_base = $enc->crypt_url("tslice=$result[at]&peer_name_id=$result[peer_name_id]&peer_server_id=$result[peer_server_id]"); + $html->set_body(' + + '); + + } + + $html->set_body('
    '.$stats_personal_count[$lang].''.$stats_peer[$lang].''.$stats_when[$lang].'
    '.$result[count].''.$nickname.'  + ('.htmlspecialchars($user_name).'@'.htmlspecialchars($server_name).') + '.$result[at].'
    '); + + } + else { + + $html->set_body('
    '.$no_archives[$lang].'
    '); + +} + +$html->set_body('
    '); +require_once("footer.php"); +?> diff --git a/jorge/simpletree.css b/jorge/simpletree.css new file mode 100644 index 0000000..2c14ea6 --- /dev/null +++ b/jorge/simpletree.css @@ -0,0 +1,28 @@ +.treeview ul{ /*CSS for Simple Tree Menu*/ +margin: 0; +padding: 0; +} + +.treeview li{ /*Style for LI elements in general (excludes an LI that contains sub lists)*/ +background: url(img/list.png) no-repeat left center; +background-color: transparent; +list-style-type: none; +padding-left: 22px; +margin-bottom: 3px; +} + +.treeview li.submenu{ /* Style for LI that contains sub lists (other ULs). */ +background: url(img/closed.png) no-repeat left 1px; +background-color: transparent; +cursor: hand !important; +cursor: pointer !important; +} + + +.treeview li.submenu ul{ /*Style for ULs that are children of LIs (submenu) */ +display: none; /*Hide them by default. Don't delete. */ +} + +.treeview .submenu ul li{ /*Style for LIs of ULs that are children of LIs (submenu) */ +cursor: default; +} diff --git a/jorge/stats.php b/jorge/stats.php new file mode 100644 index 0000000..70c0688 --- /dev/null +++ b/jorge/stats.php @@ -0,0 +1,285 @@ +set_body('
    '.$stats_vhost_select[$lang].'
    '); + +if ($vhost_valid === true) { + + + $db->total_messages($vhost_active); + $total_messages = $db->result; + $html->set_body('

    '.$stats_for[$lang].$vhost_active.'

    + '.$stats_messages[$lang].' '.number_format($total_messages[1][total_messages]).' + '.$stats_messages_b[$lang].''.number_format($total_messages[1][total_chats]).''.$stats_messages_c[$lang].'

    +
    '); + + // get dates + $today = date("Y-n-j"); + $yesterday = date("Y-n-j", strtotime("-1 day")); + $last_week = date("Y-n-j", strtotime("-7 days")); + for ($ds=0;$ds<=4;$ds++) { + + $days[$ds] = date("Y-n-j", strtotime("-$ds day")); + + } + + // Top 10 + $html->set_body(' +
    '.$stats_top[$lang].'
    '); + foreach ($days as $current_day) { + + $i=0; + $html->set_body(''.verbose_date($current_day,$months_names,$weekdays,true).' :
    '); + $db->get_top_ten($current_day,$vhost_active); + $result = $db->result; + foreach ($result as $entry) { + + $i++; + $db->get_user_name($entry[owner_id],$vhost_active); + $local_user = $db->result->username; + $db->get_user_name($entry[peer_name_id],$vhost_active); + $peer_name = $db->result->username; + $db->get_server_name($entry[peer_server_id],$vhost_active); + $peer_server = $db->result->server_name; + $html->set_body(' +  '.$i.'. '.htmlspecialchars($local_user).'@'.$vhost_active.' --> + '.htmlspecialchars($peer_name).'@'.htmlspecialchars($peer_server).' ('.$entry[count].')
    + '); + + } + $html->set_body('
    '); + + } + $html->set_body('
    '); + + // get data for graphs. We can now draw data only if we have full array. This is known issue. + $db->get_monthly_stats($vhost_active); + $result = $db->result; + + if (count($result)<30) { + + $html->status_message($stats_not_eno[$lang]); + + } + else { + + + foreach ($result as $entry) { + + $i++; + $f[$i] = $entry[time_unix]; + $d[$i] = $entry[messages]; + $e[$i] = $entry[users_total]; + + } + + // hourly stats + $db->get_hourly_stats($yesterday,$vhost_active); + $result = $db->result; + foreach ($result as $entry) { + + $hs[$entry[hour]] = $entry[value]; + } + + // weekly stats + $db->get_weekly_stats($last_week,$yesterday,$vhost_active); + $result = $db->result; + foreach ($result as $entry) { + + $idx++; + $hy[$idx] = $entry[value]; + + } + + $html->set_body(' + + +
    +

    +

    +

    +
    +
    + + + '); + + } + +} + +require_once("footer.php"); +?> diff --git a/jorge/style.css b/jorge/style.css new file mode 100644 index 0000000..335a36a --- /dev/null +++ b/jorge/style.css @@ -0,0 +1,403 @@ +body { + background-color: #ffffff; + font-family: Arial; + font-size: 9pt; + +} + +h2 { + font-family: Arial; + font-size: 10pt; + } + +input.cc { width: 400px; } +input:focus.cc2 { width: 400px; border: 2px solid #73a6ff; } +select.cc { width: 70pt; border: 1px solid gray; } +select.cc2 { width: 40pt; border: 1px solid gray; text-align: center; } +select.cc3 { text-align: center; } +select.settings { + text-align: center; + font-size: x-small; + width: 100pt; +} +input.settings { + text-align: center; + font-size: x-small; + width: 100pt; +} + +input.log { width: 250px; border: 1px solid gray;} +input:focus.log { width: 250px; border: 1px solid #73a6ff;} +input.ccc { width: 400px; border: 1px solid gray; } +textarea.ccc { width: 400px; border: 1px solid gray; } + +div.contain { + #border: 1px dashed #333; +} + +div.spacer_div { + clear: both; +} + +tr.header { + background-image: url(img/bar_new.png); + background-repeat:repeat-x; + color: #fff; + font-weight: bold; +} + +tr.foot { + background-image: url(img/bar_new.png); + background-repeat:repeat-x; + color: #fff; +} + +td.rowspace { + + padding-left: 5px; + padding-right: 5px; + + +} + +input.red { + background-color: #e0e9f7; + font-size: 12px; + color: #000000; + border:1px solid; + border-color: #ffffff; + } + +input.fav { + background-color: transparent; + font-size: 8pt; + font-weight: bold; + color: #ffffff; + border:0px solid; + border-color: #ffffff; + vertical-align: bottom; +} + +input.fav_main { + background-color: transparent; + font-size: 8pt; + font-weight: bold; + color: #65a5e4; + border:0px solid; + border-color: #ffffff; + vertical-align: bottom; +} + + +input.submit { + + background-color: transparent; + font-size: 8pt; + font-weight: bold; + color: #ffffff; + border:0px solid; + border-color: #ffffff; + vertical-align: bottom; + text-decoration: underline; +} + +div.message { + background-color: #fad163; + font-weight: bold; + text-align: center; + font-size: x-small; + width: 550px; +} + +div.system { + background-color: #ffffff; + font-weight: bold; + text-align: center; + font-size: x-small; + width: 350px; + border-width: 1px; + border-color: #0000ff; + border-style: solid; +} + +p.message { + background-color: #fad163; + font-weight: bold; + text-align: center; + font-size: x-small; + width: 400px; +} + + + + +td.message { + background-color: #fad163; + color: #fff; + font-size: x-small; + text-align: center; +} + +table.ff { + font-size: 9pt; + font-family: Arial; + } + +span.hlt { + background-color: yellow; + } + +td.time_chat { + background-color: #669dd6; + color: #fff; + text-align: center; + } + +tr.main_s { + color: black; + font-weight: bold; + font-size: 12px; + text-decoration: underline; + } + +tr.maint { + font-weight: bold; + background-color: #c3d9ff; + color: #000000; + } + + +tr.main_row_a { background-color: #e8eef7; } +tr.main_row_b { background-color: #e0e9f7; } +tr.main_row_error { background-color: #de5b5b; } +tr.main_row_headline { background-color: #9fd8ee; } +tr.main_row_message { background-color: #ade8df; } +tr.main_row_group_to { background-color: #fdffbb; } +tr.main_row_group_from { background-color: #fdffd4; } +tr.spacer { background-color: #afccf7; } +tr.spacerb { background-color: #ffffcc; } +td.main_row_special { font-size: x-small; font-style: italic; } + +tr.splitl { + + background-color: #f1f7ff; + font-size: 10px; + color: grey; + text-align: center; + +} + + + +a.menue { + color: #0707cd; +} + +a { + color: #000000; + text-decoration: none; +} + +a.mmenu { + color: white; + text-decoration: none; +} + +a:hover.mmenu { + color: white; +} + +a.foot { + color: #65a5e4; + text-decoration: none; +} + +a.nav_np { + + color: #fff; + text-decoration: none; + font-weight: bold; + +} + +a:hover.nav_np { + + color: #FFCC00; + font-weight: bold; + +} + +a.menu_chat { + color: #fff; + text-decoration: none; + +} + +a:hover { + + color: #136ef7; + text-decoration: none; + +} + +td.cnter { + + font-size: 7pt; + +} +a.delq { + color: red; +} + +a.export { + + color: #4145ff; + +} + +a.clickl { + color: #4145ff; + text-decoration: underline; + } + + +input.btn{ + color:#000000; + font-family:'trebuchet ms',helvetica,sans-serif; + font-size:84%; + font-weight:bold; + background-color:#e8eef7 ; + border:1px solid; + border-top-color:#696; + border-left-color:#696; + border-right-color:#363; + border-bottom-color:#363; +} + + +input.btn_set{ + color:#fff; + font-size: x-small; + background-color:#6daae7; + border:0px solid; + border-top-color:#696; + border-left-color:#696; + border-right-color:#363; + border-bottom-color:#363; +} + +input.c_map_class{ + color: grey; + font-size: x-small; + text-align: center; + background-color:#fff; + border:1px solid; +} + +input:focus.c_map_class { + color: grey; + font-size: x-small; + text-align: center; + background-color:#fff; + border:1px solid #73a6ff; + +} + +#tooltip.fancy { + background-color: #ffffcc; + border: 1px solid #afccf7; + color: #333333; + padding: 4px; + -moz-border-radius-bottomleft: 7px; + -moz-border-radius-bottomright: 7px; + -moz-border-radius-topleft: 0; + -moz-border-radius-topright: 7px; + opacity: .85; + max-width: 60em; + +} + + +.calbck { + background-image: url(img/cal_bck.png); + background-repeat: repeat-x; + background-color: transparent; +} + +.calbck_con { + background-image: url(img/cal_bck2.png); + background-repeat: repeat-x; + background-color: transparent; +} + +.calhead { + font-family: Tahoma; + font-size: 10px; + font-weight: bold; + color: #FFFFFF; +} +.calweek { + font-family: Tahoma; + font-size: 10px; + color: #FFFFFF; +} +.caldays { + font-family: Arial, Helvetica, sans-serif; + font-size: 10px; + color: #FFFFFF; +} +.caldaysnow { + font-family: Arial, Helvetica, sans-serif; + font-size: 10px; + color: #FFFFFF; +} +.caldaysfeat { + font-family: Arial, Helvetica, sans-serif; + font-size: 10px; + color: #b9b9c1; +} +.caldayssel { + font-family: Arial, Helvetica, sans-serif; + font-size: 10px; + color: #FFFFFF; + background-color: #346DA5; +} +.caldays2 { + font-family: Arial, Helvetica, sans-serif; + font-size: 10px; + color: #FFCC00; +} +a.caldays2 { + font-family: Arial, Helvetica, sans-serif; + font-size: 10px; + color: #FFCC00; +} + +a.caldays3 { + font-family: Arial, Helvetica, sans-serif; + font-size: 10px; + color: #ffffff; +} + +a.caldays4 { + font-family: Arial, Helvetica, sans-serif; + font-size: 10px; + color: #bebebe; +} + +a:hover.caldays3 { + + color:#FFCC00; + +} + +.calbckleft { + background-image: url(img/cal_bck_left2.png); + background-repeat: repeat-y; + background-color: transparent; +} +.calbckright { + background-image: url(img/cal_bck_right2.png); + background-repeat: repeat-y; + background-color: transparent; +} diff --git a/jorge/tools/gen_stats.php b/jorge/tools/gen_stats.php new file mode 100644 index 0000000..d5ba51e --- /dev/null +++ b/jorge/tools/gen_stats.php @@ -0,0 +1,55 @@ + jabber_org +$vhost="_VHOST_FOR_WHICH_STATS_ARE_MADE"; # dotted form ex: jabber.org + +$day_stats_a = date("Y-n-j", strtotime ("-1 day")); // if you mist stats for some day change to: date("Y-n-d", strtotime ("-1 day", strtotime("_YOUR_MISSING_DAY_"))); and generate stats manualy. +$day_stats_b = $day_stats_a; + +for ($ds=0;$ds<24;$ds++) { + + $de=$ds+1; + if ($de==24) {$de=0; $day_stats_b = date("Y-n-d", strtotime ("+1 day",strtotime($day_stats_a))); } + $hourly_t="select count(owner_id) from `logdb_messages_$day_stats_a"."_"."$xmpp_host` where timestamp > unix_timestamp('$day_stats_a $ds:00:00') and timestamp < unix_timestamp('$day_stats_b $de:00:00')"; + $result=mysql_query($hourly_t); + $row=mysql_fetch_row($result); + $stats_insert="insert into jorge_stats (day,hour,value,vhost) values('$day_stats_a','$ds','$row[0]','$vhost')"; + mysql_query($stats_insert) or die("SQL Error\n"); + +} + +print "done\n"; + + + +?> diff --git a/jorge/tools/optimize_tables.php b/jorge/tools/optimize_tables.php new file mode 100644 index 0000000..6a61849 --- /dev/null +++ b/jorge/tools/optimize_tables.php @@ -0,0 +1,63 @@ + diff --git a/jorge/tools/trash_cleanup.php b/jorge/tools/trash_cleanup.php new file mode 100644 index 0000000..ea5148a --- /dev/null +++ b/jorge/tools/trash_cleanup.php @@ -0,0 +1,116 @@ + [jorge] Cleaning up trash......."; +$conn=mysql_connect("_MYSQL_HOST_", "_USER_", "_PASSWORD_") or die ("DB connect failed\n"); +mysql_select_db ("_DB_NAME_") or die ("DB select failed\n"); +$xmpp_host="_YOUR_XMPP_HOST_"; + +$query="select owner_id, peer_name_id,peer_server_id,idx,date as tslice,vhost from pending_del where timeframe < date_format((date_sub(curdate(),interval 1 month)), '%Y-%c-%e')"; +$result=mysql_query($query); + +if (mysql_num_rows($result)>0) { + + $i=0; + + while($row=mysql_fetch_array($result)) { + + $i++; + $ch_del="delete from + `logdb_messages_$row[tslice]"."_$xmpp_host` + where + owner_id='$row[owner_id]' + and + peer_name_id='$row[peer_name_id]' + and + peer_server_id='$row[peer_server_id]' + and + ext = '$row[idx]' + "; + $li_del="delete from + jorge_mylinks + where + owner_id='$row[owner_id]' + and + ext='$row[idx]' + and + peer_name_id = '$row[peer_name_id]' + and + peer_server_id='$row[peer_server_id]' + and + datat = '$row[tslice]' + and + vhost = '$row[vhost]' + "; + $fa_del="delete from + jorge_favorites + where + owner_id='$row[owner_id]' + and + ext='$row[idx]' + and + peer_name_id = '$row[peer_name_id]' + and + peer_server_id='$row[peer_server_id]' + and + vhost = '$row[vhost]' + "; + $pe_del="delete from pending_del + where owner_id='$row[owner_id]' + and + peer_name_id = '$row[peer_name_id]' + and + peer_server_id='$row[peer_server_id]' + and + date='$row[tslice]' + and + idx='$row[idx]' + and + vhost = '$row[vhost]' + "; + mysql_query("$ch_del") or die("Error #1\n"); + mysql_query("$li_del") or die("Error #2\n"); + mysql_query("$fa_del") or dir("Error #3\n"); + mysql_query("$pe_del") or die("Error #4\n"); + + } + + print "Deleted $i chats.\n"; + + } + + else + + { + + print "Nothing to delete.\n"; + + } + +?> diff --git a/jorge/trash.php b/jorge/trash.php new file mode 100644 index 0000000..b67bc1d --- /dev/null +++ b/jorge/trash.php @@ -0,0 +1,167 @@ +set_overview('

    '.$trash_name[$lang].'

    '.$trash_desc[$lang].''); + +if ($enc->decrypt_url($_GET[a]) === true) { + + $tslice = $enc->tslice; + $talker = $enc->peer_name_id; + $server = $enc->peer_server_id; + $action = $enc->action; + $lnk = $enc->lnk; + + } + else { + + $action = null; + +} + +if ($_GET['idx']) { + + $idx = $_GET['idx']; + + if ($enc->decrypt_url($idx) === true) { + + if($db->set_ext_index($enc->single) !== true) { + + unset($idx); + unset($action); + + } + + $idx = $enc->single; + + } + else{ + + unset($idx); + unset($action); + + } + +} + +if ($action=="undelete") { + + if ($db->move_chat_from_trash($talker,$server,$tslice,$lnk,$idx) === true) { + + $back_link = $enc->crypt_url("tslice=$tslice&peer_name_id=$talker&peer_server_id=$server"); + $html->status_message($undo_info[$lang].'
    '.$trash_vit[$lang].''); + + // number of items in trash + $db->get_trash_count(); + $tr_n = $db->result->cnt; + + } + + else + + { + + unset($talker); + $html->alert_message($oper_fail[$lang]); + + } + +} + +if ($action==="delete") { + + if ($db->remove_messages_from_trash($talker,$server,$tslice) === true) { + + $html->status_message($del_info[$lang]); + // number of items in trash + $db->get_trash_count(); + $tr_n = $db->result->cnt; + + } + else { + + $html->alert_message($oper_fail[$lang]); + + } + + +} + +if ($tr_n < 1) { + + $html->set_body('

    '.$trash_empty[$lang].'

    ' ); + } + + else + + { + $html->set_body(' +
    + + + + '); + $db->get_trashed_items(); + $result = $db->result; + foreach ($result as $entry) { + + $db->get_user_name($entry[peer_name_id]); + $talker = $db->result->username; + $db->get_server_name($entry[peer_server_id]); + $server_name = $db->result->server_name; + $tslice = $entry["date"]; + $nickname = query_nick_name($ejabberd_roster,$talker,$server_name); + if (!$nickname) { + + $nickname = $not_in_r[$lang]; + unset($malpa); + + } + else { + + $malpa = "@"; + + } + $reconstruct_link = $enc->crypt_url("tslice=$tslice&peer_name_id=$entry[peer_name_id]&peer_server_id=$entry[peer_server_id]"); + $undelete_link = $enc->crypt_url("tslice=$tslice&peer_name_id=$entry[peer_name_id]&peer_server_id=$entry[peer_server_id]&lnk=$reconstruct_link&action=undelete"); + $delete_link = $enc->crypt_url("tslice=$tslice&peer_name_id=$entry[peer_name_id]&peer_server_id=$entry[peer_server_id]&lnk=$reconstruct_link&action=delete"); + $idx = $enc->crypt_url("single=".$entry[idx].""); + + $html->set_body(' + + + + + + + '); + + } + + $html->set_body(' +
    '.$my_links_chat[$lang].''.$logger_from_day[$lang].''.$del_time[$lang].'
    '.$nickname.' ('.htmlspecialchars($talker).$malpa.htmlspecialchars($server_name).')'.$tslice.''.$entry[timeframe].''.$trash_undel[$lang].''.$trash_del[$lang].'
    + '); + } + +require_once("footer.php"); +?> diff --git a/jorge/upper.php b/jorge/upper.php new file mode 100644 index 0000000..f785cd7 --- /dev/null +++ b/jorge/upper.php @@ -0,0 +1,490 @@ +get('log_status') === null) { + + header ("Location: not_enabled.php"); + exit; + +} + +// number of my links saved... +$db->get_mylinks_count(); +$my_links_count = $db->result->cnt; + +// number of items in trash +$db->get_trash_count(); +$tr_n = $db->result->cnt; + +// number of favorites +$db->get_favorites_count(); +$favorites_count = $db->result->cnt; + +// get preferences for saving +$pref_id=$_GET['set_pref']; +$pref_value=$_GET['v']; + +// save preferences ONLY. Setting language in session is done in headers, here we only save that preferences. +if ($_GET['set_pref']) { + + // Language selection + if ($pref_id === "2") { + + //Rewrite array for late reuse + $language_change = $language_support; + // Here the $pref_value is actually $lng_sw + $pref_value = $_GET['lng_sw']; + while(array_keys($language_change)) { + + $lang_key = key($language_change); + if (in_array($pref_value,$language_change[$lang_key])) { + + debug(DEBUG,"Saving language preferences into database..."); + if ($db->set_jorge_pref($pref_id,$pref_value) === false) { + + $html->alert_message($oper_fail[$lang],"message"); + debug(DEBUG,"Preferences not saved due to error"); + + } + else{ + + $html->status_message($con_saved[$lang]); + debug(DEBUG,"Preferences saved successfuly"); + + } + + break 1; + + } + + array_shift($language_change); + + } + } + + // default view type + if ($pref_id ==="1") { + + debug(DEBUG,"Saving view selection into database"); + if($pref_value === "1" OR $pref_value === "2") { + + if ($db->set_jorge_pref($pref_id,$pref_value) === false) { + + $html->alert_message($oper_fail[$lang],"message"); + debug(DEBUG,"Preferences not saved due to error"); + + } + else{ + + // Display status message only if setting via control panel + if ($_GET['ref'] === "settings") { + + $html->status_message($con_saved[$lang]); + debug(DEBUG,"Preferences saved successfuly"); + + } + + } + + $sess->set('view_type',$pref_value); + + } + } + +} + +// get preferences, if not set, fallback to standard view. +$view_type=$sess->get('view_type'); +if ($view_type=="1") { + + $view_type="main.php"; + } + elseif($view_type=="2") { + + $view_type="calendar_view.php"; + +} + +// this is menu. not nice but works ;) +if (preg_match("/search_v2.php/i",$location)) + + { + $menu_main=''.$menu_item_browser[$lang].''; + $menu_map=''.$menu_item_map[$lang].''; + $menu_search=''.$menu_item_search[$lang].''; + $menu_mylinks=''.$menu_item_links[$lang].' ('.$my_links_count.')'; + $menu_favorites=''.$menu_item_fav[$lang].'('.$favorites_count.')'; + $menu_contacts=''.$menu_item_contacts[$lang].''; + $menu_logger=''.$menu_item_logs[$lang].''; + $menu_trash=''.$menu_item_trash[$lang].'('.$tr_n.')'; + $search_loc=1; + if (TOKEN==ADMIN_NAME) { $menu_stats=' | Stats'; } + } + elseif(preg_match("/main.php/i",$location)) + { + $menu_main=''.$menu_item_browser[$lang].''; + $menu_map=''.$menu_item_map[$lang].''; + $menu_search=''.$menu_item_search[$lang].''; + $menu_mylinks=''.$menu_item_links[$lang].' ('.$my_links_count.')'; + $menu_favorites=''.$menu_item_fav[$lang].'('.$favorites_count.')'; + $menu_contacts=''.$menu_item_contacts[$lang].''; + $menu_logger=''.$menu_item_logs[$lang].''; + $menu_trash=''.$menu_item_trash[$lang].'('.$tr_n.')'; + if (TOKEN==ADMIN_NAME) { $menu_stats=' | Stats'; } + + } + elseif(preg_match("/my_links.php/i",$location)) + { + $menu_main=''.$menu_item_browser[$lang].''; + $menu_map=''.$menu_item_map[$lang].''; + $menu_search=''.$menu_item_search[$lang].''; + $menu_mylinks=''.$menu_item_links[$lang].' ('.$my_links_count.') '; + $menu_favorites=''.$menu_item_fav[$lang].'('.$favorites_count.')'; + $menu_contacts=''.$menu_item_contacts[$lang].''; + $menu_logger=''.$menu_item_logs[$lang].''; + $menu_trash=''.$menu_item_trash[$lang].'('.$tr_n.')'; + if (TOKEN==ADMIN_NAME) { $menu_stats=' | Stats'; } + + + } + elseif(preg_match("/help.php/i",$location)) + { + $menu_main=''.$menu_item_browser[$lang].''; + $menu_map=''.$menu_item_map[$lang].''; + $menu_search=''.$menu_item_search[$lang].''; + $menu_mylinks=''.$menu_item_links[$lang].' ('.$my_links_count.')'; + $menu_favorites=''.$menu_item_fav[$lang].'('.$favorites_count.')'; + $menu_contacts=''.$menu_item_contacts[$lang].''; + $menu_logger=''.$menu_item_logs[$lang].''; + $menu_trash=''.$menu_item_trash[$lang].'('.$tr_n.')'; + if (TOKEN==ADMIN_NAME) { $menu_stats=' | Stats'; } + + } + elseif(preg_match("/contacts.php/i", $location)) + { + $menu_main=''.$menu_item_browser[$lang].''; + $menu_map=''.$menu_item_map[$lang].''; + $menu_search=''.$menu_item_search[$lang].''; + $menu_mylinks=''.$menu_item_links[$lang].' ('.$my_links_count.')'; + $menu_favorites=''.$menu_item_fav[$lang].'('.$favorites_count.')'; + $menu_contacts=''.$menu_item_contacts[$lang].''; + $menu_logger=''.$menu_item_logs[$lang].''; + $menu_trash=''.$menu_item_trash[$lang].'('.$tr_n.')'; + if (TOKEN==ADMIN_NAME) { $menu_stats=' | Stats'; } + + } + elseif(preg_match("/stats.php/i", $location)) + { + $menu_main=''.$menu_item_browser[$lang].''; + $menu_map=''.$menu_item_map[$lang].''; + $menu_search=''.$menu_item_search[$lang].''; + $menu_mylinks=''.$menu_item_links[$lang].' ('.$my_links_count.')'; + $menu_favorites=''.$menu_item_fav[$lang].'('.$favorites_count.')'; + $menu_contacts=''.$menu_item_contacts[$lang].''; + $menu_logger=''.$menu_item_logs[$lang].''; + $menu_trash=''.$menu_item_trash[$lang].'('.$tr_n.')'; + if (TOKEN==ADMIN_NAME) { $menu_stats=' | Stats'; } + + + } + elseif(preg_match("/logger.php/i", $location)) + { + $menu_main=''.$menu_item_browser[$lang].''; + $menu_map=''.$menu_item_map[$lang].''; + $menu_search=''.$menu_item_search[$lang].''; + $menu_mylinks=''.$menu_item_links[$lang].' ('.$my_links_count.')'; + $menu_favorites=''.$menu_item_fav[$lang].'('.$favorites_count.')'; + $menu_contacts=''.$menu_item_contacts[$lang].''; + $menu_logger=''.$menu_item_logs[$lang].''; + $menu_trash=''.$menu_item_trash[$lang].'('.$tr_n.')'; + if (TOKEN==ADMIN_NAME) { $menu_stats=' | Stats'; } + + + } + elseif(preg_match("/trash.php/i", $location)) + { + $menu_main=''.$menu_item_browser[$lang].''; + $menu_map=''.$menu_item_map[$lang].''; + $menu_search=''.$menu_item_search[$lang].''; + $menu_mylinks=''.$menu_item_links[$lang].' ('.$my_links_count.')'; + $menu_favorites=''.$menu_item_fav[$lang].'('.$favorites_count.')'; + $menu_contacts=''.$menu_item_contacts[$lang].''; + $menu_logger=''.$menu_item_logs[$lang].''; + $menu_trash=''.$menu_item_trash[$lang].'('.$tr_n.')'; + if (TOKEN==ADMIN_NAME) { $menu_stats=' | Stats'; } + + + } + elseif(preg_match("/calendar_view.php/i", $location)) + { + $menu_main=''.$menu_item_browser[$lang].''; + $menu_map=''.$menu_item_map[$lang].''; + $menu_search=''.$menu_item_search[$lang].''; + $menu_mylinks=''.$menu_item_links[$lang].' ('.$my_links_count.')'; + $menu_favorites=''.$menu_item_fav[$lang].'('.$favorites_count.')'; + $menu_contacts=''.$menu_item_contacts[$lang].''; + $menu_logger=''.$menu_item_logs[$lang].''; + $menu_trash=''.$menu_item_trash[$lang].'('.$tr_n.')'; + if (TOKEN==ADMIN_NAME) { $menu_stats=' | Stats'; } + + + } + elseif(preg_match("/chat_map.php/i", $location)) + { + $menu_main=''.$menu_item_browser[$lang].''; + $menu_map=''.$menu_item_map[$lang].''; + $menu_search=''.$menu_item_search[$lang].''; + $menu_mylinks=''.$menu_item_links[$lang].' ('.$my_links_count.')'; + $menu_favorites=''.$menu_item_fav[$lang].'('.$favorites_count.')'; + $menu_contacts=''.$menu_item_contacts[$lang].''; + $menu_logger=''.$menu_item_logs[$lang].''; + $menu_trash=''.$menu_item_trash[$lang].'('.$tr_n.')'; + if (TOKEN==ADMIN_NAME) { $menu_stats=' | Stats'; } + + + } + elseif(preg_match("/settings.php/i", $location)) + { + $menu_main=''.$menu_item_browser[$lang].''; + $menu_map=''.$menu_item_map[$lang].''; + $menu_search=''.$menu_item_search[$lang].''; + $menu_mylinks=''.$menu_item_links[$lang].' ('.$my_links_count.')'; + $menu_favorites=''.$menu_item_fav[$lang].'('.$favorites_count.')'; + $menu_contacts=''.$menu_item_contacts[$lang].''; + $menu_logger=''.$menu_item_logs[$lang].''; + $menu_trash=''.$menu_item_trash[$lang].'('.$tr_n.')'; + if (TOKEN==ADMIN_NAME) { $menu_stats=' | Stats'; } + } + elseif(preg_match("/favorites.php/i", $location)) + { + $menu_main=''.$menu_item_browser[$lang].''; + $menu_map=''.$menu_item_map[$lang].''; + $menu_search=''.$menu_item_search[$lang].''; + $menu_mylinks=''.$menu_item_links[$lang].' ('.$my_links_count.')'; + $menu_favorites=''.$menu_item_fav[$lang].'('.$favorites_count.')'; + $menu_contacts=''.$menu_item_contacts[$lang].''; + $menu_logger=''.$menu_item_logs[$lang].''; + $menu_trash=''.$menu_item_trash[$lang].'('.$tr_n.')'; + if (TOKEN==ADMIN_NAME) { $menu_stats=' | Stats'; } + } + +// check if archivization is currently enabled... +if ($sess->get('log_status') === false) { + + $html->system_message($status_msg1[$lang]); + +} + +if ($start) { + + $cur_loc="&start=$start"; + +} + +// Show special contact? +$db->get_jorge_pref("3"); +$show_spec = $db->result->pref_value; + +// Display special contacts? +if ($show_spec === "2") { + + $db->spec_ignore(true); + + } + else { + + $db->spec_ignore(false); + +} + +//Generate quick jump +$db->get_last_day(); +$ql_date = $db->result->at; +$quick_link = $enc->crypt_url("tslice=$ql_date"); + +$html->menu(' + + + + + + + '); + +if ($mac_user === true) { + + $html->menu(''); + +} + +$html->menu(' + + + + +
    + + '.TOKEN.'@'.XMPP_HOST.'  |   + '.$menu_item_panel[$lang].'  |   + '.$sel_client[$lang].'  |   + '.$help_but[$lang].'  |  '.$log_out_b[$lang].'
    logo
    + + '); + +if ($search_loc==1) { + + if (isset($_GET[c])) { + + $enc->decrypt_url($_GET['c']); + $time2_start = $enc->time_start; + $time2_end = $enc->time_end; + + } + + else{ + + $time2_start=$_POST[time2_start]; + $time2_end=$_POST[time2_end]; + if (validate_date($time2_start) === false) { + + unset($time2_start); + } + if (validate_date($time2_start) === false) { + + unset($time2_end); + } + + } + + if ($time2_start AND $time2_end) { + + if (strtotime("$time2_start") > strtotime("$time2_end")) { + + $alert = $time_range_w[$lang]; unset ($search_phase); + + } + } + + $db->get_uniq_chat_dates(); + $result = $db->result; + foreach ($result as $row) { + + $r++; + $to_tble[$r] = $row[at]; + + } + + $html->menu(' '); + + if ($time2_start AND !$time2_end) { + + $time2_end = $to_tble[$pass_t-1]; + + } + + if (!$time2_start AND $time2_end) { + + $time2_start = $to_tble[($t+1)-$t]; + + } + +} + +$html->menu('
    Hello Mac user!
    + '.$menu_main.' | ' + .$menu_map.' | ' + .$menu_favorites.' | ' + .$menu_mylinks.' | ' + .$menu_search.' | ' + .$menu_contacts.' | ' + .$menu_logger.$menu_stats.' | ' + .$menu_trash. + ' | '.$refresh[$lang].' +
    +

    '.$alert.'

    + '); + +// Get user roster. +$rpc_roster = $ejabberd_rpc->get_roster(); + +// creater roster object and rewrite it to portable multidimentional array +$ejabberd_roster = new roster(); + +foreach ($rpc_roster as $roster_record) { + + if ($roster_record[group]=="") { + + $roster_record[group] = $con_no_g[$lang]; + + } + + // avoid contacts without nick + if ($roster_record[nick]!="") { + + $ejabberd_roster->add_item($roster_record[jid],$roster_record[nick],$roster_record[group]); + + } +} + +?> diff --git a/mod_admin_extra/COPYING b/mod_admin_extra/COPYING new file mode 100644 index 0000000..8b325f5 --- /dev/null +++ b/mod_admin_extra/COPYING @@ -0,0 +1,343 @@ +As a special exception, the authors give permission to link this program +with the OpenSSL library and distribute the resulting binary. + + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Library General +Public License instead of this License. diff --git a/mod_admin_extra/ChangeLog b/mod_admin_extra/ChangeLog new file mode 100644 index 0000000..0049160 --- /dev/null +++ b/mod_admin_extra/ChangeLog @@ -0,0 +1,117 @@ +2009-06-15 Badlop + + * src/mod_admin_extra.erl: Added command to check the password by + providing a MD5 or SHA hash. + +2009-02-16 Badlop + + * src/mod_admin_extra.erl: Fix srg_user_add arguments + +2009-02-09 Badlop + + * src/mod_admin_extra.erl: Calculation of 'stats registeredusers' + works also with non-internal authentication methods (thanks to + Francois de Metz)(EJAB-864) + +2009-01-27 Badlop + + * README.txt: Explicitely mention that this module does not work + with any ejabberd 2.0.x or older + + * src/mod_admin_extra.erl: In the srg_create command, allow to + separate group identifiers in the Display argument with escaped + newline character. Explain this possibility in the command long + description. + +2008-11-10 Badlop + + * src/mod_admin_extra.erl: Renamed command send_message to + send_message_headline. New command send_message_chat. (thanks to + Xyu) + +2008-10-31 Badlop + + * src/mod_admin_extra.erl: Deleted the duplicated command + delete_older_messages (EJAB-814). Rename the other delete_older_* + commands. + +2008-10-12 Badlop + + * README.txt: It requires ejabberd trunk SVN 1635 or newer + + * src/mod_admin_extra.erl: Renamed mod_ctlextra to + mod_admin_extra, as it now implements ejabberd commands instead of + ejabberdctl commands + +2008-04-26 Badlop + + * src/mod_ctlextra.erl: New command: ban-account + * README.txt: Documented new command + +2008-04-04 Badlop + + * src/mod_ctlextra.erl: Added new command + rosteritem-purge [options] + +2008-03-19 Badlop + + * src/mod_ctlextra.erl: Added two new commands for administration + of Shared Roster Groups: "srg-list-groups host" and "srg-get-info + group host" (thanks to Taylor Laramie) + +2008-03-09 Badlop + + * src/mod_ctlextra.erl: Show the result of each table export + +2008-03-04 Badlop + + * src/mod_ctlextra.erl: New command 'stats + onlineusersnode'. Removed old unusued code. + + * README.txt: Updated module page + +2008-01-20 Badlop + + * src/mod_ctlextra.erl: Allow to define group name with spaces + * README.txt: Likewise + +2007-11-14 Badlop + + * src/mod_ctlextra.erl: Updated to ejabberd SVN. + +2007-09-08 Badlop + + * src/mod_ctlextra.erl: Bugfix: unregister commands when module + stops. Removed all MUC-related commands. + +2007-08-29 Badlop + + * src/mod_ctlextra.erl: Added command: stats uptime-seconds. + +2007-08-24 Badlop + + * src/mod_ctlextra.erl: Fixed bug in vcard-set command that + forgot second level attributes. + +2007-08-23 Badlop + + * src/mod_ctlextra.erl: Command add-rosteritem now pushes the + roster item to the client. New command rem-rosteritem. + +2007-08-19 Badlop + + * src/mod_ctlextra.erl: Now vcard-get and vcard-set are more + compatible with mod_vcard_odbc and mod_vcard_ldap. + +2007-08-16 Badlop + + * src/mod_ctlextra.erl: add-rosteritem only adds an item in a + roster. + +2007-08-07 Badlop + + * src/mod_ctlextra.erl: Fixed indentation. + + * ChangeLog: New file to track changes. + + * README.txt: Removed Changelog section. diff --git a/mod_admin_extra/Emakefile b/mod_admin_extra/Emakefile new file mode 100644 index 0000000..b9c084f --- /dev/null +++ b/mod_admin_extra/Emakefile @@ -0,0 +1,2 @@ +{'../ejabberd-dev/src/gen_mod', [{outdir, "../ejabberd-dev/ebin"},{i,"../ejabberd-dev/include"}]}. +{'src/mod_admin_extra', [{outdir, "ebin"},{i,"../ejabberd-dev/include"}]}. diff --git a/mod_admin_extra/README.txt b/mod_admin_extra/README.txt new file mode 100644 index 0000000..7d98e2a --- /dev/null +++ b/mod_admin_extra/README.txt @@ -0,0 +1,86 @@ + + + mod_admin_extra - Additional ejabberd commands + + Author: Badlop + Homepage: http://www.ejabberd.im/mod_admin_extra + Requirements: ejabberd 2.1.10 newer + + This module DOES NOT WORK with any ejabberd 2.0.x or older. + If you have some ejabberd 2.0.x, you can still use mod_ctlextra. + + + CONFIGURATION + ============= + +Add the module to your ejabberd.cfg, on the modules section: +{modules, [ + ... + {mod_admin_extra, []}, + ... +]}. + +The configurable options are: +- module_resource: + Indicate the resource that the XMPP stanzas must use in the FROM or TO JIDs. + This is only useful in the vcard set and get commands. + The default value is "mod_admin_extra". + +In this example configuration, the users vcards can only be modified +by executing mod_admin_extra commands. +Notice that this needs the patch + https://support.process-one.net/browse/EJAB-797 +{acl, adminextraresource, {resource, "modadminextraf8x,31ad"}}. +{access, vcard_set, [ + {allow, adminextraresource}, + {deny, all}] +}. +{modules, [ + {mod_admin_extra, [ {module_resource, "modadminextraf8x,31ad"} ]}, + {mod_vcard, [ {access_set, vcard_set} ]}, + ... +]}. + + + USAGE + ===== + +Now you have several new commands in ejabberdctl. + +Description of some commands: + + - vcard-* + Example: ejabberdctl vcard-get joe myjab.net email + + - pushroster* + The file used by 'pushroster' and 'pushroster-all' must be placed: + - Windows: on the directory were you installed ejabberd: + 'C:/Program Files/ejabberd' + - Other OS: on the same directory where the .beam files are. + Example content for the roster file: + [{"bob", "example.org", "workers", "Bob"}, + {"mart", "example.org", "workers", "Mart"}, + {"Rich", "example.org", "bosses", "Rich"}]. + + - srg-create + If you want to put a group Name with blankspaces, use the characters + "' and '" to define when the Name starts and ends. + For example: + ejabberdctl srg-create g1 example.org "'Group number 1'" this_is_g1 g1 + + - ban-account + + This command kicks all the connected sessions of the account from the + server. It also changes his password to another randomly + generated, so he can't login anymore unless a server administrator + changes him again the password. + + It is possible to define the reason of the ban. The new password + also includes the reason and the date and time of the ban. + + For example, if this command is called: + ejabberdctl vhost example.org ban-account boby Spammed several MUC rooms + then the sessions of the local account which JID is boby@example.org + will be kicked, and its password will be set to something like this: + BANNED_ACCOUNT--20080425T21:45:07--2176635--Spammed_several_MUC_rooms + diff --git a/mod_admin_extra/build.bat b/mod_admin_extra/build.bat new file mode 100644 index 0000000..fff9b3b --- /dev/null +++ b/mod_admin_extra/build.bat @@ -0,0 +1 @@ +erl -pa ../../ejabberd-dev/trunk/ebin -pa ebin -make diff --git a/mod_admin_extra/build.sh b/mod_admin_extra/build.sh new file mode 100755 index 0000000..f0a0cfa --- /dev/null +++ b/mod_admin_extra/build.sh @@ -0,0 +1,2 @@ +#!/bin/sh +erl -pa ../ejabberd-dev/ebin -pz ebin -make diff --git a/mod_admin_extra/src/mod_admin_extra.erl b/mod_admin_extra/src/mod_admin_extra.erl new file mode 100644 index 0000000..a635472 --- /dev/null +++ b/mod_admin_extra/src/mod_admin_extra.erl @@ -0,0 +1,1578 @@ +%%%------------------------------------------------------------------- +%%% File : mod_admin_extra.erl +%%% Author : Badlop +%%% Purpose : Contributed administrative functions and commands +%%% Created : 10 Aug 2008 by Badlop +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2008 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., 59 Temple Place, Suite 330, Boston, MA +%%% 02111-1307 USA +%%% +%%%------------------------------------------------------------------- + +-module(mod_admin_extra). +-author('badlop@process-one.net'). + +-behaviour(gen_mod). + +-export([start/2, stop/1, + %% Node + compile/1, + load_config/1, + get_cookie/0, + remove_node/1, + export2odbc/2, + %% Accounts + set_password/3, + check_password_hash/4, + delete_old_users/1, + delete_old_users_vhost/2, + ban_account/3, + num_active_users/2, + %% Sessions + num_resources/2, + resource_num/3, + kick_session/4, + status_num/2, status_num/1, + status_list/2, status_list/1, + connected_users_info/0, + connected_users_vhost/1, + set_presence/7, + user_sessions_info/2, + %% Vcard + set_nickname/3, + get_vcard/3, + get_vcard/4, + get_vcard_multi/4, + set_vcard/4, + set_vcard/5, + %% Roster + add_rosteritem/7, + delete_rosteritem/4, + process_rosteritems/5, + get_roster/2, + push_roster/3, + push_roster_all/1, + push_alltoall/2, + %% mod_last + set_last/4, + %% mod_private + private_get/4, + private_set/3, + %% mod_shared_roster + srg_create/5, + srg_delete/2, + srg_list/1, + srg_get_info/2, + srg_get_members/2, + srg_user_add/4, + srg_user_del/4, + %% Stanza + send_message_headline/4, + send_message_chat/3, + send_stanza_c2s/4, + privacy_set/3, + %% Stats + stats/1, stats/2 + ]). + +-include("ejabberd.hrl"). +-include("ejabberd_commands.hrl"). +-include("mod_roster.hrl"). +-include("jlib.hrl"). + +%% Copied from ejabberd_sm.erl +-record(session, {sid, usr, us, priority, info}). + + +%%% +%%% gen_mod +%%% + +start(_Host, _Opts) -> + ejabberd_commands:register_commands(commands()). + +stop(_Host) -> + ejabberd_commands:unregister_commands(commands()). + + +%%% +%%% Register commands +%%% + +commands() -> + Vcard1FieldsString = "Some vcard field names in get/set_vcard are:\n" + " FN - Full Name\n" + " NICKNAME - Nickname\n" + " BDAY - Birthday\n" + " TITLE - Work: Position\n" + " ROLE - Work: Role", + + Vcard2FieldsString = "Some vcard field names and subnames in get/set_vcard2 are:\n" + " N FAMILY - Family name\n" + " N GIVEN - Given name\n" + " N MIDDLE - Middle name\n" + " ADR CTRY - Address: Country\n" + " ADR LOCALITY - Address: City\n" + " EMAIL USERID - E-Mail Address\n" + " ORG ORGNAME - Work: Company\n" + " ORG ORGUNIT - Work: Department", + + VcardXEP = "For a full list of vCard fields check XEP-0054: vcard-temp at " + "http://www.xmpp.org/extensions/xep-0054.html", + + [ + #ejabberd_commands{name = compile, tags = [erlang], + desc = "Recompile and reload Erlang source code file", + module = ?MODULE, function = compile, + args = [{file, string}], + result = {res, rescode}}, + #ejabberd_commands{name = load_config, tags = [server], + desc = "Load ejabberd configuration file", + module = ?MODULE, function = load_config, + args = [{file, string}], + result = {res, rescode}}, + #ejabberd_commands{name = get_cookie, tags = [erlang], + desc = "Get the Erlang cookie of this node", + module = ?MODULE, function = get_cookie, + args = [], + result = {cookie, string}}, + #ejabberd_commands{name = remove_node, tags = [erlang], + desc = "Remove an ejabberd node from Mnesia clustering config", + module = ?MODULE, function = remove_node, + args = [{node, string}], + result = {res, rescode}}, + #ejabberd_commands{name = export2odbc, tags = [mnesia], %% Copied to ejabberd 2.1.x after 11 + desc = "Export Mnesia tables to files in directory", + module = ?MODULE, function = export2odbc, + args = [{host, string}, {path, string}], + result = {res, rescode}}, + + #ejabberd_commands{name = num_active_users, tags = [accounts, stats], + desc = "Get number of users active in the last days", + module = ?MODULE, function = num_active_users, + args = [{host, string}, {days, integer}], + result = {users, integer}}, + #ejabberd_commands{name = delete_old_users, tags = [accounts, purge], + desc = "Delete users that didn't log in last days, or that never logged", + module = ?MODULE, function = delete_old_users, + args = [{days, integer}], + result = {res, restuple}}, + #ejabberd_commands{name = delete_old_users_vhost, tags = [accounts, purge], + desc = "Delete users that didn't log in last days in vhost, or that never logged", + module = ?MODULE, function = delete_old_users_vhost, + args = [{host, string}, {days, integer}], + result = {res, restuple}}, + + #ejabberd_commands{name = check_account, tags = [accounts], + desc = "Check if an account exists or not", + module = ejabberd_auth, function = is_user_exists, + args = [{user, string}, {host, string}], + result = {res, rescode}}, + #ejabberd_commands{name = check_password, tags = [accounts], + desc = "Check if a password is correct", + module = ejabberd_auth, function = check_password, + args = [{user, string}, {host, string}, {password, string}], + result = {res, rescode}}, + #ejabberd_commands{name = check_password_hash, tags = [accounts], + desc = "Check if the password hash is correct", + longdesc = "Allowed hash methods: md5, sha.", + module = ?MODULE, function = check_password_hash, + args = [{user, string}, {host, string}, {passwordhash, string}, {hashmethod, string}], + result = {res, rescode}}, + #ejabberd_commands{name = change_password, tags = [accounts], + desc = "Change the password of an account", + module = ?MODULE, function = set_password, + args = [{user, string}, {host, string}, {newpass, string}], + result = {res, rescode}}, + #ejabberd_commands{name = ban_account, tags = [accounts], + desc = "Ban an account: kick sessions and set random password", + module = ?MODULE, function = ban_account, + args = [{user, string}, {host, string}, {reason, string}], + result = {res, rescode}}, + + #ejabberd_commands{name = num_resources, tags = [session], + desc = "Get the number of resources of a user", + module = ?MODULE, function = num_resources, + args = [{user, string}, {host, string}], + result = {resources, integer}}, + #ejabberd_commands{name = resource_num, tags = [session], + desc = "Resource string of a session number", + module = ?MODULE, function = resource_num, + args = [{user, string}, {host, string}, {num, integer}], + result = {resource, string}}, + #ejabberd_commands{name = kick_session, tags = [session], + desc = "Kick a user session", + module = ?MODULE, function = kick_session, + args = [{user, string}, {host, string}, {resource, string}, {reason, string}], + result = {res, rescode}}, + #ejabberd_commands{name = status_num_host, tags = [session, stats], + desc = "Number of logged users with this status in host", + module = ?MODULE, function = status_num, + args = [{host, string}, {status, string}], + result = {users, integer}}, + #ejabberd_commands{name = status_num, tags = [session, stats], + desc = "Number of logged users with this status", + module = ?MODULE, function = status_num, + args = [{status, string}], + result = {users, integer}}, + #ejabberd_commands{name = status_list_host, tags = [session], + desc = "List of users logged in host with their statuses", + module = ?MODULE, function = status_list, + args = [{host, string}, {status, string}], + result = {users, {list, + {userstatus, {tuple, [ + {user, string}, + {host, string}, + {resource, string}, + {priority, integer}, + {status, string} + ]}} + }}}, + #ejabberd_commands{name = status_list, tags = [session], + desc = "List of logged users with this status", + module = ?MODULE, function = status_list, + args = [{status, string}], + result = {users, {list, + {userstatus, {tuple, [ + {user, string}, + {host, string}, + {resource, string}, + {priority, integer}, + {status, string} + ]}} + }}}, + #ejabberd_commands{name = connected_users_info, + tags = [session], + desc = "List all established sessions and their information", + module = ?MODULE, function = connected_users_info, + args = [], + result = {connected_users_info, + {list, + {sessions, {tuple, + [{jid, string}, + {connection, string}, + {ip, string}, + {port, integer}, + {priority, integer}, + {node, string}, + {uptime, integer} + ]}} + }}}, + #ejabberd_commands{name = connected_users_vhost, + tags = [session], + desc = "Get the list of established sessions in a vhost", + module = ?MODULE, function = connected_users_vhost, + args = [{host, string}], + result = {connected_users_vhost, {list, {sessions, string}}}}, + #ejabberd_commands{name = user_sessions_info, + tags = [session], + desc = "Get information about all sessions of a user", + module = ?MODULE, function = user_sessions_info, + args = [{user, string}, {host, string}], + result = {sessions_info, + {list, + {session, {tuple, + [{connection, string}, + {ip, string}, + {port, integer}, + {priority, integer}, + {node, string}, + {uptime, integer}, + {status, string}, + {resource, string}, + {statustext, string} + ]}} + }}}, + + #ejabberd_commands{name = set_presence, + tags = [session], + desc = "Set presence of a session", + module = ?MODULE, function = set_presence, + args = [{user, string}, {host, string}, + {resource, string}, {type, string}, + {show, string}, {status, string}, + {priority, string}], + result = {res, rescode}}, + + #ejabberd_commands{name = set_nickname, tags = [vcard], + desc = "Set nickname in a user's vCard", + module = ?MODULE, function = set_nickname, + args = [{user, string}, {host, string}, {nickname, string}], + result = {res, rescode}}, + + #ejabberd_commands{name = get_vcard, tags = [vcard], + desc = "Get content from a vCard field", + longdesc = Vcard1FieldsString ++ "\n" ++ Vcard2FieldsString ++ "\n\n" ++ VcardXEP, + module = ?MODULE, function = get_vcard, + args = [{user, string}, {host, string}, {name, string}], + result = {content, string}}, + #ejabberd_commands{name = get_vcard2, tags = [vcard], + desc = "Get content from a vCard field", + longdesc = Vcard2FieldsString ++ "\n\n" ++ Vcard1FieldsString ++ "\n" ++ VcardXEP, + module = ?MODULE, function = get_vcard, + args = [{user, string}, {host, string}, {name, string}, {subname, string}], + result = {content, string}}, + #ejabberd_commands{name = get_vcard2_multi, tags = [vcard], + desc = "Get multiple contents from a vCard field (requires exmpp installed)", + longdesc = Vcard2FieldsString ++ "\n\n" ++ Vcard1FieldsString ++ "\n" ++ VcardXEP, + module = ?MODULE, function = get_vcard_multi, + args = [{user, string}, {host, string}, {name, string}, {subname, string}], + result = {contents, {list, string}}}, + + #ejabberd_commands{name = set_vcard, tags = [vcard], + desc = "Set content in a vCard field", + longdesc = Vcard1FieldsString ++ "\n" ++ Vcard2FieldsString ++ "\n\n" ++ VcardXEP, + module = ?MODULE, function = set_vcard, + args = [{user, string}, {host, string}, {name, string}, {content, string}], + result = {res, rescode}}, + #ejabberd_commands{name = set_vcard2, tags = [vcard], + desc = "Set content in a vCard subfield", + longdesc = Vcard2FieldsString ++ "\n\n" ++ Vcard1FieldsString ++ "\n" ++ VcardXEP, + module = ?MODULE, function = set_vcard, + args = [{user, string}, {host, string}, {name, string}, {subname, string}, {content, string}], + result = {res, rescode}}, + #ejabberd_commands{name = set_vcard2_multi, tags = [vcard], + desc = "Set multiple contents in a vCard subfield", + longdesc = Vcard2FieldsString ++ "\n\n" ++ Vcard1FieldsString ++ "\n" ++ VcardXEP, + module = ?MODULE, function = set_vcard, + args = [{user, string}, {host, string}, {name, string}, {subname, string}, {contents, {list, string}}], + result = {res, rescode}}, + + #ejabberd_commands{name = add_rosteritem, tags = [roster], + desc = "Add an item to a user's roster (supports ODBC)", + module = ?MODULE, function = add_rosteritem, + args = [{localuser, string}, {localserver, string}, + {user, string}, {server, string}, + {nick, string}, {group, string}, + {subs, string}], + result = {res, rescode}}, + %%{"", "subs= none, from, to or both"}, + %%{"", "example: add-roster peter localhost mike server.com MiKe Employees both"}, + %%{"", "will add mike@server.com to peter@localhost roster"}, + #ejabberd_commands{name = delete_rosteritem, tags = [roster], + desc = "Delete an item from a user's roster (supports ODBC)", + module = ?MODULE, function = delete_rosteritem, + args = [{localuser, string}, {localserver, string}, + {user, string}, {server, string}], + result = {res, rescode}}, + #ejabberd_commands{name = process_rosteritems, tags = [roster], + desc = "List or delete rosteritems that match filtering options", + longdesc = "Explanation of each argument:\n" + " - action: what to do with each rosteritem that " + "matches all the filtering options\n" + " - subs: subscription type\n" + " - asks: pending subscription\n" + " - users: the JIDs of the local user\n" + " - contacts: the JIDs of the contact in the roster\n" + "\n" + "Allowed values in the arguments:\n" + " ACTION = list | delete\n" + " SUBS = SUB[:SUB]* | any\n" + " SUB = none | from | to | both\n" + " ASKS = ASK[:ASK]* | any\n" + " ASK = none | out | in\n" + " USERS = JID[:JID]* | any\n" + " CONTACTS = JID[:JID]* | any\n" + " JID = characters valid in a JID, and can use the " + "globs: *, ?, ! and [...]\n" + "\n" + "This example will list roster items with subscription " + "'none', 'from' or 'to' that have any ask property, of " + "local users which JID is in the virtual host " + "'example.org' and that the contact JID is either a " + "bare server name (without user part) or that has a " + "user part and the server part contains the word 'icq'" + ":\n list none:from:to any *@example.org *:*@*icq*", + module = ?MODULE, function = process_rosteritems, + args = [{action, string}, {subs, string}, + {asks, string}, {users, string}, + {contacts, string}], + result = {response, + {list, + {pairs, {tuple, + [{user, string}, + {contact, string} + ]}} + }}}, + #ejabberd_commands{name = get_roster, tags = [roster], + desc = "Get roster of a local user", + module = ?MODULE, function = get_roster, + args = [{user, string}, {host, string}], + result = {contacts, {list, {contact, {tuple, [ + {jid, string}, + {nick, string}, + {subscription, string}, + {ask, string}, + {group, string} + ]}}}}}, + #ejabberd_commands{name = push_roster, tags = [roster], + desc = "Push template roster from file to a user", + module = ?MODULE, function = push_roster, + args = [{file, string}, {user, string}, {host, string}], + result = {res, rescode}}, + #ejabberd_commands{name = push_roster_all, tags = [roster], + desc = "Push template roster from file to all those users", + module = ?MODULE, function = push_roster_all, + args = [{file, string}], + result = {res, rescode}}, + #ejabberd_commands{name = push_alltoall, tags = [roster], + desc = "Add all the users to all the users of Host in Group", + module = ?MODULE, function = push_alltoall, + args = [{host, string}, {group, string}], + result = {res, rescode}}, + + #ejabberd_commands{name = set_last, tags = [last], + desc = "Set last activity information", + longdesc = "Timestamp is the seconds since" + "1970-01-01 00:00:00 UTC, for example: date +%s", + module = ?MODULE, function = set_last, + args = [{user, string}, {host, string}, {timestamp, integer}, {status, string}], + result = {res, rescode}}, + + #ejabberd_commands{name = private_get, tags = [private], + desc = "Get some information from a user private storage", + module = ?MODULE, function = private_get, + args = [{user, string}, {host, string}, {element, string}, {ns, string}], + result = {res, string}}, + #ejabberd_commands{name = private_set, tags = [private], + desc = "Set to the user private storage", + module = ?MODULE, function = private_set, + args = [{user, string}, {host, string}, {element, string}], + result = {res, rescode}}, + + #ejabberd_commands{name = srg_create, tags = [shared_roster_group], + desc = "Create a Shared Roster Group", + longdesc = "If you want to specify several group " + "identifiers in the Display argument,\n" + "put \\ \" around the argument and\nseparate the " + "identifiers with \\ \\ n\n" + "For example:\n" + " ejabberdctl srg_create group3 localhost " + "name desc \\\"group1\\\\ngroup2\\\"", + module = ?MODULE, function = srg_create, + args = [{group, string}, {host, string}, + {name, string}, {description, string}, {display, string}], + result = {res, rescode}}, + #ejabberd_commands{name = srg_delete, tags = [shared_roster_group], + desc = "Delete a Shared Roster Group", + module = ?MODULE, function = srg_delete, + args = [{group, string}, {host, string}], + result = {res, rescode}}, + #ejabberd_commands{name = srg_list, tags = [shared_roster_group], + desc = "List the Shared Roster Groups in Host", + module = ?MODULE, function = srg_list, + args = [{host, string}], + result = {groups, {list, {id, string}}}}, + #ejabberd_commands{name = srg_get_info, tags = [shared_roster_group], + desc = "Get info of a Shared Roster Group", + module = ?MODULE, function = srg_get_info, + args = [{group, string}, {host, string}], + result = {informations, {list, {information, {tuple, [{key, string}, {value, string}]}}}}}, + #ejabberd_commands{name = srg_get_members, tags = [shared_roster_group], + desc = "Get members of a Shared Roster Group", + module = ?MODULE, function = srg_get_members, + args = [{group, string}, {host, string}], + result = {members, {list, {member, string}}}}, + #ejabberd_commands{name = srg_user_add, tags = [shared_roster_group], + desc = "Add the JID user@host to the Shared Roster Group", + module = ?MODULE, function = srg_user_add, + args = [{user, string}, {host, string}, {group, string}, {grouphost, string}], + result = {res, rescode}}, + #ejabberd_commands{name = srg_user_del, tags = [shared_roster_group], + desc = "Delete this JID user@host from the Shared Roster Group", + module = ?MODULE, function = srg_user_del, + args = [{user, string}, {host, string}, {group, string}, {grouphost, string}], + result = {res, rescode}}, + + #ejabberd_commands{name = send_message_chat, tags = [stanza], + desc = "Send a chat message to a local or remote bare of full JID", + module = ?MODULE, function = send_message_chat, + args = [{from, string}, {to, string}, {body, string}], + result = {res, rescode}}, + #ejabberd_commands{name = send_message_headline, tags = [stanza], + desc = "Send a headline message to a local or remote bare of full JID", + module = ?MODULE, function = send_message_headline, + args = [{from, string}, {to, string}, + {subject, string}, {body, string}], + result = {res, rescode}}, + #ejabberd_commands{name = send_stanza_c2s, tags = [stanza], + desc = "Send a stanza as if sent from a c2s session", + module = ?MODULE, function = send_stanza_c2s, + args = [{user, string}, {host, string}, {resource, string}, {stanza, string}], + result = {res, rescode}}, + #ejabberd_commands{name = privacy_set, tags = [stanza], + desc = "Send a IQ set privacy stanza for a local account", + module = ?MODULE, function = privacy_set, + args = [{user, string}, {host, string}, {xmlquery, string}], + result = {res, rescode}}, + + #ejabberd_commands{name = stats, tags = [stats], + desc = "Get statistical value: registeredusers onlineusers onlineusersnode uptimeseconds", + module = ?MODULE, function = stats, + args = [{name, string}], + result = {stat, integer}}, + #ejabberd_commands{name = stats_host, tags = [stats], + desc = "Get statistical value for this host: registeredusers onlineusers", + module = ?MODULE, function = stats, + args = [{name, string}, {host, string}], + result = {stat, integer}} + ]. + + +%%% +%%% Node +%%% + +compile(File) -> + case compile:file(File) of + ok -> ok; + _ -> error + end. + +load_config(Path) -> + ok = ejabberd_config:load_file(Path). + +get_cookie() -> + atom_to_list(erlang:get_cookie()). + +remove_node(Node) -> + mnesia:del_table_copy(schema, list_to_atom(Node)), + ok. + +export2odbc(Host, Directory) -> + Tables = [ + {export_last, last}, + {export_offline, offline}, + {export_passwd, passwd}, + {export_private_storage, private_storage}, + {export_roster, roster}, + {export_vcard, vcard}, + {export_vcard_search, vcard_search}], + Export = fun({TableFun, Table}) -> + Filename = filename:join([Directory, atom_to_list(Table)++".txt"]), + io:format("Trying to export Mnesia table '~p' on Host '~s' to file '~s'~n", [Table, Host, Filename]), + Res = (catch ejd2odbc:TableFun(Host, Filename)), + io:format(" Result: ~p~n", [Res]) + end, + lists:foreach(Export, Tables), + ok. + + +%%% +%%% Accounts +%%% + +set_password(User, Host, Password) -> + case ejabberd_auth:set_password(User, Host, Password) of + ok -> + ok; + _ -> + error + end. + +%% Copied some code from ejabberd_commands.erl +check_password_hash(User, Host, PasswordHash, HashMethod) -> + AccountPass = ejabberd_auth:get_password_s(User, Host), + AccountPassHash = case HashMethod of + "md5" -> get_md5(AccountPass); + "sha" -> get_sha(AccountPass); + _ -> undefined + end, + case AccountPassHash of + undefined -> error; + PasswordHash -> ok; + _ -> error + end. +get_md5(AccountPass) -> + lists:flatten([io_lib:format("~.16B", [X]) + || X <- binary_to_list(crypto:md5(AccountPass))]). +get_sha(AccountPass) -> + lists:flatten([io_lib:format("~.16B", [X]) + || X <- binary_to_list(crypto:sha(AccountPass))]). + +num_active_users(Host, Days) -> + list_last_activity(Host, true, Days). + +%% Code based on ejabberd/src/web/ejabberd_web_admin.erl +list_last_activity(Host, Integral, Days) -> + {MegaSecs, Secs, _MicroSecs} = now(), + TimeStamp = MegaSecs * 1000000 + Secs, + TS = TimeStamp - Days * 86400, + case catch mnesia:dirty_select( + last_activity, [{{last_activity, {'_', Host}, '$1', '_'}, + [{'>', '$1', TS}], + [{'trunc', {'/', + {'-', TimeStamp, '$1'}, + 86400}}]}]) of + {'EXIT', _Reason} -> + []; + Vals -> + Hist = histogram(Vals, Integral), + if + Hist == [] -> + 0; + true -> + Left = Days - length(Hist), + Tail = if + Integral -> + lists:duplicate(Left, lists:last(Hist)); + true -> + lists:duplicate(Left, 0) + end, + lists:nth(Days, Hist ++ Tail) + end + end. +histogram(Values, Integral) -> + histogram(lists:sort(Values), Integral, 0, 0, []). +histogram([H | T], Integral, Current, Count, Hist) when Current == H -> + histogram(T, Integral, Current, Count + 1, Hist); +histogram([H | _] = Values, Integral, Current, Count, Hist) when Current < H -> + if + Integral -> + histogram(Values, Integral, Current + 1, Count, [Count | Hist]); + true -> + histogram(Values, Integral, Current + 1, 0, [Count | Hist]) + end; +histogram([], _Integral, _Current, Count, Hist) -> + if + Count > 0 -> + lists:reverse([Count | Hist]); + true -> + lists:reverse(Hist) + end. + + +delete_old_users(Days) -> + %% Get the list of registered users + Users = ejabberd_auth:dirty_get_registered_users(), + + {removed, N, UR} = delete_old_users(Days, Users), + {ok, io_lib:format("Deleted ~p users: ~p", [N, UR])}. + +delete_old_users_vhost(Host, Days) -> + %% Get the list of registered users + Users = ejabberd_auth:get_vh_registered_users(Host), + + {removed, N, UR} = delete_old_users(Days, Users), + {ok, io_lib:format("Deleted ~p users: ~p", [N, UR])}. + +delete_old_users(Days, Users) -> + %% Convert older time + SecOlder = Days*24*60*60, + + %% Get current time + {MegaSecs, Secs, _MicroSecs} = now(), + TimeStamp_now = MegaSecs * 1000000 + Secs, + + %% For a user, remove if required and answer true + F = fun({LUser, LServer}) -> + %% Check if the user is logged + case ejabberd_sm:get_user_resources(LUser, LServer) of + %% If it isnt + [] -> + %% Look for his last_activity + case (get_lastactivity_module(LServer)):get_last_info(LUser, LServer) of + %% If it is + %% existent: + {ok, TimeStamp, _Status} -> + %% get his age + Sec = TimeStamp_now - TimeStamp, + %% If he is + if + %% younger than SecOlder: + Sec < SecOlder -> + %% do nothing + false; + %% older: + true -> + %% remove the user + ejabberd_auth:remove_user(LUser, LServer), + true + end; + %% nonexistent: + not_found -> + %% remove the user + ejabberd_auth:remove_user(LUser, LServer), + true + end; + %% Else + _ -> + %% do nothing + false + end + end, + %% Apply the function to every user in the list + Users_removed = lists:filter(F, Users), + {removed, length(Users_removed), Users_removed}. + +get_lastactivity_module(Server) -> + case lists:member(mod_last, gen_mod:loaded_modules(Server)) of + true -> mod_last; + _ -> mod_last_odbc + end. + + +%% +%% Ban account + +ban_account(User, Host, ReasonText) -> + Reason = prepare_reason(ReasonText), + kick_sessions(User, Host, Reason), + set_random_password(User, Host, Reason), + ok. + +kick_sessions(User, Server, Reason) -> + lists:map( + fun(Resource) -> + kick_this_session(User, Server, Resource, Reason) + end, + get_resources(User, Server)). + +get_resources(User, Server) -> + lists:map( + fun(Session) -> + element(3, Session#session.usr) + end, + get_sessions(User, Server)). + +get_sessions(User, Server) -> + LUser = jlib:nodeprep(User), + LServer = jlib:nameprep(Server), + Sessions = mnesia:dirty_index_read(session, {LUser, LServer}, #session.us), + true = is_list(Sessions), + Sessions. + +set_random_password(User, Server, Reason) -> + NewPass = build_random_password(Reason), + set_password_auth(User, Server, NewPass). + +build_random_password(Reason) -> + Date = jlib:timestamp_to_iso(calendar:universal_time()), + RandomString = randoms:get_string(), + "BANNED_ACCOUNT--" ++ Date ++ "--" ++ RandomString ++ "--" ++ Reason. + +set_password_auth(User, Server, Password) -> + ok = ejabberd_auth:set_password(User, Server, Password). + +prepare_reason([]) -> + "Kicked by administrator"; +prepare_reason([Reason]) -> + Reason; +prepare_reason(Reason) when is_list(Reason) -> + Reason; +prepare_reason(StringList) -> + string:join(StringList, "_"). + + +%%% +%%% Sessions +%%% + +num_resources(User, Host) -> + length(ejabberd_sm:get_user_resources(User, Host)). + +resource_num(User, Host, Num) -> + Resources = ejabberd_sm:get_user_resources(User, Host), + case (0 + lists:nth(Num, Resources); + false -> + lists:flatten(io_lib:format("Error: Wrong resource number: ~p", [Num])) + end. + +kick_session(User, Server, Resource, ReasonText) -> + kick_this_session(User, Server, Resource, prepare_reason(ReasonText)), + ok. + +kick_this_session(User, Server, Resource, Reason) -> + ejabberd_router:route( + jlib:make_jid("", "", ""), + jlib:make_jid(User, Server, Resource), + {xmlelement, "broadcast", [], [{exit, Reason}]}). + + +status_num(Host, Status) -> + length(get_status_list(Host, Status)). +status_num(Status) -> + status_num("all", Status). +status_list(Host, Status) -> + Res = get_status_list(Host, Status), + [{U, S, R, P, St} || {U, S, R, P, St} <- Res]. +status_list(Status) -> + status_list("all", Status). + + +get_status_list(Host, Status_required) -> + %% Get list of all logged users + Sessions = ejabberd_sm:dirty_get_my_sessions_list(), + %% Reformat the list + Sessions2 = [ {Session#session.usr, Session#session.sid, Session#session.priority} || Session <- Sessions], + Fhost = case Host of + "all" -> + %% All hosts are requested, so dont filter at all + fun(_, _) -> true end; + _ -> + %% Filter the list, only Host is interesting + fun(A, B) -> A == B end + end, + Sessions3 = [ {Pid, Server, Priority} || {{_User, Server, _Resource}, {_, Pid}, Priority} <- Sessions2, apply(Fhost, [Server, Host])], + %% For each Pid, get its presence + Sessions4 = [ {ejabberd_c2s:get_presence(Pid), Server, Priority} || {Pid, Server, Priority} <- Sessions3], + %% Filter by status + Fstatus = case Status_required of + "all" -> + fun(_, _) -> true end; + _ -> + fun(A, B) -> A == B end + end, + [{User, Server, Resource, Priority, stringize(Status_text)} + || {{User, Resource, Status, Status_text}, Server, Priority} <- Sessions4, + apply(Fstatus, [Status, Status_required])]. + +connected_users_info() -> + USRIs = dirty_get_sessions_list2(), + CurrentSec = calendar:datetime_to_gregorian_seconds({date(), time()}), + lists:map( + fun([{U, S, R}, {Now, Pid}, Priority, Info]) -> + Conn = proplists:get_value(conn, Info), + {Ip, Port} = proplists:get_value(ip, Info), + IPS = inet_parse:ntoa(Ip), + NodeS = atom_to_list(node(Pid)), + Uptime = CurrentSec - calendar:datetime_to_gregorian_seconds( + calendar:now_to_local_time(Now)), + {[U, $@, S, $/, R], atom_to_list(Conn), IPS, Port, Priority, NodeS, Uptime} + end, + USRIs). + +connected_users_vhost(Host) -> + USRs = ejabberd_sm:get_vh_session_list(Host), + [ [U, $@, S, $/, R] || {U, S, R} <- USRs]. + +%% Code copied from ejabberd_sm.erl and customized +dirty_get_sessions_list2() -> + mnesia:dirty_select( + session, + [{#session{usr = '$1', sid = '$2', priority = '$3', info = '$4', _ = '_'}, + [], + [['$1', '$2', '$3', '$4']]}]). + +%% Make string more print-friendly +stringize(String) -> + %% Replace newline characters with other code + ejabberd_regexp:greplace(String, "\n", "\\n"). + +set_presence(User, Host, Resource, Type, Show, Status, Priority) -> + Pid = ejabberd_sm:get_session_pid(User, Host, Resource), + USR = User ++ "@" ++ Host ++ "/" ++ Resource, + US = User ++ "@" ++ Host, + Message = {route_xmlstreamelement, + {xmlelement, "presence", + [{"from", USR}, {"to", US}, {"type", Type}], + [{xmlelement, "show", [], [{xmlcdata, Show}]}, + {xmlelement, "status", [], [{xmlcdata, Status}]}, + {xmlelement, "priority", [], [{xmlcdata, Priority}]}]}}, + Pid ! Message. + +user_sessions_info(User, Host) -> + CurrentSec = calendar:datetime_to_gregorian_seconds({date(), time()}), + US = {User, Host}, + Sessions = case catch mnesia:dirty_index_read(session, US, #session.us) of + {'EXIT', _Reason} -> + []; + Ss -> + Ss + end, + lists:map( + fun(Session) -> + {_U, _S, Resource} = Session#session.usr, + {Now, Pid} = Session#session.sid, + {_U, _Resource, Status, StatusText} = ejabberd_c2s:get_presence(Pid), + Info = Session#session.info, + Priority = Session#session.priority, + Conn = proplists:get_value(conn, Info), + {Ip, Port} = proplists:get_value(ip, Info), + IPS = inet_parse:ntoa(Ip), + NodeS = atom_to_list(node(Pid)), + Uptime = CurrentSec - calendar:datetime_to_gregorian_seconds( + calendar:now_to_local_time(Now)), + {atom_to_list(Conn), IPS, Port, Priority, NodeS, Uptime, Status, Resource, StatusText} + end, + Sessions). + + +%%% +%%% Vcard +%%% + +set_nickname(User, Host, Nickname) -> + R = mod_vcard:process_sm_iq( + {jid, User, Host, "", User, Host, ""}, + {jid, User, Host, "", User, Host, ""}, + {iq, "", set, "", "en", + {xmlelement, "vCard", + [{"xmlns", "vcard-temp"}], [ + {xmlelement, "NICKNAME", [], [{xmlcdata, Nickname}]} + ] + }}), + case R of + {iq, [], result, [], _L, []} -> + ok; + _ -> + error + end. + +get_vcard(User, Host, Name) -> + [Res | _] = get_vcard_content(User, Host, [Name]), + Res. + +get_vcard(User, Host, Name, Subname) -> + [Res | _] = get_vcard_content(User, Host, [Name, Subname]), + Res. + +get_vcard_multi(User, Host, Name, Subname) -> + get_vcard_content(User, Host, [Name, Subname]). + +set_vcard(User, Host, Name, SomeContent) -> + set_vcard_content(User, Host, [Name], SomeContent). + +set_vcard(User, Host, Name, Subname, SomeContent) -> + set_vcard_content(User, Host, [Name, Subname], SomeContent). + + +%% +%% Internal vcard + +get_module_resource(Server) -> + case gen_mod:get_module_opt(Server, ?MODULE, module_resource, none) of + none -> atom_to_list(?MODULE); + R when is_list(R) -> R + end. + +get_vcard_content(User, Server, Data) -> + [{_, Module, Function, _Opts}] = ets:lookup(sm_iqtable, {?NS_VCARD, Server}), + JID = jlib:make_jid(User, Server, get_module_resource(Server)), + IQ = #iq{type = get, xmlns = ?NS_VCARD}, + IQr = Module:Function(JID, JID, IQ), + case IQr#iq.sub_el of + [A1] -> + case get_vcard(Data, A1) of + [] -> throw(error_no_value_found_in_vcard); + ElemList -> [xml:get_tag_cdata(Elem) || Elem <- ElemList] + end; + [] -> + throw(error_no_vcard_found) + end. + +get_vcard([Data1, Data2], A1) -> + case get_subtag(A1, Data1) of + false -> false; + A2List -> lists:flatten([get_vcard([Data2], A2) || A2 <- A2List]) + end; + +get_vcard([Data], A1) -> + get_subtag(A1, Data). + +get_subtag(Xmlelement, Name) -> + case code:ensure_loaded(exmpp_xml) of + {error, _} -> + [get_subtag_xml(Xmlelement, Name)]; + {module, exmpp_xml} -> + get_subtag_exmpp(Xmlelement, Name) + end. + +get_subtag_xml(Xmlelement, Name) -> + xml:get_subtag(Xmlelement, Name). + +get_subtag_exmpp(Xmlelement, Name) -> + Xmlel = exmpp_xml:xmlelement_to_xmlel(Xmlelement), + XmlelList = exmpp_xml:get_elements(Xmlel, Name), + [exmpp_xml:xmlel_to_xmlelement(Xmlel2) || Xmlel2 <- XmlelList]. + +set_vcard_content(User, Server, Data, SomeContent) -> + ContentList = case SomeContent of + [Char | _] when not is_list(Char) -> [SomeContent]; + [Char | _] when is_list(Char) -> SomeContent + end, + [{_, Module, Function, _Opts}] = ets:lookup(sm_iqtable, {?NS_VCARD, Server}), + JID = jlib:make_jid(User, Server, get_module_resource(Server)), + IQ = #iq{type = get, xmlns = ?NS_VCARD}, + IQr = Module:Function(JID, JID, IQ), + + %% Get old vcard + A4 = case IQr#iq.sub_el of + [A1] -> + {_, _, _, A2} = A1, + update_vcard_els(Data, ContentList, A2); + [] -> + update_vcard_els(Data, ContentList, []) + end, + + %% Build new vcard + SubEl = {xmlelement, "vCard", [{"xmlns","vcard-temp"}], A4}, + IQ2 = #iq{type=set, sub_el = SubEl}, + + Module:Function(JID, JID, IQ2), + ok. + +update_vcard_els(Data, ContentList, Els1) -> + Els2 = lists:keysort(2, Els1), + [Data1 | Data2] = Data, + NewEls = case Data2 of + [] -> + [{xmlelement, Data1, [], [{xmlcdata,Content}]} || Content <- ContentList]; + [D2] -> + OldEl = case lists:keysearch(Data1, 2, Els2) of + {value, A} -> A; + false -> {xmlelement, Data1, [], []} + end, + {xmlelement, _, _, ContentOld1} = OldEl, + Content2 = [{xmlelement, D2, [], [{xmlcdata,Content}]} || Content <- ContentList], + ContentOld2 = [A || {_, X, _, _} = A <- ContentOld1, X/=D2], + ContentOld3 = lists:keysort(2, ContentOld2), + ContentNew = lists:keymerge(2, Content2, ContentOld3), + [{xmlelement, Data1, [], ContentNew}] + end, + Els3 = lists:keydelete(Data1, 2, Els2), + lists:keymerge(2, NewEls, Els3). + + +%%% +%%% Roster +%%% + +add_rosteritem(LocalUser, LocalServer, User, Server, Nick, Group, Subs) -> + case add_rosteritem(LocalUser, LocalServer, User, Server, Nick, Group, list_to_atom(Subs), []) of + {atomic, ok} -> + push_roster_item(LocalUser, LocalServer, User, Server, {add, Nick, Subs, Group}), + ok; + _ -> + error + end. + +add_rosteritem(LU, LS, User, Server, Nick, Group, Subscription, Xattrs) -> + subscribe(LU, LS, User, Server, Nick, Group, Subscription, Xattrs). + +subscribe(LU, LS, User, Server, Nick, Group, Subscription, _Xattrs) -> + SubscriptionS = case is_atom(Subscription) of + true -> atom_to_list(Subscription); + false -> Subscription + end, + ItemEl = build_roster_item(User, Server, {add, Nick, SubscriptionS, Group}), + {ok, M} = loaded_module(LS,[mod_roster_odbc,mod_roster]), + M:set_items( + LU, LS, + {xmlelement,"query", + [{"xmlns","jabber:iq:roster"}], + [ItemEl]}). + +delete_rosteritem(LocalUser, LocalServer, User, Server) -> + case unsubscribe(LocalUser, LocalServer, User, Server) of + {atomic, ok} -> + push_roster_item(LocalUser, LocalServer, User, Server, remove), + ok; + _ -> + error + end. + +unsubscribe(LU, LS, User, Server) -> + ItemEl = build_roster_item(User, Server, remove), + {ok, M} = loaded_module(LS,[mod_roster_odbc,mod_roster]), + M:set_items( + LU, LS, + {xmlelement,"query", + [{"xmlns","jabber:iq:roster"}], + [ItemEl]}). + +loaded_module(Domain,Options) -> + LoadedModules = gen_mod:loaded_modules(Domain), + case lists:filter(fun(Module) -> + lists:member(Module, LoadedModules) + end, Options) of + [M|_] -> {ok, M}; + [] -> {error,not_found} + end. + +%% ----------------------------- +%% Get Roster +%% ----------------------------- + +get_roster(User, Server) -> + Items = ejabberd_hooks:run_fold(roster_get, Server, [], [{User, Server}]), + make_roster_xmlrpc(Items). + +%% Note: if a contact is in several groups, the contact is returned +%% several times, each one in a different group. +make_roster_xmlrpc(Roster) -> + lists:foldl( + fun(Item, Res) -> + JIDS = jlib:jid_to_string(Item#roster.jid), + Nick = Item#roster.name, + Subs = atom_to_list(Item#roster.subscription), + Ask = atom_to_list(Item#roster.ask), + Groups = case Item#roster.groups of + [] -> [""]; + Gs -> Gs + end, + ItemsX = [{JIDS, Nick, Subs, Ask, Group} + || Group <- Groups], + ItemsX ++ Res + end, + [], + Roster). + + +%%----------------------------- +%% Push Roster from file +%%----------------------------- + +push_roster(File, User, Server) -> + {ok, [Roster]} = file:consult(File), + subscribe_roster({User, Server, "", User}, Roster). + +push_roster_all(File) -> + {ok, [Roster]} = file:consult(File), + subscribe_all(Roster). + +subscribe_all(Roster) -> + subscribe_all(Roster, Roster). +subscribe_all([], _) -> + ok; +subscribe_all([User1 | Users], Roster) -> + subscribe_roster(User1, Roster), + subscribe_all(Users, Roster). + +subscribe_roster(_, []) -> + ok; +%% Do not subscribe a user to itself +subscribe_roster({Name, Server, Group, Nick}, [{Name, Server, _, _} | Roster]) -> + subscribe_roster({Name, Server, Group, Nick}, Roster); +%% Subscribe Name2 to Name1 +subscribe_roster({Name1, Server1, Group1, Nick1}, [{Name2, Server2, Group2, Nick2} | Roster]) -> + subscribe(Name1, Server1, Name2, Server2, Nick2, Group2, both, []), + subscribe_roster({Name1, Server1, Group1, Nick1}, Roster). + +push_alltoall(S, G) -> + Users = ejabberd_auth:get_vh_registered_users(S), + Users2 = build_list_users(G, Users, []), + subscribe_all(Users2), + ok. + +build_list_users(_Group, [], Res) -> + Res; +build_list_users(Group, [{User, Server}|Users], Res) -> + build_list_users(Group, Users, [{User, Server, Group, User}|Res]). + +%% @spec(LU, LS, U, S, Action) -> ok +%% Action = {add, Nick, Subs, Group} | remove +%% @doc Push to the roster of account LU@LS the contact U@S. +%% The specific action to perform is defined in Action. +push_roster_item(LU, LS, U, S, Action) -> + lists:foreach(fun(R) -> + push_roster_item(LU, LS, R, U, S, Action) + end, ejabberd_sm:get_user_resources(LU, LS)). + +push_roster_item(LU, LS, R, U, S, Action) -> + LJID = jlib:make_jid(LU, LS, R), + BroadcastEl = build_broadcast(U, S, Action), + ejabberd_router:route(LJID, LJID, BroadcastEl), + Item = build_roster_item(U, S, Action), + ResIQ = build_iq_roster_push(Item), + ejabberd_router:route(LJID, LJID, ResIQ). + +build_roster_item(U, S, {add, Nick, Subs, Group}) -> + {xmlelement, "item", + [{"jid", jlib:jid_to_string(jlib:make_jid(U, S, ""))}, + {"name", Nick}, + {"subscription", Subs}], + [{xmlelement, "group", [], [{xmlcdata, Group}]}] + }; +build_roster_item(U, S, remove) -> + {xmlelement, "item", + [{"jid", jlib:jid_to_string(jlib:make_jid(U, S, ""))}, + {"subscription", "remove"}], + [] + }. + +build_iq_roster_push(Item) -> + {xmlelement, "iq", + [{"type", "set"}, {"id", "push"}], + [{xmlelement, "query", + [{"xmlns", ?NS_ROSTER}], + [Item] + } + ] + }. + +build_broadcast(U, S, {add, _Nick, Subs, _Group}) -> + build_broadcast(U, S, list_to_atom(Subs)); +build_broadcast(U, S, remove) -> + build_broadcast(U, S, none); +%% @spec (U::string(), S::string(), Subs::atom()) -> any() +%% Subs = both | from | to | none +build_broadcast(U, S, SubsAtom) when is_atom(SubsAtom) -> + {xmlelement, "broadcast", [], + [{item, {U, S, ""}, SubsAtom}] + }. + +%%% +%%% Last Activity +%%% + +set_last(User, Server, Timestamp, Status) -> + Mod = get_lastactivity_module(Server), + Mod:store_last_info(User, Server, Timestamp, Status). + +%%% +%%% Private Storage +%%% + +%% Example usage: +%% $ ejabberdctl private_set badlop localhost "\Cluth\" +%% $ ejabberdctl private_get badlop localhost aa bb +%% Cluth + +private_get(Username, Host, Element, Ns) -> + From = jlib:make_jid(Username, Host, ""), + To = jlib:make_jid(Username, Host, ""), + IQ = {iq, "", get, ?NS_PRIVATE, "", + {xmlelement,"query", + [{"xmlns",?NS_PRIVATE}], + [{xmlelement, Element, [{"xmlns", Ns}], []}]}}, + ResIq = mod_private:process_sm_iq(From, To, IQ), + [{xmlelement,"query", + [{"xmlns","jabber:iq:private"}], + [SubEl]}] = ResIq#iq.sub_el, + xml:element_to_string(SubEl). + +private_set(Username, Host, ElementString) -> + case xml_stream:parse_element(ElementString) of + {error, Error} -> + io:format("Error found parsing the element:~n ~p~nError: ~p~n", + [ElementString, Error]), + error; + Xml -> + private_set2(Username, Host, Xml) + end. + +private_set2(Username, Host, Xml) -> + From = jlib:make_jid(Username, Host, ""), + To = jlib:make_jid(Username, Host, ""), + IQ = {iq, "", set, ?NS_PRIVATE, "", + {xmlelement,"query", + [{"xmlns",?NS_PRIVATE}], + [Xml]}}, + mod_private:process_sm_iq(From, To, IQ), + ok. + +%%% +%%% Shared Roster Groups +%%% + +srg_create(Group, Host, Name, Description, Display) -> + DisplayList = case Display of + [] -> []; + _ -> ejabberd_regexp:split(Display, "\\\\n") + end, + Opts = [{name, Name}, + {displayed_groups, DisplayList}, + {description, Description}], + {atomic, ok} = mod_shared_roster:create_group(Host, Group, Opts), + ok. + +srg_delete(Group, Host) -> + {atomic, ok} = mod_shared_roster:delete_group(Host, Group), + ok. + +srg_list(Host) -> + lists:sort(mod_shared_roster:list_groups(Host)). + +srg_get_info(Group, Host) -> + Opts = mod_shared_roster:get_group_opts(Host,Group), + [{io_lib:format("~p", [Title]), + io_lib:format("~p", [Value])} || {Title, Value} <- Opts]. + +srg_get_members(Group, Host) -> + Members = mod_shared_roster:get_group_explicit_users(Host,Group), + [jlib:jid_to_string(jlib:make_jid(MUser, MServer, "")) + || {MUser, MServer} <- Members]. + +srg_user_add(User, Host, Group, GroupHost) -> + {atomic, ok} = mod_shared_roster:add_user_to_group(GroupHost, {User, Host}, Group), + ok. + +srg_user_del(User, Host, Group, GroupHost) -> + {atomic, ok} = mod_shared_roster:remove_user_from_group(GroupHost, {User, Host}, Group), + ok. + + +%%% +%%% Stanza +%%% + +%% @doc Send a chat message to a Jabber account. +%% @spec (From::string(), To::string(), Body::string()) -> ok +send_message_chat(From, To, Body) -> + Packet = build_packet(message_chat, [Body]), + send_packet_all_resources(From, To, Packet). + +%% @doc Send a headline message to a Jabber account. +%% @spec (From::string(), To::string(), Subject::string(), Body::string()) -> ok +send_message_headline(From, To, Subject, Body) -> + Packet = build_packet(message_headline, [Subject, Body]), + send_packet_all_resources(From, To, Packet). + +%% @doc Send a packet to a Jabber account. +%% If a resource was specified in the JID, +%% the packet is sent only to that specific resource. +%% If no resource was specified in the JID, +%% and the user is remote or local but offline, +%% the packet is sent to the bare JID. +%% If the user is local and is online in several resources, +%% the packet is sent to all its resources. +send_packet_all_resources(FromJIDString, ToJIDString, Packet) -> + FromJID = jlib:string_to_jid(FromJIDString), + ToJID = jlib:string_to_jid(ToJIDString), + ToUser = ToJID#jid.user, + ToServer = ToJID#jid.server, + case ToJID#jid.resource of + "" -> + send_packet_all_resources(FromJID, ToUser, ToServer, Packet); + Res -> + send_packet_all_resources(FromJID, ToUser, ToServer, Res, Packet) + end. + +send_packet_all_resources(FromJID, ToUser, ToServer, Packet) -> + case ejabberd_sm:get_user_resources(ToUser, ToServer) of + [] -> + send_packet_all_resources(FromJID, ToUser, ToServer, "", Packet); + ToResources -> + lists:foreach( + fun(ToResource) -> + send_packet_all_resources(FromJID, ToUser, ToServer, + ToResource, Packet) + end, + ToResources) + end. + +send_packet_all_resources(FromJID, ToU, ToS, ToR, Packet) -> + ToJID = jlib:make_jid(ToU, ToS, ToR), + ejabberd_router:route(FromJID, ToJID, Packet). + + +build_packet(message_chat, [Body]) -> + {xmlelement, "message", + [{"type", "chat"}, {"id", randoms:get_string()}], + [{xmlelement, "body", [], [{xmlcdata, Body}]}] + }; +build_packet(message_headline, [Subject, Body]) -> + {xmlelement, "message", + [{"type", "headline"}, {"id", randoms:get_string()}], + [{xmlelement, "subject", [], [{xmlcdata, Subject}]}, + {xmlelement, "body", [], [{xmlcdata, Body}]} + ] + }. + +send_stanza_c2s(Username, Host, Resource, Stanza) -> + C2sPid = ejabberd_sm:get_session_pid(Username, Host, Resource), + XmlEl = xml_stream:parse_element(Stanza), + p1_fsm:send_event(C2sPid, {xmlstreamelement, XmlEl}). + +privacy_set(Username, Host, QueryS) -> + From = jlib:string_to_jid(Username ++ "@" ++ Host), + To = jlib:string_to_jid(Host), + QueryEl = xml_stream:parse_element(QueryS), + StanzaEl = {xmlelement, "iq", [{"type", "set"}], [QueryEl]}, + IQ = jlib:iq_query_info(StanzaEl), + ejabberd_hooks:run_fold( + privacy_iq_set, + Host, + {error, ?ERR_FEATURE_NOT_IMPLEMENTED}, + [From, To, IQ] + ), + ok. + +%%% +%%% Stats +%%% + +stats(Name) -> + case Name of + "uptimeseconds" -> trunc(element(1, erlang:statistics(wall_clock))/1000); + "registeredusers" -> length(ejabberd_auth:dirty_get_registered_users()); + "onlineusersnode" -> length(ejabberd_sm:dirty_get_my_sessions_list()); + "onlineusers" -> length(ejabberd_sm:dirty_get_sessions_list()) + end. + +stats(Name, Host) -> + case Name of + "registeredusers" -> length(ejabberd_auth:get_vh_registered_users(Host)); + "onlineusers" -> length(ejabberd_sm:get_vh_session_list(Host)) + end. + + + +%%----------------------------- +%% Purge roster items +%%----------------------------- + +process_rosteritems(ActionS, SubsS, AsksS, UsersS, ContactsS) -> + Action = case ActionS of + "list" -> list; + "delete" -> delete + end, + + Subs = lists:foldl( + fun(any, _) -> [none, from, to, both]; + (Sub, Subs) -> [Sub | Subs] + end, + [], + [list_to_atom(S) || S <- string:tokens(SubsS, ":")] + ), + + Asks = lists:foldl( + fun(any, _) -> [none, out, in]; + (Ask, Asks) -> [Ask | Asks] + end, + [], + [list_to_atom(S) || S <- string:tokens(AsksS, ":")] + ), + + Users = lists:foldl( + fun("any", _) -> ["*", "*@*"]; + (U, Us) -> [U | Us] + end, + [], + [S || S <- string:tokens(UsersS, ":")] + ), + + Contacts = lists:foldl( + fun("any", _) -> ["*", "*@*"]; + (U, Us) -> [U | Us] + end, + [], + [S || S <- string:tokens(ContactsS, ":")] + ), + + case rosteritem_purge({Action, Subs, Asks, Users, Contacts}) of + {atomic, Res} -> + Res; + {error, Reason} -> + io:format("Error purging rosteritems: ~p~n", [Reason]), + error; + {badrpc, Reason} -> + io:format("BadRPC purging rosteritems: ~p~n", [Reason]), + error + end. + +%% @spec ({Action::atom(), Subs::[atom()], Asks::[atom()], User::string(), Contact::string()}) -> {atomic, ok} +rosteritem_purge(Options) -> + Num_rosteritems = mnesia:table_info(roster, size), + io:format("There are ~p roster items in total.~n", [Num_rosteritems]), + Key = mnesia:dirty_first(roster), + Res = rip(Key, Options, {0, Num_rosteritems, 0, 0}, []), + {atomic, Res}. + +rip('$end_of_table', _Options, Counters, Res) -> + print_progress_line(Counters), + Res; +rip(Key, Options, {Pr, NT, NV, ND}, Res) -> + Key_next = mnesia:dirty_next(roster, Key), + {Action, _, _, _, _} = Options, + {ND2, Res2} = case decide_rip(Key, Options) of + true -> + Jids = apply_action(Action, Key), + {ND+1, [Jids | Res]}; + false -> + {ND, Res} + end, + NV2 = NV+1, + Pr2 = print_progress_line({Pr, NT, NV2, ND2}), + rip(Key_next, Options, {Pr2, NT, NV2, ND2}, Res2). + +apply_action(list, Key) -> + {User, Server, JID} = Key, + {RUser, RServer, _} = JID, + Jid1string = User ++ "@" ++ Server, + Jid2string = RUser ++ "@" ++ RServer, + io:format("Matches: ~s ~s~n", [Jid1string, Jid2string]), + {Jid1string, Jid2string}; +apply_action(delete, Key) -> + R = apply_action(list, Key), + mnesia:dirty_delete(roster, Key), + R. + +print_progress_line({Pr, NT, NV, ND}) -> + Pr2 = trunc((NV/NT)*100), + case Pr == Pr2 of + true -> + ok; + false -> + io:format("Progress ~p% - visited ~p - deleted ~p~n", [Pr2, NV, ND]) + end, + Pr2. + +decide_rip(Key, {_Action, Subs, Asks, User, Contact}) -> + case catch mnesia:dirty_read(roster, Key) of + [RI] -> + lists:member(RI#roster.subscription, Subs) + andalso lists:member(RI#roster.ask, Asks) + andalso decide_rip_jid(RI#roster.us, User) + andalso decide_rip_jid(RI#roster.jid, Contact); + _ -> + false + end. + +%% Returns true if the server of the JID is included in the servers +decide_rip_jid({UName, UServer, _UResource}, Match_list) -> + decide_rip_jid({UName, UServer}, Match_list); +decide_rip_jid({UName, UServer}, Match_list) -> + lists:any( + fun(Match_string) -> + MJID = jlib:string_to_jid(Match_string), + MName = MJID#jid.luser, + MServer = MJID#jid.lserver, + Is_server = is_glob_match(UServer, MServer), + case MName of + [] when UName == [] -> + Is_server; + [] -> + false; + _ -> + Is_server + andalso is_glob_match(UName, MName) + end + end, + Match_list). + +%% Copied from ejabberd-2.0.0/src/acl.erl +is_regexp_match(String, RegExp) -> + case ejabberd_regexp:run(String, RegExp) of + nomatch -> + false; + match -> + true; + {error, ErrDesc} -> + io:format( + "Wrong regexp ~p in ACL: ~p", + [RegExp, ErrDesc]), + false + end. +is_glob_match(String, [$! | Glob]) -> + not is_regexp_match(String, ejabberd_regexp:sh_to_awk(Glob)); +is_glob_match(String, Glob) -> + is_regexp_match(String, ejabberd_regexp:sh_to_awk(Glob)). diff --git a/mod_admin_extra/src/mod_ecomm_test.erl b/mod_admin_extra/src/mod_ecomm_test.erl new file mode 100644 index 0000000..53b98c5 --- /dev/null +++ b/mod_admin_extra/src/mod_ecomm_test.erl @@ -0,0 +1,427 @@ +%%%------------------------------------------------------------------- +%%% File : mod_ecomm_test.erl +%%% Author : Badlop +%%% Purpose : Simple commands for testing +%%% Created : 10 Aug 2008 by Badlop +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2008 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., 59 Temple Place, Suite 330, Boston, MA +%%% 02111-1307 USA +%%% +%%%------------------------------------------------------------------- + +-module(mod_ecomm_test). +-author('badlop@process-one.net'). + +-behaviour(gen_mod). + +-export([start/2, stop/1, + %% Take: test arguments + take_integer/1, + take_string/1, + take_integer_string/2, + take_tuple_2integer/1, + take_tuple_2string/1, + take_list_integer/1, + take_list_string/1, + %% Echo: test arguments and result + echo_integer/1, + echo_string/1, + echo_integer_string/2, + echo_list_integer/1, + echo_list_string/1, + echo_integer_list_string/2, + echo_isatils/4, + %% Tell: test result + tell_atom/1, + tell_rescode/1, + tell_restuple/1, + tell_tuple_3integer/0, + tell_tuple_3string/0, + tell_tuple_3atom/0, + tell_tuple_3list/0, + tell_list_3integer/0, + tell_list_3string/0, + tell_list_3atom/0, + tell_list_3tuple/0, + %% Realistic + this_crashes/1, + this_wrong_args/1, + this_wrong_return/0, + pow/2, seq/2, substrs/1, splitjid/1, splitjids/1 + ]). + +-include("ejabberd.hrl"). +-include("ejabberd_commands.hrl"). +-include("jlib.hrl"). + +start(_Host, _Opts) -> + ejabberd_commands:register_commands(commands()). + +stop(_Host) -> + ejabberd_commands:unregister_commands(commands()). + +%%% +%%% ejabberd commands +%%% + +commands() -> + [ + + #ejabberd_commands{name = take_integer, tags = [test], + desc = "Take Integer in args, give Integer zero", + module = ?MODULE, function = take_integer, + args = [{thisinteger, integer}], + result = {zero, integer}}, + + #ejabberd_commands{name = take_string, tags = [test], + desc = "Take String, give Integer zero", + module = ?MODULE, function = take_string, + args = [{thisstring, string}], + result = {zero, integer}}, + + #ejabberd_commands{name = take_integer_string, tags = [test], + desc = "Take integer and string, give Integer zero", + module = ?MODULE, function = take_integer_string, + args = [{thisinteger, integer}, {thisstring, string}], + result = {zero, integer}}, + + %% Not supported by ejabberd_ctl + #ejabberd_commands{name = take_tuple_2integer, tags = [test], + desc = "Take Tuple of two integers, give Integer zero", + module = ?MODULE, function = take_tuple_2integer, + args = [{thistuple, {tuple, [{thisinteger1, integer}, {thisinteger2, integer}]}}], + result = {zero, integer}}, + %% Not supported by ejabberd_ctl + #ejabberd_commands{name = take_tuple_2string, tags = [test], + desc = "Take Tuple of two strings, give Integer zero", + module = ?MODULE, function = take_tuple_2string, + args = [{thistuple, {tuple, [{thisstring1, string}, {thisstring2, string}]}}], + result = {zero, integer}}, + %% Not supported by ejabberd_ctl + #ejabberd_commands{name = take_list_integer, tags = [test], + desc = "Take List of integers, give Integer zero", + module = ?MODULE, function = take_list_integer, + args = [{thislist, {list, {thisinteger, integer}}}], + result = {zero, integer}}, + %% Not supported by ejabberd_ctl + #ejabberd_commands{name = take_list_string, tags = [test], + desc = "Take List of strings, give Integer zero", + module = ?MODULE, function = take_list_string, + args = [{thislist, {list, {thisstring, string}}}], + result = {zero, integer}}, + + #ejabberd_commands{name = echo_integer, tags = [test], + desc = "Echo Integer", + module = ?MODULE, function = echo_integer, + args = [{thisinteger, integer}], + result = {thatinteger, integer}}, + #ejabberd_commands{name = echo_string, tags = [test], + desc = "Echo String", + module = ?MODULE, function = echo_string, + args = [{thisstring, string}], + result = {thatstring, string}}, + #ejabberd_commands{name = echo_integer_string, tags = [test], + desc = "Echo integer and string, in result as a tuple", + module = ?MODULE, function = echo_integer_string, + args = [{thisinteger, integer}, {thisstring, string}], + result = {thistuple, {tuple, [{thisinteger, integer}, {thisstring, string}]}}}, + %% Not supported by ejabberd_ctl + #ejabberd_commands{name = echo_list_integer, tags = [test], + desc = "Echo List of integers", + module = ?MODULE, function = echo_list_integer, + args = [{thislist, {list, {thisinteger, integer}}}], + result = {thatlist, {list, {thatinteger, integer}}}}, + %% Not supported by ejabberd_ctl + #ejabberd_commands{name = echo_list_string, tags = [test], + desc = "Echo List of strings", + module = ?MODULE, function = echo_list_string, + args = [{thislist, {list, {thisstring, string}}}], + result = {thatlist, {list, {thatstring, string}}}}, + %% Not supported by ejabberd_ctl + #ejabberd_commands{name = echo_integer_list_string, tags = [test], + desc = "Echo an integer and List of strings", + module = ?MODULE, function = echo_integer_list_string, + args = [{thisinteger, integer}, {thislist, {list, {thisstring, string}}}], + result = {thistuple, {tuple, [{thatinteger, integer}, {thatlist, {list, {thatstring, string}}}]}}}, + %% Not supported by ejabberd_ctl + #ejabberd_commands{name = echo_isatils, tags = [test], + desc = "Echo integer, string, atom and tuple of integer and list of strings", + module = ?MODULE, function = echo_isatils, + args = [{thisinteger, integer}, + {thisstring, string}, + {thisatom, atom}, + {thistuple, {tuple, [ + {listlen, integer}, + {thislist, {list, {contentstring, string}}} + ]}} + ], + result = {results, {tuple, [{thatinteger, integer}, + {thatstring, string}, + {thatatom, atom}, + {thattuple, {tuple, [ + {listlen, integer}, + {thatlist, {list, {contentstring, string}}} + ]}} + ]}}}, + + #ejabberd_commands{name = tell_atom, tags = [test], + desc = "Tell Atom, give Integer zero", + module = ?MODULE, function = tell_atom, + args = [{thisinteger, integer}], + result = {thisatom, atom}}, + #ejabberd_commands{name = tell_rescode, tags = [test], + desc = "Tell rescode", + module = ?MODULE, function = tell_rescode, + args = [{thisinteger, integer}], + result = {res, rescode}}, + #ejabberd_commands{name = tell_restuple, tags = [test], + desc = "Tell restuple", + module = ?MODULE, function = tell_restuple, + args = [{thisinteger, integer}], + result = {res, restuple}}, + #ejabberd_commands{name = tell_tuple_3integer, tags = [test], + desc = "Tell a tuple with 3 integers", + module = ?MODULE, function = tell_tuple_3integer, + args = [], + result = {thattuple, {tuple, [{first, integer}, + {second, integer}, + {third, integer}]}}}, + #ejabberd_commands{name = tell_tuple_3string, tags = [test], + desc = "Tell a tuple with 3 strings", + module = ?MODULE, function = tell_tuple_3string, + args = [], + result = {thattuple, {tuple, [{first, string}, + {second, string}, + {third, string}]}}}, + #ejabberd_commands{name = tell_tuple_3atom, tags = [test], + desc = "Tell a tuple with 3 atoms", + module = ?MODULE, function = tell_tuple_3atom, + args = [], + result = {thattuple, {tuple, [{first, atom}, + {second, atom}, + {third, atom}]}}}, + #ejabberd_commands{name = tell_tuple_3list, tags = [test], + desc = "Tell a tuple with 3 lists", + module = ?MODULE, function = tell_tuple_3list, + args = [], + result = {thattuple, {tuple, + [{first, {list, + {thisinteger, integer}}}, + {second, {list, + {thisstring, string}}}, + {third, {list, + {thisatom, atom}}}]}}}, + + #ejabberd_commands{name = tell_list_3integer, tags = [test], + desc = "Tell a list with 3 integers", + module = ?MODULE, function = tell_list_3integer, + args = [], + result = {thatlist, {list, {thisinteger, integer}}}}, + #ejabberd_commands{name = tell_list_3string, tags = [test], + desc = "Tell a list with 3 strings", + module = ?MODULE, function = tell_list_3string, + args = [], + result = {thatlist, {list, {thisstring, string}}}}, + #ejabberd_commands{name = tell_list_3atom, tags = [test], + desc = "Tell a list with 3 atoms", + module = ?MODULE, function = tell_list_3atom, + args = [], + result = {thatlist, {list, {thisatom, atom}}}}, + #ejabberd_commands{name = tell_list_3tuple, tags = [test], + desc = "Tell a list with 3 tuples", + module = ?MODULE, function = tell_list_3tuple, + args = [], + result = {thatlist, {list, {thistuple, + {tuple, + [{thisinteger, integer}, + {thistring, string}, + {thisatom, atom}]}}}}}, + + #ejabberd_commands{name = this_crashes, tags = [test], + desc = "This command crashes: test+5", + module = ?MODULE, function = this_crashes, + args = [{aninteger, integer}], + result = {result, integer}}, + #ejabberd_commands{name = this_wrong_args, tags = [test], + desc = "This problematic command defines 2 arguments but function expects 1", + module = ?MODULE, function = this_wrong_args, + args = [{a, integer}, {b, integer}], + result = {result, integer}}, + #ejabberd_commands{name = this_wrong_return, tags = [test], + desc = "This problematic command doesn't give a proper return", + module = ?MODULE, function = this_wrong_return, + args = [], + result = {result, integer}}, + + #ejabberd_commands{name = pow, tags = [test], + desc = "Return the power of base for exponent", + longdesc = "This is an example command. The formula is:\n" + " power = base ^ exponent", + module = ?MODULE, function = pow, + args = [{base, integer}, {exponent, integer}], + result = {power, integer}}, + + #ejabberd_commands{name = seq, tags = [test], + desc = "Return list of integers between two integers", + module = ?MODULE, function = seq, + args = [{from, integer}, {to, integer}], + result = {sequence, {list, {intermediate, integer}}}}, + + #ejabberd_commands{name = substrs, tags = [test], + desc = "Return list of substrings of length increasing", + module = ?MODULE, function = substrs, + args = [{word, string}], + result = {substrings, {list, {miniword, string}}}}, + + #ejabberd_commands{name = splitjid, tags = [test], + desc = "Split JID in parts: user, server, resource", + module = ?MODULE, function = splitjid, + args = [{jid, string}], + result = {jidparts, {tuple, [{user, string}, + {server, string}, + {resource, string}]}}}, + + %% Not supported by ejabberd_ctl because uses 'list' in the arguments + #ejabberd_commands{name = splitjids, tags = [test], + desc = "Split JIDs in parts: user, server, resource", + module = ?MODULE, function = splitjids, + args = [{jids, {list, {jid, string}}}], + result = {jidsparts, + {list, {jidparts, + {tuple, [{user, string}, + {server, string}, + {resource, string}]}}}}} + + ]. + +%%% +%%% Take +%%% + +take_integer(A) when is_integer(A) -> 0. +take_string(A) when is_list(A) -> 0. +take_integer_string(A, B) + when is_integer(A) and is_list(B) -> + 0. +take_tuple_2integer({A, B}) + when is_integer(A) and is_integer(B) -> + 0. +take_tuple_2string({A, B}) + when is_list(A) and is_list(B) -> + 0. +take_list_integer(L) + when is_list(L) -> + true = lists:all(fun(A) -> is_integer(A) end, L), + 0. +take_list_string(L) + when is_list(L) -> + true = lists:all(fun(A) -> is_list(A) end, L), + 0. + +%%% +%%% Echo +%%% + +echo_integer(A) when is_integer(A) -> A. +echo_string(A) when is_list(A) -> A. +echo_integer_string(A, B) when is_integer(A) and is_list(B) -> {A, B}. +echo_list_integer(L) + when is_list(L) -> + true = lists:all(fun(A) -> is_integer(A) end, L), + L. +echo_list_string(L) + when is_list(L) -> + true = lists:all(fun(A) -> is_list(A) end, L), + L. +echo_integer_list_string(I, L) + when is_integer(I) and is_list(L) -> + true = lists:all(fun(A) -> is_list(A) end, L), + {I, L}. +echo_isatils(I, S, A, {II, L}) + when is_integer(I) and is_list(S) and is_atom(A) and is_integer(II) and is_list(L) -> + true = lists:all(fun(SS) -> is_list(SS) end, L), + {I, S, A, {I, L}}. + + +%%% +%%% Tell +%%% + +tell_atom(0) -> zero; +tell_atom(1) -> one; +tell_atom(A) when is_integer(A) -> greater_than_one. + +tell_rescode(0) -> ok; +tell_rescode(1) -> true; +tell_rescode(2) -> error; +tell_rescode(3) -> false; +tell_rescode(4) -> whatever. + +tell_restuple(0) -> {ok, "All OK"}; +tell_restuple(1) -> {true, "Successful result"}; +tell_restuple(2) -> {error, "This is an error message"}. + +tell_tuple_3integer() -> {123, 456, 789}. +tell_tuple_3string() -> {"Tell", "me", "a tuple please"}. +tell_tuple_3atom() -> {ok, works, perfectly}. +tell_tuple_3list() -> {[1, 23, 456], ["Tell", "me"], [all, is, ok]}. + +tell_list_3integer() -> [123, 456, 789]. +tell_list_3string() -> ["Tell", "me", "a tuple please"]. +tell_list_3atom() -> [ok, works, perfectly]. +tell_list_3tuple() -> + [{123, "abcdefghijkl", first}, + {593, "this string", morning}, + {999, "Sleeping dog", not_seen}]. + + +%%% +%%% Realistic +%%% + +%% This function will crash for sure +this_crashes(Integer) -> + test + Integer. + +this_wrong_args(Integer) -> + Integer + 1. + +this_wrong_return() -> + "this is a string". + +pow(Base, Exponent) -> + PowFloat = math:pow(Base, Exponent), + round(PowFloat). + +seq(From, To) -> + lists:seq(From, To). + +%% For "stick" returns: s st sti stic stick +substrs(Word) -> + Lengths = lists:seq(1, string:len(Word)), + [string:substr(Word, 1, Length) || Length <- Lengths]. + +splitjid(String) -> + JID = jlib:string_to_jid(String), + {JID#jid.user, + JID#jid.server, + JID#jid.resource}. + +splitjids(Strings) -> + [splitjid(String) || String <- Strings]. + diff --git a/mod_archive/Emakefile b/mod_archive/Emakefile new file mode 100644 index 0000000..ea1cf1d --- /dev/null +++ b/mod_archive/Emakefile @@ -0,0 +1,5 @@ +{'../ejabberd-dev/src/gen_mod', [{outdir, "../ejabberd-dev/ebin"},{i,"../ejabberd-dev/include"}]}. +{'src/mod_archive', [{outdir, "ebin"},{i,"../ejabberd-dev/include"}]}. +{'src/mod_archive_sql', [{outdir, "ebin"},{i,"../ejabberd-dev/include"}]}. +{'src/mod_archive_odbc', [{outdir, "ebin"},{i,"../ejabberd-dev/include"}]}. +{'src/mod_archive_webview', [{outdir, "ebin"},{i,"../ejabberd-dev/include"}]}. diff --git a/mod_archive/LICENSE.txt b/mod_archive/LICENSE.txt new file mode 100644 index 0000000..30404ce --- /dev/null +++ b/mod_archive/LICENSE.txt @@ -0,0 +1 @@ +TODO \ No newline at end of file diff --git a/mod_archive/README.txt b/mod_archive/README.txt new file mode 100644 index 0000000..034aabc --- /dev/null +++ b/mod_archive/README.txt @@ -0,0 +1,73 @@ + + mod_archive - Message Archiving (XEP-0136) + +There are three different modules, the main difference between them is the storage method: + - mod_archive uses Mnesia + - mod_archive_sql uses PostgreSQL + - mod_archive_odbc uses MySQL or SQLite3 + +As of today (2008-03-15) mod_archive_odbc is the most complete and maintained. + +And another module is used to view archive onlines + - mod_archive_webview webviewer for mod_archive_odbc + + + MOD_ARCHIVE + =========== + +Author: Olivier Goffart + +This module does support almost all the XEP-0136 version 0.6 except otr (off-the-record). + +Features + - Automatic archiving + - User may enable/disable automatic archiving for one contact or globally + - Manual archiving + - Retrieve or remove archive + - XEP-0059 + +Not Supported + - Off the record + - Groupchats message + +Options + - save_default: true or false: whether or not messages should be saved by default + - session_duration: The time in seconds before a session timeout (for a collection). The default value is 30 minutes. + +Support of XEP-136 on Jabber clients + - JWChat: Implemented, but does not work, since it implements an old version. An update on JWChat is expected in the mid-term. + - Kopete: Planned for the mid-term. + + + + MOD_ARCHIVE_SQL + =============== + +Author: Alexey Shchepin +Based in mod_archive, author: Olivier Goffart + + + + MOD_ARCHIVE_ODBC + ================ + +Author: Alexander Tsvyashchenko +Based in mod_archive, author: Olivier Goffart +Based in mod_archive_sql, author: Alexey Shchepin + +For a detailed documentation about this module, please refer to +http://www.ndl.kiev.ua/typo/articles/2007/11/14/mod_archive_odbc-release + + + MOD_ARCHIVE_WEBVIEW + =================== +Author: Olivier Goffart + +This module woks with the database of mod_archive_odbc + +Edit ejabberd.cfg and add the HTTP and module definitions: {["archive"], mod_archive_webview} to the list of request handler +{listen, [ {5280, ejabberd_http, [ %... + {request_handlers, [{["archive"], mod_archive_webview} + ]} ]} ]}. + +then go on http://your.server.com:5280/archive diff --git a/mod_archive/build.bat b/mod_archive/build.bat new file mode 100644 index 0000000..fff9b3b --- /dev/null +++ b/mod_archive/build.bat @@ -0,0 +1 @@ +erl -pa ../../ejabberd-dev/trunk/ebin -pa ebin -make diff --git a/mod_archive/build.sh b/mod_archive/build.sh new file mode 100755 index 0000000..f0a0cfa --- /dev/null +++ b/mod_archive/build.sh @@ -0,0 +1,2 @@ +#!/bin/sh +erl -pa ../ejabberd-dev/ebin -pz ebin -make diff --git a/mod_archive/src/mod_archive.erl b/mod_archive/src/mod_archive.erl new file mode 100644 index 0000000..f4c8dcf --- /dev/null +++ b/mod_archive/src/mod_archive.erl @@ -0,0 +1,1076 @@ +%%%---------------------------------------------------------------------- +%%% File : mod_archive.erl +%%% Author : Olivier Goffart +%%% Purpose : Message Archiving (JEP-0136) +%%% Created : 19 Aug 2006 +%%%---------------------------------------------------------------------- + +%% Version 0.0.1 2006-08-19 +%% Version 0.0.2 2006-08-21 +%% Version 0.0.3 2006-08-22 +%% Version 0.0.4 2006-09-10 (RSM JEP-0059 v0.13 JEP-0136 v0.6 with RSM) + + +%% Options: +%% save_default -> true | false if messages are stored by default or not +%% session_duration -> time in secondes before the timeout of a session + + +-module(mod_archive). +-author('ogoffart@kde.org'). + +-behaviour(gen_server). +-behaviour(gen_mod). + +-export([start_link/2, start/2, stop/1, + remove_user/2, send_packet/3, receive_packet/4, + process_iq/3, process_local_iq/3, + get_disco_features/5]). + +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). + +-include("ejabberd.hrl"). +-include("jlib.hrl"). + +-record(state, {host, storages, save_default, session_duration}). + +-define(PROCNAME, ejabberd_mod_archive). +-define(NS_ARCHIVE, + "http://www.xmpp.org/extensions/xep-0136.html#ns"). +-define(NS_ARCHIVE_MANAGE, + "http://www.xmpp.org/extensions/xep-0136.html#ns-manage"). +-define(NS_ARCHIVE_PREF, + "http://www.xmpp.org/extensions/xep-0136.html#ns-pref"). +-define(NS_ARCHIVE_MANUAL, + "http://www.xmpp.org/extensions/xep-0136.html#ns-manual"). +-define(INFINITY, calendar:datetime_to_gregorian_seconds({{2038,1,19},{0,0,0}})). + +-define(MYDEBUG(Format, Args), + io:format("D(~p:~p:~p) : " ++ Format ++ "~n", + [calendar:local_time(), ?MODULE, ?LINE] ++ Args)). + + +%NOTE i was not sure what format to adopt for archive_option. otr_list is unused +-record(archive_options, + {us, + default = unset, + save_list = [], + nosave_list = [], + otr_list = []}). + +%-record(archive_options, {usj, us, jid, type, value}). + +-record(archive_message, + {usjs, + us, + jid, + start, + message_list = [], + subject = ""}). + +-record(msg, {direction, secs, body}). + +%%==================================================================== +%% API +%%==================================================================== +%%-------------------------------------------------------------------- +%% Function: start_link() -> {ok,Pid} | ignore | {error,Error} +%% Description: Starts the server +%%-------------------------------------------------------------------- +start_link(Host, Opts) -> + Proc = gen_mod:get_module_proc(Host, ?PROCNAME), + gen_server:start_link({local, Proc}, ?MODULE, [Host, Opts], []). + +start(Host, Opts) -> + Proc = gen_mod:get_module_proc(Host, ?PROCNAME), + ChildSpec = + {Proc, + {?MODULE, start_link, [Host, Opts]}, + temporary, + 1000, + worker, + [?MODULE]}, + supervisor:start_child(ejabberd_sup, ChildSpec). + +stop(Host) -> + Proc = gen_mod:get_module_proc(Host, ?PROCNAME), + gen_server:call(Proc, stop), + supervisor:delete_child(ejabberd_sup, Proc). + +%%==================================================================== +%% gen_server callbacks +%%==================================================================== + +%%-------------------------------------------------------------------- +%% Function: init(Args) -> {ok, State} | +%% {ok, State, Timeout} | +%% ignore | +%% {stop, Reason} +%% Description: Initiates the server +%%-------------------------------------------------------------------- +init([Host, Opts]) -> + IQDisc = gen_mod:get_opt(iqdisc, Opts, one_queue), + SaveDefault = gen_mod:get_opt(save_default, Opts, false), + SessionDuration = gen_mod:get_opt(session_duration, Opts, 1300), + mnesia:create_table(archive_options, + [{disc_copies, [node()]}, + {attributes, record_info(fields, archive_options)}]), + mnesia:create_table(archive_message, + [{disc_copies, [node()]}, + {attributes, record_info(fields, archive_message)}]), +% mnesia:add_table_index(archive_options, us), + mnesia:add_table_index(archive_message, us), + ejabberd_hooks:add(remove_user, Host, ?MODULE, remove_user, 50), + gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_ARCHIVE, ?MODULE, process_iq, IQDisc), + gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_ARCHIVE, ?MODULE, process_local_iq, IQDisc), + ejabberd_hooks:add(user_send_packet, Host, ?MODULE, send_packet, 90), + ejabberd_hooks:add(user_receive_packet, Host, ?MODULE, receive_packet, 90), + ejabberd_hooks:add(disco_local_features, Host, ?MODULE, get_disco_features, 99), + {ok, #state{host = Host, + storages = [], + save_default = SaveDefault, + session_duration = SessionDuration}}. + +%%-------------------------------------------------------------------- +%% Function: %% handle_call(Request, From, State) -> {reply, Reply, State} | +%% {reply, Reply, State, Timeout} | +%% {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, Reply, State} | +%% {stop, Reason, State} +%% Description: Handling call messages +%%-------------------------------------------------------------------- +handle_call(get_save_default, _From, State) -> + {reply, State#state.save_default, State}; +handle_call(stop, _From, State) -> + {stop, normal, ok, State}. + +%%-------------------------------------------------------------------- +%% Function: handle_cast(Msg, State) -> {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, State} +%% Description: Handling cast messages +%%-------------------------------------------------------------------- +handle_cast({addlog, Direction, LUser, LServer, JID, P}, State) -> + Storages = State#state.storages, + NewStorages = + case should_store_jid(LUser, LServer, JID, + State#state.save_default) of + false -> + Storages; + true -> + case parse_message(P) of + ignore -> + Storages; + Body -> + catch do_log(Storages, LUser, LServer, JID, + Direction, Body, + State#state.session_duration) + end + end, + {noreply, State#state{storages = NewStorages}}; +handle_cast(_Msg, State) -> + {noreply, State}. + +%%-------------------------------------------------------------------- +%% Function: handle_info(Info, State) -> {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, State} +%% Description: Handling all non call/cast messages +%%-------------------------------------------------------------------- +handle_info(_Info, State) -> + {noreply, State}. + +%%-------------------------------------------------------------------- +%% Function: terminate(Reason, State) -> void() +%% Description: This function is called by a gen_server when it is about to +%% terminate. It should be the opposite of Module:init/1 and do any necessary +%% cleaning up. When it returns, the gen_server terminates with Reason. +%% The return value is ignored. +%%-------------------------------------------------------------------- +terminate(_Reason, State) -> + Host = State#state.host, + ejabberd_hooks:delete(remove_user, Host, ?MODULE, remove_user, 50), + gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_ARCHIVE), + gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_ARCHIVE), + ejabberd_hooks:delete(user_send_packet, Host, ?MODULE, send_packet, 90), + ejabberd_hooks:delete(user_receive_packet, Host, ?MODULE, receive_packet, 90), + ejabberd_hooks:delete(disco_local_features, Host, ?MODULE, get_disco_features, 99), + ok. + +%%-------------------------------------------------------------------- +%% Func: code_change(OldVsn, State, Extra) -> {ok, NewState} +%% Description: Convert process state when code is changed +%%-------------------------------------------------------------------- +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%-------------------------------------------------------------------- +%%% Internal functions +%%-------------------------------------------------------------------- + +%% Workaround the fact that if the client send +%% it end up like +process_iq(From, To, IQ) -> + #iq{sub_el = SubEl} = IQ, + #jid{lserver = LServer, luser = LUser} = To, + #jid{luser = FromUser} = From, + case {LUser, LServer, lists:member(LServer, ?MYHOSTS)} of + {FromUser, _, true} -> + process_local_iq(From, To, IQ); + {"", _, true} -> + process_local_iq(From, To, IQ); + {"", "", _} -> + process_local_iq(From, To, IQ); + _ -> + IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]} + end. + +process_local_iq(From, To, #iq{sub_el = SubEl} = IQ) -> + case lists:member(From#jid.lserver, ?MYHOSTS) of + false -> + IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]}; + true -> + {xmlelement, Name, _Attrs, _Els} = SubEl, + case Name of + "pref" -> process_local_iq_save(From, To, IQ); + "auto" -> process_local_iq_auto(From, To, IQ); + %%"otr" -> process_local_iq_otr(From, To, IQ); + "list" -> process_local_iq_list(From, To, IQ); + "retrieve" -> process_local_iq_retrieve(From, To, IQ); + "save" -> process_local_iq_store(From, To, IQ); + "remove" -> process_local_iq_remove(From, To, IQ); + _ -> IQ#iq{type = error, + sub_el = [SubEl, ?ERR_FEATURE_NOT_IMPLEMENTED]} + end + end. + + +remove_user(User, Server) -> + LUser = jlib:nodeprep(User), + LServer = jlib:nameprep(Server), + US = {LUser, LServer}, + F = fun() -> + lists:foreach( + fun(R) -> + mnesia:delete_object(R) + end, + mnesia:index_read(archive_message, US, #archive_message.us)), + mnesia:delete({archive_options, US}) + end, + mnesia:transaction(F). + +get_disco_features(Acc, _From, _To, "", _Lang) -> + Features = + case Acc of + {result, I} -> I; + _ -> [] + end, + {result, Features ++ [?NS_ARCHIVE_MANAGE, + ?NS_ARCHIVE_PREF, + ?NS_ARCHIVE_MANUAL]}; + +get_disco_features(Acc, _From, _To, _Node, _Lang) -> + Acc. + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% 3 Automated archiving +%% + +send_packet(From, To, P) -> + Host = From#jid.lserver, + Proc = gen_mod:get_module_proc(Host, ?PROCNAME), + gen_server:cast(Proc, {addlog, to, From#jid.luser, Host, To, P}). + +receive_packet(_JID, From, To, P) -> + Host = To#jid.lserver, + Proc = gen_mod:get_module_proc(Host, ?PROCNAME), + gen_server:cast(Proc, {addlog, from, To#jid.luser, Host, From, P}). + + +% parce a message and return the body string if sicessfull, return ignore if the message should not be stored +parse_message({xmlelement, "message", _, _} = Packet) -> + case xml:get_subtag(Packet, "body") of + false -> ignore; + Body_xml -> + case xml:get_tag_attr_s("type", Packet) of + "groupchat" -> ignore; + _ -> xml:get_tag_cdata(Body_xml) + end + end; +parse_message(_) -> ignore. + +% archive the message Body return the list of new Storages +% Storages: a list of open storages key (usjs) +% LUser, LServer : the local user's information +% Jid : the contact's jid +% Body : the message body +do_log(Storages, LUser, LServer, Jid, Direction, Body, Session_Duration) -> + NStorages = smart_find_storage(LUser, LServer, Jid, Storages, get_timestamp() + Session_Duration), + [{Tm, {_, _, _, Start} = Key} | _] = NStorages, + Message = #msg{direction=Direction, secs=(Tm-Start), body = Body}, + mnesia:transaction(fun() -> + NE = case mnesia:read({archive_message, Key}) of + [] -> + #archive_message{usjs= Key, + us = {LUser, LServer}, + jid = jlib:jid_tolower(jlib:jid_remove_resource(Jid)), + start = Tm, + message_list = [Message]}; + [E] -> E#archive_message{message_list=lists:append(E#archive_message.message_list, [Message])} + end, + mnesia:write(NE) + end), + NStorages. + +%find a storage for Jid and move it on the begin on the storage list, if none are found, a new storage is created, old storages element are removed +smart_find_storage(LUser, LServer, Jid, [C | Tail], TimeStampLimit) -> + NGid=jlib:jid_remove_resource(jlib:jid_tolower(Jid)), + case C of + {_, {LUser, LServer, NGid, _} = St} -> + [{get_timestamp(), St} | Tail]; + {Tm, _} -> + if Tm > TimeStampLimit -> + smart_find_storage(LUser, LServer, Jid, [], TimeStampLimit); + true -> + [UJ | NT] = smart_find_storage(LUser, LServer, Jid, Tail, TimeStampLimit), + [UJ | [C | NT]] + end + end; + + +smart_find_storage(LUser, LServer, Jid, [], _Limit) -> + Tm = get_timestamp(), + [{Tm, {LUser, LServer, jlib:jid_tolower(jlib:jid_remove_resource(Jid)), Tm}}]. + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% 3.1 Preferences +%% + +process_local_iq_save(From, _To, #iq{type = Type, sub_el = SubEl} = IQ) -> + Result = case Type of + set -> + {xmlelement, _Name, _Attrs, Els} = SubEl, + process_save_set(From#jid.luser, From#jid.lserver, Els); + get -> + process_save_get(From#jid.luser, From#jid.lserver) + end, + case Result of + {result, R} -> + IQ#iq{type = result, sub_el = [R]}; + ok -> + broadcast_iq(From, IQ#iq{type = set, sub_el=[SubEl]}), + IQ#iq{type = result, sub_el = []}; + {error, E} -> + IQ#iq{type = error, sub_el = [SubEl, E]}; + _ -> + IQ#iq{type = error, sub_el = [SubEl, ?ERR_INTERNAL_SERVER_ERROR]} + end. + + + + +% return {error, xmlelement} or {result, xmlelement} +process_save_get(LUser, LServer) -> + case catch mnesia:dirty_read(archive_options, {LUser, LServer}) of + {'EXIT', _Reason} -> + {error, ?ERR_INTERNAL_SERVER_ERROR}; + [] -> + {result, + {xmlelement, "pref", [{"xmlns", ?NS_ARCHIVE}], + default_element(LServer)}}; + [#archive_options{default = Default, + save_list = SaveList, + nosave_list = NoSaveList}] -> + LItems = lists:append( + lists:map(fun(J) -> + {xmlelement, "item", + [{"jid", jlib:jid_to_string(J)}, + {"save","body"}], + []} + end, SaveList), + lists:map(fun(J) -> + {xmlelement, "item", + [{"jid", jlib:jid_to_string(J)}, + {"save","false"}], + []} + end, NoSaveList)), + DItem = case Default of + true -> % TODO: + [{xmlelement, "default", [{"save", "body"}], []}]; + false -> + [{xmlelement, "default", [{"save", "false"}], []}]; + _ -> + default_element(LServer) + end, + {result, {xmlelement, "save", [{"xmlns", ?NS_ARCHIVE}], DItem ++ LItems}} + end. + +%return the element +default_element(Host) -> + Proc = gen_mod:get_module_proc(Host, ?PROCNAME), + AutoSave = gen_server:call(Proc, get_save_default), + SaveAttr = if + AutoSave -> "true"; + true -> "false" + end, + [{xmlelement, "default", [{"save", "false"}, {"otr", "forbid"}], []}, + {xmlelement, "auto", [{"save", SaveAttr}], []}]. + + +% return {error, xmlelement} or {result, xmlelement} or ok +process_save_set(LUser, LServer, Elms) -> + F = fun() -> + NE = case mnesia:read({archive_options, {LUser, LServer}}) of + [] -> + #archive_options{us = {LUser, LServer}}; + [E] -> + E + end, + SNE = transaction_parse_save_elem(NE, Elms), + case SNE of + {error, _} -> SNE; + _ -> mnesia:write(SNE) + end + end, + case mnesia:transaction(F) of + {atomic, {error, _} = Error} -> + Error; + {atomic, _} -> + ok; + _ -> + {error, ?ERR_INTERNAL_SERVER_ERROR} + end. + + +% transaction_parse_save_elem(archive_options, ListOfXmlElement) -> #archive_options +% parse the list of xml element, and modify the given archive_option +transaction_parse_save_elem(Options, [{xmlelement, "default", Attrs, _} | Tail]) -> + V = case xml:get_attr_s("save", Attrs) of + "true" -> true; + "false" -> false; + _ -> unset + end, + transaction_parse_save_elem(Options#archive_options{default = V}, Tail); + +transaction_parse_save_elem(Options, [{xmlelement, "item", Attrs, _} | Tail]) -> + case jlib:string_to_jid(xml:get_attr_s("jid", Attrs)) of + error -> {error, ?ERR_JID_MALFORMED}; + #jid{luser = LUser, lserver = LServer, lresource = LResource} -> + JID = {LUser, LServer, LResource}, + case xml:get_attr_s("save", Attrs) of + "body" -> + transaction_parse_save_elem( + Options#archive_options{ + save_list = [JID | lists:delete(JID, Options#archive_options.save_list)], + nosave_list = lists:delete(JID, Options#archive_options.nosave_list) + }, Tail); + "false" -> + transaction_parse_save_elem( + Options#archive_options{ + save_list = lists:delete(JID, Options#archive_options.save_list), + nosave_list = [JID | lists:delete(JID, Options#archive_options.nosave_list)] + }, Tail); + _ -> + transaction_parse_save_elem( + Options#archive_options{ + save_list = lists:delete(JID, Options#archive_options.save_list), + nosave_list = lists:delete(JID, Options#archive_options.nosave_list) + }, Tail) + end + end; + +transaction_parse_save_elem(Options, []) -> Options; +transaction_parse_save_elem(Options, [_ | Tail]) -> + transaction_parse_save_elem(Options, Tail). + + +broadcast_iq(#jid{luser = User, lserver = Server}, IQ) -> + Fun = fun(Resource) -> + ejabberd_router:route( + jlib:make_jid("", Server, ""), + jlib:make_jid(User, Server, Resource), + jlib:iq_to_xml(IQ#iq{id="push"})) + end, + lists:foreach(Fun, ejabberd_sm:get_user_resources(User,Server)). + + + +process_local_iq_auto(From, _To, #iq{type = Type, sub_el = SubEl} = IQ) -> + Result = + case Type of + set -> + {xmlelement, _Name, Attrs, _Els} = SubEl, + Auto = case xml:get_attr_s("save", Attrs) of + "true" -> true; + "false" -> false; + _ -> unset + end, + case Auto of + unset -> + {error, ?ERR_BAD_REQUEST}; + _ -> + LUser = From#jid.luser, + LServer = From#jid.lserver, + F = fun() -> + Opts = + case mnesia:read({archive_options, + {LUser, LServer}}) of + [] -> + #archive_options{us = {LUser, LServer}}; + [E] -> + E + end, + mnesia:write(Opts#archive_options{ + default = Auto}) + end, + mnesia:transaction(F), + {result, []} + end; + get -> + {error, ?ERR_BAD_REQUEST} + end, + case Result of + {result, R} -> + IQ#iq{type = result, sub_el = R}; + {error, E} -> + IQ#iq{type = error, sub_el = [SubEl, E]}; + _ -> + IQ#iq{type = error, sub_el = [SubEl, ?ERR_INTERNAL_SERVER_ERROR]} + end. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% 3.1 Off-the-Record Mode +%% + +%TODO + + + + + + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% Utility function + +% Return true if LUser@LServer should log message for the contact JID +should_store_jid(LUser, LServer, Jid, Service_Default) -> + case catch mnesia:dirty_read(archive_options, {LUser, LServer}) of + [#archive_options{default = Default, save_list = Save_list, nosave_list = Nosave_list}] -> + Jid_t = jlib:jid_tolower(Jid), + Jid_b = jlib:jid_remove_resource(Jid_t), + A = lists:member(Jid_t, Save_list), + B = lists:member(Jid_t, Nosave_list), + C = lists:member(Jid_b, Save_list), + D = lists:member(Jid_b, Nosave_list), + if A -> true; + B -> false; + C -> true; + D -> false; + Default == true -> true; + Default == false -> false; + true -> Service_Default + end; + _ -> Service_Default + end. + + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% 4. Manual Archiving +%% + + +process_local_iq_store(From, _To, #iq{type = Type, sub_el = SubEl} = IQ) -> + #jid{luser = LUser, lserver = LServer} = From, + case Type of + get -> + IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]}; + set -> + case parse_store_element (LUser, LServer, SubEl) of + {error, E} -> IQ#iq{type = error, sub_el = [SubEl, E]}; + Collection -> + case mnesia:transaction(fun() -> mnesia:write(Collection) end) of + {atomic, _} -> + IQ#iq{type = result, sub_el=[]}; + _ -> + IQ#iq{type = error, sub_el = [SubEl, ?ERR_INTERNAL_SERVER_ERROR]} + end + end + end. + +% return a #archive_message StoreElem is an xmlelement, or return {error, E} +parse_store_element(LUser, LServer, + {xmlelement, "save", _ChatAttrs, ChatSubEls}) -> + case xml:remove_cdata(ChatSubEls) of + [{xmlelement, "chat", Attrs, SubEls}] -> + case index_from_argument(LUser, LServer, Attrs) of + {error, E} -> {error, E}; + {LUser, LServer, Jid, Start} = Index -> + Messages = parse_store_element_sub(SubEls), + #archive_message{usjs = Index, + us = {LUser, LServer}, + jid = Jid, + start = Start, + subject = xml:get_attr_s("subject", Attrs), + message_list = Messages} + end; + _ -> + {error, ?ERR_BAD_REQUEST} + end. + +% TODO: utc attribute, catch list_to_integer errors + +parse_store_element_sub([{xmlelement, Dir, _, _} = E | Tail]) -> + [#msg{direction = list_to_atom(Dir), + secs = list_to_integer(xml:get_tag_attr_s("secs", E)), + body = xml:get_tag_cdata(xml:get_subtag(E,"body"))} | + parse_store_element_sub(Tail)]; + +parse_store_element_sub([]) -> []; +parse_store_element_sub([_ | Tail]) -> parse_store_element_sub(Tail). + + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% 5. Archive Management +%% + + +process_local_iq_list(From, _To, #iq{type = Type, sub_el = SubEl} = IQ) -> + #jid{luser = LUser, lserver = LServer} = From, + case Type of + set -> + IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]}; + get -> + {xmlelement, _, Attrs, SubEls} = SubEl, + RSM = parse_rsm(SubEls), + ?MYDEBUG("RSM Results: ~p ~n", [RSM]), + Result = case parse_root_argument(Attrs) of + {error, E} -> {error, E}; + {interval, Start, Stop} -> + get_list(LUser, LServer, Start, Stop, '_'); + {interval, Start, Stop, Jid} -> + get_list(LUser, LServer, Start, Stop, Jid); + {index, Jid, Start} -> + get_list(LUser, LServer, Start, infinity, Jid); + _ -> {error, ?ERR_BAD_REQUEST} + end, + case Result of + {ok, Items} -> + FunId = fun(El) -> ?MYDEBUG("FunId ~p ~n", [El]), integer_to_list(element(5,El)) end, + FunCompare = fun(Id, El) -> + Id2 = list_to_integer(FunId(El)), + Id1 = list_to_integer(Id), + if Id1 == Id2 -> equal; + Id1 > Id2 -> greater; + Id1 < Id2 -> smaller + end + end, + case catch execute_rsm(RSM, lists:keysort(5, Items), FunId,FunCompare) of + {error, R} -> + IQ#iq{type = error, sub_el = [SubEl, R]}; + {'EXIT', Errr} -> + ?MYDEBUG("INTERNAL ERROR ~p ~n", [Errr]), + IQ#iq{type = error, sub_el = [SubEl, ?ERR_INTERNAL_SERVER_ERROR]}; + {RSM_Elem, Items2} -> + Zero = calendar:datetime_to_gregorian_seconds({{1970, 1, 1}, {0, 0, 0}}), + Fun = fun(A) -> + Seconds= A#archive_message.start - Zero, + Start2 = jlib:now_to_utc_string({Seconds div 1000000, Seconds rem 1000000, 0}), + Args0 = [{"with", jlib:jid_to_string(A#archive_message.jid)}, {"start", Start2}], + Args = case A#archive_message.subject of + "" -> Args0; + Subject -> [{"subject",Subject} | Args0] + end, + {xmlelement, "chat", Args, []} + end, + IQ#iq{type = result, sub_el = [{xmlelement, "list", [{"xmlns", ?NS_ARCHIVE}], lists:append(lists:map(Fun, Items2),[RSM_Elem])}]} + end; + {error, R} -> + IQ#iq{type = error, sub_el = [SubEl, R]} + end + end. + + + + +process_local_iq_retrieve(From, _To, #iq{type = Type, sub_el = SubEl} = IQ) -> + #jid{luser = LUser, lserver = LServer} = From, + case Type of + set -> + IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]}; + get -> + {xmlelement, _, Attrs, SubEls} = SubEl, + RSM = parse_rsm(SubEls), + case index_from_argument(LUser, LServer, Attrs) of + {error, E} -> + IQ#iq{type = error, sub_el = [SubEl, E]}; + Index -> + case retrieve_collection(Index, RSM) of + {error, Err} -> + IQ#iq{type = error, sub_el = [SubEl, Err]}; + Store -> + IQ#iq{type = result, sub_el = [Store]} + end + end + end. + + +retrieve_collection(Index, RSM) -> + case get_collection(Index) of + {error, E} -> + {error, E}; + A -> + Zero = calendar:datetime_to_gregorian_seconds({{1970, 1, 1}, {0, 0, 0}}), + Seconds = A#archive_message.start-Zero, + Start2 = jlib:now_to_utc_string({Seconds div 1000000, Seconds rem 1000000, 0}), + Args0 = [{"xmlns", ?NS_ARCHIVE}, {"with", jlib:jid_to_string(A#archive_message.jid)}, {"start", Start2}], + Args = case A#archive_message.subject of + "" -> Args0; + Subject -> [{"subject", Subject} | Args0] + end, + case catch execute_rsm(RSM, A#archive_message.message_list, index, index) of + {error, R} -> + {error, R}; + {'EXIT', _} -> + {error, ?ERR_INTERNAL_SERVER_ERROR}; + {RSM_Elem, Items} -> + Format_Fun = + fun(Elem) -> + {xmlelement, atom_to_list(Elem#msg.direction), + [{"secs", integer_to_list(Elem#msg.secs)}], + [{xmlelement, "body", [], [{xmlcdata, Elem#msg.body}]}]} + end, + {xmlelement, "chat", Args, lists:append(lists:map(Format_Fun, Items), [RSM_Elem])} + end + end. + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% 5.3 Removing a Collection +%% + + +process_local_iq_remove(From, _To, #iq{type = Type, sub_el = SubEl} = IQ) -> + #jid{luser = LUser, lserver = LServer} = From, + case Type of + get -> + IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]}; + set -> + {xmlelement, _, Attrs, _} = SubEl, + Result = case parse_root_argument(Attrs) of + {error, E} -> IQ#iq{type = error, sub_el = [SubEl, E]}; + {interval, Start, Stop} -> process_remove_interval(LUser, LServer, Start, Stop, '_'); + {interval, Start, Stop, Jid} -> process_remove_interval(LUser, LServer, Start, Stop, Jid); + {index, Jid, Start} -> process_remove_index({LUser, LServer, Jid, Start}) + end, + case Result of + {error, Ee} -> IQ#iq{type = error, sub_el = [SubEl, Ee]}; + ok -> IQ#iq{type = result, sub_el=[]} + end + end. + +process_remove_index(Index) -> + case mnesia:transaction(fun() -> mnesia:delete({archive_message, Index}) end) of + {atomic, _} -> + ok; + {aborted, _} -> + {error, ?ERR_ITEM_NOT_FOUND} + end. + +process_remove_interval(LUser, LServer, Start, End, With) -> + Fun = fun() -> + Pat = #archive_message{usjs= '_', us = {LUser, LServer}, jid= With, + start='$1', message_list='_', subject = '_'}, + Guard = [{'>=', '$1', Start},{'<', '$1', End}], + + lists:foreach(fun(R) -> mnesia:delete_object(R) end, + mnesia:select(archive_message, [{Pat, Guard, ['$_']}])) + end, + + case mnesia:transaction(Fun) of + {atomic, _} -> + ok; + {aborted, _} -> + {error, ?ERR_INTERNAL_SERVER_ERROR} + end. + + + +% return {ok, [{#archive_message}]} or {error, xmlelement} +% With is a tuple Jid, or '_' +get_list(LUser, LServer, Start, End, With) -> + case mnesia:transaction(fun() -> + Pat = #archive_message{usjs= '_', us = {LUser, LServer}, jid= With, + start='$1', message_list='_', subject = '_'}, + Guard = [{'>=', '$1', Start},{'<', '$1', End}], + mnesia:select(archive_message, [{Pat, Guard, ['$_']}]) + end) of + {atomic, Result} -> + {ok, Result}; + {aborted, _} -> + {error, ?ERRT_INTERNAL_SERVER_ERROR("", "plop")} + end. + + +% Index is {LUser, LServer, With, Start} +get_collection(Index) -> + case catch mnesia:dirty_read(archive_message, Index) of + {'EXIT', _Reason} -> + {error, ?ERR_INTERNAL_SERVER_ERROR}; + [] -> + {error, ?ERR_ITEM_NOT_FOUND}; + [C] -> C + end. + + + + +%%%%%%%%%%%%%%%%%%%%%%%%%% +% Utility + + +% return either {error, Err} or {LUser, LServer, Jid, Start} +index_from_argument(LUser, LServer, Attrs) -> + case parse_root_argument(Attrs) of + {error, E} -> {error, E}; + {index, Jid, Start} -> {LUser, LServer, Jid, Start}; + _ -> {error, ?ERR_BAD_REQUEST} + end. + +%parse commons arguments of root elements +parse_root_argument(Attrs) -> + case parse_root_argument_aux(Attrs, {undefined, undefined, undefined}) of + {error, E} -> {error, E}; + {{ok,Jid}, {ok,Start}, undefined} -> {index, Jid, Start}; + {{ok,Jid}, undefined, undefined} -> {interval, 0, ?INFINITY, Jid}; + {{ok,Jid}, {ok,Start}, {ok,Stop}} -> {interval, Start, Stop, Jid}; + {undefined, {ok,Start}, {ok,Stop}} -> {interval, Start, Stop}; + {undefined, undefined, undefined} -> {interval, 0, ?INFINITY}; + _ -> {error, ?ERR_BAD_REQUEST} + end. + +parse_root_argument_aux([{"with", JidStr} | Tail], {_, AS, AE}) -> + case jlib:string_to_jid(JidStr) of + error -> {error, ?ERR_JID_MALFORMED}; + JidS -> + Jid = jlib:jid_tolower(JidS), + parse_root_argument_aux(Tail, {{ok, Jid}, AS, AE}) + end; +parse_root_argument_aux([{"start", Str} | Tail], {AW, _, AE}) -> + case jlib:datetime_string_to_timestamp(Str) of + undefined -> {error, ?ERR_BAD_REQUEST}; + No -> + Val = calendar:datetime_to_gregorian_seconds(calendar:now_to_datetime(No)), + parse_root_argument_aux(Tail, {AW, {ok, Val}, AE}) + end; +parse_root_argument_aux([{"end", Str} | Tail], {AW, AS, _}) -> + case jlib:datetime_string_to_timestamp(Str) of + undefined -> {error, ?ERR_BAD_REQUEST}; + No -> + Val = calendar:datetime_to_gregorian_seconds(calendar:now_to_datetime(No)), + parse_root_argument_aux(Tail, {AW, AS, {ok, Val}}) + end; +parse_root_argument_aux([_ | Tail], A) -> + parse_root_argument_aux(Tail, A); +parse_root_argument_aux([], A) -> A. + + + +get_timestamp() -> + calendar:datetime_to_gregorian_seconds(calendar:universal_time()). + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% Result Set Management (JEP-0059) +% +% USAGE: +% RSM = parce_rsm(Xmlelement) +% case execute_rsm(RSM, List, GetIdFun, IdCompareFun) of +% {error, E} -> ....; +% {RSMElement, Items} -> +% SubElements = lists:append(lists:map(Format_Fun, Items), [RSMElement]), +% ...; +% end + +-define(MY_NS_RSM, "http://jabber.org/protocol/rsm"). + + + + +% {Start, Max, Order} | error | none % | count + +parse_rsm([A | Tail]) -> + ?MYDEBUG("parse RSM elem ~p ", [A]), + case A of + {xmlelement, _, Attrs1, _} -> + case xml:get_attr_s("xmlns", Attrs1) of + ?MY_NS_RSM -> + parse_rsm(A); + HEPO -> + ?MYDEBUG("HEPO ~p ", [HEPO]), + parse_rsm(Tail) + end; + _ -> + parse_rsm(Tail) + end; +parse_rsm([]) -> + none; + +% parse_rsm({xmlelement, "count", _, _}) -> +% count; + +parse_rsm({xmlelement, "set", _, SubEls}) -> + parse_rsm_aux(SubEls, {0, infinity, normal}); + +parse_rsm(_) -> + error. + +parse_rsm_aux([{xmlelement, "max", _Attrs, Contents} | Tail], Acc) -> + case catch list_to_integer(xml:get_cdata(Contents)) of + P when is_integer(P) -> + case Acc of + {Start, infinity, Order} -> + parse_rsm_aux(Tail, {Start, P, Order}); + _ -> + error + end; + HEPO -> + ?MYDEBUG(" Not an INTEGER ~p ", [HEPO]), + error + end; + +parse_rsm_aux([{xmlelement, "index", _Attrs, Contents} | Tail], Acc) -> + case catch list_to_integer(xml:get_cdata(Contents)) of + P when is_integer(P) -> + case Acc of + {0, Max, normal} -> + parse_rsm_aux(Tail, {P, Max, normal}); + _ -> + error + end; + _ -> + error + end; + +parse_rsm_aux([{xmlelement, "after", _Attrs, Contents} | Tail], Acc) -> + case Acc of + {0, Max, normal} -> + parse_rsm_aux(Tail, {{id, xml:get_cdata(Contents)}, Max, normal}); + _ -> + error + end; + + +parse_rsm_aux([{xmlelement, "before", _Attrs, Contents} | Tail], Acc) -> + case Acc of + {0, Max, normal} -> + case xml:get_cdata(Contents) of + [] -> + parse_rsm_aux(Tail, {0, Max, reversed}); + CD -> + parse_rsm_aux(Tail, {{id, CD}, Max, reversed}) + end; + _ -> + error + end; + +parse_rsm_aux([_ | Tail], Acc) -> + parse_rsm_aux(Tail, Acc); +parse_rsm_aux([], Acc) -> + Acc. + +% RSM = {Start, Max, Order} +% GetId = fun(Elem) -> Id +% IdCompare = fun(Id, Elem) -> equal | greater | smaller +% +% -> {RSMElement, List} | {error, ErrElement} + +execute_rsm(RSM, List, GetId, IdCompare) -> + ?MYDEBUG("execute_rsm RSM ~p ~n", [RSM]), + case execute_rsm_aux(RSM, List, IdCompare, 0) of + none -> + {{xmlcdata, ""}, List}; +% count -> +% {{xmlelement, "count", [{"xmlns", ?MY_NS_RSM}], [{xmlcdata, integer_to_list(length(List))}]}, []}; + {error, E} -> + {error, E}; + {_, []} -> + {{xmlelement, "set", [{"xmlns", ?MY_NS_RSM}], [{xmlelement, "count", [], [{xmlcdata, integer_to_list(length(List))}]}]}, []}; + {Index, L} -> + case GetId of + index -> + {make_rsm(Index, integer_to_list(Index), integer_to_list(Index+length(L)),length(List)), L}; + _ -> + {make_rsm(Index, GetId(hd(L)), GetId(lists:last(L)),length(List)), L} + end + end. + + +% execute_rsm_aux(count, _List, _, _) -> +% count; + +execute_rsm_aux(none, _List, _, _) -> + none; + +execute_rsm_aux(error, _List, _, _) -> + {error, ?ERR_BAD_REQUEST}; + +execute_rsm_aux({S, M, reversed}, List, IdFun, Acc) -> + {NewFun,NewS} = case IdFun of + index -> + {index, + case S of + {id, IdentIndex} -> + integer_to_list(length(List) - list_to_integer(IdentIndex)); + _ -> S + end}; + _ -> + {fun(Index, Elem) -> + case IdFun(Index, Elem) of + equal -> equal; + greater -> smaller; + smaller -> greater; + O -> O + end + end, + S} + end, + {Index, L2} = execute_rsm_aux({NewS,M,normal}, lists:reverse(List), NewFun, 0), + {Acc + length(List) - Index - length(L2), lists:reverse(L2)}; + +execute_rsm_aux({{id,I}, M, normal}, List, index, Acc) -> + execute_rsm_aux({list_to_integer(I), M, normal}, List, index, Acc); + +execute_rsm_aux({{id,I}, M, normal} = RSM, [E | Tail], IdFun, Acc) -> + case IdFun(I, E) of + smaller -> + execute_rsm_aux(RSM, Tail, IdFun, Acc + 1); + _ -> + execute_rsm_aux({0, M, normal}, [E | Tail], IdFun, Acc) + end; + +execute_rsm_aux({{id,_}, _, normal}, [], _, Acc) -> + {Acc, []}; + +execute_rsm_aux({0, infinity, normal}, List, _, Acc) -> + {Acc, List}; + +execute_rsm_aux({_, 0, _}, _, _, Acc) -> + {Acc, []}; + +execute_rsm_aux({S, M, _}, List, _, Acc) when is_integer(S) and is_integer(M) -> + ?MYDEBUG("execute_rsm_aux sublist ~p ~n", [{S,M,List,Acc}]), + {Acc + S, lists:sublist(List, S+1,M)}. + +make_rsm(FirstIndex, FirstId, LastId, Count) -> + {xmlelement, "set", [{"xmlns", ?MY_NS_RSM}], [ + {xmlelement, "first", [{"index", integer_to_list(FirstIndex)}], [{xmlcdata, FirstId}]}, + {xmlelement, "last", [], [{xmlcdata, LastId}]}, + {xmlelement, "count", [], [{xmlcdata, integer_to_list(Count)}]}]}. diff --git a/mod_archive/src/mod_archive_odbc.erl b/mod_archive/src/mod_archive_odbc.erl new file mode 100644 index 0000000..d25ab19 --- /dev/null +++ b/mod_archive/src/mod_archive_odbc.erl @@ -0,0 +1,2570 @@ +%%%---------------------------------------------------------------------- +%%% File : mod_archive_odbc.erl +%%% Author : Olivier Goffart (origial mnesia version), +%%% Alexey Shchepin (PostgreSQL version), +%%% Alexander Tsvyashchenko (ODBC version) +%%% Purpose : Message Archiving using SQL DB (JEP-0136) +%%% Created : 19 Aug 2006 by Olivier Goffart +%%% Version : 1.0.1 +%%% Id : $Id$ +%%%---------------------------------------------------------------------- + +%%%---------------------------------------------------------------------- + +%% Options: +%% +%% default_auto_save -> true | false - is auto-save turned on by default or not; +%% if true, default 'save' attribute will be set to 'body'. +%% +%% enforce_default_auto_save -> true | false - is auto-save default mode +%% enforced or not; if true, requests to change it are discarded. +%% +%% default_expire -> default time in seconds before collections are wiped out - +%% or infinity atom. +%% +%% enforce_min_expire -> minimal time in seconds before collections are wiped out +%% that the user is allowed to set - or infinity atom. +%% +%% enforce_max_expire -> maximal time in seconds before collections are wiped out +%% that the user is allowed to set - or infinity atom. +%% +%% replication_expire -> time in seconds before 'removed' replication +%% information if wiped out or infinity atom to disable. +%% +%% session_duration -> time in secondes before the timeout of a session. +%% +%% wipeout_interval -> time in seconds between wipeout runs or infinity atom +%% to disable. +%% +%% +%% Please note that according to XEP-136 only the following auto_save +%% combinations are valid: +%% +%% 1) default_auto_save = true, enforce_default_auto_save = true +%% 2) default_auto_save = false, enforce_default_auto_save = false +%% +%% Implementation will happily work with any combination of these, +%% though - for example, for personal ejabberd server, until all clients +%% support XEP-136, combination default_auto_save = true, +%% enforce_default_auto_save = false is quite logical, while for some +%% public ejabberd server with lots of users and shortage of disk space +%% default_auto_save = false, enforce_default_auto_save = true might be +%% desirable. +%% +%% Default values: +%% - default_auto_save = false +%% - enforce_default_auto_save = false +%% - default_expire = infinity +%% - enforce_min_expire = 0 +%% - enforce_max_expire = infinity +%% - replication_expire = 31536000 (= 1 year) +%% - session_duration = 1800 +%% - wipeout_interval = 86400 (= 1 day) + + +-module(mod_archive_odbc). +-author('ogoffart@kde.org'). +-author('alexey@process-one.net'). +-author('ejabberd@ndl.kiev.ua'). + +-behaviour(gen_server). +-behaviour(gen_mod). + +-export([start_link/2, start/2, stop/1, + remove_user/2, + send_packet/3, + receive_packet/3, + receive_packet/4, + process_iq/3, process_local_iq/3, + get_disco_features/5]). + +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). + +-include("ejabberd.hrl"). +-include("jlib.hrl"). + +-record(state, {host, + sessions, + session_duration}). + +-define(PROCNAME, ejabberd_mod_archive_odbc). +-define(NS_ARCHIVE, + "http://www.xmpp.org/extensions/xep-0136.html#ns"). +-define(NS_ARCHIVE_AUTO, + "http://www.xmpp.org/extensions/xep-0136.html#ns-auto"). +-define(NS_ARCHIVE_MANAGE, + "http://www.xmpp.org/extensions/xep-0136.html#ns-manage"). +-define(NS_ARCHIVE_PREF, + "http://www.xmpp.org/extensions/xep-0136.html#ns-pref"). +-define(NS_ARCHIVE_MANUAL, + "http://www.xmpp.org/extensions/xep-0136.html#ns-manual"). +-define(INFINITY, calendar:datetime_to_gregorian_seconds({{2038,1,19},{0,0,0}})). + +%% Should be OK for most of modern DBs, I hope ... +-define(MAX_QUERY_LENGTH, 32768). + +-define(MYDEBUG(Format, Args), + io:format("D(~p:~p:~p) : " ++ Format ++ "~n", + [calendar:local_time(), ?MODULE, ?LINE] ++ Args)). + +-record(archive_jid_prefs, + {us, + jid, + save = undefined, + expire = undefined, + otr = undefined}). + +-record(archive_global_prefs, + {us, + save = undefined, + expire = undefined, + otr = undefined, + method_auto = undefined, + method_local = undefined, + method_manual = undefined, + auto_save = undefined}). + +-record(archive_collection, + {id, + us, + jid, + utc, + change_by, + change_utc, + deleted, + subject = "", + prev = [], + next = [], + thread = "", + crypt = false, + extra = ""}). + +-record(archive_message, + {id, + coll_id, + utc, + direction, + body, + name = ""}). + +%%==================================================================== +%% API +%%==================================================================== +%%-------------------------------------------------------------------- +%% Function: start_link() -> {ok,Pid} | ignore | {error,Error} +%% Description: Starts the server +%%-------------------------------------------------------------------- +start_link(Host, Opts) -> + Proc = gen_mod:get_module_proc(Host, ?PROCNAME), + gen_server:start_link({local, Proc}, ?MODULE, [Host, Opts], []). + +start(Host, Opts) -> + Proc = gen_mod:get_module_proc(Host, ?PROCNAME), + ChildSpec = + {Proc, + {?MODULE, start_link, [Host, Opts]}, + permanent, + 1000, + worker, + [?MODULE]}, + supervisor:start_child(ejabberd_sup, ChildSpec). +%% ejabberd-1.x compatibility code +%% NOTE: keepalive is not supported in ejabberd 1.x, so +%% you'll either need to turn off connections timeout in DB +%% configuration or invent smth else ... +%% ChildSpecODBC = +%% {gen_mod:get_module_proc(Host, ejabberd_odbc_sup), +%% {ejabberd_odbc_sup, start_link, [Host]}, +%% permanent, +%% infinity, +%% supervisor, +%% [ejabberd_odbc_sup]}, +%% supervisor:start_child(ejabberd_sup, ChildSpecODBC). +%% EOF ejabberd-1.x compatibility code + +stop(Host) -> + Proc = gen_mod:get_module_proc(Host, ?PROCNAME), + gen_server:call(Proc, stop), + supervisor:delete_child(ejabberd_sup, Proc). +%% ejabberd-1.x compatibility code +%% ProcODBC = gen_mod:get_module_proc(Host, ejabberd_odbc_sup), +%% gen_server:call(ProcODBC, stop), +%% supervisor:delete_child(ejabberd_sup, ProcODBC). +%% EOF ejabberd-1.x compatibility code + +%%==================================================================== +%% gen_server callbacks +%%==================================================================== + +%%-------------------------------------------------------------------- +%% Function: init(Args) -> {ok, State} | +%% {ok, State, Timeout} | +%% ignore | +%% {stop, Reason} +%% Description: Initiates the server +%%-------------------------------------------------------------------- +init([Host, Opts]) -> + IQDisc = gen_mod:get_opt(iqdisc, Opts, one_queue), + SessionDuration = gen_mod:get_opt(session_duration, Opts, 1800), + WipeOutInterval = gen_mod:get_opt(wipeout_interval, Opts, 86400), + ejabberd_hooks:add(remove_user, Host, ?MODULE, remove_user, 50), + gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_ARCHIVE, ?MODULE, process_iq, IQDisc), + gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_ARCHIVE, ?MODULE, process_local_iq, IQDisc), + ejabberd_hooks:add(user_send_packet, Host, ?MODULE, send_packet, 90), + ejabberd_hooks:add(user_receive_packet, Host, ?MODULE, receive_packet, 90), + ejabberd_hooks:add(offline_message_hook, Host, ?MODULE, receive_packet, 35), + ejabberd_hooks:add(disco_local_features, Host, ?MODULE, get_disco_features, 99), + timer:send_interval(1000 * SessionDuration div 2, clean_sessions), + case WipeOutInterval of + infinity -> []; + N when is_integer(N) -> timer:send_interval(1000 * N, wipeout_collections) + end, + {ok, #state{host = Host, + sessions = dict:new(), + session_duration = SessionDuration}}. + +%%-------------------------------------------------------------------- +%% Function: %% handle_call(Request, From, State) -> {reply, Reply, State} | +%% {reply, Reply, State, Timeout} | +%% {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, Reply, State} | +%% {stop, Reason, State} +%% Description: Handling call messages +%%-------------------------------------------------------------------- +handle_call(stop, _From, State) -> + {stop, normal, ok, State}. + +%%-------------------------------------------------------------------- +%% Function: handle_cast(Msg, State) -> {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, State} +%% Description: Handling cast messages +%%-------------------------------------------------------------------- +handle_cast({addlog, Type, Direction, LUser, LServer, LResource, JID, Thread, Subject, Nick, Body}, State) -> + Sessions = State#state.sessions, + NewSessions = + case should_store_jid({LUser, LServer}, JID) of + false -> + Sessions; + true when Type == "groupchat", Direction == to -> + Sessions; + true -> + do_log(Sessions, LUser, LServer, LResource, JID, + Type, Direction, Thread, Subject, Nick, Body, + State#state.session_duration) + end, + {noreply, State#state{sessions = NewSessions}}; +handle_cast(_Msg, State) -> + {noreply, State}. + +%%-------------------------------------------------------------------- +%% Function: handle_info(Info, State) -> {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, State} +%% Description: Handling all non call/cast messages +%%-------------------------------------------------------------------- +handle_info(clean_sessions, State) -> + Sessions = State#state.sessions, + Timeout = State#state.session_duration, + TS = get_timestamp(), + F = fun(_, Value)-> + dict:filter(fun(_, {_Start, Last, _, _}) -> + TS - Last =< Timeout + end, Value) + end, + FilteredSessions = dict:map(F, Sessions), + NewSessions = dict:filter(fun(_Key, Value) -> + dict:fetch_keys(Value) /= [] + end, FilteredSessions), + {noreply, State#state{sessions = NewSessions}}; + +handle_info(wipeout_collections, State) -> + expire_collections(State#state.host), + {noreply, State}; + +handle_info(_Info, State) -> + {noreply, State}. + +%%-------------------------------------------------------------------- +%% Function: terminate(Reason, State) -> void() +%% Description: This function is called by a gen_server when it is about to +%% terminate. It should be the opposite of Module:init/1 and do any necessary +%% cleaning up. When it returns, the gen_server terminates with Reason. +%% The return value is ignored. +%%-------------------------------------------------------------------- +terminate(_Reason, State) -> + Host = State#state.host, + ejabberd_hooks:delete(remove_user, Host, ?MODULE, remove_user, 50), + gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_ARCHIVE), + gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_ARCHIVE), + ejabberd_hooks:delete(user_send_packet, Host, ?MODULE, send_packet, 90), + ejabberd_hooks:delete(user_receive_packet, Host, ?MODULE, receive_packet, 90), + ejabberd_hooks:delete(offline_message_hook, Host, ?MODULE, receive_packet, 35), + ejabberd_hooks:delete(disco_local_features, Host, ?MODULE, get_disco_features, 99), + ok. + +%%-------------------------------------------------------------------- +%% Func: code_change(OldVsn, State, Extra) -> {ok, NewState} +%% Description: Convert process state when code is changed +%%-------------------------------------------------------------------- +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%-------------------------------------------------------------------- +%%% Internal functions +%%-------------------------------------------------------------------- + +%% Workaround the fact that if the client send +%% it end up like +process_iq(From, To, IQ) -> + #iq{sub_el = SubEl} = IQ, + #jid{lserver = LServer, luser = LUser} = To, + #jid{luser = FromUser} = From, + case {LUser, LServer, lists:member(LServer, ?MYHOSTS)} of + {FromUser, _, true} -> + process_local_iq(From, To, IQ); + {"", _, true} -> + process_local_iq(From, To, IQ); + {"", "", _} -> + process_local_iq(From, To, IQ); + _ -> + IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]} + end. + +process_local_iq(From, To, #iq{sub_el = SubEl} = IQ) -> + case lists:member(From#jid.lserver, ?MYHOSTS) of + false -> + IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]}; + true -> + {xmlelement, Name, _Attrs, _Els} = SubEl, + F = fun() -> + case Name of + "pref" -> process_local_iq_pref(From, To, IQ); + "auto" -> process_local_iq_auto(From, To, IQ); + "list" -> process_local_iq_list(From, To, IQ); + "retrieve" -> process_local_iq_retrieve(From, To, IQ); + "save" -> process_local_iq_save(From, To, IQ); + "remove" -> process_local_iq_remove(From, To, IQ); + "modified" -> process_local_iq_modified(From, To, IQ); + _ -> IQ#iq{type = error, + sub_el = [SubEl, ?ERR_FEATURE_NOT_IMPLEMENTED]} + end + end, + %% All IQ processing functions should return either {result, xmlelement} or + %% {error, xmlelement} - other returns mean smth is seriously wrong + %% with the code itself. + case catch F() of + {result, R} -> + IQ#iq{type = result, sub_el = R}; + {error, Err} -> + IQ#iq{type = error, + sub_el = [SubEl, Err]}; + {'EXIT', Ex} -> + ?ERROR_MSG("catched unhandled exception: ~p", [Ex]), + IQ#iq{type = error, + sub_el = [SubEl, ?ERR_INTERNAL_SERVER_ERROR]}; + Res -> + ?ERROR_MSG("unexpected result: ~p", [Res]), + IQ#iq{type = error, + sub_el = [SubEl, ?ERR_INTERNAL_SERVER_ERROR]} + end + end. + + +remove_user(User, Server) -> + LUser = jlib:nodeprep(User), + LServer = jlib:nameprep(Server), + US = {LUser, LServer}, + SUS = get_us_escaped(US), + F = fun() -> + run_sql_query( + ["delete from archive_jid_prefs " + "where us = ", SUS]), + run_sql_query( + ["delete from archive_global_prefs " + "where us = ", SUS]), + run_sql_query( + ["delete from archive_collections " + "where us = ", SUS]) + end, + run_sql_transaction(LServer, F). + +get_disco_features(Acc, _From, _To, "", _Lang) -> + Features = + case Acc of + {result, I} -> I; + _ -> [] + end, + {result, Features ++ [?NS_ARCHIVE_MANAGE, + ?NS_ARCHIVE_AUTO, + ?NS_ARCHIVE_PREF, + ?NS_ARCHIVE_MANUAL]}; + +get_disco_features(Acc, _From, _To, _Node, _Lang) -> + Acc. + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% 3 Automated archiving +%% + +send_packet(From, To, Packet) -> + add_log(to, From#jid.luser, From#jid.lserver, From#jid.lresource, To, Packet). + +receive_packet(From, To, Packet) -> + add_log(from, To#jid.luser, To#jid.lserver, To#jid.lresource, From, Packet). + +receive_packet(_JID, From, To, Packet) -> + receive_packet(From, To, Packet). + +add_log(Direction, LUser, LServer, LResource, JID, Packet) -> + case parse_message(Packet) of + {Type, Thread, Subject, Nick, Body} -> + Proc = gen_mod:get_module_proc(LServer, ?PROCNAME), + gen_server:cast( + Proc, {addlog, Type, Direction, LUser, LServer, LResource, JID, Thread, Subject, Nick, Body}); + _ -> + ok + end. + +%% Parse the message and return {Thread, Subject, Body} strings if successful +parse_message({xmlelement, "message", _, _} = Packet) -> + case xml:get_tag_attr_s("type", Packet) of + Type when Type == ""; + Type == "normal"; + Type == "chat"; + Type == "groupchat" -> + case xml:get_subtag(Packet, "body") of + false -> + ""; + _ -> + {Type, + xml:get_path_s(Packet, [{elem, "thread"}, cdata]), + xml:get_path_s(Packet, [{elem, "subject"}, cdata]), + xml:get_path_s(Packet, [{elem, "nick"}, cdata]), + xml:get_path_s(Packet, [{elem, "body"}, cdata])} + end; + _ -> + "" + end; +parse_message(_) -> + "". + +%% archive the message Body return new Sessions +%% Sessions: a dict of open sessions +%% LUser, LServer : the local user's information +%% Jid : the contact's jid +%% Body : the message body +do_log(Sessions, LUser, LServer, LResource, JID, Type, Direction, Thread, Subject, + Nick, Body, SessionDuration) -> + LowJID = jlib:jid_tolower(JID), + {_, _, Resource} = LowJID, + F = fun() -> + {NewSessions, _Start, TS, CID, NewRes} = + find_storage(LUser, LServer, LowJID, Thread, Sessions, + SessionDuration, Type), + LJID = jlib:jid_tolower(jlib:make_jid(LUser, LServer, LResource)), + update_collection_partial(CID, LServer, Thread, Subject, NewRes, LJID, TS), + M = #archive_message{coll_id = CID, + utc = TS, + direction = Direction, + name = + if Type == "groupchat" -> + if Nick /= "" -> + Nick; + true -> + Resource + end; + true -> "" + end, + body = Body}, + store_message(LServer, M), + NewSessions + end, + case run_sql_transaction(LServer, F) of + {error, Err} -> + ?ERROR_MSG("error when performing automated archiving: ~p", [Err]), + Sessions; + R -> %?MYDEBUG("successfull automated archiving: ~p", [R]), + R + end. + +find_storage(LUser, LServer, JID, Thread, Sessions, Timeout, Type) -> + %% + %% In fact there's small problem with resources: we can send the message + %% to recepient without specifying resource (typically, when sending the first + %% message), or with it (for subsequent ones). On the other hand, + %% remote sender will always (?) use resource when sending us the message. + %% + %% This means that we either should strip resouce completely from JID when + %% creating the key (which is easy, but not nice, as then all messages will be + %% put into single collection without treating resources at all), or use more + %% intelligent approach to match the appropriate collection to our message. + %% + %% Additionally we'd like to use "thread" to differentiate between different + %% conversations, to put them into different collections. + %% + %% Here is the approach we use: + %% + %% 1) There's two levels key schema: first key is JID without resource, second-level + %% key is the thread. If thread is not present, {no_thread, Resource} is used + %% instead. + %% 2) If thread is specified in the message - just use both-levels keys normally, + %% reusing existing collections if there's a match or creating new one if no + %% matching collection found. + %% 3) Otherwise use first-level key to get all sub-items, then + %% * If resource IS specified: search for matching resource: + %% - if found - use it. + %% - if not, search for sub-item with empty resource. If found, use it + %% and rewrite its resource to ours, notifying the caller to store collection. + %% If not - create new one. + %% * If resource IS NOT specified: use the most recent sub-item or + %% create new if none exists, notifying the caller about change of + %% resource, if needed. + %% + {_, _, ResourceIn} = JID, + %% Assume empty Resource for groupchat messages so that they're recorded + %% to the same collection. + Resource = if Type == "groupchat" -> ""; true -> ResourceIn end, + Key1 = {LUser, LServer, jlib:jid_remove_resource(JID)}, + TS = get_timestamp(), + case dict:find(Key1, Sessions) of + error -> + new_dict_answer(Key1, TS, Thread, Resource, Sessions); + {ok, Val1} -> + if Thread /= "" -> + case dict:find(Thread, Val1) of + error -> + new_dict_answer(Key1, TS, Thread, Resource, Sessions); + {ok, Val2} -> + updated_dict_answer(Key1, Val2, TS, Thread, + Resource, Sessions, Timeout) + end; + true -> + if Resource /= "" -> + case dict:find({no_thread, Resource}, Val1) of + {ok, Val2} -> + updated_dict_answer(Key1, Val2, TS, Thread, + Resource, Sessions, Timeout); + error -> + case dict:find({no_thread, ""}, Val1) of + error -> + new_dict_answer(Key1, TS, Thread, Resource, + Sessions); + {ok, Val2} -> + updated_dict_answer(Key1, Val2, TS, + Thread, Resource, Sessions, Timeout) + end + end; + true -> + F = fun(_, Value, {_, MaxLast, _, _} = OldVal) -> + {_, Last, _, CurRes} = Value, + if ((Type /= "groupchat") and (Last > MaxLast)) or + ((Type == "groupchat") and (CurRes == "")) -> + Value; + true -> OldVal + end + end, + case dict:fold(F, {-1, -1, null, ""}, Val1) of + {_, -1, _, _} -> + new_dict_answer(Key1, TS, Thread, Resource, + Sessions); + {_, _, _, Res} = Val2 -> + updated_dict_answer(Key1, Val2, TS, Thread, + Res, Sessions, Timeout) + end + end + end + end. + +updated_dict(Key1, Start, TS, CID, Thread, Resource, Sessions) -> + Val1 = case dict:find(Key1, Sessions) of + error -> dict:new(); + {ok, V} -> V + end, + Key2 = if Thread /= "" -> Thread; true -> {no_thread, Resource} end, + Val2 = {Start, TS, CID, Resource}, + NVal1 = dict:store(Key2, Val2, Val1), + dict:store(Key1, NVal1, Sessions). + +updated_dict_answer(Key1, {Start, Last, CID, _OldRes}, TS, Thread, Resource, + Sessions, Timeout) -> + if TS - Last > Timeout -> + new_dict_answer(Key1, TS, Thread, Resource, Sessions); + true -> + {updated_dict(Key1, Start, TS, CID, Thread, Resource, Sessions), + Start, TS, CID, Resource} + end. + +new_dict_answer({LUser, LServer, JID} = Key1, TS, Thread, Resource, Sessions) -> + CID = get_collection_id({LUser, LServer, JID, TS}), + {updated_dict(Key1, TS, TS, CID, Thread, Resource, Sessions), + TS, TS, CID, Resource}. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% 3.1 Preferences +%% + +process_local_iq_pref(From, _To, #iq{type = Type, sub_el = SubEl} = IQ) -> + Result = case Type of + set -> + {xmlelement, _Name, _Attrs, Els} = SubEl, + process_save_set(From#jid.luser, From#jid.lserver, Els); + get -> + process_save_get(From#jid.luser, From#jid.lserver) + end, + case Result of + ok -> + broadcast_iq(From, IQ#iq{type = set, sub_el=[SubEl]}), + {result, []}; + R -> R + end. + +%% returns {error, xmlelement} or {result, xmlelement} +process_save_get(LUser, LServer) -> + F = + fun() -> + %% Request prefs for all of JIDs + LItems = + lists:map( + fun(J) -> + jid_prefs_to_xml(J) + end, get_all_jids_prefs({LUser, LServer})), + GPrefs = get_global_prefs({LUser, LServer}), + DGPrefs = default_global_prefs({LUser, LServer}), + UnSet = + if (GPrefs#archive_global_prefs.save /= undefined) or + (GPrefs#archive_global_prefs.expire /= undefined) or + (GPrefs#archive_global_prefs.otr /= undefined) -> "false"; + true -> "true" + end, + DItem = global_prefs_to_xml(GPrefs, DGPrefs, UnSet), + {result, [{xmlelement, "pref", [{"xmlns", ?NS_ARCHIVE}], DItem ++ LItems}]} + end, + run_sql_transaction(LServer, F). + +jid_prefs_to_xml(Pref) -> + Save = Pref#archive_jid_prefs.save, + Expire = Pref#archive_jid_prefs.expire, + OTR = Pref#archive_jid_prefs.otr, + {xmlelement, "item", + [{"jid", jlib:jid_to_string(Pref#archive_jid_prefs.jid)}] ++ + if Save /= undefined -> + [{"save", atom_to_list(Save)}]; + true -> + [] + end ++ + if Expire /= undefined, Expire /= infinity -> + [{"expire", integer_to_list(Expire)}]; + true -> + [] + end ++ + if OTR /= undefined -> + [{"otr", atom_to_list(OTR)}]; + true -> + [] + end, []}. + +global_prefs_to_xml(GPrefs, DGPrefs, UnSet) -> + Prefs = list_to_tuple( + lists:zipwith( + fun(Item1, Item2) -> + if Item1 /= undefined -> Item1; + true -> Item2 + end + end, + tuple_to_list(GPrefs), + tuple_to_list(DGPrefs))), + Expire = Prefs#archive_global_prefs.expire, + [{xmlelement, "default", + [{"save", atom_to_list(Prefs#archive_global_prefs.save)}] ++ + if Expire /= infinity -> [{"expire", integer_to_list(Expire)}]; true -> [] end ++ + [{"otr", atom_to_list(Prefs#archive_global_prefs.otr)}, + {"unset", UnSet}], + []}, + {xmlelement, "method", + [{"type", "auto"}, + {"use", atom_to_list(Prefs#archive_global_prefs.method_auto)}], + []}, + {xmlelement, "method", + [{"type", "local"}, + {"use", atom_to_list(Prefs#archive_global_prefs.method_local)}], + []}, + {xmlelement, "method", + [{"type", "manual"}, + {"use", atom_to_list(Prefs#archive_global_prefs.method_manual)}], + []}, + {xmlelement, "auto", + [{"save", atom_to_list(Prefs#archive_global_prefs.auto_save)}], + []}]. + +%% Returns the archive_global_prefs record filled with default values +default_global_prefs({_, LServer} = US) -> + DefaultAutoSave = gen_mod:get_module_opt(LServer, ?MODULE, default_auto_save, false), + DefaultExpire = gen_mod:get_module_opt(LServer, ?MODULE, default_expire, infinity), + #archive_global_prefs{us = US, + save = if DefaultAutoSave -> body; true -> false end, + expire = DefaultExpire, + method_auto = if DefaultAutoSave -> prefer; true -> concede end, + method_local = concede, + method_manual = if DefaultAutoSave -> concede; true -> prefer end, + auto_save = DefaultAutoSave, + otr = forbid}. + + +%% Return {error, xmlelement} or ok +process_save_set(LUser, LServer, Elems) -> + F = + fun() -> + US = {LUser, LServer}, + GPrefs = get_global_prefs(US), + GPrefs1 = GPrefs#archive_global_prefs{us = US}, + parse_save_elem(GPrefs1, Elems), + ok + end, + run_sql_transaction(LServer, F). + +parse_save_elem(GPrefs, [{xmlelement, "default", Attrs, _} | Tail]) -> + {Save, Expire, OTR} = get_main_prefs_from_attrs(Attrs), + GPrefs1 = GPrefs#archive_global_prefs{save = Save, expire = Expire, otr = OTR}, + parse_save_elem(GPrefs1, Tail); + +parse_save_elem(GPrefs, [{xmlelement, "method", Attrs, _} | Tail]) -> + Use = + case xml:get_attr_s("use", Attrs) of + "concede" -> concede; + "forbid" -> forbid; + "prefer" -> prefer; + "" -> undefined; + _ -> throw({error, ?ERR_BAD_REQUEST}) + end, + GPrefs1 = + case xml:get_attr_s("type", Attrs) of + "auto" -> GPrefs#archive_global_prefs{method_auto = Use}; + "local" -> GPrefs#archive_global_prefs{method_local = Use}; + "manual" -> GPrefs#archive_global_prefs{method_manual = Use}; + "" -> GPrefs; + _ -> throw({error, ?ERR_BAD_REQUEST}) + end, + parse_save_elem(GPrefs1, Tail); + +parse_save_elem(GPrefs, [{xmlelement, "auto", Attrs, _} | Tail]) -> + GPrefs1 = + case xml:get_attr_s("save", Attrs) of + "true" -> GPrefs#archive_global_prefs{auto_save = true}; + "false" -> GPrefs#archive_global_prefs{auto_save = false}; + _ -> throw({error, ?ERR_BAD_REQUEST}) + end, + parse_save_elem(GPrefs1, Tail); + +parse_save_elem(GPrefs, [{xmlelement, "item", Attrs, _} | Tail]) -> + case jlib:string_to_jid(xml:get_attr_s("jid", Attrs)) of + error -> throw({error, ?ERR_JID_MALFORMED}); + JID -> + LJID = jlib:jid_tolower(JID), + {Save, Expire, OTR} = get_main_prefs_from_attrs(Attrs), + Prefs = #archive_jid_prefs{us = GPrefs#archive_global_prefs.us, + jid = LJID, + save = Save, + expire = Expire, + otr = OTR}, + store_jid_prefs(Prefs) + end, + parse_save_elem(GPrefs, Tail); + +parse_save_elem(GPrefs, []) -> + store_global_prefs(GPrefs); + +parse_save_elem(GPrefs, [_ | Tail]) -> + parse_save_elem(GPrefs, Tail). + +get_main_prefs_from_attrs(Attrs) -> + Save = + case xml:get_attr_s("save", Attrs) of + "body" -> body; + "false" -> false; + "" -> undefined; + _ -> throw({error, ?ERR_BAD_REQUEST}) + end, + Expire = + case xml:get_attr_s("expire", Attrs) of + "" -> undefined; + N -> case catch list_to_integer(N) of + NR when is_integer(NR) -> NR; + _ -> throw({eror, ?ERR_BAD_REQUEST}) + end + end, + OTR = + case xml:get_attr_s("otr", Attrs) of + "" -> undefined; + V -> list_to_atom(V) + end, + {Save, Expire, OTR}. + + +broadcast_iq(#jid{luser = User, lserver = Server}, IQ) -> + Fun = fun(Resource) -> + ejabberd_router:route( + jlib:make_jid("", Server, ""), + jlib:make_jid(User, Server, Resource), + jlib:iq_to_xml(IQ#iq{id="push"})) + end, + lists:foreach(Fun, ejabberd_sm:get_user_resources(User,Server)). + + +process_local_iq_auto(From, _To, #iq{type = Type, sub_el = SubEl}) -> + case Type of + set -> + {xmlelement, _Name, Attrs, _Els} = SubEl, + Auto = + case xml:get_attr_s("save", Attrs) of + "true" -> true; + "false" -> false; + _ -> throw({error, ?ERR_BAD_REQUEST}) + end, + LUser = From#jid.luser, + LServer = From#jid.lserver, + F = + fun() -> + US = {LUser, LServer}, + GPrefs = get_global_prefs(US), + GPrefs1 = GPrefs#archive_global_prefs{us = US, auto_save = Auto}, + store_global_prefs(GPrefs1), + {result, []} + end, + run_sql_transaction(LServer, F); + get -> + throw({error, ?ERR_BAD_REQUEST}) + end. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% Utility function + +%% Return true if LUser@LServer should log message for the contact JID +should_store_jid({_, LServer} = US, JID) -> + F = + fun() -> + GPrefs = get_global_prefs(US), + case GPrefs#archive_global_prefs.auto_save of + false -> + false; + true -> + should_store_jid_full_check(US, JID); + undefined -> + DGPrefs = default_global_prefs(US), + if + DGPrefs#archive_global_prefs.auto_save == false -> + false; + true -> + should_store_jid_full_check(US, JID) + end + end + end, + case run_sql_transaction(LServer, F) of + {error, Err} -> ?ERROR_MSG("should_store_jid failed: ~p", [Err]), false; + R -> R + end. + +should_store_jid_full_check(US, JID) -> + {User, Server, Res} = jlib:jid_tolower(JID), + Prefs1 = get_jid_prefs(US, {User, Server, Res}), + Save1 = Prefs1#archive_jid_prefs.save, + Save2 = + if Save1 == undefined -> + Prefs2 = get_jid_prefs(US, {User, Server, ""}), + Prefs2#archive_jid_prefs.save; + true -> Save1 + end, + Save3 = + if Save2 == undefined -> + Prefs3 = get_jid_prefs(US, {"", Server, ""}), + Prefs3#archive_jid_prefs.save; + true -> Save2 + end, + case Save3 of + false -> false; + _ -> true + end. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% 4. Manual Archiving +%% + + +process_local_iq_save(From, _To, #iq{type = Type, sub_el = SubEl}) -> + #jid{luser = LUser, lserver = LServer, lresource = LResource} = From, + case Type of + get -> + throw({error, ?ERR_NOT_ALLOWED}); + set -> + {C, Msgs} = parse_store_element(LUser, LServer, SubEl), + F = + fun() -> + CID = get_collection_id({LUser, LServer, + C#archive_collection.jid, + C#archive_collection.utc}), + C1 = get_collection_by_id(CID), + LJID = jlib:jid_tolower(jlib:make_jid(LUser, LServer, LResource)), + C2 = C#archive_collection{id = CID, + change_by = LJID, + change_utc = get_timestamp()}, + C3 = + list_to_tuple( + lists:zipwith( + fun(ValOld, ValNew) -> + case ValNew of + undefined -> ValOld; + _ -> ValNew + end + end, + tuple_to_list(C1), tuple_to_list(C2))), + store_collection(C3), + store_messages(LServer, CID, Msgs), + {result, []} + end, + run_sql_transaction(LServer, F) + end. + +%% return a {#archive_collection, list of #archive_message} or {error, xmlelement} +parse_store_element(LUser, LServer, + {xmlelement, "save", _ChatAttrs, ChatSubEls}) -> + case xml:remove_cdata(ChatSubEls) of + [{xmlelement, "chat", Attrs, SubEls} = SubEl] -> + {LUser, LServer, Jid, Start} = link_from_argument(LUser, LServer, SubEl), + Extra = xml:get_subtag(SubEl, "x"), + C = #archive_collection{us = {LUser, LServer}, + jid = Jid, + utc = Start, + prev = get_link_as_list(SubEls, "previous"), + next = get_link_as_list(SubEls, "next"), + subject = case xml:get_attr("subject", Attrs) of + {value, Val} -> Val; + false -> undefined + end, + thread = case xml:get_attr("thread", Attrs) of + {value, Val} -> Val; + false -> undefined + end, + extra = + if Extra /= false -> encode_extra(Extra); + true -> undefined + end}, + Messages = parse_store_element_sub(SubEls, Start), + {C, Messages}; + _ -> + throw({error, ?ERR_BAD_REQUEST}) + end. + +parse_store_element_sub([{xmlelement, Dir, _, _} = E | Tail], Start) + when Dir == "from"; + Dir == "to"; + Dir == "note" -> + UTC = + case xml:get_tag_attr_s("secs", E) of + "" -> + case xml:get_tag_attr_s("utc", E) of + "" -> throw({error, ?ERR_BAD_REQUEST}); + Val -> get_seconds_from_datetime_string(Val) + end; + Secs -> + Start + + case list_to_integer(Secs) of + N when is_integer(N) -> N; + _ -> throw({error, ?ERR_BAD_REQUEST}) + end + end, + Body = if Dir == "note" -> xml:get_tag_cdata(E); + true -> xml:get_tag_cdata(xml:get_subtag(E,"body")) + end, + [#archive_message{direction = list_to_atom(Dir), + utc = UTC, + body = Body, + name = xml:get_tag_attr_s("name", E)} | + parse_store_element_sub(Tail, Start)]; + +parse_store_element_sub([], _) -> []; + +parse_store_element_sub([_ | Tail], Start) -> parse_store_element_sub(Tail, Start). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% 5. Archive Management +%% + + +process_local_iq_list(From, _To, #iq{type = Type, sub_el = SubEl}) -> + #jid{luser = LUser, lserver = LServer} = From, + case Type of + set -> + throw({error, ?ERR_NOT_ALLOWED}); + get -> + {xmlelement, _, _, SubEls} = SubEl, + RSM = parse_rsm(SubEls), + F = fun() -> + {interval, Start, Stop, JID} = parse_root_argument(SubEl), + Req = get_combined_req(Start, Stop, RSM), + {ok, Items, RSM_Elem} = get_collections_links(LUser, LServer, Req, JID), + {result, [{xmlelement, "list", + [{"xmlns", ?NS_ARCHIVE}], + lists:append( + lists:map( + fun(C) -> + collection_link_to_xml("chat", C) + end, Items), + RSM_Elem)}]} + end, + run_sql_transaction(LServer, F) + end. + + +process_local_iq_retrieve(From, _To, #iq{type = Type, sub_el = SubEl}) -> + #jid{luser = LUser, lserver = LServer} = From, + case Type of + set -> + throw({error, ?ERR_NOT_ALLOWED}); + get -> + {xmlelement, _, _, SubEls} = SubEl, + RSM = parse_rsm(SubEls), + F = fun() -> + Link = link_from_argument(LUser, LServer, SubEl), + Store = retrieve_collection_and_msgs(Link, RSM), + {result, Store} + end, + run_sql_transaction(LServer, F) + end. + + +retrieve_collection_and_msgs(Link, RSM) -> + C = get_collection(Link), + {ok, Items, RSM_Elem} = get_messages(C, RSM), + {_, _, Attrs, SubEls} = collection_to_xml(C), + [{xmlelement, "chat", Attrs, + lists:append([SubEls, + lists:map( + fun(M) -> + message_to_xml(M, C#archive_collection.utc) + end, Items), + RSM_Elem])}]. + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% 5.3 Removing a Collection +%% + + +process_local_iq_remove(From, _To, #iq{type = Type, sub_el = SubEl}) -> + #jid{luser = LUser, lserver = LServer, lresource = LResource} = From, + case Type of + get -> + throw({error, ?ERR_NOT_ALLOWED}); + set -> + {xmlelement, _, _, _} = SubEl, + {interval, Start, Stop, Jid} = parse_root_argument(SubEl), + process_remove_interval(LUser, LServer, LResource, Start, Stop, Jid) + end. + +process_remove_interval(LUser, LServer, LResource, Start, End, With) -> + SUS = get_us_escaped({LUser, LServer}), + WithCond = case With of + undefined -> + ""; + JID -> + {SUser, SServer, SResource} = get_jid_escaped(JID), + SServerNonEmpty = is_non_empty(SServer), + SUserNonEmpty = is_non_empty(SUser), + SResourceNonEmpty = is_non_empty(SResource), + [if SServerNonEmpty == true -> ["with_server = ", SServer, " "]; + true -> "" + end, + if SUserNonEmpty == true -> [" and with_user = ", SUser, " "]; + true -> "" + end, + if SResourceNonEmpty == true -> [" and with_resource = ", SResource, " "]; + true -> "" + end] + end, + TimeCond = + case {Start, End} of + {undefined, undefined} -> + ""; + {undefined, _} -> + SEnd = encode_timestamp(End), + ["and utc < ", SEnd, " "]; + {_, undefined} -> + SStart = encode_timestamp(Start), + ["and utc = ", SStart, " "]; + _ -> + SStart = encode_timestamp(Start), + SEnd = encode_timestamp(End), + ["and utc >= ", SStart, " " + "and utc < ", SEnd, " "] + end, + F = + fun() -> + WhereCond = + ["where us = ", SUS, " ", + "and deleted = 0 ", + TimeCond, + if WithCond /= "" -> ["and ", WithCond]; + true -> "" + end], + TS = get_timestamp(), + LJID = jlib:jid_tolower(jlib:make_jid(LUser, LServer, LResource)), + case jlib:tolower(gen_mod:get_module_opt(LServer, ?MODULE, database_type, "")) of + %% MySQL has severe limitations for triggers: they cannot update the same table + %% they're invoked for, so we have to do that here. + %% However, yet another limitation is that in UPDATE MySQL cannot use the same table + %% in subquery which is being updated - so we have to cheat here, see + %% http://www.xaprb.com/blog/2006/06/23/how-to-select-from-an-update-target-in-mysql/ + "mysql" -> + run_sql_query( + ["update archive_collections " + "set next_id = NULL " + "where next_id in " + "(select id from " + "(select id from archive_collections ", + WhereCond, ") as x)"]), + run_sql_query( + ["update archive_collections " + "set prev_id = NULL " + "where prev_id in " + "(select id from " + "(select id from archive_collections ", + WhereCond, ") as x)"]); + _ -> ok % Nothing to be done, all work should be done by trigger + end, + case run_sql_query( + ["update archive_collections " + "set deleted = 1, " + "subject = '', " + "thread = '', " + "extra = '', " + "prev_id = NULL, " + "next_id = NULL, " + "change_by = ", get_jid_full_escaped(LJID), ", " + "change_utc = ", encode_timestamp(TS), " ", + WhereCond]) of + {deleted, 0} -> throw({error, ?ERR_ITEM_NOT_FOUND}); + Res -> Res + end, + {result, []} + end, + run_sql_transaction(LServer, F). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% 10. Replication +%% + + +process_local_iq_modified(From, _To, #iq{type = Type, sub_el = SubEl}) -> + #jid{luser = LUser, lserver = LServer} = From, + case Type of + set -> + {error, ?ERR_NOT_ALLOWED}; + get -> + {xmlelement, _, _, SubEls} = SubEl, + RSM = parse_rsm(SubEls), + F = + fun() -> + {interval, Start, Stop, _} = parse_root_argument(SubEl), + {{range, {Start1, _}, {_, _}, _}, _} = RSM, + StartPresent = xml:get_tag_attr_s("start", SubEl) == "", + {ok, Items, RSM_Item} = + if not is_integer(Start1), StartPresent -> + get_modified_legacy(LUser, LServer, RSM); + true -> + Req = get_combined_req(Start, Stop, RSM), + get_modified(LUser, LServer, Req) + end, + {result, [{xmlelement, "modified", + [{"xmlns", ?NS_ARCHIVE}], + lists:append( + lists:map( + fun(AC) -> + change_to_xml(AC) + end, Items), + RSM_Item)}]} + end, + run_sql_transaction(LServer, F) + end. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% X.x Utility functions to interact with the database + +%% +%% The design is as follows: +%% +%% * For collections: +%% 1) When get_collection_id is called, ID of this collection from database is +%% returned - if needed, the entity is created first using the information +%% supplied in get_collection_id call (which is only partial). +%% 2) After having ID, you can call store_collection function to actually +%% put meaningful values into it. +%% +%% * For messages: +%% As messages are always created, only store_message is supported. +%% +%% * For prefs: +%% Just store_ functions are provided, as the info is the key on its own. +%% + +%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% +%% get_collection_id related functions +%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +%% +%% Adds new collection or returns ID of existing one. +%% +get_collection_id({LUser, LServer, JID, Start}) -> + SUS = get_us_escaped({LUser, LServer}), + SJID = get_jid_escaped(JID), + {SUser, SServer, SResource} = SJID, + SUTC = encode_timestamp(Start), + case get_collection_id_raw(SUS, SJID, SUTC) of + %% Collection is present already - just return its ID + ID when is_integer(ID) -> ID; + _ -> + %% Insert new collection. + InsVals = [SUS, ",", + SUser, ",", + SServer, ",", + SResource, ",", + SUTC, ",", + "0"], + run_sql_query(["insert into archive_collections" + "(us, with_user, with_server, with_resource, utc, deleted) " + "values(", InsVals, ")"]), + case get_last_inserted_id(LServer, "archive_collections") of + error -> get_collection_id_raw(SUS, SJID, SUTC); + ID -> ID + end + end. + +get_collection_id_raw(SUS, {SUser, SServer, SResource}, SUTC) -> + case run_sql_query(["select id from archive_collections " + "where us = ", SUS, " " + "and with_user = ", SUser, " " + "and with_server = ", SServer," ", + "and with_resource = ", SResource, " " + "and utc = ", SUTC]) of + {selected, _, Rs} when Rs /= [] -> + {ID} = lists:last(lists:sort(Rs)), + decode_integer(ID); + _ -> {error, ?ERR_BAD_REQUEST} + end. + +%% +%% The following functions deal with links that can be present in collections. +%% +get_link_as_list([{xmlelement, Tag, Attrs, _} | _], Name) + when Tag == Name -> + if Attrs /= [] -> + {jlib:jid_tolower(jlib:string_to_jid(xml:get_attr_s("with", Attrs))), + get_seconds_from_datetime_string(xml:get_attr_s("start", Attrs))}; + true -> + [] + end; + +get_link_as_list([], _) -> undefined; + +get_link_as_list([_ | Tail], Name) -> get_link_as_list(Tail, Name). + +get_collection_link_id({LUser, LServer}, {With, Start}) -> + get_collection_id({LUser, LServer, With, Start}); + +get_collection_link_id({_, _}, _) -> null. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% +%% Store functions. +%% These functions update collections, messages or preferences respectively +%% that exist already in database (or, for prefs, possibly creating them). +%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +store_collection(C) -> + US = C#archive_collection.us, + {_, LServer} = US, + JID = C#archive_collection.jid, + %% We assume that the only part of JID store_collection can change + %% is resource, after figuring out conversation's recipient actual resource. + %% Currently it's not needed anymore as update_collection_partial() is provided, + %% but we still leave it here just in case. + {_, _, SResource} = get_jid_escaped(JID), + SCHUTC = encode_timestamp(C#archive_collection.change_utc), + SByJID = get_jid_full_escaped(C#archive_collection.change_by), + CPrevID = get_collection_link_id(US, C#archive_collection.prev), + CNextID = get_collection_link_id(US, C#archive_collection.next), + SSubject = escape_str(LServer, C#archive_collection.subject), + SThread = escape_str(LServer, C#archive_collection.thread), + SExtra = escape_str(LServer, C#archive_collection.extra), + CollVals = ["with_resource = ", SResource, ", " + "deleted = 0, " + "change_by = ", SByJID, ", " + "change_utc = ", SCHUTC, ", " + "prev_id = ", escape(CPrevID), ", " + "next_id = ", escape(CNextID), ", " + "subject = ", SSubject, ", " + "thread = ", SThread, ", " + "extra = ", SExtra], + run_sql_query(["update archive_collections set ", + CollVals, " where id = ", + escape(C#archive_collection.id)]). + +%% +%% partial collection update, useful for quick update when autosaving +%% +update_collection_partial(CID, LServer, Thread, Subject, NewRes, LJID, TS) -> + SResource = escape(NewRes), + SByJID = get_jid_full_escaped(LJID), + SCHUTC = encode_timestamp(TS), + SSubject = escape_str(LServer, Subject), + SThread = escape_str(LServer, Thread), + CollVals = ["with_resource = ", SResource, ", " + "change_by = ", SByJID, ", " + "change_utc = ", SCHUTC, ", " + "subject = ", SSubject, ", " + "thread = ", SThread], + run_sql_query(["update archive_collections set ", + CollVals, " where id = ", + escape(CID)]). + + +%% store_message is somewhat special as it is never called for existing message - +%% therefore we can optimize it by using only one INSERT command, unlike +%% collections and changes, where we have to do SELECT -> [INSERT -> SELECT] -> UPDATE +store_message(LServer, Msg) -> + run_sql_query([get_store_msg_header(), get_message_values_stmt(LServer, Msg)]). + +%% +%% Stores multiple messages using multiple insert SQL operator - for those RDBMS'es that +%% support this syntax +%% +store_messages(LServer, CID, Msgs) -> + case jlib:tolower(gen_mod:get_module_opt(LServer, ?MODULE, database_type, "")) of + "sqlite" -> + %% Single inserts + lists:map( + fun(Msg) -> + Msg1 = Msg#archive_message{coll_id = CID}, + store_message(LServer, Msg1) + end, Msgs); + _ -> + %% Multiple inserts + Header = get_store_msg_header(), + Res = + lists:foldl( + fun(Msg, AccIn) -> + Msg1 = Msg#archive_message{coll_id = CID}, + Values = get_message_values_stmt(LServer, Msg1), + if AccIn == "" -> + Header ++ Values; + true -> + Len = lists:flatlength(AccIn) + lists:flatlength(Values), + if Len < ?MAX_QUERY_LENGTH -> + AccIn ++ "," ++ Values; + true -> + run_sql_query(AccIn), + Header ++ Values + end + end + end, + "", Msgs), + if Res /= "" -> + run_sql_query(Res); + true -> ok + end + end. + +get_store_msg_header() -> + "insert into archive_messages(coll_id, utc, dir, name, body) values". + +get_message_values_stmt(LServer, Msg) -> + SDirection = escape(case Msg#archive_message.direction of + to -> 1; + from -> 0; + note -> 2 + end), + SName = escape_str(LServer, Msg#archive_message.name), + SBody = escape_str(LServer, Msg#archive_message.body), + ["(", escape(Msg#archive_message.coll_id), ", ", + encode_timestamp(Msg#archive_message.utc), ", ", + SDirection, ", ", + SName, ", ", + SBody, ")"]. + +%% store global prefs, either creating them or updating existing ones. +store_global_prefs(GPrefs) -> + US = GPrefs#archive_global_prefs.us, + {_, LServer} = US, + validate_global_prefs(LServer, + GPrefs#archive_global_prefs.auto_save, + GPrefs#archive_global_prefs.save, + GPrefs#archive_global_prefs.expire), + SPrefs = escape_global_prefs(GPrefs), + Fields = ["save", "expire", "otr", + "method_auto", "method_local", "method_manual", + "auto_save"], + SUS = get_us_escaped(US), + case run_sql_query(["select us from archive_global_prefs " + "where us = ", SUS]) of + {selected, _, Rs} when Rs /= [] -> + run_sql_query(["update archive_global_prefs set ", + put_commas(combine_names_vals(Fields, SPrefs)), + " where us = ", SUS]); + _ -> + run_sql_query(["insert into archive_global_prefs(" + "us, ", put_commas(Fields), ") " + "values(", SUS, ", ", put_commas(SPrefs), ")"]) + end. + +escape_global_prefs(GPrefs) -> + escape_common_prefs(GPrefs#archive_global_prefs.save, + GPrefs#archive_global_prefs.expire, + GPrefs#archive_global_prefs.otr) ++ + lists:map( + fun(V) -> + case V of + undefined -> "null"; + prefer -> "0"; + concede -> "1"; + forbid -> "2"; + _ -> throw({error, ?ERR_BAD_REQUEST}) + end + end, + [GPrefs#archive_global_prefs.method_auto, + GPrefs#archive_global_prefs.method_local, + GPrefs#archive_global_prefs.method_manual]) ++ + [case GPrefs#archive_global_prefs.auto_save of + true -> "1"; + false -> "0"; + undefined -> "null"; + _ -> throw({error, ?ERR_BAD_REQUEST}) + end]. + +%% store jid prefs, either creating them or updating existing ones. +store_jid_prefs(Prefs) -> + US = Prefs#archive_jid_prefs.us, + {_, LServer} = US, + validate_common_prefs(LServer, + Prefs#archive_jid_prefs.save, + Prefs#archive_jid_prefs.expire), + SPrefs = escape_jid_prefs(Prefs), + Fields = ["save", "expire", "otr"], + SUS = get_us_escaped(US), + {SUser, SServer, SRes} = get_jid_escaped(Prefs#archive_jid_prefs.jid), + case run_sql_query(["select us from archive_jid_prefs " + "where us = ", SUS, " " + "and with_user = ", SUser, " " + "and with_server = ", SServer, " " + "and with_resource = ", SRes]) of + {selected, _, Rs} when Rs /= [] -> + run_sql_query(["update archive_jid_prefs set ", + put_commas(combine_names_vals(Fields, SPrefs)), + " where us = ", SUS, " " + "and with_user = ", SUser, " " + "and with_server = ", SServer, " " + "and with_resource = ", SRes]); + _ -> + run_sql_query(["insert into archive_jid_prefs(" + "us, with_user, with_server, with_resource, ", + put_commas(Fields), ") " + "values(", SUS, ", ", + SUser, ", ", + SServer, ", ", + SRes, ", ", + put_commas(SPrefs), ")"]) + end. + +escape_jid_prefs(Prefs) -> + escape_common_prefs(Prefs#archive_jid_prefs.save, + Prefs#archive_jid_prefs.expire, + Prefs#archive_jid_prefs.otr). + +validate_global_prefs(LServer, AutoSave, Save, Expire) -> + DefAutoSave = gen_mod:get_module_opt(LServer, ?MODULE, default_auto_save, false), + EnforceDefAutoSave = gen_mod:get_module_opt(LServer, ?MODULE, enforce_default_auto_save, false), + %% Should we enforce our default auto save policy? + %% User is trying to change auto_save to the option other than enforced. + if EnforceDefAutoSave and (DefAutoSave /= AutoSave) and (AutoSave /= undefined) -> + throw({error, ?ERR_FEATURE_NOT_IMPLEMENTED}); + true -> ok + end, + validate_common_prefs(LServer, Save, Expire). + +validate_common_prefs(LServer, Save, Expire) -> + DefAutoSave = gen_mod:get_module_opt(LServer, ?MODULE, default_auto_save, false), + EnforceDefAutoSave = gen_mod:get_module_opt(LServer, ?MODULE, enforce_default_auto_save, false), + %% Should we enforce our default auto save policy? + if EnforceDefAutoSave and + %% auto-save=true is enforced but user is trying to put "save" element to smth other + %% than body (thus effectively turning saving off). + (DefAutoSave and (Save /= body) and (Save /= undefined)) -> + throw({error, ?ERR_FEATURE_NOT_IMPLEMENTED}); + true -> ok + end, + EnforceMinExpire = gen_mod:get_module_opt(LServer, ?MODULE, enforce_min_expire, 0), + if (Expire /= undefined) and (Expire /= infinity) and + ((EnforceMinExpire == infinity) or (Expire < EnforceMinExpire)) -> + throw({error, ?ERR_FEATURE_NOT_IMPLEMENTED}); + true -> ok + end, + EnforceMaxExpire = gen_mod:get_module_opt(LServer, ?MODULE, enforce_max_expire, infinity), + if (EnforceMaxExpire /= infinity) and (Expire /= undefined) and + ((Expire == infinity) or (Expire > EnforceMaxExpire)) -> + throw({error, ?ERR_FEATURE_NOT_IMPLEMENTED}); + true -> ok + end. + +escape_common_prefs(Save, Expire, OTR) -> + [case Save of + body -> "1"; + false -> "0"; + undefined -> "null"; + _ -> throw({error, ?ERR_FEATURE_NOT_IMPLEMENTED}) + end, + case Expire of + infinity -> "null"; + undefined -> "null"; + N -> integer_to_list(N) + end, + case OTR of + undefined -> "null"; + approve -> "0"; + concede -> "1"; + forbid -> "2"; + oppose -> "3"; + prefer -> "4"; + require -> "5"; + _ -> throw({error, ?ERR_BAD_REQUEST}) + end]. + +put_commas(Vals) -> + lists:foldl( + fun(V, AccIn) -> + if AccIn /= "" -> AccIn ++ ", " ++ V; + true -> AccIn ++ V + end + end, + "", Vals). + +combine_names_vals(Names, Vals) -> + lists:zipwith( + fun(Name, Val) -> + [Name, " = ", Val] + end, Names, Vals). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% +%% This function should return the last inserted auto-generated ID, +%% if supported by database. If not - second lookup will be performed +%% to fetch new ID. Typically this should be safe, although, probably, +%% slightly slower. +%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%% +get_last_inserted_id(LServer, Table) -> + case jlib:tolower(gen_mod:get_module_opt(LServer, ?MODULE, database_type, "")) of + "mysql" -> {selected, _, [{ID}]} = run_sql_query(["select LAST_INSERT_ID()"]), + decode_integer(ID); + "sqlite" -> {selected, _, [{ID}]} = run_sql_query(["select last_insert_rowid()"]), + decode_integer(ID); + "pgsql" -> {selected, _, [{ID}]} = run_sql_query(["select currval('", + Table, "_id_seq')"]), decode_integer(ID); + _ -> + error + end. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% +%% Helper functions to deal with RSM and main commands restrictions. +%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +get_combined_req(Start, End, {{index, Index}, Max}) -> + {{{index, Index}, Max}, Start, End}; + +get_combined_req(Start, End, {{range, {RStart, StartID}, {REnd, EndID}, Order}, Max}) -> + StartLarger = timestamp_to_integer(Start) > timestamp_to_integer(RStart), + StartLink = if StartLarger -> + {Start, undefined}; + true -> + {RStart, StartID} + end, + EndSmaller = timestamp_to_integer(End) < timestamp_to_integer(REnd), + EndLink = if EndSmaller -> + {End, undefined}; + true -> + {REnd, EndID} + end, + {{{range, StartLink, EndLink, Order}, Max}, Start, End}; + +get_combined_req(Start, End, []) -> + {{{range, {Start, undefined}, {End, undefined}, normal}, undefined}, Start, End}; + +get_combined_req(_, _, _) -> + throw({error, ?ERR_BAD_REQUEST}). + +reverse_items_if_needed(Items, {{range, {_, _}, {_, _}, reversed}, _}) -> lists:reverse(Items); +reverse_items_if_needed(Items, _) -> Items. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% +%% Helper functions with common code for SQL queries +%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +get_request_part_times(UTCField, {Start, StartID}, {End, EndID}) -> + %% If IDs are specified - always use strict comparisons, as "non-strictness" will be added by IDs. + %% If start is from command attribute - it should be ">=" according to XEP-136. + %% However, if we're called for "modified" - use ">", not ">=". + GTS = if UTCField == "change_utc"; StartID /= undefined -> ">"; true -> ">=" end, + SStart = encode_timestamp(Start), + SEnd = encode_timestamp(End), + StartCond = [UTCField, " ", GTS, " ", SStart, " "], + EndCond = [UTCField, " < ", SEnd, " "], + [if StartID == undefined -> + ["and ", StartCond, " "]; + true -> + ["and (", StartCond, " or (", UTCField, " = ", SStart, " " + "and id > ", escape(StartID), ")) "] end, + if EndID == undefined -> + ["and ", EndCond, " "]; + true -> + ["and (", EndCond, " or (", UTCField, " = ", SEnd, " " + "and id < ", escape(EndID), ")) "] end]. + +get_request_part_range(UTCField, {{range, {Start, StartID}, {End, EndID}, Order}, Max}) -> + [get_request_part_times(UTCField, {Start, StartID}, {End, EndID}), + "order by ", UTCField, " ", + if Order == reversed -> "desc, "; true -> ", " end, + "id ", + if Order == reversed -> "desc "; true -> "" end, + if Max /= undefined -> ["limit ", escape(Max)]; true -> "" end]; + +get_request_part_range(UTCField, {{index, Index}, Max}) -> + ["order by ", UTCField, ", id ", + if Max /= undefined -> ["limit ", escape(Max)]; true -> "" end, + "offset ", escape(Index)]. + + +%% +%% This function returns collections links that satisfy request restrictions +%% +get_collections_links(LUser, LServer, {RSM, Start, End}, JID) -> + SUS = get_us_escaped({LUser, LServer}), + SJID = if JID /= undefined -> get_jid_escaped(JID); true -> {undefined, undefined, undefined} end, + Count = get_collections_links_count(SUS, SJID, {Start, undefined}, {End, undefined}), + if Count == 0 -> + {ok, [], []}; + true -> + Links = get_collections_links_query(SUS, SJID, RSM), + CHTime = get_datetime_string_from_seconds(get_collections_links_change_time(SUS, SJID, Start, End)), + if Links == [] -> + {ok, [], make_rsm(undefined, undefined, undefined, CHTime, Count)}; + true -> + Links1 = reverse_items_if_needed(Links, RSM), + [{FirstID, _FirstJID, FirstUTC} | _] = Links1, + {LastID, _LastJID, LastUTC} = lists:last(Links1), + FirstIndex = get_collections_links_count(SUS, SJID, {Start, undefined}, + {FirstUTC, FirstID}), + {ok, Links1, make_rsm(FirstIndex, + make_rsm_range_item(FirstUTC, FirstID), + make_rsm_range_item(LastUTC, LastID), + CHTime, + Count)} + end + end. + +get_collections_link_req_where(SUS, {SUser, SServer, SResource}, AlsoDeleted) -> + ServerNonEmpty = is_non_empty(SServer), + UserNonEmpty = is_non_empty(SUser), + ResNonEmpty = is_non_empty(SResource), + ["where us = ", SUS, " ", + if AlsoDeleted -> ""; + true -> "and deleted = 0 " + end, + if ServerNonEmpty == true -> ["and with_server = ", SServer, " "]; true -> "" end, + if UserNonEmpty == true -> ["and with_user = ", SUser, " "]; true -> "" end, + if ResNonEmpty == true -> ["and with_resource = ", SResource, " "]; true -> "" end]. + +get_collections_links_count(SUS, SJID, Start, End) -> + get_collections_links_count_tmpl(SUS, SJID, "utc", false, Start, End). + +get_collections_links_count_tmpl(SUS, SJID, Field, AlsoDeleted, {Start, StartID}, {End, EndID}) -> + {selected, _, [{Count}]} = + run_sql_query(["select count(*) from archive_collections ", + get_collections_link_req_where(SUS, SJID, AlsoDeleted), + get_request_part_times(Field, {Start, StartID}, {End, EndID})]), + decode_integer(Count). + +get_collections_links_query(SUS, SJID, RSM) -> + case run_sql_query(["select id, with_user, with_server, with_resource, utc " + "from archive_collections ", + get_collections_link_req_where(SUS, SJID, false), + get_request_part_range("utc", RSM)]) of + {selected, _, Rs} -> get_collections_links_list(Rs) + end. + +get_collections_links_change_time(SUS, SJID, Start, End) -> + case run_sql_query(["select max(change_utc) from archive_collections ", + get_collections_link_req_where(SUS, SJID, false), + get_request_part_times("utc", {Start, undefined}, {End, undefined})]) of + {selected, _, [{CHTime}]} -> decode_timestamp(CHTime) + end. + +get_collections_links_list(CLs) -> + lists:map(fun(CL) -> get_collection_link_from_query_result(CL) end, CLs). + +get_collection_link_from_query_result({ID, User, Server, Resource, UTC}) -> + %% We do not create a full-blown record here as we do not have enough info - and + %% just do not need it. + {decode_integer(ID), + jlib:jid_tolower(jlib:make_jid(User, Server, Resource)), + decode_timestamp(UTC)}. + +collection_link_to_xml(Name, {_, JID, UTC}) -> collection_link_to_xml(Name, {JID, UTC}); + +collection_link_to_xml(Name, {JID, UTC}) -> + {xmlelement, Name, + [{"with", jlib:jid_to_string(JID)}, + {"start", get_datetime_string_from_seconds(UTC)}], + []}; + +collection_link_to_xml(_, _) -> []. + + +%% +%% This function returns full collection given its link +%% If several collections exist (which would violate XEP-136, +%% but seems to be still possible, though highly unlikely) +%% it returns the first one of them. +%% +get_collection({LUser, LServer, JID, Start}) -> + SUS = get_us_escaped({LUser, LServer}), + {SUser, SServer, SRes} = get_jid_escaped(JID), + SUTC = encode_timestamp(Start), + case run_sql_query(["select * " + "from archive_collections " + "where us = ", SUS, " " + "and deleted = 0 " + "and with_user = ", SUser, " " + "and with_server = ", SServer, " " + "and with_resource = ", SRes, " " + "and utc = ", SUTC]) of + {selected, _, [C | _]} -> get_collection_from_query_result(C); + _ -> throw({error, ?ERR_ITEM_NOT_FOUND}) + end. + +get_collection_by_id(CID) -> + {selected, _, [C | _]} = run_sql_query(["select * " + "from archive_collections " + "where id = ", escape(CID)]), + get_collection_from_query_result(C). + +get_collection_from_query_result({CID, PrevId, NextId, US, User, Server, Resource, UTC, + ChBy, ChUTC, Deleted, Subject, Thread, Crypt, Extra}) -> + #archive_collection{id = decode_integer(CID), + us = get_us_separated(US), + jid = jlib:jid_tolower(jlib:make_jid(User, Server, Resource)), + utc = decode_timestamp(UTC), + prev = get_collection_link_by_id(decode_integer(PrevId)), + next = get_collection_link_by_id(decode_integer(NextId)), + change_by = case ChBy of + null -> {undefined, undefined, undefined}; + R -> jlib:jid_tolower(jlib:string_to_jid(R)) + end, + change_utc = case ChUTC of + null -> undefined; + R -> decode_timestamp(R) + end, + deleted = case decode_integer(Deleted) of + 0 -> false; + 1 -> true; + _ -> throw({error, ?ERR_INTERNAL_SERVER_ERROR}) + end, + subject = case Subject of + null -> undefined; + R -> R + end, + thread = case Thread of + null -> undefined; + R -> R + end, + crypt = case decode_integer(Crypt) of + null -> false; + R -> R == 1 + end, + extra = case Extra of + null -> undefined; + R -> R + end}. + +get_collection_link_by_id(null) -> []; + +get_collection_link_by_id(CID) -> + {selected, _, [{_, User, Server, Resource, UTC} | _]} = + run_sql_query(["select id, with_user, with_server, with_resource, utc " + "from archive_collections " + "where id = ", escape(CID)]), + {jlib:jid_tolower(jlib:make_jid(User, Server, Resource)), decode_timestamp(UTC)}. + +collection_to_xml(C) -> + PrevLink = collection_link_to_xml("previous", C#archive_collection.prev), + NextLink = collection_link_to_xml("next", C#archive_collection.next), + PrevXML = if PrevLink /= [] -> [PrevLink]; true -> [] end, + NextXML = if NextLink /= [] -> [NextLink]; true -> [] end, + ExtraNonEmpty = is_non_empty(C#archive_collection.extra), + ExtraXML = if ExtraNonEmpty == true -> [decode_extra(C#archive_collection.extra)]; true -> [] end, + {xmlelement, "chat", + lists:append([ + [{"with", jlib:jid_to_string(C#archive_collection.jid)}], + [{"start", get_datetime_string_from_seconds(C#archive_collection.utc)}], + if C#archive_collection.subject /= "", + C#archive_collection.subject /= undefined -> + [{"subject", C#archive_collection.subject}]; + true -> + [] + end, + if C#archive_collection.thread /= "", + C#archive_collection.thread /= undefined -> + [{"thread", C#archive_collection.thread}]; + true -> + [] + end, + if C#archive_collection.crypt -> [{"crypt", "true"}]; + true -> [] + end]), + lists:append([ + PrevXML, + NextXML, + ExtraXML])}. + + +%% +%% This function returns messages that satisfy request restrictions +%% +get_messages(C, RSM) -> + CID = C#archive_collection.id, + Count = get_messages_count(CID, {0, undefined}, {infinity, undefined}), + if Count == 0 -> + {ok, [], []}; + true -> + Msgs = get_messages_query(CID, RSM), + CHTime = get_datetime_string_from_seconds(C#archive_collection.change_utc), + if Msgs == [] -> + {ok, [], make_rsm(undefined, undefined, undefined, CHTime, Count)}; + true -> + Msgs1 = reverse_items_if_needed(Msgs, RSM), + [FirstMsg | _] = Msgs1, + LastMsg = lists:last(Msgs1), + {FirstUTC, FirstID} = {FirstMsg#archive_message.utc, FirstMsg#archive_message.id}, + {LastUTC, LastID} = {LastMsg#archive_message.utc, LastMsg#archive_message.id}, + FirstIndex = get_messages_count(CID, {0, undefined}, {FirstUTC, FirstID}), + {ok, Msgs1, make_rsm(FirstIndex, + make_rsm_range_item(FirstUTC, FirstID), + make_rsm_range_item(LastUTC, LastID), + CHTime, + Count)} + end + end. + +get_messages_count(CID, {Start, StartID}, {End, EndID}) -> + {selected, _, [{Count}]} = + run_sql_query(["select count(*) from archive_messages " + "where coll_id = ", escape(CID), " ", + get_request_part_times("utc", {Start, StartID}, {End, EndID})]), + decode_integer(Count). + +get_messages_query(CID, RSM) -> + case run_sql_query(["select * from archive_messages " + "where coll_id = ", escape(CID), " ", + get_request_part_range("utc", RSM)]) of + {selected, _, Rs} -> get_messages_list(Rs) + end. + +get_message_from_query_result({MID, CID, UTC, Dir, Body, Name}) -> + #archive_message{id = decode_integer(MID), + coll_id = decode_integer(CID), + utc = decode_timestamp(UTC), + direction = case decode_integer(Dir) of + 0 -> from; + 1 -> to; + 2 -> note + end, + body = Body, + name = Name}. + +get_messages_list(Msgs) -> + lists:map(fun(Msg) -> get_message_from_query_result(Msg) end, Msgs). + +message_to_xml(M, Start) -> + Dir = atom_to_list(M#archive_message.direction), + Secs = M#archive_message.utc - Start, + {xmlelement, Dir, + lists:append([ + if Dir == "note"; Secs < 0 -> + UTCStr = get_datetime_string_from_seconds(M#archive_message.utc), + [{"utc", UTCStr}]; + true -> [{"secs", integer_to_list(Secs)}] + end, + if M#archive_message.name /= "" -> [{"name", M#archive_message.name}]; + true -> [] + end]), + [if Dir == "note" -> {xmlcdata, M#archive_message.body}; + true -> {xmlelement, "body", [], [{xmlcdata, M#archive_message.body}]} + end]}. + +%% +%% This function returns modifications that satisfy request restrictions. +%% +get_modified(LUser, LServer, {RSM, Start, End}) -> + SUS = get_us_escaped({LUser, LServer}), + Count = get_modified_count(SUS, {Start, undefined}, {End, undefined}), + if Count == 0 -> {ok, [], []}; + true -> + Changes = get_modified_raw(SUS, RSM), + MaxCHTime = get_datetime_string_from_seconds(get_modified_max_change_time(SUS, Start, End)), + if Changes == [] -> + {ok, [], make_rsm(undefined, undefined, undefined, MaxCHTime, Count)}; + true -> + Changes1 = reverse_items_if_needed(Changes, RSM), + [FirstCH | _] = Changes1, + LastCH = lists:last(Changes1), + FirstUTC = FirstCH#archive_collection.change_utc, + FirstID = FirstCH#archive_collection.id, + LastUTC = LastCH#archive_collection.change_utc, + LastID = LastCH#archive_collection.id, + FirstIndex = get_modified_count(SUS, {Start, undefined}, + {FirstUTC, FirstID}), + {ok, Changes1, make_rsm(FirstIndex, + make_rsm_range_item(FirstUTC, FirstID), + make_rsm_range_item(LastUTC, LastID), + MaxCHTime, + Count)} + end + end. + +get_modified_count(SUS, Start, End) -> + get_collections_links_count_tmpl(SUS, {undefined, undefined, undefined}, + "change_utc", true, Start, End). + +get_modified_raw(SUS, RSM) -> + {selected, _, Rs} = + run_sql_query(["select id, us, change_by, with_user, with_server, with_resource, " + "utc, change_utc, deleted from archive_collections ", + get_collections_link_req_where(SUS, {undefined, undefined, undefined}, true), + get_request_part_range("change_utc", RSM)]), + lists:map(fun(Change) -> get_change_from_query_result(Change) end, Rs). + +get_modified_max_change_time(SUS, Start, End) -> + {selected, _, [{CHTime}]} = + run_sql_query(["select max(change_utc) ", + "from archive_collections ", + get_collections_link_req_where(SUS, {undefined, undefined, undefined}, true), + get_request_part_times("change_utc", {Start, undefined}, {End, undefined})]), + decode_timestamp(CHTime). + +%% +%% This is implementation of replication as specified in XEP-136. As the whole concept +%% is broken (see below) you should not use it, it is provided onlt for compliance with +%% the XEP. +%% +%% !!! NOTE !!! : poor decision about "after" usage in replication in XEP-136 breaks +%% down things if there are several changes with the same time and RSM request stops +%% somewhere between them - there's no way to get all remaining items. +%% +get_modified_legacy(LUser, LServer, {{range, {Start, undefined}, {_, _}, _}, Max}) -> + SUS = get_us_escaped({LUser, LServer}), + Secs = get_seconds_from_datetime_string(Start), + Count = get_modified_count(SUS, {Secs, undefined}, {infinity, undefined}), + if Count == 0 -> {ok, [], []}; + true -> + Changes = get_modified_raw(SUS, {{range, {Secs, undefined}, {infinity, undefined}, normal}, Max}), + MaxCHTime = get_datetime_string_from_seconds(get_modified_max_change_time(SUS, Secs, infinity)), + if Changes == [] -> + {ok, [], make_rsm(undefined, undefined, undefined, MaxCHTime, Count)}; + true -> + %% We do not check for reversing changes here - we do not process "before" in + %% legacy mode anyway. + [FirstCH | _] = Changes, + LastCH = lists:last(Changes), + FirstUTC = FirstCH#archive_collection.change_utc, + LastUTC = LastCH#archive_collection.change_utc, + FirstIndex = get_modified_count(SUS, {Secs, undefined}, + {FirstUTC, undefined}), + {ok, Changes, make_rsm(FirstIndex, + get_datetime_string_from_seconds(FirstUTC), + get_datetime_string_from_seconds(LastUTC), + MaxCHTime, + Count)} + end + end. + +get_change_from_query_result({CID, US, By, User, Server, Resource, UTC, CHUTC, Deleted}) -> + #archive_collection{id = decode_integer(CID), + us = get_us_separated(US), + change_by = jlib:jid_tolower(jlib:string_to_jid(By)), + jid = jlib:jid_tolower(jlib:make_jid(User, Server, Resource)), + utc = decode_timestamp(UTC), + deleted = decode_integer(Deleted), + change_utc = decode_timestamp(CHUTC)}. + +change_to_xml(C) -> + CHType = + case C#archive_collection.deleted of + 1 -> "removed"; + 0 -> "changed" + end, + {xmlelement, CHType, + [{"with", jlib:jid_to_string(C#archive_collection.jid)}, + {"start", get_datetime_string_from_seconds(C#archive_collection.utc)}, + {"by", jlib:jid_to_string(C#archive_collection.change_by)}], []}. + + + +%% +%% Preferences-related retrieval functions +%% + +get_global_prefs(US) -> + SUS = get_us_escaped(US), + case run_sql_query(["select * from archive_global_prefs " + "where us = ", SUS]) of + {selected, _, [C | _]} -> get_global_prefs_from_query_result(C); + _ -> #archive_global_prefs{} + end. + +get_global_prefs_from_query_result({US, Save, Expire, OTR, + MAuto, MLocal, MManual, AutoSave}) -> + {RSave, RExpire, ROTR} = get_common_prefs_from_query_result(Save, Expire, OTR), + #archive_global_prefs{us = get_us_separated(US), + save = RSave, + expire = RExpire, + otr = ROTR, + method_auto = get_method_from_query_result(MAuto), + method_local = get_method_from_query_result(MLocal), + method_manual = get_method_from_query_result(MManual), + auto_save = case decode_integer(AutoSave) of + 1 -> true; + 0 -> false; + null -> undefined + end}. + +get_method_from_query_result(Method) -> + case decode_integer(Method) of + 0 -> prefer; + 1 -> concede; + 2 -> forbid; + _ -> undefined + end. + +get_jid_prefs(US, JID) -> + SUS = get_us_escaped(US), + {SUser, SServer, SRes} = get_jid_escaped(JID), + case run_sql_query(["select * from archive_jid_prefs " + "where us = ", SUS, " " + "and with_user = ", SUser, " " + "and with_server = ", SServer, " " + "and with_resource = ", SRes]) of + {selected, _, [C | _]} -> get_jid_prefs_from_query_result(C); + _ -> #archive_jid_prefs{} + end. + +get_all_jids_prefs(US) -> + SUS = get_us_escaped(US), + case run_sql_query(["select * from archive_jid_prefs " + "where us = ", SUS]) of + {selected, _, Rs} -> lists:map(fun(P) -> get_jid_prefs_from_query_result(P) end, Rs); + _ -> throw({error, ?ERR_INTERNAL_SERVER_ERROR}) + end. + +get_jid_prefs_from_query_result({US, User, Server, Resource, Save, Expire, OTR}) -> + {RSave, RExpire, ROTR} = + get_common_prefs_from_query_result(Save, Expire, OTR), + #archive_jid_prefs{us = get_us_separated(US), + jid = jlib:jid_tolower(jlib:make_jid(User, Server, Resource)), + save = RSave, + expire = RExpire, + otr = ROTR}. + +get_common_prefs_from_query_result(Save, Expire, OTR) -> + {case decode_integer(Save) of + 1 -> body; + 0 -> false; + _ -> undefined + end, + case decode_integer(Expire) of + null -> undefined; + N -> N + end, + case decode_integer(OTR) of + 0 -> approve; + 1 -> concede; + 2 -> forbid; + 3 -> oppose; + 4 -> prefer; + 5 -> require; + _ -> undefined + end}. + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% +%% Dealing with collections expiration +%% +%% TODO: looks scaring, but I do not see any other realistic way to do it +%% without involving the caller ... +%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%% +expire_collections(Host) -> + STS = encode_timestamp(get_timestamp()), + + ExpiredByPrefJID = [get_expired_str(Host, "archive_jid_prefs.expire", "utc"), " < ", STS], + ExpiredByPrefGlobal = [get_expired_str(Host, "archive_global_prefs.expire", "utc"), " < ", STS], + ExpiredByDefault = case gen_mod:get_module_opt(Host, ?MODULE, default_expire, infinity) of + infinity -> ""; + N -> [get_expired_str(Host, integer_to_list(N), "utc"), " < ", STS] + end, + + ExistsFullJID = ["exists (select * from archive_jid_prefs " + "where archive_collections.us = archive_jid_prefs.us " + "and archive_collections.with_server = archive_jid_prefs.with_server " + "and archive_collections.with_user = archive_jid_prefs.with_user " + "and archive_collections.with_resource = archive_jid_prefs.with_resource"], + ExistsBareJID = ["exists (select * from archive_jid_prefs " + "where archive_collections.us = archive_jid_prefs.us " + "and archive_collections.with_server = archive_jid_prefs.with_server " + "and archive_collections.with_user = archive_jid_prefs.with_user " + "and archive_jid_prefs.with_resource = ''"], + ExistsDomainJID = ["exists (select * from archive_jid_prefs " + "where archive_collections.us = archive_jid_prefs.us " + "and archive_collections.with_server = archive_jid_prefs.with_server " + "and archive_jid_prefs.with_user = '' " + "and archive_jid_prefs.with_resource = ''"], + ExistsGlobal = ["exists (select * from archive_global_prefs " + "where archive_collections.us = archive_global_prefs.us"], + + F = fun() -> + run_sql_query([ + "update archive_collections " + "set deleted = 1, " + "change_by = ", escape(Host), ", " + "change_utc = ", STS, " " + "where deleted = 0 and (", + + ExistsFullJID, " and ", ExpiredByPrefJID, ") " + + "or not ", ExistsFullJID, ") and ", ExistsBareJID, " and ", ExpiredByPrefJID, ") " + + "or not ", ExistsFullJID, ") and not ", ExistsBareJID, ") and ", ExistsDomainJID, + " and ", ExpiredByPrefJID, ") " + + "or not ", ExistsFullJID, ") and not ", ExistsBareJID, ") and not ", ExistsDomainJID, ") " + "and ", ExistsGlobal, " and ", ExpiredByPrefGlobal, ") ", + + if ExpiredByDefault /= "" -> + ["or not ", ExistsFullJID, ") and not ", ExistsBareJID, ") and not ", ExistsDomainJID, ") " + "and not ", ExistsGlobal, ") and ", ExpiredByDefault]; + true -> "" + end, + ")"]), + case gen_mod:get_module_opt(Host, ?MODULE, replication_expire, 31536000) of + infinity -> []; + N1 -> + run_sql_query(["delete from archive_collections " + "where deleted = 1 " + "and ", get_expired_str(Host, integer_to_list(N1), "change_utc"), " < ", STS]) + end + end, + run_sql_transaction(Host, F). + +get_expired_str(Host, ExpExpr, UTCField) -> + case jlib:tolower(gen_mod:get_module_opt(Host, ?MODULE, database_type, "")) of + "mysql" -> ["timestampadd(second, ", ExpExpr, ", archive_collections.", UTCField, ")"]; + "sqlite" -> ["datetime(archive_collections.", UTCField, ", '+' || ", ExpExpr, " || ' seconds')"]; + "pgsql" -> ["timestamp archive_collections.", UTCField, " + interval ", ExpExpr, " || ' seconds'"]; + _ -> throw({error, ?ERR_INTERNAL_SERVER_ERROR}) + end. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% +%% Utility functions to make database interaction easier. +%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +%% Noone seems to follow standards these days :-( +%% We have to perform DB-specific escaping,as f.e. SQLite does not understand +%% '\' as escaping character (which is exactly in accordance with the standard, +%% by the way), while most other DBs do. + +%% Generic, DB-independent escaping for integers and simple strings. +escape(null) -> + "null"; +escape(undefined) -> + "null"; +escape(infinity) -> + integer_to_list(?INFINITY); +escape(Num) when is_integer(Num) -> + integer_to_list(Num); +escape(Str) -> + "'" ++ [escape_chars(C) || C <- Str] ++ "'". + +%% DB-specific strings escaping. +escape_str(_, null) -> + "null"; +escape_str(_, undefined) -> + "null"; +escape_str(LServer, Str) -> + case jlib:tolower(gen_mod:get_module_opt(LServer, ?MODULE, database_type, "")) of + "sqlite" -> "'" ++ [escape_chars(C) || C <- Str] ++ "'"; + _ -> "'" ++ ejabberd_odbc:escape(Str) ++ "'" + end. + +%% Characters to escape +escape_chars($') -> "''"; +escape_chars(C) -> C. + +%% Assume that if there are no sub-elements for "x" tag - this is +%% extra info removal request +encode_extra({xmlelement, "x", _, []}) -> + ""; +%% We could try to use BLOBs here, but base64 in text columns should +%% be more porable and should be enough - it's unlikely someone +%% will store much info here anyway. +encode_extra(Extra) -> + jlib:encode_base64(binary_to_list(term_to_binary(Extra))). + +decode_extra(Extra) -> + binary_to_term(list_to_binary(jlib:decode_base64(Extra))). + +encode_timestamp(infinity) -> + escape(get_sql_datetime_string_from_seconds(?INFINITY)); + +encode_timestamp(TS) -> + escape(get_sql_datetime_string_from_seconds(TS)). + +decode_timestamp(Str) -> + get_seconds_from_sql_datetime_string(Str). + +timestamp_to_integer(infinity) -> + ?INFINITY; +timestamp_to_integer(Num) -> + Num. + +get_us_escaped({LUser, LServer}) -> + escape(LUser ++ "@" ++ LServer). + +get_us_separated(US) -> + JID = jlib:string_to_jid(US), + #jid{luser = LUser, lserver = LServer} = JID, + {LUser, LServer}. + +get_jid_escaped({LUser, LServer, LResource}) -> + {escape(LUser), escape(LServer), escape(LResource)}. + +get_jid_full_escaped({LUser, LServer, undefined}) -> + escape(LUser ++ "@" ++ LServer); +get_jid_full_escaped({LUser, LServer, ""}) -> + escape(LUser ++ "@" ++ LServer); +get_jid_full_escaped({LUser, LServer, LResource}) -> + escape(LUser ++ "@" ++ LServer ++ "/" ++ LResource). + +decode_integer(Val) when is_integer(Val) -> + Val; +decode_integer(null) -> + null; +decode_integer(Val) -> + list_to_integer(Val). + +is_non_empty(null) -> false; +is_non_empty(undefined) -> false; +is_non_empty("") -> false; +is_non_empty("''") -> false; +is_non_empty(_) -> true. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% +%% Date-time handling. +%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +get_seconds_from_datetime_string(Str) -> + case jlib:datetime_string_to_timestamp(Str) of + undefined -> throw({error, ?ERR_BAD_REQUEST}); + No -> + calendar:datetime_to_gregorian_seconds(calendar:now_to_datetime(No)) + end. + +get_datetime_string_from_seconds(Secs) -> + Zero = calendar:datetime_to_gregorian_seconds({{1970, 1, 1}, {0, 0, 0}}), + Secs2 = Secs - Zero, + jlib:now_to_utc_string({Secs2 div 1000000, Secs2 rem 1000000, 0}). + +get_seconds_from_sql_datetime_string(Str) -> + case sql_datetime_string_to_timestamp(Str) of + undefined -> throw({error, ?ERR_BAD_REQUEST}); + No -> + calendar:datetime_to_gregorian_seconds(calendar:now_to_datetime(No)) + end. + +get_sql_datetime_string_from_seconds(Secs) -> + Zero = calendar:datetime_to_gregorian_seconds({{1970, 1, 1}, {0, 0, 0}}), + Secs2 = Secs - Zero, + now_to_utc_sql_datetime({Secs2 div 1000000, Secs2 rem 1000000, 0}). + +%% We do not output MicroSecs as our timestamps are seconds-based anyway, also +%% it may help to be more portable between SQL servers. +now_to_utc_sql_datetime({MegaSecs, Secs, MicroSecs}) -> + {{Year, Month, Day}, {Hour, Minute, Second}} = + calendar:now_to_universal_time({MegaSecs, Secs, MicroSecs}), + lists:flatten( + io_lib:format("~4..0w-~2..0w-~2..0w ~2..0w:~2..0w:~2..0w", + [Year, Month, Day, Hour, Minute, Second])). + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% +%% Copy-paste-modified from jlib.erl, as jlib:datetime_string_to_timestamp does not tolerate SQL syntax. +%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +%% 'yyyy-mm-dd hh:mm:ss[.sss]' -> {MegaSecs, Secs, MicroSecs} +sql_datetime_string_to_timestamp(TimeStr) -> + case catch parse_sql_datetime(TimeStr) of + {'EXIT', _Err} -> + undefined; + TimeStamp -> + TimeStamp + end. + +parse_sql_datetime(TimeStr) -> + [Date, Time] = string:tokens(TimeStr, " "), + D = parse_date(Date), + {T, MS, TZH, TZM} = parse_time(Time), + S = calendar:datetime_to_gregorian_seconds({D, T}), + S1 = calendar:datetime_to_gregorian_seconds({{1970, 1, 1}, {0, 0, 0}}), + Seconds = (S - S1) - TZH * 60 * 60 - TZM * 60, + {Seconds div 1000000, Seconds rem 1000000, MS}. + +%% yyyy-mm-dd +parse_date(Date) -> + [Y, M, D] = string:tokens(Date, "-"), + Date1 = {list_to_integer(Y), list_to_integer(M), list_to_integer(D)}, + case calendar:valid_date(Date1) of + true -> + Date1; + _ -> + false + end. + +%% hh:mm:ss[.sss] +parse_time(Time) -> + [HMS | T] = string:tokens(Time, "."), + MS = case T of + [] -> + 0; + [Val] -> + list_to_integer(string:left(Val, 6, $0)) + end, + [H, M, S] = string:tokens(HMS, ":"), + {[H1, M1, S1], true} = check_list([{H, 24}, {M, 60}, {S, 60}]), + {{H1, M1, S1}, MS, 0, 0}. + +check_list(List) -> + lists:mapfoldl( + fun({L, N}, B)-> + V = list_to_integer(L), + if + (V >= 0) and (V =< N) -> + {V, B}; + true -> + {false, false} + end + end, true, List). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% +%% End of copy-paste-modified +%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +get_timestamp() -> + calendar:datetime_to_gregorian_seconds(calendar:universal_time()). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% +%% Wrapper functions to perform queries and transactions. +%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +run_sql_query(Query) -> + %%?MYDEBUG("running query: ~p", [lists:flatten(Query)]), + case catch ejabberd_odbc:sql_query_t(Query) of + {'EXIT', Err} -> + ?ERROR_MSG("unhandled exception during query: ~p", [Err]), + exit(Err); + {error, Err} -> + ?ERROR_MSG("error during query: ~p", [Err]), + throw({error, Err}); + aborted -> + ?ERROR_MSG("query aborted", []), + throw(aborted); + R -> %?MYDEBUG("query result: ~p", [R]), + R + end. + +run_sql_transaction(LServer, F) -> + DBHost = gen_mod:get_module_opt(LServer, ?MODULE, db_host, LServer), + case ejabberd_odbc:sql_transaction(DBHost, F) of + {atomic, R} -> + %%?MYDEBUG("succeeded transaction: ~p", [R]), + R; + {error, Err} -> {error, Err}; + E -> + ?ERROR_MSG("failed transaction: ~p, stack: ~p", [E, process_info(self(),backtrace)]), + {error, ?ERR_INTERNAL_SERVER_ERROR} + end. + +%% return either {error, Err} or {LUser, LServer, Jid, Start} +link_from_argument(LUser, LServer, Elem) -> + case parse_root_argument(Elem) of + {error, E} -> {error, E}; + {interval, Start, _, JID} when Start /= 0, + JID /= undefined -> + {LUser, LServer, JID, Start}; + _ -> throw({error, ?ERR_BAD_REQUEST}) + end. + +%%parse commons arguments of root elements + +parse_root_argument({xmlelement, _, Attrs, _}) -> + With = xml:get_attr_s("with", Attrs), + Start = xml:get_attr_s("start", Attrs), + End = xml:get_attr_s("end", Attrs), + {interval, + if Start /= "" -> get_seconds_from_datetime_string(Start); true -> 0 end, + if End /= "" -> get_seconds_from_datetime_string(End); true -> infinity end, + if With /= "" -> jlib:jid_tolower(jlib:string_to_jid(With)); true -> undefined end}. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% Result Set Management (JEP-0059) +%% +%% +-define(MY_NS_RSM, "http://jabber.org/protocol/rsm"). + + +%% If "index" is specified, returns {{index, Index}, Max}, +%% otherwise returns {{range, {StartTime, StartID}, {EndTime, EndID}, Order}, Max} +%% where Order == 'normal' means that up to Max elements should be output +%% from Start, and 'reversed' - from End respectively. + +%% !!! TODO: rewrite in "parse_root_argument" style, without recursion. + +parse_rsm([A | Tail]) -> + case A of + {xmlelement, _, Attrs1, _} -> + case xml:get_attr_s("xmlns", Attrs1) of + ?MY_NS_RSM -> + parse_rsm(A); + _ -> + parse_rsm(Tail) + end; + _ -> + parse_rsm(Tail) + end; +parse_rsm([]) -> + {{range, {0, undefined}, {infinity, undefined}, normal}, undefined}; + +parse_rsm({xmlelement, "set", _, SubEls}) -> + parse_rsm_aux(SubEls, {{range, {0, undefined}, {infinity, undefined}, normal}, undefined}); + +parse_rsm(_) -> + throw({error, ?ERR_BAD_REQUEST}). + +parse_rsm_aux([{xmlelement, "max", _Attrs, Contents} | Tail], Acc) -> + case catch list_to_integer(xml:get_cdata(Contents)) of + P when is_integer(P) -> + case Acc of + {Req, undefined} -> + parse_rsm_aux(Tail, {Req, P}); + _ -> + throw({error, ?ERR_BAD_REQUEST}) + end; + _ -> + throw({error, ?ERR_BAD_REQUEST}) + end; + +parse_rsm_aux([{xmlelement, "index", _Attrs, Contents} | Tail], Acc) -> + case catch list_to_integer(xml:get_cdata(Contents)) of + P when is_integer(P) -> + case Acc of + {{range, {0, undefined}, {infinity, undefined}, normal}, Max} -> + parse_rsm_aux(Tail, {{index, P}, Max}); + _ -> + throw({error, ?ERR_BAD_REQUEST}) + end; + _ -> + throw({error, ?ERR_BAD_REQUEST}) + end; + +parse_rsm_aux([{xmlelement, "after", _Attrs, Contents} | Tail], Acc) -> + case Acc of + {{range, {0, undefined}, {infinity, undefined}, normal}, Max} -> + parse_rsm_aux(Tail, {{range, parse_rsm_range_item(xml:get_cdata(Contents)), {infinity, undefined}, normal}, Max}); + _ -> + throw({error, ?ERR_BAD_REQUEST}) + end; + +parse_rsm_aux([{xmlelement, "before", _Attrs, Contents} | Tail], Acc) -> + case Acc of + {{range, {0, undefined}, {infinity, undefined}, normal}, Max} -> + BT = case xml:get_cdata(Contents) of + [] -> {infinity, undefined}; + CD -> parse_rsm_range_item(CD) + end, + parse_rsm_aux(Tail, {{range, {0, undefined}, BT, reversed}, Max}); + _ -> + throw({error, ?ERR_BAD_REQUEST}) + end; + +parse_rsm_aux([_ | Tail], Acc) -> + parse_rsm_aux(Tail, Acc); +parse_rsm_aux([], Acc) -> + Acc. + +make_rsm(undefined, undefined, undefined, Changed, Count) -> + [{xmlelement, "set", [{"xmlns", ?MY_NS_RSM}], [ + {xmlelement, "changed", [], [{xmlcdata, Changed}]}, + {xmlelement, "count", [], [{xmlcdata, integer_to_list(Count)}]}]}]; + +make_rsm(FirstIndex, FirstId, LastId, Changed, Count) -> + [{xmlelement, "set", [{"xmlns", ?MY_NS_RSM}], [ + {xmlelement, "first", [{"index", integer_to_list(FirstIndex)}], [{xmlcdata, FirstId}]}, + {xmlelement, "last", [], [{xmlcdata, LastId}]}, + {xmlelement, "changed", [], [{xmlcdata, Changed}]}, + {xmlelement, "count", [], [{xmlcdata, integer_to_list(Count)}]}]}]. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% +%% Utility functions for RSM +%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +parse_rsm_range_item(Item) -> + Len = string:len(Item), + Pos = string:chr(Item, $@), + if Pos == 0 -> + %% It must be either bad request or stupid RSM-136 special case for replication :-( + %% It's not easy to distinguish between them here, so we just return at least smth, + %% so that it can be dealt later with. + {Item, undefined}; + true -> + %% we do not care about exact length in second "sublist", it should only be bigger than string length. + {list_to_integer(lists:sublist(Item, Pos - 1)), list_to_integer(lists:sublist(Item, Pos + 1, Len))} + end. + +make_rsm_range_item(UTC, ID) -> + integer_to_list(UTC) ++ "@" ++ integer_to_list(ID). diff --git a/mod_archive/src/mod_archive_odbc_mysql.sql b/mod_archive/src/mod_archive_odbc_mysql.sql new file mode 100644 index 0000000..2b3ee7b --- /dev/null +++ b/mod_archive/src/mod_archive_odbc_mysql.sql @@ -0,0 +1,83 @@ +CREATE DATABASE IF NOT EXISTS ejabberd CHARACTER SET utf8 COLLATE utf8_general_ci; + +USE ejabberd; + +SET table_type=InnoDB; + +CREATE TABLE archive_collections(id INTEGER NOT NULL AUTO_INCREMENT, + prev_id INTEGER, + next_id INTEGER, + us VARCHAR(2047) NOT NULL, + with_user VARCHAR(1023) NOT NULL, + with_server VARCHAR(1023) NOT NULL, + with_resource VARCHAR(1023) NOT NULL, + utc DATETIME NOT NULL, + change_by VARCHAR(3071), + change_utc DATETIME, + deleted TINYINT, + subject VARCHAR(1023), + thread VARCHAR(1023), + crypt TINYINT, + extra VARCHAR(32767), + PRIMARY KEY(id)) + CHARACTER SET utf8 + COLLATE utf8_general_ci; +CREATE INDEX IDX_archive_colls_with ON archive_collections(us(16),with_user(8),with_server(8),utc); +CREATE INDEX IDX_archive_colls_prev_id ON archive_collections(prev_id); +CREATE INDEX IDX_archive_colls_next_id ON archive_collections(next_id); +CREATE INDEX IDX_archive_colls_utc ON archive_collections(us(16),utc); +CREATE INDEX IDX_archive_colls_change ON archive_collections(deleted,change_utc); + +CREATE TABLE archive_messages(id INTEGER NOT NULL AUTO_INCREMENT, + coll_id INTEGER NOT NULL, + utc DATETIME NOT NULL, + dir TINYINT, + body VARCHAR(63488), + name VARCHAR(1023), + PRIMARY KEY(id)) + CHARACTER SET utf8 + COLLATE utf8_general_ci; +CREATE INDEX IDX_archive_msgs_coll_id ON archive_messages(coll_id,utc); + +CREATE TABLE archive_jid_prefs(us VARCHAR(2047) NOT NULL, + with_user VARCHAR(1023) NOT NULL, + with_server VARCHAR(1023) NOT NULL, + with_resource VARCHAR(1023) NOT NULL, + save TINYINT, + expire INTEGER, + otr TINYINT, + PRIMARY KEY (us(16),with_user(8),with_server(8),with_resource(8))) + CHARACTER SET utf8 + COLLATE utf8_general_ci; + +CREATE TABLE archive_global_prefs(us VARCHAR(2047) NOT NULL, + save TINYINT, + expire INTEGER, + otr TINYINT, + method_auto TINYINT, + method_local TINYINT, + method_manual TINYINT, + auto_save TINYINT, + PRIMARY KEY (us(16))) + CHARACTER SET utf8 + COLLATE utf8_general_ci; + +DELIMITER | + +CREATE TRIGGER archive_collections_delete BEFORE DELETE ON archive_collections +FOR EACH ROW +BEGIN + DELETE FROM archive_messages WHERE coll_id = OLD.id; +END; +| + +CREATE TRIGGER archive_collections_update BEFORE UPDATE ON archive_collections +FOR EACH ROW +BEGIN + IF NEW.deleted = 1 THEN + DELETE FROM archive_messages WHERE coll_id = NEW.id; + END IF; +END; +| + +DELIMITER ; diff --git a/mod_archive/src/mod_archive_odbc_pgsql.sql b/mod_archive/src/mod_archive_odbc_pgsql.sql new file mode 100644 index 0000000..32b12b6 --- /dev/null +++ b/mod_archive/src/mod_archive_odbc_pgsql.sql @@ -0,0 +1,72 @@ +DROP TABLE archive_collections; + +CREATE TABLE archive_collections(id SERIAL not null, + prev_id INTEGER, + next_id INTEGER, + us VARCHAR(2047) NOT NULL, + with_user VARCHAR(1023) NOT NULL, + with_server VARCHAR(1023) NOT NULL, + with_resource VARCHAR(1023) NOT NULL, + utc timestamp NOT NULL, + change_by VARCHAR(3071), + change_utc timestamp, + deleted INTEGER, + subject VARCHAR(1023), + thread VARCHAR(1023), + crypt INTEGER, + extra VARCHAR(32767), + PRIMARY KEY(id)); +CREATE INDEX IDX_archive_colls_prev_id ON archive_collections(prev_id); +CREATE INDEX IDX_archive_colls_next_id ON archive_collections(next_id); +CREATE INDEX IDX_archive_colls_us ON archive_collections(us); +CREATE INDEX IDX_archive_colls_with_server ON archive_collections(with_server); +CREATE INDEX IDX_archive_colls_with_user ON archive_collections(with_user); +CREATE INDEX IDX_archive_colls_with_resource ON archive_collections(with_resource); +CREATE INDEX IDX_archive_colls_utc ON archive_collections(utc); +CREATE INDEX IDX_archive_colls_change_utc ON archive_collections(change_utc); + +DROP TABLE archive_messages; +CREATE TABLE archive_messages(id SERIAL NOT NULL, + coll_id INTEGER NOT NULL, + utc timestamp NOT NULL, + dir INTEGER, + body VARCHAR(65535), + name VARCHAR(1023), + PRIMARY KEY(id)); +CREATE INDEX IDX_archive_msgs_coll_id ON archive_messages(coll_id); +CREATE INDEX IDX_archive_msgs_utc ON archive_messages(utc); + +DROP TABLE archive_jid_prefs; +CREATE TABLE archive_jid_prefs(us VARCHAR(2047) NOT NULL, + with_user VARCHAR(1023) NOT NULL, + with_server VARCHAR(1023) NOT NULL, + with_resource VARCHAR(1023) NOT NULL, + save integer, + expire INTEGER, + otr integer); +CREATE INDEX IDX_archive_jid_prefs_us ON archive_jid_prefs(us); +CREATE INDEX IDX_archive_jid_prefs_with_user ON archive_jid_prefs(with_user); +CREATE INDEX IDX_archive_jid_prefs_with_server ON archive_jid_prefs(with_server); +CREATE INDEX IDX_archive_jid_prefs_with_resource ON archive_jid_prefs(with_resource); + +DROP TABLE archive_global_prefs; +CREATE TABLE archive_global_prefs(us VARCHAR(2047) NOT NULL, + save integer, + expire INTEGER, + otr integer, + method_auto integer, + method_local integer, + method_manual integer, + auto_save integer); +CREATE INDEX IDX_archive_global_prefs_us ON archive_global_prefs(us); + + +CREATE RULE archive_collections_delete AS ON DELETE + TO archive_collections + DO DELETE FROM archive_messages WHERE coll_id = OLD.id; + +CREATE RULE archive_collections_update AS ON UPDATE + TO archive_collections + DO DELETE FROM archive_messages WHERE coll_id = NEW.id and NEW.deleted=1; + + diff --git a/mod_archive/src/mod_archive_odbc_sqlite3.sql b/mod_archive/src/mod_archive_odbc_sqlite3.sql new file mode 100644 index 0000000..6363217 --- /dev/null +++ b/mod_archive/src/mod_archive_odbc_sqlite3.sql @@ -0,0 +1,70 @@ +CREATE TABLE archive_collections(id INTEGER NOT NULL, + prev_id INTEGER, + next_id INTEGER, + us VARCHAR(2047) NOT NULL, + with_user VARCHAR(1023) NOT NULL, + with_server VARCHAR(1023) NOT NULL, + with_resource VARCHAR(1023) NOT NULL, + utc DATETIME NOT NULL, + change_by VARCHAR(3071), + change_utc DATETIME, + deleted INTEGER, + subject VARCHAR(1023), + thread VARCHAR(1023), + crypt INTEGER, + extra VARCHAR(32767), + PRIMARY KEY(id)); +CREATE INDEX IDX_archive_colls_prev_id ON archive_collections(prev_id); +CREATE INDEX IDX_archive_colls_next_id ON archive_collections(next_id); +CREATE INDEX IDX_archive_colls_us ON archive_collections(us); +CREATE INDEX IDX_archive_colls_with_server ON archive_collections(with_server); +CREATE INDEX IDX_archive_colls_with_user ON archive_collections(with_user); +CREATE INDEX IDX_archive_colls_with_resource ON archive_collections(with_resource); +CREATE INDEX IDX_archive_colls_utc ON archive_collections(utc); +CREATE INDEX IDX_archive_colls_change_utc ON archive_collections(change_utc); + +CREATE TABLE archive_messages(id INTEGER NOT NULL, + coll_id INTEGER NOT NULL, + utc DATETIME NOT NULL, + dir INTEGER, + body VARCHAR(65535), + name VARCHAR(1023), + PRIMARY KEY(id)); +CREATE INDEX IDX_archive_msgs_coll_id ON archive_messages(coll_id); +CREATE INDEX IDX_archive_msgs_utc ON archive_messages(utc); + +CREATE TABLE archive_jid_prefs(us VARCHAR(2047) NOT NULL, + with_user VARCHAR(1023) NOT NULL, + with_server VARCHAR(1023) NOT NULL, + with_resource VARCHAR(1023) NOT NULL, + save INTEGER, + expire INTEGER, + otr INTEGER, + PRIMARY KEY(us, with_user, with_server, with_resource)); +CREATE INDEX IDX_archive_jid_prefs_us ON archive_jid_prefs(us); + +CREATE TABLE archive_global_prefs(us VARCHAR(2047) NOT NULL, + save INTEGER, + expire INTEGER, + otr INTEGER, + method_auto INTEGER, + method_local INTEGER, + method_manual INTEGER, + auto_save INTEGER, + PRIMARY KEY(us)); + +CREATE TRIGGER archive_collections_delete BEFORE DELETE ON archive_collections +FOR EACH ROW +BEGIN + DELETE FROM archive_messages WHERE coll_id = OLD.id; + UPDATE archive_collections SET prev_id = null WHERE prev_id = OLD.id; + UPDATE archive_collections SET next_id = null WHERE next_id = OLD.id; +END; + +CREATE TRIGGER archive_collections_update BEFORE UPDATE ON archive_collections +FOR EACH ROW WHEN NEW.deleted = 1 +BEGIN + DELETE FROM archive_messages WHERE coll_id = NEW.id; + UPDATE archive_collections SET prev_id = null WHERE prev_id = NEW.id; + UPDATE archive_collections SET next_id = null WHERE next_id = NEW.id; +END; \ No newline at end of file diff --git a/mod_archive/src/mod_archive_sql.erl b/mod_archive/src/mod_archive_sql.erl new file mode 100644 index 0000000..27f8150 --- /dev/null +++ b/mod_archive/src/mod_archive_sql.erl @@ -0,0 +1,1379 @@ +%%%---------------------------------------------------------------------- +%%% File : mod_archive_sql.erl +%%% Author : Olivier Goffart +%%% Purpose : Message Archiving using SQL DB (JEP-0136) +%%% Created : 19 Aug 2006 by Olivier Goffart +%%%---------------------------------------------------------------------- + +%% Options: +%% save_default -> true | false if messages are stored by default or not +%% session_duration -> time in secondes before the timeout of a session + + +-module(mod_archive_sql). +-author('ogoffart@kde.org'). +-author('alexey@process-one.net'). + +-behaviour(gen_server). +-behaviour(gen_mod). + +-export([start_link/2, start/2, stop/1, + remove_user/2, + send_packet/3, + receive_packet/3, + receive_packet/4, + process_iq/3, process_local_iq/3, + get_disco_features/5]). + +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). + +-include("ejabberd.hrl"). +-include("jlib.hrl"). + +-record(state, {host, + sessions, + save_default, + session_duration}). + +-define(PROCNAME, ejabberd_mod_archive_sql). +-define(NS_ARCHIVE, + "http://www.xmpp.org/extensions/xep-0136.html#ns"). +-define(NS_ARCHIVE_MANAGE, + "http://www.xmpp.org/extensions/xep-0136.html#ns-manage"). +-define(NS_ARCHIVE_PREF, + "http://www.xmpp.org/extensions/xep-0136.html#ns-pref"). +-define(NS_ARCHIVE_MANUAL, + "http://www.xmpp.org/extensions/xep-0136.html#ns-manual"). +-define(INFINITY, calendar:datetime_to_gregorian_seconds({{2038,1,19},{0,0,0}})). +-define(DICT, dict). + +-define(MYDEBUG(Format, Args), + io:format("D(~p:~p:~p) : " ++ Format ++ "~n", + [calendar:local_time(), ?MODULE, ?LINE] ++ Args)). + + +%NOTE i was not sure what format to adopt for archive_option. otr_list is unused +-record(archive_options, + {us, + default = unset, + save_list = [], + nosave_list = [], + otr_list = []}). + +%-record(archive_options, {usj, us, jid, type, value}). + +-record(archive_message, + {usjs, + us, + jid, + start, + message_list = [], + subject = ""}). + +-record(msg, {direction, secs, body}). + +%%==================================================================== +%% API +%%==================================================================== +%%-------------------------------------------------------------------- +%% Function: start_link() -> {ok,Pid} | ignore | {error,Error} +%% Description: Starts the server +%%-------------------------------------------------------------------- +start_link(Host, Opts) -> + Proc = gen_mod:get_module_proc(Host, ?PROCNAME), + gen_server:start_link({local, Proc}, ?MODULE, [Host, Opts], []). + +start(Host, Opts) -> + DBHost = gen_mod:get_opt(db_host, Opts, Host), + if + DBHost /= Host -> + PGChildSpec = + {gen_mod:get_module_proc(DBHost, ejabberd_odbc_sup), + {ejabberd_odbc_sup, start_link, [DBHost]}, + temporary, + infinity, + supervisor, + [ejabberd_odbc_sup]}, + supervisor:start_child(ejabberd_sup, PGChildSpec); + true -> + ok + end, + Proc = gen_mod:get_module_proc(Host, ?PROCNAME), + ChildSpec = + {Proc, + {?MODULE, start_link, [Host, Opts]}, + temporary, + 1000, + worker, + [?MODULE]}, + supervisor:start_child(ejabberd_sup, ChildSpec). + +stop(Host) -> + Proc = gen_mod:get_module_proc(Host, ?PROCNAME), + gen_server:call(Proc, stop), + supervisor:delete_child(ejabberd_sup, Proc). + +%%==================================================================== +%% gen_server callbacks +%%==================================================================== + +%%-------------------------------------------------------------------- +%% Function: init(Args) -> {ok, State} | +%% {ok, State, Timeout} | +%% ignore | +%% {stop, Reason} +%% Description: Initiates the server +%%-------------------------------------------------------------------- +init([Host, Opts]) -> + IQDisc = gen_mod:get_opt(iqdisc, Opts, one_queue), + SaveDefault = gen_mod:get_opt(save_default, Opts, false), + SessionDuration = gen_mod:get_opt(session_duration, Opts, 900), + mnesia:create_table(archive_options, + [{disc_copies, [node()]}, + {attributes, record_info(fields, archive_options)}]), + mnesia:create_table(archive_message, + [{disc_copies, [node()]}, + {attributes, record_info(fields, archive_message)}]), +% mnesia:add_table_index(archive_options, us), + mnesia:add_table_index(archive_message, us), + ejabberd_hooks:add(remove_user, Host, ?MODULE, remove_user, 50), + gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_ARCHIVE, ?MODULE, process_iq, IQDisc), + gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_ARCHIVE, ?MODULE, process_local_iq, IQDisc), + ejabberd_hooks:add(user_send_packet, Host, ?MODULE, send_packet, 90), + ejabberd_hooks:add(user_receive_packet, Host, ?MODULE, receive_packet, 90), + ejabberd_hooks:add(offline_message_hook, Host, ?MODULE, receive_packet, 35), + ejabberd_hooks:add(disco_local_features, Host, ?MODULE, get_disco_features, 99), + timer:send_interval(1000 * SessionDuration div 2, clean_sessions), + {ok, #state{host = Host, + sessions = ?DICT:new(), + save_default = SaveDefault, + session_duration = SessionDuration}}. + +%%-------------------------------------------------------------------- +%% Function: %% handle_call(Request, From, State) -> {reply, Reply, State} | +%% {reply, Reply, State, Timeout} | +%% {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, Reply, State} | +%% {stop, Reason, State} +%% Description: Handling call messages +%%-------------------------------------------------------------------- +handle_call(get_save_default, _From, State) -> + {reply, State#state.save_default, State}; +handle_call(stop, _From, State) -> + {stop, normal, ok, State}. + +%%-------------------------------------------------------------------- +%% Function: handle_cast(Msg, State) -> {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, State} +%% Description: Handling cast messages +%%-------------------------------------------------------------------- +handle_cast({addlog, Direction, LUser, LServer, JID, Body}, State) -> + Sessions = State#state.sessions, + NewSessions = + case should_store_jid(LUser, LServer, JID, + State#state.save_default) of + false -> + Sessions; + true -> + do_log(Sessions, LUser, LServer, JID, + Direction, Body, + State#state.session_duration) + end, + {noreply, State#state{sessions = NewSessions}}; +handle_cast(_Msg, State) -> + {noreply, State}. + +%%-------------------------------------------------------------------- +%% Function: handle_info(Info, State) -> {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, State} +%% Description: Handling all non call/cast messages +%%-------------------------------------------------------------------- +handle_info(clean_sessions, State) -> + Sessions = State#state.sessions, + Timeout = State#state.session_duration, + TS = get_timestamp(), + NewSessions = ?DICT:filter(fun(_Key, {_Start, Last}) -> + TS - Last =< Timeout + end, Sessions), + {noreply, State#state{sessions = NewSessions}}; + +handle_info(_Info, State) -> + {noreply, State}. + +%%-------------------------------------------------------------------- +%% Function: terminate(Reason, State) -> void() +%% Description: This function is called by a gen_server when it is about to +%% terminate. It should be the opposite of Module:init/1 and do any necessary +%% cleaning up. When it returns, the gen_server terminates with Reason. +%% The return value is ignored. +%%-------------------------------------------------------------------- +terminate(_Reason, State) -> + Host = State#state.host, + ejabberd_hooks:delete(remove_user, Host, ?MODULE, remove_user, 50), + gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_ARCHIVE), + gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_ARCHIVE), + ejabberd_hooks:delete(user_send_packet, Host, ?MODULE, send_packet, 90), + ejabberd_hooks:delete(user_receive_packet, Host, ?MODULE, receive_packet, 90), + ejabberd_hooks:delete(offline_message_hook, Host, ?MODULE, receive_packet, 35), + ejabberd_hooks:delete(disco_local_features, Host, ?MODULE, get_disco_features, 99), + ok. + +%%-------------------------------------------------------------------- +%% Func: code_change(OldVsn, State, Extra) -> {ok, NewState} +%% Description: Convert process state when code is changed +%%-------------------------------------------------------------------- +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%-------------------------------------------------------------------- +%%% Internal functions +%%-------------------------------------------------------------------- + +%% Workaround the fact that if the client send +%% it end up like +process_iq(From, To, IQ) -> + #iq{sub_el = SubEl} = IQ, + #jid{lserver = LServer, luser = LUser} = To, + #jid{luser = FromUser} = From, + case {LUser, LServer, lists:member(LServer, ?MYHOSTS)} of + {FromUser, _, true} -> + process_local_iq(From, To, IQ); + {"", _, true} -> + process_local_iq(From, To, IQ); + {"", "", _} -> + process_local_iq(From, To, IQ); + _ -> + IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]} + end. + +process_local_iq(From, To, #iq{sub_el = SubEl} = IQ) -> + case lists:member(From#jid.lserver, ?MYHOSTS) of + false -> + IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]}; + true -> + {xmlelement, Name, _Attrs, _Els} = SubEl, + case Name of + "pref" -> process_local_iq_pref(From, To, IQ); + "auto" -> process_local_iq_auto(From, To, IQ); + %%"otr" -> process_local_iq_otr(From, To, IQ); + "list" -> process_local_iq_list(From, To, IQ); + "retrieve" -> process_local_iq_retrieve(From, To, IQ); + "save" -> process_local_iq_save(From, To, IQ); + "remove" -> process_local_iq_remove(From, To, IQ); + _ -> IQ#iq{type = error, + sub_el = [SubEl, ?ERR_FEATURE_NOT_IMPLEMENTED]} + end + end. + + +remove_user(User, Server) -> + LUser = jlib:nodeprep(User), + LServer = jlib:nameprep(Server), + US = {LUser, LServer}, + Username = ejabberd_odbc:escape(LUser), + DBHost = gen_mod:get_module_opt(LServer, ?MODULE, db_host, LServer), + F = fun() -> + ejabberd_odbc:sql_query_t( + ["delete from archive_collection " + "where username = '", Username, "'"]) + end, + ejabberd_odbc:sql_transaction(DBHost, F), + F = fun() -> + mnesia:delete({archive_options, US}) + end, + mnesia:transaction(F). + +get_disco_features(Acc, _From, _To, "", _Lang) -> + Features = + case Acc of + {result, I} -> I; + _ -> [] + end, + {result, Features ++ [?NS_ARCHIVE_MANAGE, + ?NS_ARCHIVE_PREF, + ?NS_ARCHIVE_MANUAL]}; + +get_disco_features(Acc, _From, _To, _Node, _Lang) -> + Acc. + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% 3 Automated archiving +%% + +send_packet(From, To, Packet) -> + add_log(to, From#jid.luser, From#jid.lserver, To, Packet). + +receive_packet(From, To, Packet) -> + add_log(from, To#jid.luser, To#jid.lserver, From, Packet). + +receive_packet(_JID, From, To, Packet) -> + receive_packet(From, To, Packet). + +add_log(Direction, LUser, LServer, JID, Packet) -> + case parse_message(Packet) of + "" -> + ok; + Body -> + Proc = gen_mod:get_module_proc(LServer, ?PROCNAME), + gen_server:cast( + Proc, {addlog, Direction, LUser, LServer, JID, Body}) + end. + +% Parse the message and return the body string if successful +parse_message({xmlelement, "message", _, _} = Packet) -> + case xml:get_tag_attr_s("type", Packet) of + Type when Type == ""; + Type == "normal"; + Type == "chat" -> + xml:get_path_s(Packet, [{elem, "body"}, cdata]); + _ -> + "" + end; +parse_message(_) -> + "". + +% archive the message Body return new Sessions +% Sessions: a dict of open sessions +% LUser, LServer : the local user's information +% Jid : the contact's jid +% Body : the message body +do_log(Sessions, LUser, LServer, JID, Direction, Body, SessionDuration) -> + DBHost = gen_mod:get_module_opt(LServer, ?MODULE, db_host, LServer), + {NewSessions, Start, Secs} = + find_storage(LUser, LServer, JID, Sessions, + SessionDuration), + Username = ejabberd_odbc:escape(LUser), + LJID = jlib:jid_tolower(JID), + SJIDU = ejabberd_odbc:escape(element(1, LJID)), + SJIDS = ejabberd_odbc:escape(element(2, LJID)), + SJIDR = ejabberd_odbc:escape(element(3, LJID)), + SStart = ejabberd_odbc:escape(integer_to_list(Start)), + SDirection = case Direction of + to -> + "true"; + from -> + "false" + end, + SSecs = ejabberd_odbc:escape(integer_to_list(Secs)), + SBody = ejabberd_odbc:escape(Body), + SUTC = "0", + SName = "", + SJID1 = "", + F = fun() -> + SSID = + case ejabberd_odbc:sql_query_t( + ["select sid from archive_collection " + "where username = '", Username, "' ", + "and jid_u = '", SJIDU, "' ", + "and jid_s = '", SJIDS, "' ", + "and jid_r = '", SJIDR, "' ", + "and start = '", SStart, "'"]) of + {selected, ["sid"], [{SID}]} -> + ["'", ejabberd_odbc:escape(SID), "'"]; + {selected, ["sid"], []} -> + SSubject = "", + CollVals = + ["currval('archive_collection_sid_seq'),", + "'", Username, "'," + "'", SJIDU, "'," + "'", SJIDS, "'," + "'", SJIDR, "'," + "'", SStart, "'," + "'", SSubject, "'"], + ejabberd_odbc:sql_query_t( + "select nextval('archive_collection_sid_seq')"), + ejabberd_odbc:sql_query_t( + ["insert into archive_collection(" + " sid, username," + " jid_u, jid_s, jid_r," + " start, subject) " + " values (", CollVals, ");"]), + "currval('archive_collection_sid_seq')" + end, + MsgVals = + [SSID, "," + "'", SDirection, "'," + "'", SSecs, "'," + "'", SUTC, "'," + "'", SName, "'," + "'", SJID1, "'," + "'", SBody, "'"], + ejabberd_odbc:sql_query_t( + ["insert into archive_message(" + " sid, direction_to, secs, utc," + " name, jid, body) " + " values (", MsgVals, ");"]) + end, + ejabberd_odbc:sql_transaction(DBHost, F), + NewSessions. + +find_storage(LUser, LServer, JID, Sessions, Timeout) -> + LJID = jlib:jid_tolower(JID), + Key = {LUser, LServer, LJID}, + case ?DICT:find(Key, Sessions) of + error -> + TS = get_timestamp(), + {?DICT:store(Key, {TS, TS}, Sessions), TS, 0}; + {ok, {Start, Last}} -> + TS = get_timestamp(), + if + TS - Last > Timeout -> + {?DICT:store(Key, {TS, TS}, Sessions), TS, 0}; + true -> + {?DICT:store(Key, {Start, TS}, Sessions), + Start, TS - Last} + end + end. + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% 3.1 Preferences +%% + +process_local_iq_pref(From, _To, #iq{type = Type, sub_el = SubEl} = IQ) -> + Result = case Type of + set -> + {xmlelement, _Name, _Attrs, Els} = SubEl, + process_save_set(From#jid.luser, From#jid.lserver, Els); + get -> + process_save_get(From#jid.luser, From#jid.lserver) + end, + case Result of + {result, R} -> + IQ#iq{type = result, sub_el = [R]}; + ok -> + broadcast_iq(From, IQ#iq{type = set, sub_el=[SubEl]}), + IQ#iq{type = result, sub_el = []}; + {error, E} -> + IQ#iq{type = error, sub_el = [SubEl, E]}; + _ -> + IQ#iq{type = error, sub_el = [SubEl, ?ERR_INTERNAL_SERVER_ERROR]} + end. + + + + +% return {error, xmlelement} or {result, xmlelement} +process_save_get(LUser, LServer) -> + case catch mnesia:dirty_read(archive_options, {LUser, LServer}) of + {'EXIT', _Reason} -> + {error, ?ERR_INTERNAL_SERVER_ERROR}; + [] -> + {result, + {xmlelement, "pref", [{"xmlns", ?NS_ARCHIVE}], + default_element(LServer)}}; + [#archive_options{default = Default, + save_list = SaveList, + nosave_list = NoSaveList}] -> + LItems = lists:append( + lists:map(fun(J) -> + {xmlelement, "item", + [{"jid", jlib:jid_to_string(J)}, + {"save","body"}], + []} + end, SaveList), + lists:map(fun(J) -> + {xmlelement, "item", + [{"jid", jlib:jid_to_string(J)}, + {"save","false"}], + []} + end, NoSaveList)), + DItem = case Default of + true -> % TODO: + [{xmlelement, "default", [{"save", "body"}], []}]; + false -> + [{xmlelement, "default", [{"save", "false"}], []}]; + _ -> + default_element(LServer) + end, + {result, {xmlelement, "save", [{"xmlns", ?NS_ARCHIVE}], DItem ++ LItems}} + end. + +%return the element +default_element(Host) -> + Proc = gen_mod:get_module_proc(Host, ?PROCNAME), + AutoSave = gen_server:call(Proc, get_save_default), + SaveAttr = if + AutoSave -> "true"; + true -> "false" + end, + [{xmlelement, "default", [{"save", "false"}, {"otr", "forbid"}], []}, + {xmlelement, "auto", [{"save", SaveAttr}], []}]. + + +% return {error, xmlelement} or {result, xmlelement} or ok +process_save_set(LUser, LServer, Elms) -> + F = fun() -> + NE = case mnesia:read({archive_options, {LUser, LServer}}) of + [] -> + #archive_options{us = {LUser, LServer}}; + [E] -> + E + end, + SNE = transaction_parse_save_elem(NE, Elms), + case SNE of + {error, _} -> SNE; + _ -> mnesia:write(SNE) + end + end, + case mnesia:transaction(F) of + {atomic, {error, _} = Error} -> + Error; + {atomic, _} -> + ok; + _ -> + {error, ?ERR_INTERNAL_SERVER_ERROR} + end. + + +% transaction_parse_save_elem(archive_options, ListOfXmlElement) -> #archive_options +% parse the list of xml element, and modify the given archive_option +transaction_parse_save_elem(Options, [{xmlelement, "default", Attrs, _} | Tail]) -> + V = case xml:get_attr_s("save", Attrs) of + "true" -> true; + "false" -> false; + _ -> unset + end, + transaction_parse_save_elem(Options#archive_options{default = V}, Tail); + +transaction_parse_save_elem(Options, [{xmlelement, "item", Attrs, _} | Tail]) -> + case jlib:string_to_jid(xml:get_attr_s("jid", Attrs)) of + error -> {error, ?ERR_JID_MALFORMED}; + #jid{luser = LUser, lserver = LServer, lresource = LResource} -> + JID = {LUser, LServer, LResource}, + case xml:get_attr_s("save", Attrs) of + "body" -> + transaction_parse_save_elem( + Options#archive_options{ + save_list = [JID | lists:delete(JID, Options#archive_options.save_list)], + nosave_list = lists:delete(JID, Options#archive_options.nosave_list) + }, Tail); + "false" -> + transaction_parse_save_elem( + Options#archive_options{ + save_list = lists:delete(JID, Options#archive_options.save_list), + nosave_list = [JID | lists:delete(JID, Options#archive_options.nosave_list)] + }, Tail); + _ -> + transaction_parse_save_elem( + Options#archive_options{ + save_list = lists:delete(JID, Options#archive_options.save_list), + nosave_list = lists:delete(JID, Options#archive_options.nosave_list) + }, Tail) + end + end; + +transaction_parse_save_elem(Options, []) -> Options; +transaction_parse_save_elem(Options, [_ | Tail]) -> + transaction_parse_save_elem(Options, Tail). + + +broadcast_iq(#jid{luser = User, lserver = Server}, IQ) -> + Fun = fun(Resource) -> + ejabberd_router:route( + jlib:make_jid("", Server, ""), + jlib:make_jid(User, Server, Resource), + jlib:iq_to_xml(IQ#iq{id="push"})) + end, + lists:foreach(Fun, ejabberd_sm:get_user_resources(User,Server)). + + + +process_local_iq_auto(From, _To, #iq{type = Type, sub_el = SubEl} = IQ) -> + Result = + case Type of + set -> + {xmlelement, _Name, Attrs, _Els} = SubEl, + Auto = case xml:get_attr_s("save", Attrs) of + "true" -> true; + "false" -> false; + _ -> unset + end, + case Auto of + unset -> + {error, ?ERR_BAD_REQUEST}; + _ -> + LUser = From#jid.luser, + LServer = From#jid.lserver, + F = fun() -> + Opts = + case mnesia:read({archive_options, + {LUser, LServer}}) of + [] -> + #archive_options{us = {LUser, LServer}}; + [E] -> + E + end, + mnesia:write(Opts#archive_options{ + default = Auto}) + end, + mnesia:transaction(F), + {result, []} + end; + get -> + {error, ?ERR_BAD_REQUEST} + end, + case Result of + {result, R} -> + IQ#iq{type = result, sub_el = R}; + {error, E} -> + IQ#iq{type = error, sub_el = [SubEl, E]}; + _ -> + IQ#iq{type = error, sub_el = [SubEl, ?ERR_INTERNAL_SERVER_ERROR]} + end. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% 3.1 Off-the-Record Mode +%% + +%TODO + + + + + + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% Utility function + +% Return true if LUser@LServer should log message for the contact JID +should_store_jid(LUser, LServer, Jid, Service_Default) -> + case catch mnesia:dirty_read(archive_options, {LUser, LServer}) of + [#archive_options{default = Default, + save_list = SaveList, + nosave_list = NoSaveList}] -> + Jid_t = jlib:jid_tolower(Jid), + Jid_b = jlib:jid_remove_resource(Jid_t), + A = lists:member(Jid_t, SaveList), + B = lists:member(Jid_t, NoSaveList), + C = lists:member(Jid_b, SaveList), + D = lists:member(Jid_b, NoSaveList), + if A -> true; + B -> false; + C -> true; + D -> false; + Default == true -> true; + Default == false -> false; + true -> Service_Default + end; + _ -> + Service_Default + end. + + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% 4. Manual Archiving +%% + + +process_local_iq_save(From, _To, #iq{type = Type, sub_el = SubEl} = IQ) -> + #jid{luser = LUser, lserver = LServer} = From, + case Type of + get -> + IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]}; + set -> + case parse_store_element (LUser, LServer, SubEl) of + {error, E} -> IQ#iq{type = error, sub_el = [SubEl, E]}; + Collection -> + DBHost = gen_mod:get_module_opt(LServer, ?MODULE, db_host, LServer), + F = fun() -> + store_collection(Collection) + end, + case ejabberd_odbc:sql_transaction(DBHost, F) of + {atomic, _} -> + IQ#iq{type = result, sub_el = []}; + _ -> + IQ#iq{type = error, sub_el = [SubEl, ?ERR_INTERNAL_SERVER_ERROR]} + end + end + end. + +% return a #archive_message StoreElem is an xmlelement, or return {error, E} +parse_store_element(LUser, LServer, + {xmlelement, "save", _ChatAttrs, ChatSubEls}) -> + case xml:remove_cdata(ChatSubEls) of + [{xmlelement, "chat", Attrs, SubEls}] -> + case index_from_argument(LUser, LServer, Attrs) of + {error, E} -> {error, E}; + {LUser, LServer, Jid, Start} = Index -> + Messages = parse_store_element_sub(SubEls), + #archive_message{usjs = Index, + us = {LUser, LServer}, + jid = Jid, + start = Start, + subject = xml:get_attr_s("subject", Attrs), + message_list = Messages} + end; + _ -> + {error, ?ERR_BAD_REQUEST} + end. + +% TODO: utc attribute, catch list_to_integer errors + +parse_store_element_sub([{xmlelement, Dir, _, _} = E | Tail]) + when Dir == "from"; + Dir == "to" -> + [#msg{direction = list_to_atom(Dir), + secs = list_to_integer(xml:get_tag_attr_s("secs", E)), + body = xml:get_tag_cdata(xml:get_subtag(E,"body"))} | + parse_store_element_sub(Tail)]; + +parse_store_element_sub([]) -> []; +parse_store_element_sub([_ | Tail]) -> parse_store_element_sub(Tail). + + +store_collection(Collection) -> + {LUser, _LServer, JID, Start} = Collection#archive_message.usjs, + LJID = jlib:jid_tolower(JID), + Username = ejabberd_odbc:escape(LUser), + SJIDU = ejabberd_odbc:escape(element(1, LJID)), + SJIDS = ejabberd_odbc:escape(element(2, LJID)), + SJIDR = ejabberd_odbc:escape(element(3, LJID)), + SStart = ejabberd_odbc:escape(integer_to_list(Start)), + SSubject = ejabberd_odbc:escape(Collection#archive_message.subject), + CollVals = + ["currval('archive_collection_sid_seq'),", + "'", Username, "'," + "'", SJIDU, "'," + "'", SJIDS, "'," + "'", SJIDR, "'," + "'", SStart, "'," + "'", SSubject, "'"], + ejabberd_odbc:sql_query_t( + ["delete from archive_collection " + " where username = '", Username, "' " + " and jid_u = '", SJIDU, "' ", + " and jid_s = '", SJIDS, "' ", + " and jid_r = '", SJIDR, "';"]), + ejabberd_odbc:sql_query_t( + "select nextval('archive_collection_sid_seq')"), + ejabberd_odbc:sql_query_t( + ["insert into archive_collection(" + " sid, username, jid_u, jid_s, jid_r, start, subject) " + " values (", CollVals, ");"]), + lists:map( + fun(#msg{direction = Direction, secs = Secs, body = Body}) -> + SDirection = case Direction of + to -> + "true"; + from -> + "false" + end, + SSecs = ejabberd_odbc:escape(integer_to_list(Secs)), + SBody = ejabberd_odbc:escape(Body), + SUTC = "0", + SName = "", + SJID1 = "", + MsgVals = + ["'", SDirection, "'," + "'", SSecs, "'," + "'", SUTC, "'," + "'", SName, "'," + "'", SJID1, "'," + "'", SBody, "'"], + ejabberd_odbc:sql_query_t( + ["insert into archive_message(" + " sid, direction_to, secs, utc," + " name, jid, body) " + " values (currval('archive_collection_sid_seq')," + " ", MsgVals, ");"]) + end, Collection#archive_message.message_list). + + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% 5. Archive Management +%% + + +process_local_iq_list(From, _To, #iq{type = Type, sub_el = SubEl} = IQ) -> + #jid{luser = LUser, lserver = LServer} = From, + case Type of + set -> + IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]}; + get -> + {xmlelement, _, Attrs, SubEls} = SubEl, + RSM = parse_rsm(SubEls), + %?MYDEBUG("RSM Results: ~p ~n", [RSM]), + Result = case parse_root_argument(Attrs) of + {error, E} -> {error, E}; + {interval, Start, Stop, JID} -> + get_list(LUser, LServer, Start, Stop, JID); + _ -> + {error, ?ERR_BAD_REQUEST} + end, + case Result of + {ok, Items} -> + FunId = fun(El) -> + %?MYDEBUG("FunId ~p ~n", [El]), + integer_to_list(element(5, El)) + end, + FunCompare = fun(Id, El) -> + Id2 = list_to_integer(FunId(El)), + Id1 = list_to_integer(Id), + if Id1 == Id2 -> equal; + Id1 > Id2 -> greater; + Id1 < Id2 -> smaller + end + end, + case catch execute_rsm(RSM, lists:keysort(5, Items), FunId,FunCompare) of + {error, R} -> + IQ#iq{type = error, sub_el = [SubEl, R]}; + {'EXIT', Errr} -> + %?MYDEBUG("INTERNAL ERROR ~p ~n", [Errr]), + IQ#iq{type = error, + sub_el = [SubEl, ?ERR_INTERNAL_SERVER_ERROR]}; + {RSM_Elem, Items2} -> + Zero = calendar:datetime_to_gregorian_seconds( + {{1970, 1, 1}, {0, 0, 0}}), + Fun = fun(A) -> + Seconds= A#archive_message.start - Zero, + Start2 = jlib:now_to_utc_string( + {Seconds div 1000000, + Seconds rem 1000000, + 0}), + Args0 = [{"with", jlib:jid_to_string(A#archive_message.jid)}, + {"start", Start2}], + Args = case A#archive_message.subject of + "" -> + Args0; + Subject -> + [{"subject",Subject} | Args0] + end, + {xmlelement, "chat", Args, []} + end, + IQ#iq{type = result, + sub_el = [{xmlelement, "list", + [{"xmlns", ?NS_ARCHIVE}], + lists:append( + lists:map(Fun, Items2), + [RSM_Elem])}]} + end; + {error, R} -> + IQ#iq{type = error, sub_el = [SubEl, R]} + end + end. + + + + +process_local_iq_retrieve(From, _To, #iq{type = Type, sub_el = SubEl} = IQ) -> + #jid{luser = LUser, lserver = LServer} = From, + case Type of + set -> + IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]}; + get -> + {xmlelement, _, Attrs, SubEls} = SubEl, + RSM = parse_rsm(SubEls), + case index_from_argument(LUser, LServer, Attrs) of + {error, E} -> + IQ#iq{type = error, sub_el = [SubEl, E]}; + Index -> + case retrieve_collection(Index, RSM) of + {error, Err} -> + IQ#iq{type = error, sub_el = [SubEl, Err]}; + Store -> + IQ#iq{type = result, sub_el = [Store]} + end + end + end. + + +retrieve_collection(Index, RSM) -> + case get_collection(Index) of + {error, E} -> + {error, E}; + A -> + Zero = calendar:datetime_to_gregorian_seconds({{1970, 1, 1}, {0, 0, 0}}), + Seconds = A#archive_message.start-Zero, + Start2 = jlib:now_to_utc_string({Seconds div 1000000, Seconds rem 1000000, 0}), + Args0 = [{"xmlns", ?NS_ARCHIVE}, {"with", jlib:jid_to_string(A#archive_message.jid)}, {"start", Start2}], + Args = case A#archive_message.subject of + "" -> Args0; + Subject -> [{"subject", Subject} | Args0] + end, + case catch execute_rsm(RSM, A#archive_message.message_list, index, index) of + {error, R} -> + {error, R}; + {'EXIT', _} -> + {error, ?ERR_INTERNAL_SERVER_ERROR}; + {RSM_Elem, Items} -> + Format_Fun = + fun(Elem) -> + {xmlelement, atom_to_list(Elem#msg.direction), + [{"secs", integer_to_list(Elem#msg.secs)}], + [{xmlelement, "body", [], [{xmlcdata, Elem#msg.body}]}]} + end, + {xmlelement, "chat", Args, lists:append(lists:map(Format_Fun, Items), [RSM_Elem])} + end + end. + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% 5.3 Removing a Collection +%% + + +process_local_iq_remove(From, _To, #iq{type = Type, sub_el = SubEl} = IQ) -> + #jid{luser = LUser, lserver = LServer} = From, + case Type of + get -> + IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]}; + set -> + {xmlelement, _, Attrs, _} = SubEl, + Result = + case parse_root_argument(Attrs) of + {error, E} -> + IQ#iq{type = error, sub_el = [SubEl, E]}; + {interval, Start, Stop, Jid} -> + process_remove_interval( + LUser, LServer, Start, Stop, Jid) + end, + case Result of + {error, Ee} -> + IQ#iq{type = error, sub_el = [SubEl, Ee]}; + ok -> + IQ#iq{type = result, sub_el=[]} + end + end. + +process_remove_interval(LUser, LServer, Start, End, With) -> + DBHost = gen_mod:get_module_opt(LServer, ?MODULE, db_host, LServer), + Username = ejabberd_odbc:escape(LUser), + WithCond = case With of + undefined -> + ""; + JID -> + LJID = jlib:jid_tolower(JID), + case LJID of + {"", LJIDS, ""} -> + SJIDS = ejabberd_odbc:escape(LJIDS), + [" and jid_s = '", SJIDS, "'"]; + {LJIDU, LJIDS, ""} -> + SJIDU = ejabberd_odbc:escape(LJIDU), + SJIDS = ejabberd_odbc:escape(LJIDS), + [" and jid_u = '", SJIDU, "'", + " and jid_s = '", SJIDS, "'"]; + {LJIDU, LJIDS, LJIDR} -> + SJIDU = ejabberd_odbc:escape(LJIDU), + SJIDS = ejabberd_odbc:escape(LJIDS), + SJIDR = ejabberd_odbc:escape(LJIDR), + [" and jid_u = '", SJIDU, "'", + " and jid_s = '", SJIDS, "'", + " and jid_r = '", SJIDR, "'"] + end + end, + TimeCond = + case {Start, End} of + {undefined, undefined} -> + ""; + {undefined, _} -> + SEnd = ejabberd_odbc:escape(integer_to_list(End)), + [" and start <= '", SEnd, "'"]; + {_, undefined} -> + SStart = ejabberd_odbc:escape(integer_to_list(Start)), + [" and start = '", SStart, "'"]; + _ -> + SStart = ejabberd_odbc:escape(integer_to_list(Start)), + SEnd = ejabberd_odbc:escape(integer_to_list(End)), + [" and start >= '", SStart, "'" + " and start <= '", SEnd, "'"] + end, + F = fun() -> + ejabberd_odbc:sql_query_t( + ["delete from archive_collection " + "where username = '", Username, "' ", + TimeCond, + WithCond]) + end, + case ejabberd_odbc:sql_transaction(DBHost, F) of + {atomic, _} -> + ok; + {aborted, _} -> + {error, ?ERR_INTERNAL_SERVER_ERROR} + end. + + + +% return {ok, [{#archive_message}]} or {error, xmlelement} +% With is a tuple Jid, or '_' +get_list(LUser, LServer, Start, End, With) -> + DBHost = gen_mod:get_module_opt(LServer, ?MODULE, db_host, LServer), + Username = ejabberd_odbc:escape(LUser), + WithCond = case With of + undefined -> + ""; + JID -> + LJID = jlib:jid_tolower(JID), + case LJID of + {"", LJIDS, ""} -> + SJIDS = ejabberd_odbc:escape(LJIDS), + [" and jid_s = '", SJIDS, "'"]; + {LJIDU, LJIDS, ""} -> + SJIDU = ejabberd_odbc:escape(LJIDU), + SJIDS = ejabberd_odbc:escape(LJIDS), + [" and jid_u = '", SJIDU, "'", + " and jid_s = '", SJIDS, "'"]; + {LJIDU, LJIDS, LJIDR} -> + SJIDU = ejabberd_odbc:escape(LJIDU), + SJIDS = ejabberd_odbc:escape(LJIDS), + SJIDR = ejabberd_odbc:escape(LJIDR), + [" and jid_u = '", SJIDU, "'", + " and jid_s = '", SJIDS, "'", + " and jid_r = '", SJIDR, "'"] + end + end, + StartCond = + case Start of + undefined -> + ""; + _ -> + SStart = ejabberd_odbc:escape(integer_to_list(Start)), + [" and start >= '", SStart, "'"] + end, + EndCond = + case End of + undefined -> + ""; + _ -> + SEnd = ejabberd_odbc:escape(integer_to_list(End)), + [" and start <= '", SEnd, "'"] + end, + case catch ejabberd_odbc:sql_query( + DBHost, + ["select jid_u, jid_s, jid_r, start, subject " + "from archive_collection " + "where username = '", Username, "' ", + StartCond, + EndCond, + WithCond]) of + {selected, ["jid_u", "jid_s", "jid_r", "start", "subject"], Rs} -> + Result = + lists:map( + fun({SJIDU, SJIDS, SJIDR, SStart1, Subject}) -> + JID1 = jlib:make_jid(SJIDU, SJIDS, SJIDR), + Start1 = list_to_integer(SStart1), + Index = {LUser, LServer, JID1, Start1}, + #archive_message{ + usjs = Index, + us = {LUser, LServer}, + jid = JID1, + start = Start1, + subject = Subject, + message_list = []} + end, Rs), + {ok, Result}; + _ -> + {error, ?ERR_INTERNAL_SERVER_ERROR} + end. + + +% Index is {LUser, LServer, With, Start} +get_collection(Index) -> + {LUser, LServer, JID, Start} = Index, + DBHost = gen_mod:get_module_opt(LServer, ?MODULE, db_host, LServer), + LJID = jlib:jid_tolower(JID), + Username = ejabberd_odbc:escape(LUser), + SJIDU = ejabberd_odbc:escape(element(1, LJID)), + SJIDS = ejabberd_odbc:escape(element(2, LJID)), + SJIDR = ejabberd_odbc:escape(element(3, LJID)), + SStart = ejabberd_odbc:escape(integer_to_list(Start)), + case catch ejabberd_odbc:sql_query( + DBHost, + ["select sid, subject from archive_collection " + "where username = '", Username, "' ", + "and jid_u = '", SJIDU, "' ", + "and jid_s = '", SJIDS, "' ", + "and jid_r = '", SJIDR, "' ", + "and start = '", SStart, "'"]) of + {selected, ["sid", "subject"], [{SID, Subject}]} -> + SSID = ejabberd_odbc:escape(SID), + case catch ejabberd_odbc:sql_query( + DBHost, + ["select direction_to, secs, utc, name, jid, body " + "from archive_message " + "where sid = '", SSID, "' ", + "order by ser"]) of + {selected, ["direction_to", "secs", "utc", + "name", "jid", "body"], Rs} -> + Msgs = lists:map( + fun({DirectionTo, SSecs, SUTC, + SName, SJID, Body}) -> + Direction = + if + DirectionTo == "t" -> + to; + DirectionTo == "f" -> + from + end, + Secs = list_to_integer(SSecs), + #msg{direction = Direction, + secs = Secs, + body = Body} + end, Rs), + #archive_message{usjs = Index, + us = {LUser, LServer}, + jid = JID, + start = Start, + message_list = Msgs, + subject = Subject}; + _ -> + {error, ?ERR_INTERNAL_SERVER_ERROR} + end; + {selected, ["sid", "subject"], []} -> + {error, ?ERR_ITEM_NOT_FOUND}; + _ -> + {error, ?ERR_INTERNAL_SERVER_ERROR} + end. + + + + +%%%%%%%%%%%%%%%%%%%%%%%%%% +% Utility + + +% return either {error, Err} or {LUser, LServer, Jid, Start} +index_from_argument(LUser, LServer, Attrs) -> + case parse_root_argument(Attrs) of + {error, E} -> {error, E}; + {interval, Start, _, JID} when Start /= undefined, + JID /= undefined -> + {LUser, LServer, JID, Start}; + _ -> {error, ?ERR_BAD_REQUEST} + end. + +%parse commons arguments of root elements +parse_root_argument(Attrs) -> + case parse_root_argument_aux(Attrs, {undefined, undefined, undefined}) of + {error, E} -> {error, E}; + {JID, Start, Stop} -> {interval, Start, Stop, JID}; + _ -> {error, ?ERR_BAD_REQUEST} + end. + +parse_root_argument_aux([{"with", JidStr} | Tail], {_, AS, AE}) -> + case jlib:string_to_jid(JidStr) of + error -> {error, ?ERR_JID_MALFORMED}; + JID -> + LJID = jlib:jid_tolower(JID), + parse_root_argument_aux(Tail, {LJID, AS, AE}) + end; +parse_root_argument_aux([{"start", Str} | Tail], {AW, _, AE}) -> + case jlib:datetime_string_to_timestamp(Str) of + undefined -> {error, ?ERR_BAD_REQUEST}; + No -> + Val = calendar:datetime_to_gregorian_seconds(calendar:now_to_datetime(No)), + parse_root_argument_aux(Tail, {AW, Val, AE}) + end; +parse_root_argument_aux([{"end", Str} | Tail], {AW, AS, _}) -> + case jlib:datetime_string_to_timestamp(Str) of + undefined -> {error, ?ERR_BAD_REQUEST}; + No -> + Val = calendar:datetime_to_gregorian_seconds(calendar:now_to_datetime(No)), + parse_root_argument_aux(Tail, {AW, AS, Val}) + end; +parse_root_argument_aux([_ | Tail], A) -> + parse_root_argument_aux(Tail, A); +parse_root_argument_aux([], A) -> A. + + + +get_timestamp() -> + calendar:datetime_to_gregorian_seconds(calendar:universal_time()). + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% Result Set Management (JEP-0059) +% +% USAGE: +% RSM = parce_rsm(Xmlelement) +% case execute_rsm(RSM, List, GetIdFun, IdCompareFun) of +% {error, E} -> ....; +% {RSMElement, Items} -> +% SubElements = lists:append(lists:map(Format_Fun, Items), [RSMElement]), +% ...; +% end + +-define(MY_NS_RSM, "http://jabber.org/protocol/rsm"). + + + + +% {Start, Max, Order} | error | none % | count + +parse_rsm([A | Tail]) -> + %?MYDEBUG("parse RSM elem ~p ", [A]), + case A of + {xmlelement, _, Attrs1, _} -> + case xml:get_attr_s("xmlns", Attrs1) of + ?MY_NS_RSM -> + parse_rsm(A); + HEPO -> + %?MYDEBUG("HEPO ~p ", [HEPO]), + parse_rsm(Tail) + end; + _ -> + parse_rsm(Tail) + end; +parse_rsm([]) -> + none; + +% parse_rsm({xmlelement, "count", _, _}) -> +% count; + +parse_rsm({xmlelement, "set", _, SubEls}) -> + parse_rsm_aux(SubEls, {0, infinity, normal}); + +parse_rsm(_) -> + error. + +parse_rsm_aux([{xmlelement, "max", _Attrs, Contents} | Tail], Acc) -> + case catch list_to_integer(xml:get_cdata(Contents)) of + P when is_integer(P) -> + case Acc of + {Start, infinity, Order} -> + parse_rsm_aux(Tail, {Start, P, Order}); + _ -> + error + end; + HEPO -> + %?MYDEBUG(" Not an INTEGER ~p ", [HEPO]), + error + end; + +parse_rsm_aux([{xmlelement, "index", _Attrs, Contents} | Tail], Acc) -> + case catch list_to_integer(xml:get_cdata(Contents)) of + P when is_integer(P) -> + case Acc of + {0, Max, normal} -> + parse_rsm_aux(Tail, {P, Max, normal}); + _ -> + error + end; + _ -> + error + end; + +parse_rsm_aux([{xmlelement, "after", _Attrs, Contents} | Tail], Acc) -> + case Acc of + {0, Max, normal} -> + parse_rsm_aux(Tail, {{id, xml:get_cdata(Contents)}, Max, normal}); + _ -> + error + end; + + +parse_rsm_aux([{xmlelement, "before", _Attrs, Contents} | Tail], Acc) -> + case Acc of + {0, Max, normal} -> + case xml:get_cdata(Contents) of + [] -> + parse_rsm_aux(Tail, {0, Max, reversed}); + CD -> + parse_rsm_aux(Tail, {{id, CD}, Max, reversed}) + end; + _ -> + error + end; + +parse_rsm_aux([_ | Tail], Acc) -> + parse_rsm_aux(Tail, Acc); +parse_rsm_aux([], Acc) -> + Acc. + +% RSM = {Start, Max, Order} +% GetId = fun(Elem) -> Id +% IdCompare = fun(Id, Elem) -> equal | greater | smaller +% +% -> {RSMElement, List} | {error, ErrElement} + +execute_rsm(RSM, List, GetId, IdCompare) -> + %?MYDEBUG("execute_rsm RSM ~p ~n", [RSM]), + case execute_rsm_aux(RSM, List, IdCompare, 0) of + none -> + {{xmlcdata, ""}, List}; +% count -> +% {{xmlelement, "count", [{"xmlns", ?MY_NS_RSM}], [{xmlcdata, integer_to_list(length(List))}]}, []}; + {error, E} -> + {error, E}; + {_, []} -> + {{xmlelement, "set", [{"xmlns", ?MY_NS_RSM}], [{xmlelement, "count", [], [{xmlcdata, integer_to_list(length(List))}]}]}, []}; + {Index, L} -> + case GetId of + index -> + {make_rsm(Index, integer_to_list(Index), integer_to_list(Index+length(L)),length(List)), L}; + _ -> + {make_rsm(Index, GetId(hd(L)), GetId(lists:last(L)),length(List)), L} + end + end. + + +% execute_rsm_aux(count, _List, _, _) -> +% count; + +execute_rsm_aux(none, _List, _, _) -> + none; + +execute_rsm_aux(error, _List, _, _) -> + {error, ?ERR_BAD_REQUEST}; + +execute_rsm_aux({S, M, reversed}, List, IdFun, Acc) -> + {NewFun,NewS} = case IdFun of + index -> + {index, + case S of + {id, IdentIndex} -> + integer_to_list(length(List) - list_to_integer(IdentIndex)); + _ -> S + end}; + _ -> + {fun(Index, Elem) -> + case IdFun(Index, Elem) of + equal -> equal; + greater -> smaller; + smaller -> greater; + O -> O + end + end, + S} + end, + {Index, L2} = execute_rsm_aux({NewS,M,normal}, lists:reverse(List), NewFun, 0), + {Acc + length(List) - Index - length(L2), lists:reverse(L2)}; + +execute_rsm_aux({{id,I}, M, normal}, List, index, Acc) -> + execute_rsm_aux({list_to_integer(I), M, normal}, List, index, Acc); + +execute_rsm_aux({{id,I}, M, normal} = RSM, [E | Tail], IdFun, Acc) -> + case IdFun(I, E) of + smaller -> + execute_rsm_aux(RSM, Tail, IdFun, Acc + 1); + _ -> + execute_rsm_aux({0, M, normal}, [E | Tail], IdFun, Acc) + end; + +execute_rsm_aux({{id,_}, _, normal}, [], _, Acc) -> + {Acc, []}; + +execute_rsm_aux({0, infinity, normal}, List, _, Acc) -> + {Acc, List}; + +execute_rsm_aux({_, 0, _}, _, _, Acc) -> + {Acc, []}; + +execute_rsm_aux({S, M, _}, List, _, Acc) when is_integer(S) and is_integer(M) -> + %?MYDEBUG("execute_rsm_aux sublist ~p ~n", [{S,M,List,Acc}]), + {Acc + S, lists:sublist(List, S+1,M)}. + +make_rsm(FirstIndex, FirstId, LastId, Count) -> + {xmlelement, "set", [{"xmlns", ?MY_NS_RSM}], [ + {xmlelement, "first", [{"index", integer_to_list(FirstIndex)}], [{xmlcdata, FirstId}]}, + {xmlelement, "last", [], [{xmlcdata, LastId}]}, + {xmlelement, "count", [], [{xmlcdata, integer_to_list(Count)}]}]}. diff --git a/mod_archive/src/mod_archive_webview.erl b/mod_archive/src/mod_archive_webview.erl new file mode 100644 index 0000000..d8a5fa2 --- /dev/null +++ b/mod_archive/src/mod_archive_webview.erl @@ -0,0 +1,553 @@ +%%%---------------------------------------------------------------------- +%%% File : mod_archive_webview.erl +%%% Author : Olivier Goffart +%%% Purpose : Online viewer of message archive. (to be used with mod_archive_odbc) +%%% Created : +%%% Id : +%%%---------------------------------------------------------------------- + +-module(mod_archive_webview). +-author('ogoffart@kde.org'). + +-export([ + process/2 + ]). + +-include("ejabberd.hrl"). +-include("jlib.hrl"). +-include("web/ejabberd_http.hrl"). +-include("web/ejabberd_web_admin.hrl"). %for all the defines + +-define(LINK(L) , "/archive/" ++ L). +%-define(P(Els), ?XE("p", Els)). +-define(PC(Text), ?XE("p", [?C(Text)])). +-define(PCT(Text), ?PC(?T(Text))). + + +-define(MYDEBUG(Format, Args), + io:format("D(~p:~p:~p) : " ++ Format ++ "~n", + [calendar:local_time(), ?MODULE, ?LINE] ++ Args)). + + +%%%---------------------------------------------------------------------- +%%% REQUEST HANDLERS +%%%---------------------------------------------------------------------- + +%process([], Request) -> process2([], Request, {}). + +process(["style.css"], _) -> + {200,[{"Content-Type", "text/css"}], " +#navigation li { list-style:none; display:inline; } +.message_from, .message_to { margin:0; padding:0; } +.message_from .time { color:#BD2134; font-weight:bold; } +.message_from .jid { color:#BD2134; font-weight:bold; } +.message_to .time { color:#1E6CC6; font-weight:bold; } +.message_to .jid { color:#1E6CC6; font-weight:bold; } +.search_result a { display:block; } +.search_result em { display:block; color:green; } +/*a.link_prev { float:left } */ +a.link_next { float:right } +.message_body { white-space: pre-wrap; } +"}; + +process(Path, #request{auth = Auth} = Request) -> + ?MYDEBUG("Requested ~p ~p", [Path, Request]), + + case get_auth(Auth) of + {User, Server} -> + process2(Path, Request, {User, Server}); + unauthorized -> + {401, [{"WWW-Authenticate", "basic realm=\"ejabberd-archives\""}], + ejabberd_web:make_xhtml([{xmlelement, "h1", [], + [{xmlcdata, "401 Unauthorized"}]}])} + end. + + +%process2(["config" | tail], #request{lang = Lang } = Request , {User, Server}) -> + +process2(["config" ], #request{lang = Lang } = _Request , {User, Server}) -> + make_xhtml(?T("Config"), + [?XE("h3", [?CT("Global Settings")]) ] ++ global_config_form({User, Server}, Lang) ++ + [?XE("h3", [?CT("Specific Contact Settings")]) ] ++ contact_config_form({User, Server}, Lang) ++ + [?X("hr"), ?ACT(?LINK("config/complex"),"Advanced settings")] , Lang); + +process2(["config" , "submit", "global"], #request{q = Query } = Request , US) -> + submit_config_global( Query , US), + process2(["config"], Request, US); + +% process2(["config" , "submit", "contact"], #request{q = Query } = Request , US) -> +% submit_config_contact( Query , US), +% process2(["config"], Request, US); + +process2(["config" , "complex" ], #request{lang = Lang } = _Request , {User, Server}) -> + make_xhtml(?T("Config"), + [?XE("h3", [?CT("Global Settings")]) ] ++ global_config_form_complex({User, Server}, Lang) ++ + [?XE("h3", [?CT("Specific Contact Settings")]) ] ++ contact_config_form({User, Server}, Lang) ++ + [?X("hr"), ?ACT(?LINK("config"),"Simple settings")] , Lang); + +process2(["config" , "submit", "complex_global"], #request{q = Query } = Request , US) -> + submit_config_global_complex( Query , US), + process2(["config/complex"], Request, US); + +process2(["contact"], #request{lang = Lang } = _Request , US) -> + make_xhtml(?T("Contact List"), [ + ?XE("ul", lists:map( fun({Node,Server,Count}) -> + With = jlib:jid_to_string({Node,Server,""}), + ?LI([?AC(?LINK("contact/" ++ ejabberd_http:url_encode(With)), With ) , + ?C(" (" ++ Count ++")")] ) end, + get_contacts(US))) + ], Lang); + +process2(["contact" , Jid], #request{lang = Lang } = _Request , US) -> + make_xhtml(?T("Chat with ") ++ Jid, contact_config(Jid,US,Lang) ++ + [?XE("ul", lists:map( fun({Id, Node, Server, Resource, Utc, Subject }) -> + With = jlib:jid_to_string({Node,Server,Resource}), + ?LI([?AC(?LINK("show/" ++ integer_to_list(Id)), "On " ++ Utc ++ " with " ++ With ++ " -> " ++ escape_str(Subject) )] ) end, + get_collection_list(jlib:string_to_jid(Jid), US))) + ], Lang); + +process2(["show" , Id], #request{lang = Lang } = _Request , US) -> + { With, Utc, Subject, List, NPId } = get_collection(Id, US), + [Date, _Time] = string:tokens(Utc, " "), + make_xhtml(?T("Chat with ") ++ jlib:jid_to_string(With) ++ ?T(" on ") ++ Date ++ ?T(" : ") ++ escape_str(Subject), + lists:map(fun(Msg) -> format_message(Msg,With, US) end, List) + ++ links_previous_next(NPId, Lang) + ++ [?X("hr"), ?XAE("form",[{"action",?LINK("edit/" ++ Id)},{"metohd","post"}], + [?XE("label",[?CT("Edit subject: "), + ?INPUT("text","subject",escape_str(Subject))]), + ?INPUT("submit","submit",?T("Ok"))]), + ?XAE("form",[{"action",?LINK("delete/" ++ Id)},{"metohd","post"}, + {"onsubmit","return confirm('"++ ?T("Do you realy want to delete this chat") ++"')"}], + [?INPUT("hidden","id",Id),?INPUT("submit","delete",?T("Delete"))])] + , Lang); + +process2(["edit" , Id], #request{ q = Query} = Request , US) -> + case lists:keysearch("subject", 1, Query) of + {value, {_, Subject}} -> change_subject(Id,Subject,US); + _ -> ok + end, + process2(["show", Id] , Request, US); + +process2(["delete" , Id], #request{q = Query, lang=Lang} = _Request , US) -> + case lists:keysearch("id", 1, Query) of + {value, {_, Id2}} when Id==Id2 -> + delete_collection(Id,US), make_xhtml("Chat deleted",[],Lang); + _ -> ?ERR_INTERNAL_SERVER_ERROR + end; + +process2(["search"], #request{lang = Lang } = Request , US) -> + make_xhtml(?T("Search"), [ + search_form(Request, US) ], Lang); + +process2(["search", "results"], #request{lang = Lang } = Request , US) -> + make_xhtml(?T("Search"), [ search_form(Request, US) | search_results(Request, US)], Lang); + +process2([], #request{lang = Lang } = _Request , {LUser,LServer}) -> + make_xhtml(?T("Archives viewer"),[?PCT("Welcome " ++ LUser ++ "@" ++ LServer)], Lang); + +process2(_, #request{lang = Lang } = _Request , _US) -> + make_xhtml(?T("404 File not found"),[], Lang). + +%------------------------------ + +make_xhtml(Title, Els, Lang) -> + {200, [html], + {xmlelement, "html", [{"xmlns", "http://www.w3.org/1999/xhtml"}, + {"xml:lang", Lang}, + {"lang", Lang}], + [{xmlelement, "head", [], + [?XE("title", [?C(Title) , ?CT(" - ejabberd Web Archive Viewer")]), + {xmlelement, "meta", [{"http-equiv", "Content-Type"}, + {"content", "text/html; charset=utf-8"}], []}, + {xmlelement, "link", [{"href", ?LINK("style.css")}, + {"type", "text/css"}, + {"rel", "stylesheet"}], []}]}, + ?XE("body", + [?XAE("div", [{"id", "container"}], + [?XAE("div", [{"id", "header"}], + [?XE("h1", [?CT("Archives Viewer")])]), + ?XAE("div", [{"id", "navigation"}], + [?XE("ul", + [?LI([?ACT(?LINK("config"), "Config")]), ?C(" "), + ?LI([?ACT(?LINK("contact"), "Browse")]), ?C(" "), + ?LI([?ACT(?LINK("search"), "Search")])])]), ?C(" "), + ?XAE("div", [{"id", "content"}], [ ?XE("h2", [?C(Title)]) | Els])])])]}}. + + +get_auth(Auth) -> + case Auth of + {SJID, P} -> + case jlib:string_to_jid(SJID) of + error -> + unauthorized; + #jid{user = U, server = S} -> + case ejabberd_auth:check_password(U, S, P) of + true -> + {U, S}; + false -> + unauthorized + end + end; + _ -> + unauthorized + end. + +select_element(Name, List, Value1) -> + Value = if is_integer(Value1) -> integer_to_list(Value1); true -> Value1 end, + ?XAE("select",[{"name",Name}],lists:map( + fun({Key,Text}) -> ?XAE("option", + case Key of + Value -> [{"value",Value},{"selected","selected"}]; + _ -> [{"value",Key}] + end, [?C(Text)]) end, List)). + +table_element(Rows) -> + ?XE("table",lists:map(fun(Cols)-> ?XE("tr", lists:map(fun(Ct)-> ?XE("td",Ct) end, Cols)) end, Rows)). + +%------------------------ + +format_message({ Utc, Dir, Body } ,{WithU,WithS,WithR}, {LUser,LServer} ) -> + {From, Class} = case Dir of + 0 -> { jlib:jid_to_string({WithU,WithS,WithR}) , "message_from" } ; + 1 -> { jlib:jid_to_string({LUser,LServer,""}) , "message_to" } + end, + [_Date, Time] = string:tokens(Utc, " "), + ?XAE("p", [{"class", Class}] , [ ?XAE("span", [{"class","time"}], [?C("["++Time++"]")]), ?C(" "), + ?XAE("span", [{"class","jid"}], [?C(From)]), ?C(": "), + ?XAE("span", [{"class","message_body"}], [?C(Body)])]). + +contact_config(Jid,{LUser,LServer},Lang) -> + %run_sql_transaction(LServer, fun() -> run_sql_query("") end) + %[?XE("p",[?CT("Automatic archive with this contact is " + Au ), ]. + []. + + +global_config_form({LUser,LServer},Lang) -> + {Save,Expire,Auto_save} = + case run_sql_transaction(LServer, fun() -> run_sql_query( + "SELECT save,expire,auto_save" + " FROM archive_global_prefs" + " WHERE us = " ++ get_us_escaped({LUser,LServer}) ) end) of + {selected, _ , [ Ok ]} -> Ok; + {selected, _ , [ ]} -> { -1, -1, -1 } + end, + [?XAE("form",[{"action",?LINK("config/submit/global")}], + [?XE("label",[?CT("Disable or enable automatic archiving globaly: "), select_element("global_auto_save", + [{"-1",?T("--Server Default--")},{"1",?T("Enabled")},{"0",?T("Disabled")}],decode_integer(Auto_save))]), + ?BR, + ?XE("label",[?CT("Default for contact not specified bellow : "), select_element("global_save", + [{"-1",?T("--Server Default--")},{"1",?T("Enabled")},{"0",?T("Disabled")}],decode_integer(Save))]), + ?BR, ?XE("label",[?CT("Default expiration time: "), ?INPUT("text","global_expire",integer_to_list(decode_integer(Expire)))]), + ?CT("(number of seconds before deleting message, '-1' = server default)"), + ?BR, ?INPUTT("submit","global_submit","Submit")] + )]. + +global_config_form_complex({LUser,LServer},Lang) -> + {Save,Expire,Otr,Method_auto,Method_local,Method_manual,Auto_save} = + case run_sql_transaction(LServer, fun() -> run_sql_query( + "SELECT save,expire,otr,method_auto,method_local,method_manual,auto_save" + " FROM archive_global_prefs" + " WHERE us = " ++ get_us_escaped({LUser,LServer}) ) end) of + {selected, _ , [ Ok ]} -> Ok; + {selected, _ , [ ]} -> { -1, -1, -1, -1, -1, -1, -1 } + end, + MethodList = [ {"-1",?T("--Undefined--")}, {"0",?T("Prefer")}, {"1",?T("Concede")}, {"2",?T("Forbid")} ], + [?XAE("form",[{"action",?LINK("config/submit/complex_global")}],[table_element([[ + [?XE("label",[?CT("Save: "), select_element("global_save",[{"-1",?T("--Default--")},{"1",?T("Enabled")},{"0",?T("Disabled")}],decode_integer(Save))])], + [?XE("label",[?CT("Expire: "), ?INPUT("text","global_expire",integer_to_list(decode_integer(Expire)))])], + [?XE("label",[?CT("Otr: "), select_element("global_otr",[{"-1",?T("--Undefined--")}, + {"0",?T("Approve")}, + {"1",?T("Concede")}, + {"2",?T("Forbid")}, + {"3",?T("Oppose")}, + {"4",?T("Prefer")}, + {"5",?T("Require")} ],decode_integer(Otr))])], + [?XE("label",[?CT("Auto Method: "), select_element("global_method_auto", MethodList,decode_integer(Method_auto))])], + [?XE("label",[?CT("Local Method: "), select_element("global_method_local", MethodList,decode_integer(Method_local))])], + [?XE("label",[?CT("Manual Method: "), select_element("global_method_manual", MethodList,decode_integer(Method_manual))])], + [?XE("label",[?CT("Auto Save "), select_element("global_auto_save", + [{"-1",?T("--Default--")},{"1",?T("Enabled")},{"0",?T("Disabled")}],decode_integer(Auto_save))])], + [?INPUT("submit","global_modify",?T("Modify"))] + ]])])]. + + +contact_config_form({LUser,LServer},Lang) -> + {selected, _, List} = + run_sql_transaction(LServer, fun() -> run_sql_query( + "SELECT with_user,with_server,with_resource,save,expire" + " FROM archive_jid_prefs" + " WHERE us = " ++ get_us_escaped({LUser,LServer}) ) end), + [ table_element([[[?CT("JID")],[?CT("Auto archive")],[?CT("Expire")]] | + lists:map(fun({WithU,WithS,WithR,Save,Expire}) -> + [ [?C(jlib:jid_to_string({WithU,WithS,WithR}))], + [case decode_integer(Save) of 1 -> ?CT("Enabled"); 0 -> ?CT("Disabled"); _ -> ?CT("Default") end], + [?C(integer_to_list(decode_integer(Expire)))] ] end , List ) ]) %, +% ?XAE("form",[{"action",?LINK("config/submit/contact")}], +% [?XE("label",[?CT("Add/Modify settings for Jid: "), ?INPUT("text","jid","")]), +% ?BR, +% ?XE("label",[?CT("Archiving : "), select_element("save", +% [{"-1",?T("--Default--")},{"1",?T("Enabled")},{"0",?T("Disabled")}],"-1")]), +% ?BR, ?XE("label",[?CT("Expiration time: "), ?INPUT("text","expire","-1")]), +% ?BR, ?INPUTT("submit","submit","Submit")] +% ) + ]. + + + +get_from_query_escaped(Key,Query) -> + {value, {_, Value}} = lists:keysearch(Key, 1, Query), + case Value of + -1 -> "NULL"; + Integer when is_integer(Integer) -> Integer; + "-1" -> "NULL"; + Value -> "'" ++ ejabberd_odbc:escape(Value) ++ "'" + end. + +submit_config_global(Query , {LUser,LServer}) -> + SUS = get_us_escaped({LUser,LServer}), + SQLQuery = + "UPDATE archive_global_prefs" + " SET save = " ++ get_from_query_escaped("global_save",Query) ++ "," + " expire = " ++ get_from_query_escaped("global_expire",Query) ++ "," + " auto_save = " ++ get_from_query_escaped("global_auto_save",Query) ++ + " WHERE us = " ++ SUS, + F = fun() -> + %TODO: use REPLACE + case run_sql_query("SELECT us FROM archive_global_prefs WHERE us = " ++ SUS) of + {selected, _, Rs} when Rs /= [] -> ok; + _ -> run_sql_query("INSERT INTO archive_global_prefs (us) VALUES (" ++ SUS ++ ")") + end, + run_sql_query(SQLQuery) end, + run_sql_transaction(LServer, F). + +submit_config_global_complex(Query , {LUser,LServer}) -> + SUS = get_us_escaped({LUser,LServer}), + SQLQuery = + "UPDATE archive_global_prefs" + " SET save = " ++ get_from_query_escaped("global_save",Query) ++ "," + " expire = " ++ get_from_query_escaped("global_expire",Query) ++ "," + " otr = " ++ get_from_query_escaped("global_otr",Query) ++ "," + " method_auto = " ++ get_from_query_escaped("global_method_auto",Query) ++ "," + " method_local = " ++ get_from_query_escaped("global_method_local",Query) ++ "," + " method_manual = " ++ get_from_query_escaped("global_method_manual",Query) ++ "," + " auto_save = " ++ get_from_query_escaped("global_auto_save",Query) ++ + " WHERE us = " ++ SUS, + F = fun() -> + %TODO: use REPLACE + case run_sql_query("SELECT us FROM archive_global_prefs WHERE us = " ++ SUS) of + {selected, _, Rs} when Rs /= [] -> ok; + _ -> run_sql_query("INSERT INTO archive_global_prefs (us) VALUES (" ++ SUS ++ ")") + end, + run_sql_query(SQLQuery) end, + run_sql_transaction(LServer, F). + +get_from_query_with_default(Key,Query,Default) -> + case lists:keysearch(Key, 1, Query) of + {value, {_, Value}} -> Value; + _ -> Default + end. + +search_form( #request{lang = Lang, q = Query } = _Request, US) -> + ?XAE("form",[{"method","post"},{"action", ?LINK("search/results")}], + [ ?XE("label", [?CT("With: ") , + select_element("with" , [ { "", "--All--" } | + lists:map( fun({Node,Server,_Count}) -> + With = jlib:jid_to_string({Node,Server,""}), + {With, With} end, + get_contacts(US)) ], + get_from_query_with_default("with",Query,""))]), + ?BR, + ?XE("label", [?CT("From: ") , ?INPUT("text","from", get_from_query_with_default("from",Query,"")), + ?CT(" (date in SQL format, may be empty)")]), + ?BR, + ?XE("label", [?CT("To: ") , ?INPUT("text","to", get_from_query_with_default("to",Query,"")), + ?CT(" (date in SQL format, may be empty)")]), + ?BR, + ?XE("label", [?CT("Search keyword: ") , ?INPUT("text","keywords", + get_from_query_with_default("keywords",Query,""))]), + ?BR, + ?INPUT("submit","search",?T("Search")) ]). + + + +search_results( #request{lang = Lang, q = Query } = _Request, {_, LServer} = US) -> + With = case lists:keysearch("with", 1, Query) of + {value, {_, Value}} -> case jlib:string_to_jid(Value) of + #jid{ luser = Node , lserver = Server , lresource = "" } -> + " AND with_user ='" ++ ejabberd_odbc:escape(Node) ++ "'" ++ + " AND with_server ='" ++ ejabberd_odbc:escape(Server) ++ "'"; + #jid{ luser = Node , lserver = Server , lresource = Resource } -> + " AND with_user ='" ++ ejabberd_odbc:escape(Node) ++ "'" ++ + " AND with_server ='" ++ ejabberd_odbc:escape(Server) ++ "'" ++ + " AND with_resource ='" ++ ejabberd_odbc:escape(Resource) ++ "'"; + _ -> "" + end; + _ -> "" + end, + From = case lists:keysearch("from", 1, Query) of + {value, {_, V1}} when V1 /= "" -> " AND M.utc >= '" ++ ejabberd_odbc:escape(V1) ++ "'"; + _ -> "" + end, + To = case lists:keysearch("to", 1, Query) of + {value, {_, V2}} when V2 /= "" -> " AND M.utc <= '" ++ ejabberd_odbc:escape(V2) ++ "'"; + _ -> "" + end, + Kw = case lists:keysearch("keywords", 1, Query) of + {value, {_, V3}} when V3 /= "" -> " AND body LIKE '%" ++ ejabberd_odbc:escape(V3) ++ "%'"; + _ -> "" + end, + + F = fun() -> run_sql_query( + "SELECT coll_id,subject,with_user,with_server,with_resource,C.utc,body" + " FROM archive_collections as C, archive_messages as M" + " WHERE C.id = M.coll_id AND C.us = " ++ get_us_escaped(US) ++ + " AND C.deleted='0'" ++ With ++ From ++ To ++ Kw ++ + " GROUP BY coll_id") end, + case run_sql_transaction(LServer,F) of + {selected, _ , []} -> [?PCT("No matches")]; + {selected, _ , Results} -> + lists:map(fun(R) -> format_search_result(R,Lang) end, Results) + end. + +format_search_result( {Id,Subject,User,Server,Resource,Utc,Body} ,_Lang) -> + ?XAE("p",[{"class","search_result"}], + [?AC(?LINK("show/" ++ integer_to_list(Id)), jlib:jid_to_string({User,Server,Resource}) ++ " : " ++ escape_str(Subject)), + ?C(Body), ?XE("em",[?C(Utc)]) ] ). + +links_previous_next({PrevId,NextId},Lang) -> + [?XAE("p",[{"class","links_previous_next"}], + links_previous_next_aux("link_prev", ?T("Previous"), PrevId) ++ [?C(" ")] ++ + links_previous_next_aux("link_next", ?T("Next"), NextId))]. + +links_previous_next_aux(Class, Text, Id) -> + case Id of + -1 -> []; + _ -> [?XAE("a",[{"href",?LINK("show/" ++ integer_to_list(Id))},{"class",Class}], [?C(Text)])] + end. + +%------------------------ + +get_contacts({LUser, LServer}) -> + Fun = fun() -> + {selected, _ , Contacts} = run_sql_query("SELECT with_user,with_server,COUNT(*)" + " FROM archive_collections" + " WHERE us = " ++ get_us_escaped({LUser,LServer}) ++ " AND deleted=0" + " GROUP BY with_user,with_server"), + Contacts end, + run_sql_transaction(LServer, Fun). + +get_collection_list(Jid, {LUser, LServer}) -> + {WithU, WithS, _} = get_jid_escaped(Jid), + Fun = fun() -> + {selected, _ , List} = run_sql_query("SELECT id,with_user,with_server,with_resource,utc,subject" + " FROM archive_collections" + " WHERE us = " ++ get_us_escaped({LUser,LServer}) ++ + " AND deleted=0 " + " AND with_user = " ++ WithU ++ + " AND with_server = " ++ WithS), + List end, + run_sql_transaction(LServer, Fun). + +get_collection(Id,{LUser,LServer}) -> + Fun = fun() -> + SUS = get_us_escaped({LUser,LServer}), + {selected, _ , [{WithU, WithS, WithR, Utc, Subject}] } = run_sql_query( + "SELECT with_user,with_server,with_resource,utc,subject" + " FROM archive_collections" + " WHERE id = '" ++ ejabberd_odbc:escape(Id) ++ "'" + " AND us = " ++ SUS), + %If the previous query fail, that mean the collection doesn't exist or is not + % one of the users connection. + + {selected, _ , List} = run_sql_query("SELECT utc,dir,body" + " FROM archive_messages" + " WHERE coll_id = '" ++ ejabberd_odbc:escape(Id) ++ "'"), + NextId = case run_sql_query("SELECT id" + " FROM archive_collections" + " WHERE us = " ++ SUS ++ " AND deleted=0 " + " AND with_user ='" ++ ejabberd_odbc:escape(WithU) ++ "'" ++ + " AND with_server ='" ++ ejabberd_odbc:escape(WithS) ++ "'" ++ + " AND utc > '" ++ Utc ++ "'" ++ + " ORDER BY utc LIMIT 1") of + {selected, _ , [{V1}]} -> V1; + _ -> -1 + end, + PrevId = case run_sql_query("SELECT id" + " FROM archive_collections" + " WHERE us = " ++ SUS ++ " AND deleted=0 " + " AND with_user ='" ++ ejabberd_odbc:escape(WithU) ++ "'" ++ + " AND with_server ='" ++ ejabberd_odbc:escape(WithS) ++ "'" ++ + " AND utc < '" ++ Utc ++ "'" ++ + " ORDER BY utc DESC LIMIT 1") of + {selected, _ , [{V2}]} -> V2; + _ -> -1 + end, + { {WithU,WithS,WithR} , Utc, Subject , List, {PrevId,NextId}} end, + run_sql_transaction(LServer, Fun). + +change_subject(Id,Subject,{LUser,LServer}) -> + run_sql_transaction(LServer, fun() -> run_sql_query( + "UPDATE archive_collections" + " SET subject='"++ ejabberd_odbc:escape(Subject)++"'," + " change_utc=NOW(), change_by='webview'" + " WHERE id = '" ++ ejabberd_odbc:escape(Id) ++ "'" + " AND us = " ++ get_us_escaped({LUser,LServer})) end). + +delete_collection(Id,{LUser,LServer}) -> + run_sql_transaction(LServer, fun() -> run_sql_query( + "UPDATE archive_collections" + " SET deleted=1, subject='', thread = '', extra = '', prev_id = NULL, next_id = NULL," + " change_utc=NOW(), change_by='webview'" + " WHERE id = '" ++ ejabberd_odbc:escape(Id) ++ "'" + " AND us = " ++ get_us_escaped({LUser,LServer})) end). + +%------------------------ +% from mod_archive_odbc +run_sql_query(Query) -> + ?MYDEBUG("running query: ~p", [lists:flatten(Query)]), + case catch ejabberd_odbc:sql_query_t(Query) of + {'EXIT', Err} -> + ?ERROR_MSG("unhandled exception during query: ~p", [Err]), + exit(Err); + {error, Err} -> + ?ERROR_MSG("error during query: ~p", [Err]), + throw({error, Err}); + aborted -> + ?ERROR_MSG("query aborted ~p", [Query]), + throw(aborted); + R -> ?MYDEBUG("query result: ~p", [R]), + R + end. + +run_sql_transaction(LServer, F) -> + DBHost = gen_mod:get_module_opt(LServer, ?MODULE, db_host, LServer), + case ejabberd_odbc:sql_transaction(DBHost, F) of + {atomic, R} -> + ?MYDEBUG("succeeded transaction: ~p", [R]), + R; + {error, Err} -> {error, Err}; + E -> + ?ERROR_MSG("failed transaction: ~p, stack: ~p", [E, process_info(self(),backtrace)]), + {error, ?ERR_INTERNAL_SERVER_ERROR} + end. + +get_us_escaped({LUser, LServer}) -> + "'" ++ ejabberd_odbc:escape(LUser ++ "@" ++ LServer) ++ "'". + +get_jid_escaped({LUser, LServer, LResource}) -> + {"'" ++ ejabberd_odbc:escape(LUser), "'" ++ ejabberd_odbc:escape(LServer), + "'" ++ ejabberd_odbc:escape(LResource)}; + +get_jid_escaped(#jid{luser = LUser, lserver = LServer, lresource=LResource}) -> + {"'" ++ ejabberd_odbc:escape(LUser) ++ "'", "'" ++ ejabberd_odbc:escape(LServer) ++ "'", + "'" ++ ejabberd_odbc:escape(LResource) ++ "'"}. + +decode_integer(Val) when is_integer(Val) -> + Val; +decode_integer(null) -> + -1; +decode_integer(Val) -> + list_to_integer(Val). + +escape_str(null) -> ""; +escape_str(Str) -> Str. diff --git a/mod_archive/src/msgs/mod_archive_webview.pot b/mod_archive/src/msgs/mod_archive_webview.pot new file mode 100644 index 0000000..d3a28d2 --- /dev/null +++ b/mod_archive/src/msgs/mod_archive_webview.pot @@ -0,0 +1,226 @@ +msgid "" +msgstr "" +"Project-Id-Version: 2.1.x\n" +"X-Language: Language Name\n" +"Last-Translator: Translator name and contact method\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: mod_archive_webview.erl:69 mod_archive_webview.erl:83 +#: mod_archive_webview.erl:171 +msgid "Config" +msgstr "" + +#: mod_archive_webview.erl:70 mod_archive_webview.erl:84 +msgid "Global Settings" +msgstr "" + +#: mod_archive_webview.erl:71 mod_archive_webview.erl:85 +msgid "Specific Contact Settings" +msgstr "" + +#: mod_archive_webview.erl:72 +msgid "Advanced settings" +msgstr "" + +#: mod_archive_webview.erl:86 +msgid "Simple settings" +msgstr "" + +#: mod_archive_webview.erl:93 +msgid "Contact List" +msgstr "" + +#: mod_archive_webview.erl:102 mod_archive_webview.erl:112 +msgid "Chat with " +msgstr "" + +#: mod_archive_webview.erl:112 +msgid " : " +msgstr "" + +#: mod_archive_webview.erl:112 +msgid " on " +msgstr "" + +#: mod_archive_webview.erl:116 +msgid "Edit subject: " +msgstr "" + +#: mod_archive_webview.erl:118 +msgid "Ok" +msgstr "" + +#: mod_archive_webview.erl:120 +msgid "Do you realy want to delete this chat" +msgstr "" + +#: mod_archive_webview.erl:139 mod_archive_webview.erl:143 +#: mod_archive_webview.erl:173 mod_archive_webview.erl:369 +msgid "Search" +msgstr "" + +#: mod_archive_webview.erl:146 +msgid "Archives viewer" +msgstr "" + +#: mod_archive_webview.erl:149 +msgid "404 File not found" +msgstr "" + +#: mod_archive_webview.erl:159 +msgid " - ejabberd Web Archive Viewer" +msgstr "" + +#: mod_archive_webview.erl:168 +msgid "Archives Viewer" +msgstr "" + +#: mod_archive_webview.erl:172 +msgid "Browse" +msgstr "" + +#: mod_archive_webview.erl:235 +msgid "Disable or enable automatic archiving globaly: " +msgstr "" + +#: mod_archive_webview.erl:236 mod_archive_webview.erl:239 +msgid "--Server Default--" +msgstr "" + +#: mod_archive_webview.erl:236 mod_archive_webview.erl:239 +#: mod_archive_webview.erl:256 mod_archive_webview.erl:269 +#: mod_archive_webview.erl:283 +msgid "Disabled" +msgstr "" + +#: mod_archive_webview.erl:236 mod_archive_webview.erl:239 +#: mod_archive_webview.erl:256 mod_archive_webview.erl:269 +#: mod_archive_webview.erl:283 +msgid "Enabled" +msgstr "" + +#: mod_archive_webview.erl:238 +msgid "Default for contact not specified bellow : " +msgstr "" + +#: mod_archive_webview.erl:240 +msgid "Default expiration time: " +msgstr "" + +#: mod_archive_webview.erl:241 +msgid "(number of seconds before deleting message, '-1' = server default)" +msgstr "" + +#: mod_archive_webview.erl:254 mod_archive_webview.erl:258 +msgid "--Undefined--" +msgstr "" + +#: mod_archive_webview.erl:254 mod_archive_webview.erl:260 +msgid "Concede" +msgstr "" + +#: mod_archive_webview.erl:254 mod_archive_webview.erl:261 +msgid "Forbid" +msgstr "" + +#: mod_archive_webview.erl:254 mod_archive_webview.erl:263 +msgid "Prefer" +msgstr "" + +#: mod_archive_webview.erl:256 mod_archive_webview.erl:269 +msgid "--Default--" +msgstr "" + +#: mod_archive_webview.erl:256 +msgid "Save: " +msgstr "" + +#: mod_archive_webview.erl:257 +msgid "Expire: " +msgstr "" + +#: mod_archive_webview.erl:258 +msgid "Otr: " +msgstr "" + +#: mod_archive_webview.erl:259 +msgid "Approve" +msgstr "" + +#: mod_archive_webview.erl:262 +msgid "Oppose" +msgstr "" + +#: mod_archive_webview.erl:264 +msgid "Require" +msgstr "" + +#: mod_archive_webview.erl:265 +msgid "Auto Method: " +msgstr "" + +#: mod_archive_webview.erl:266 +msgid "Local Method: " +msgstr "" + +#: mod_archive_webview.erl:267 +msgid "Manual Method: " +msgstr "" + +#: mod_archive_webview.erl:268 +msgid "Auto Save " +msgstr "" + +#: mod_archive_webview.erl:270 +msgid "Modify" +msgstr "" + +#: mod_archive_webview.erl:280 +msgid "Auto archive" +msgstr "" + +#: mod_archive_webview.erl:280 +msgid "Expire" +msgstr "" + +#: mod_archive_webview.erl:280 +msgid "JID" +msgstr "" + +#: mod_archive_webview.erl:283 +msgid "Default" +msgstr "" + +#: mod_archive_webview.erl:352 +msgid "With: " +msgstr "" + +#: mod_archive_webview.erl:360 +msgid "From: " +msgstr "" + +#: mod_archive_webview.erl:361 mod_archive_webview.erl:364 +msgid " (date in SQL format, may be empty)" +msgstr "" + +#: mod_archive_webview.erl:363 +msgid "To: " +msgstr "" + +#: mod_archive_webview.erl:366 +msgid "Search keyword: " +msgstr "" + +#: mod_archive_webview.erl:407 +msgid "No matches" +msgstr "" + +#: mod_archive_webview.erl:419 +msgid "Previous" +msgstr "" + +#: mod_archive_webview.erl:420 +msgid "Next" +msgstr "" diff --git a/mod_archive/src/msgs/pl.mod_archive_webview.po b/mod_archive/src/msgs/pl.mod_archive_webview.po new file mode 100644 index 0000000..07c0ad6 --- /dev/null +++ b/mod_archive/src/msgs/pl.mod_archive_webview.po @@ -0,0 +1,226 @@ +msgid "" +msgstr "" +"Project-Id-Version: 2.1.x\n" +"X-Language: Language Name\n" +"Last-Translator: Andrzej Smyk\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: mod_archive_webview.erl:69 mod_archive_webview.erl:83 +#: mod_archive_webview.erl:171 +msgid "Config" +msgstr "Konfiguracja" + +#: mod_archive_webview.erl:70 mod_archive_webview.erl:84 +msgid "Global Settings" +msgstr "Ustawienia globalne" + +#: mod_archive_webview.erl:71 mod_archive_webview.erl:85 +msgid "Specific Contact Settings" +msgstr "Specyficzne ustawienia kontaktu" + +#: mod_archive_webview.erl:72 +msgid "Advanced settings" +msgstr "Ustawienia zaawansowane" + +#: mod_archive_webview.erl:86 +msgid "Simple settings" +msgstr "Ustawienia podstawowe" + +#: mod_archive_webview.erl:93 +msgid "Contact List" +msgstr "Lista kontaktów" + +#: mod_archive_webview.erl:102 mod_archive_webview.erl:112 +msgid "Chat with " +msgstr "Rozmowa z " + +#: mod_archive_webview.erl:112 +msgid " : " +msgstr " : " + +#: mod_archive_webview.erl:112 +msgid " on " +msgstr " na " + +#: mod_archive_webview.erl:116 +msgid "Edit subject: " +msgstr "Edycja tematu: " + +#: mod_archive_webview.erl:118 +msgid "Ok" +msgstr "" + +#: mod_archive_webview.erl:120 +msgid "Do you realy want to delete this chat" +msgstr "Czy na pewno chcesz usunąć ten chat" + +#: mod_archive_webview.erl:139 mod_archive_webview.erl:143 +#: mod_archive_webview.erl:173 mod_archive_webview.erl:369 +msgid "Search" +msgstr "Szukaj" + +#: mod_archive_webview.erl:146 +msgid "Archives viewer" +msgstr "PrzeglÄ…darka archiwów" + +#: mod_archive_webview.erl:149 +msgid "404 File not found" +msgstr "404 Pliku nie znaleziono" + +#: mod_archive_webview.erl:159 +msgid " - ejabberd Web Archive Viewer" +msgstr "" + +#: mod_archive_webview.erl:168 +msgid "Archives Viewer" +msgstr "PrzeglÄ…darka Archiwów" + +#: mod_archive_webview.erl:172 +msgid "Browse" +msgstr "PrzeglÄ…daj" + +#: mod_archive_webview.erl:235 +msgid "Disable or enable automatic archiving globaly: " +msgstr "WÅ‚Ä…cz lub wyÅ‚Ä…cz automatyczne archiwizowanie globalnie: " + +#: mod_archive_webview.erl:236 mod_archive_webview.erl:239 +msgid "--Server Default--" +msgstr "--DomyÅ›lne ustawienia serwera--" + +#: mod_archive_webview.erl:236 mod_archive_webview.erl:239 +#: mod_archive_webview.erl:256 mod_archive_webview.erl:269 +#: mod_archive_webview.erl:283 +msgid "Disabled" +msgstr "WÅ‚Ä…czone" + +#: mod_archive_webview.erl:236 mod_archive_webview.erl:239 +#: mod_archive_webview.erl:256 mod_archive_webview.erl:269 +#: mod_archive_webview.erl:283 +msgid "Enabled" +msgstr "WyÅ‚Ä…czone" + +#: mod_archive_webview.erl:238 +msgid "Default for contact not specified bellow : " +msgstr "DomyÅ›lnie dla kontaktu nie zaznaczonego poniżej : " + +#: mod_archive_webview.erl:240 +msgid "Default expiration time: " +msgstr "DomyÅ›lny czas wygaÅ›niÄ™cia: " + +#: mod_archive_webview.erl:241 +msgid "(number of seconds before deleting message, '-1' = server default)" +msgstr "(liczba sekund przed wykasowaniem wiadomoÅ›ci, '-1' = domyÅ›lne ustawienie serwera" + +#: mod_archive_webview.erl:254 mod_archive_webview.erl:258 +msgid "--Undefined--" +msgstr "--Niezdefiniowane--" + +#: mod_archive_webview.erl:254 mod_archive_webview.erl:260 +msgid "Concede" +msgstr "UstÄ™puje" + +#: mod_archive_webview.erl:254 mod_archive_webview.erl:261 +msgid "Forbid" +msgstr "Zabrania" + +#: mod_archive_webview.erl:254 mod_archive_webview.erl:263 +msgid "Prefer" +msgstr "Preferuje" + +#: mod_archive_webview.erl:256 mod_archive_webview.erl:269 +msgid "--Default--" +msgstr "--DomyÅ›lnie--" + +#: mod_archive_webview.erl:256 +msgid "Save: " +msgstr "Zachowaj: " + +#: mod_archive_webview.erl:257 +msgid "Expire: " +msgstr "UpÅ‚ywa: " + +#: mod_archive_webview.erl:258 +msgid "Otr: " +msgstr "" + +#: mod_archive_webview.erl:259 +msgid "Approve" +msgstr "Akceptuje" + +#: mod_archive_webview.erl:262 +msgid "Oppose" +msgstr "Oponuje" + +#: mod_archive_webview.erl:264 +msgid "Require" +msgstr "Wymaga" + +#: mod_archive_webview.erl:265 +msgid "Auto Method: " +msgstr "Metoda automatyczna: " + +#: mod_archive_webview.erl:266 +msgid "Local Method: " +msgstr "Metoda lokalna: " + +#: mod_archive_webview.erl:267 +msgid "Manual Method: " +msgstr "Metoda manualna: " + +#: mod_archive_webview.erl:268 +msgid "Auto Save " +msgstr "Automatyczne zachowanie " + +#: mod_archive_webview.erl:270 +msgid "Modify" +msgstr "Modyfikuj" + +#: mod_archive_webview.erl:280 +msgid "Auto archive" +msgstr "Autoarchiwizacja" + +#: mod_archive_webview.erl:280 +msgid "Expire" +msgstr "UpÅ‚ywa" + +#: mod_archive_webview.erl:280 +msgid "JID" +msgstr "JID" + +#: mod_archive_webview.erl:283 +msgid "Default" +msgstr "DomyÅ›lnie" + +#: mod_archive_webview.erl:352 +msgid "With: " +msgstr "Z: " + +#: mod_archive_webview.erl:360 +msgid "From: " +msgstr "Od: " + +#: mod_archive_webview.erl:361 mod_archive_webview.erl:364 +msgid " (date in SQL format, may be empty)" +msgstr "" + +#: mod_archive_webview.erl:363 +msgid "To: " +msgstr "Do: " + +#: mod_archive_webview.erl:366 +msgid "Search keyword: " +msgstr "Szukaj sÅ‚owa: " + +#: mod_archive_webview.erl:407 +msgid "No matches" +msgstr "Brak wyników" + +#: mod_archive_webview.erl:419 +msgid "Previous" +msgstr "Poprzednie" + +#: mod_archive_webview.erl:420 +msgid "Next" +msgstr "NastÄ™pne" diff --git a/mod_archive/src/pg_mod_archive.sql b/mod_archive/src/pg_mod_archive.sql new file mode 100644 index 0000000..9638aad --- /dev/null +++ b/mod_archive/src/pg_mod_archive.sql @@ -0,0 +1,60 @@ +CREATE SEQUENCE archive_collection_sid_seq; + +CREATE TABLE archive_collection ( + sid bigint NOT NULL DEFAULT nextval('archive_collection_sid_seq') PRIMARY KEY, + username text NOT NULL, + jid_u text NOT NULL, + jid_s text NOT NULL, + jid_r text NOT NULL, + start int8 NOT NULL, + subject text NOT NULL +); + +CREATE INDEX i_archive_collection_username ON archive_collection USING btree (username); +CREATE INDEX i_archive_collection_user_jid ON archive_collection USING btree (username, jid_u, jid_s); +CREATE UNIQUE INDEX i_archive_collection_user_jid_start ON archive_collection USING btree (username, jid_u, jid_s, start); + +CREATE SEQUENCE archive_message_ser_seq; + +CREATE TABLE archive_message ( + sid bigint REFERENCES archive_collection ON DELETE CASCADE, + ser bigint NOT NULL DEFAULT nextval('archive_message_ser_seq'), + direction_to boolean NOT NULL, + secs int NOT NULL, + utc int8 NOT NULL, + name text NOT NULL, + jid text NOT NULL, + body text NOT NULL +); + +CREATE INDEX i_archive_message_sid ON archive_message USING btree (sid); + +-- FTS support +-- Requires tsearch2 and btree_gist + +ALTER TABLE archive_collection ADD COLUMN fts tsvector NOT NULL DEFAULT to_tsvector(''); + +CREATE INDEX i_archive_collection_fts on archive_collection USING gist(username, fts); + +CREATE RULE r_archive_fts_update AS ON INSERT TO archive_message DO ALSO + UPDATE archive_collection + SET fts = fts || to_tsvector(translate(NEW.body, '<>&', ' ')) + WHERE sid = NEW.sid; + +--CREATE TABLE archive_fts ( +-- sid bigint PRIMARY KEY REFERENCES archive_collection ON DELETE CASCADE, +-- username text NOT NULL, +-- fts tsvector +--); +-- +--CREATE INDEX i_archive_fts on archive_fts USING gist(username, fts); +-- +--CREATE RULE r_archive_fts_empty AS ON INSERT TO archive_collection DO ALSO +-- INSERT INTO archive_fts(sid, username, fts) +-- VALUES (NEW.sid, NEW.username, to_tsvector('')); +-- +--CREATE RULE r_archive_fts_update AS ON INSERT TO archive_message DO ALSO +-- UPDATE archive_fts +-- SET fts = fts || to_tsvector(translate(NEW.body, '<>&', ' ')) +-- WHERE sid = NEW.sid; + diff --git a/mod_cron/COPYING b/mod_cron/COPYING new file mode 100644 index 0000000..8b325f5 --- /dev/null +++ b/mod_cron/COPYING @@ -0,0 +1,343 @@ +As a special exception, the authors give permission to link this program +with the OpenSSL library and distribute the resulting binary. + + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Library General +Public License instead of this License. diff --git a/mod_cron/ChangeLog b/mod_cron/ChangeLog new file mode 100644 index 0000000..9f6b8a7 --- /dev/null +++ b/mod_cron/ChangeLog @@ -0,0 +1,33 @@ +2009-06-09 Badlop + + * src/mod_cron.erl: Fix paths (thanks to Bman) + +2008-10-12 Badlop + + * src/mod_cron.erl: Update from ctl to commands (EJAB-694) + +2008-07-29 Badlop + + * src/mod_cron.erl: Fix call to ejabberd_hooks:delete + +2007-12-26 Badlop + + * src/mod_cron.erl: Translate menu items of webadmin hooks in each + module (EJAB-485) + +2007-08-31 Badlop + + * src/mod_cron.erl (web_page_host): Small improvement on page + format. + +2007-08-23 Badlop + + * src/mod_cron.erl: Added page in Web Admin; currently only tasks + view is possible. * README.txt: Idem. + +2007-08-05 Badlop + + * ChangeLog: New file to track changes. + + * src/mod_cron.erl: Fixed indentation. + diff --git a/mod_cron/Emakefile b/mod_cron/Emakefile new file mode 100644 index 0000000..27441a8 --- /dev/null +++ b/mod_cron/Emakefile @@ -0,0 +1,2 @@ +{'../ejabberd-dev/src/gen_mod', [{outdir, "../ejabberd-dev/ebin"},{i,"../ejabberd-dev/include"}]}. +{'src/mod_cron', [{outdir, "ebin"},{i,"../ejabberd-dev/include"}]}. diff --git a/mod_cron/README.txt b/mod_cron/README.txt new file mode 100644 index 0000000..cf0f471 --- /dev/null +++ b/mod_cron/README.txt @@ -0,0 +1,76 @@ + + mod_cron - Execute scheduled commands + + http://www.ejabberd.im/mod_cron + Author: Badlop + Requirements: ejabberd trunk SVN 1635 or newer + + +This module allows advanced ejabberd administrators to schedule commands for +periodic and automatic execution. This module is a similar concept than the +*nix's cron program. Obviously, the admin must know in advance which module, +function and arguments to use, so this module is not intended for starting +administrators. + +Each time a scheduled task finish its execution, a message is printed in the +ejabberd log file. + + + BASIC CONFIGURATION + =================== + +Add the module to your ejabberd.cfg, on the modules section: +{modules, [ + ... + {mod_cron, []}, + ... +]}. + + + TASK SYNTAX + =========== + +Each task is described using a tuple with this syntax: + {Time, Time_units, Module, Function, Arguments} +Where: + Time is an integer. + Time_units indicates the time unit you use. It can be: seconds, minutes, hours, days. + Module and Function are the exact call you want to schedule. + Arguments is a list of arguments. It can be emtpy. + +For example, let's define some dummy tasks: + * Every 3 hours, print on the log file some info about mnesia: + {3, hours, mnesia, info, []} + + * Every day, try to register certain account: + {1, days, ejabberd_auth, try_register, ["tommy", "jabber.example.org", "MyP455WorD"]} + + + TASKS ON EJABBERD.CFG + ===================== + +Now that you know how to define new tasks, you can add the ones you want on ejabberd.cfg +For example: +{modules, [ + ... + {mod_cron, [{tasks, [ + {3, hours, mnesia, info, []}, + {1, days, ejabberd_auth, try_register, ["aaa", "atenea", "aaaaaa"]} + ]}]}, + ... +]}. + + + EJABBERD COMMANDS + ================= + +This module provides two new commands that can be executed using ejabberdctl: + * cron-list: list scheduled tasks + * cron-del taskid: delete this task from the schedule + + + WEB ADMIN + ========= + +This module provides a page in the Host section of the Web Admin. +Currently that page only allows to view the tasks scheduled for that host. diff --git a/mod_cron/build.bat b/mod_cron/build.bat new file mode 100644 index 0000000..fff9b3b --- /dev/null +++ b/mod_cron/build.bat @@ -0,0 +1 @@ +erl -pa ../../ejabberd-dev/trunk/ebin -pa ebin -make diff --git a/mod_cron/build.sh b/mod_cron/build.sh new file mode 100755 index 0000000..f0a0cfa --- /dev/null +++ b/mod_cron/build.sh @@ -0,0 +1,2 @@ +#!/bin/sh +erl -pa ../ejabberd-dev/ebin -pz ebin -make diff --git a/mod_cron/src/mod_cron.erl b/mod_cron/src/mod_cron.erl new file mode 100644 index 0000000..107200d --- /dev/null +++ b/mod_cron/src/mod_cron.erl @@ -0,0 +1,181 @@ +%%%---------------------------------------------------------------------- +%%% File : mod_cron.erl +%%% Author : Badlop +%%% Purpose : Execute scheduled tasks +%%% Created : 12 July 2007 +%%% Id : $Id: mod_cron.erl 1034 2009-11-17 21:44:17Z badlop $ +%%%---------------------------------------------------------------------- + +-module(mod_cron). +-author('badlop@ono.com'). + +-behaviour(gen_mod). + +-export([ + cron_list/1, cron_del/1, + run_task/3, + web_menu_host/3, web_page_host/3, + start/2, + stop/1]). + +-include("ejabberd_commands.hrl"). +-include("ejabberd.hrl"). +-include("web/ejabberd_http.hrl"). +-include("web/ejabberd_web_admin.hrl"). + +-record(task, {taskid, timerref, host, task}). + + +%% --------------------- +%% gen_mod +%% --------------------- + +start(Host, Opts) -> + ejabberd_commands:register_commands(commands()), + ejabberd_hooks:add(webadmin_menu_host, Host, ?MODULE, web_menu_host, 50), + ejabberd_hooks:add(webadmin_page_host, Host, ?MODULE, web_page_host, 50), + Tasks = gen_mod:get_opt(tasks, Opts, []), + catch ets:new(cron_tasks, [ordered_set, named_table, public, {keypos, 2}]), + [add_task(Host, Task) || Task <- Tasks]. + +stop(Host) -> + ejabberd_commands:unregister_commands(commands()), + ejabberd_hooks:delete(webadmin_menu_host, Host, ?MODULE, web_menu_host, 50), + ejabberd_hooks:delete(webadmin_page_host, Host, ?MODULE, web_page_host, 50), + %% Delete tasks of this host + [delete_task(Task) || Task <- get_tasks(Host)]. + + +%% --------------------- +%% Task management +%% --------------------- + +%% Method to add new task +add_task(Host, Task) -> + {Time_num, Time_unit, Mod, Fun, Args} = Task, + + %% Convert to miliseconds + Time = case Time_unit of + seconds -> timer:seconds(Time_num); + minutes -> timer:minutes(Time_num); + hours -> timer:hours(Time_num); + days -> timer:hours(Time_num)*24 + end, + + %% Start timer + {ok, TimerRef} = timer:apply_interval(Time, ?MODULE, run_task, [Mod, Fun, Args]), + + %% Get new task identifier + TaskId = get_new_taskid(), + + %% Store TRef + Taskr = #task{ + taskid = TaskId, + timerref = TimerRef, + host = Host, + task = Task + }, + + ets:insert(cron_tasks, Taskr). + +get_new_taskid() -> + case ets:last(cron_tasks) of + '$end_of_table' -> 0; + Id -> Id + 1 + end. + +%% Method to run existing task +run_task(Mod, Fun, Args) -> + case catch apply(Mod, Fun, Args) of + {'EXIT', Reason} -> + ?ERROR_MSG("Error in scheduled task ~p:~p~p:~n~p", [Mod, Fun, Args, Reason]); + {error, Reason} -> + ?ERROR_MSG("Error in scheduled task ~p:~p~p:~n~p", [Mod, Fun, Args, Reason]); + ok -> + ?INFO_MSG("Scheduled task ~p:~p~p finished ok", [Mod, Fun, Args]); + Res -> + ?INFO_MSG("Scheduled task ~p:~p~p returned:~n~p", [Mod, Fun, Args, Res]) + end. + +%% Method to delete task, given a taskid +delete_taskid(TaskId) -> + [Task] = ets:lookup(cron_tasks, TaskId), + delete_task(Task). + +%% Method to delete task, given the whole task +delete_task(Task) -> + timer:cancel(Task#task.timerref), + ets:delete(cron_tasks, Task#task.taskid). + +%% Method to know existing tasks on a given host +get_tasks(Host) -> + ets:select(cron_tasks, [{#task{host = Host, _ = '_'}, [], ['$_']}]). + +%% Method to know taskids of existing tasks on a given host +%%get_tasks_ids(Host) -> +%% L = ets:match(cron_tasks, #task{host = Host, taskid = '$1', _ = '_'}), +%% [Id || [Id] <- L]. + + +%% --------------------- +%% ejabberd commands +%% --------------------- + +commands() -> + [ + #ejabberd_commands{name = cron_list, tags = [cron], + desc = "List tasks scheduled in a host", + module = ?MODULE, function = cron_list, + args = [{host, string}], + result = {tasks, {list, {task, {tuple, [{id, integer}, {task, string}]}}}}}, + #ejabberd_commands{name = cron_del, tags = [cron], + desc = "Delete this task from the schedule", + module = ?MODULE, function = cron_del, + args = [{taskid, integer}], + result = {res, rescode}} + ]. + +cron_list(Host) -> + Tasks = get_tasks(Host), + [{T#task.taskid, io_lib:format("~p", [T#task.task])} || T <- Tasks]. + +cron_del(TaskId_string) -> + TaskId = list_to_integer(TaskId_string), + delete_taskid(TaskId), + ok. + + +%% --------------------- +%% Web Admin +%% --------------------- + +web_menu_host(Acc, _Host, Lang) -> + [{"cron", ?T("Cron Tasks")} | Acc]. + +web_page_host(_, Host, + #request{path = ["cron"], + lang = Lang} = _Request) -> + Tasks = get_tasks(Host), + Tasks_table = make_tasks_table(Tasks, Lang), + Res = [?XC("h1", "Cron Tasks")] ++ Tasks_table, + {stop, Res}; +web_page_host(Acc, _, _) -> Acc. + +make_tasks_table(Tasks, Lang) -> + TList = lists:map( + fun(T) -> + {Time_num, Time_unit, Mod, Fun, Args} = T#task.task, + ?XE("tr", + [?XC("td", integer_to_list(Time_num) ++" " ++ atom_to_list(Time_unit)), + ?XC("td", atom_to_list(Mod)), + ?XC("td", atom_to_list(Fun)), + ?XC("td", io_lib:format("~p", [Args]))]) + end, Tasks), + [?XE("table", + [?XE("thead", + [?XE("tr", + [?XCT("td", "Periodicity"), + ?XCT("td", "Module"), + ?XCT("td", "Function"), + ?XCT("td", "Arguments")])]), + ?XE("tbody", TList)])]. diff --git a/mod_ctlextra/COPYING b/mod_ctlextra/COPYING new file mode 100644 index 0000000..8b325f5 --- /dev/null +++ b/mod_ctlextra/COPYING @@ -0,0 +1,343 @@ +As a special exception, the authors give permission to link this program +with the OpenSSL library and distribute the resulting binary. + + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Library General +Public License instead of this License. diff --git a/mod_ctlextra/ChangeLog b/mod_ctlextra/ChangeLog new file mode 100644 index 0000000..1d366d4 --- /dev/null +++ b/mod_ctlextra/ChangeLog @@ -0,0 +1,92 @@ +2009-06-26 Badlop + + * src/mod_ctlextra.erl: Fix load-config call return. + +2009-02-09 Badlop + + * src/mod_ctlextra.erl: Calculation of 'stats registeredusers' + works also with non-internal authentication methods (thanks to + Francois de Metz)(EJAB-864) + +2008-10-31 Badlop + + * src/mod_ctlextra.erl: Remove duplicated command + delete-older-messages (EJAB-789) + +2008-10-12 Badlop + + * README.txt: Explain that the new mod_admin_extra provides the + same features and much more + +2008-04-26 Badlop + + * src/mod_ctlextra.erl: New command: ban-account + * README.txt: Documented new command + +2008-04-04 Badlop + + * src/mod_ctlextra.erl: Added new command + rosteritem-purge [options] + +2008-03-19 Badlop + + * src/mod_ctlextra.erl: Added two new commands for administration + of Shared Roster Groups: "srg-list-groups host" and "srg-get-info + group host" (thanks to Taylor Laramie) + +2008-03-09 Badlop + + * src/mod_ctlextra.erl: Show the result of each table export + +2008-03-04 Badlop + + * src/mod_ctlextra.erl: New command 'stats + onlineusersnode'. Removed old unusued code. + + * README.txt: Updated module page + +2008-01-20 Badlop + + * src/mod_ctlextra.erl: Allow to define group name with spaces + * README.txt: Likewise + +2007-11-14 Badlop + + * src/mod_ctlextra.erl: Updated to ejabberd SVN. + +2007-09-08 Badlop + + * src/mod_ctlextra.erl: Bugfix: unregister commands when module + stops. Removed all MUC-related commands. + +2007-08-29 Badlop + + * src/mod_ctlextra.erl: Added command: stats uptime-seconds. + +2007-08-24 Badlop + + * src/mod_ctlextra.erl: Fixed bug in vcard-set command that + forgot second level attributes. + +2007-08-23 Badlop + + * src/mod_ctlextra.erl: Command add-rosteritem now pushes the + roster item to the client. New command rem-rosteritem. + +2007-08-19 Badlop + + * src/mod_ctlextra.erl: Now vcard-get and vcard-set are more + compatible with mod_vcard_odbc and mod_vcard_ldap. + +2007-08-16 Badlop + + * src/mod_ctlextra.erl: add-rosteritem only adds an item in a + roster. + +2007-08-07 Badlop + + * src/mod_ctlextra.erl: Fixed indentation. + + * ChangeLog: New file to track changes. + + * README.txt: Removed Changelog section. diff --git a/mod_ctlextra/Emakefile b/mod_ctlextra/Emakefile new file mode 100644 index 0000000..6f9b6dd --- /dev/null +++ b/mod_ctlextra/Emakefile @@ -0,0 +1,2 @@ +{'../ejabberd-dev/src/gen_mod', [{outdir, "../ejabberd-dev/ebin"},{i,"../ejabberd-dev/include"}]}. +{'src/mod_ctlextra', [{outdir, "ebin"},{i,"../ejabberd-dev/include"}]}. diff --git a/mod_ctlextra/README.txt b/mod_ctlextra/README.txt new file mode 100644 index 0000000..bac2362 --- /dev/null +++ b/mod_ctlextra/README.txt @@ -0,0 +1,74 @@ + + + mod_ctlextra - Additional commands for ejabberdctl + + Homepage: http://www.ejabberd.im/mod_ctlextra + Author: Badlop + Module for ejabberd 2.0.* + + + IMPORTANT + ========= + +This module will only receive bugfixes. All the features provided by +this module are also provided by the new module mod_admin_extra. + +If you are using ejabberd 2.1.0 or newer, use mod_admin_extra instead. +It provides generic ejabberd +commands that can be executed not only with ejabberdctl, but also with +other ejabberd-command frontends. + + + CONFIGURATION + ============= + +Add the module to your ejabberd.cfg, on the modules section: +{modules, [ + ... + {mod_ctlextra, []}, + ... +]}. + + + USAGE + ===== + +Now you have several new commands in ejabberdctl. + +Description of some commands: + + - vcard-* + Example: ejabberdctl eja@host vcard-get joe myjab.net email + + - pushroster* + The file used by 'pushroster' and 'pushroster-all' must be placed: + - Windows: on the directory were you installed ejabberd: + 'C:/Program Files/ejabberd' + - Other OS: on the same directory where the .beam files are. + Example content for the roster file: + [{"bob", "example.org", "workers", "Bob"}, + {"mart", "example.org", "workers", "Mart"}, + {"Rich", "example.org", "bosses", "Rich"}]. + + - srg-create + If you want to put a group Name with blankspaces, use the characters + "' and '" to define when the Name starts and ends. + For example: + ejabberdctl srg-create g1 example.org "'Group number 1'" this_is_g1 g1 + + - ban-account + + This command kicks all the connected sessions of the account from the + server. It also changes his password to another randomly + generated, so he can't login anymore unless a server administrator + changes him again the password. + + It is possible to define the reason of the ban. The new password + also includes the reason and the date and time of the ban. + + For example, if this command is called: + ejabberdctl vhost example.org ban-account boby Spammed several MUC rooms + then the sessions of the local account which JID is boby@example.org + will be kicked, and its password will be set to something like this: + BANNED_ACCOUNT--20080425T21:45:07--2176635--Spammed_several_MUC_rooms + diff --git a/mod_ctlextra/build.bat b/mod_ctlextra/build.bat new file mode 100644 index 0000000..fff9b3b --- /dev/null +++ b/mod_ctlextra/build.bat @@ -0,0 +1 @@ +erl -pa ../../ejabberd-dev/trunk/ebin -pa ebin -make diff --git a/mod_ctlextra/build.sh b/mod_ctlextra/build.sh new file mode 100755 index 0000000..f0a0cfa --- /dev/null +++ b/mod_ctlextra/build.sh @@ -0,0 +1,2 @@ +#!/bin/sh +erl -pa ../ejabberd-dev/ebin -pz ebin -make diff --git a/mod_ctlextra/src/mod_ctlextra.erl b/mod_ctlextra/src/mod_ctlextra.erl new file mode 100644 index 0000000..bc61024 --- /dev/null +++ b/mod_ctlextra/src/mod_ctlextra.erl @@ -0,0 +1,895 @@ +%%%---------------------------------------------------------------------- +%%% File : mod_ctlextra.erl +%%% Author : Badlop +%%% Purpose : Adds more commands to ejabberd_ctl +%%% Created : 30 Nov 2006 by Badlop +%%% Id : $Id: mod_ctlextra.erl 1020 2009-08-30 10:13:34Z badlop $ +%%%---------------------------------------------------------------------- + +-module(mod_ctlextra). +-author('badlop@ono.com'). + +-behaviour(gen_mod). + +-export([start/2, + stop/1, + ctl_process/2, + ctl_process/3 + ]). + +-include("ejabberd.hrl"). +-include("ejabberd_ctl.hrl"). +-include("jlib.hrl"). +-include("mod_roster.hrl"). + +%% Copied from ejabberd_sm.erl +-record(session, {sid, usr, us, priority, info}). + +-compile(export_all). + +%%------------- +%% gen_mod +%%------------- + +start(Host, _Opts) -> + ejabberd_ctl:register_commands(commands_global(), ?MODULE, ctl_process), + ejabberd_ctl:register_commands(Host, commands_host(), ?MODULE, ctl_process). + +stop(Host) -> + ejabberd_ctl:unregister_commands(commands_global(), ?MODULE, ctl_process), + ejabberd_ctl:unregister_commands(Host, commands_host(), ?MODULE, ctl_process). + +commands_global() -> + [ + {"compile file", "recompile and reload file"}, + {"load-config file", "load config from file"}, + {"remove-node nodename", "remove an ejabberd node from the database"}, + + %% ejabberd_auth + {"delete-older-users days", "delete users that have not logged in the last 'days'"}, + {"delete-older-users-vhost host days", "delete users that not logged in last 'days' in 'host'"}, + {"set-password user server password", "set password to user@server"}, + + %% ejd2odbc + {"export2odbc server output", "export Mnesia tables on server to files on output directory"}, + + %% mod_shared_roster + {"srg-create group host name description display", "create the group with options"}, + {"srg-delete group host", "delete the group"}, + {"srg-user-add user server group host", "add user@server to group on host"}, + {"srg-user-del user server group host", "delete user@server from group on host"}, + {"srg-list-groups host", "list the shared roster groups from host"}, + {"srg-get-info group host", "get info of a specific group on host"}, + + %% mod_vcard + {"vcard-get user host data [data2]", "get data from the vCard of the user"}, + {"vcard-set user host data [data2] content", "set data to content on the vCard"}, + + %% mod_announce + %% announce_send_online host message + %% announce_send_all host, message + + %% mod_roster + {"add-rosteritem user1 server1 user2 server2 nick group subs", "Add user2@server2 to user1@server1's roster"}, + %%{"", "subs= none, from, to or both"}, + %%{"", "example: add-roster peter localhost mike server.com MiKe Employees both"}, + %%{"", "will add mike@server.com to peter@localhost roster"}, + {"rem-rosteritem user1 server1 user2 server2", "Remove user2@server2 from user1@server1's roster"}, + {"rosteritem-purge [options]", "Purge all rosteritems that match filtering options"}, + {"pushroster file user server", "push template roster in file to user@server"}, + {"pushroster-all file", "push template roster in file to all those users"}, + {"push-alltoall server group", "adds all the users to all the users in Group"}, + + {"status-list status", "list the logged users with status"}, + {"status-num status", "number of logged users with status"}, + + {"stats registeredusers", "number of registered users"}, + {"stats onlineusers", "number of logged users"}, + {"stats onlineusersnode", "number of logged users in the ejabberd node"}, + {"stats uptime-seconds", "uptime of ejabberd node in seconds"}, + + %% misc + {"get-cookie", "get the Erlang cookie of this node"}, + {"killsession user server resource", "kill a user session"} + ]. + +commands_host() -> + [ + %% mod_last + {"num-active-users days", "number of users active in the last 'days'"}, + {"status-list status", "list the logged users with status"}, + {"status-num status", "number of logged users with status"}, + {"stats registeredusers", "number of registered users"}, + {"stats onlineusers", "number of logged users"}, + + %% misc + {"ban-account username [reason]", "ban account: kick sessions and change password"} + ]. + + +%%------------- +%% Commands global +%%------------- + +ctl_process(_Val, ["delete-older-users", Days]) -> + {removed, N, UR} = delete_older_users(list_to_integer(Days)), + io:format("Deleted ~p users: ~p~n", [N, UR]), + ?STATUS_SUCCESS; + +ctl_process(_Val, ["delete-older-users-vhost", Host, Days]) -> + {removed, N, UR} = delete_older_users_vhost(Host, list_to_integer(Days)), + io:format("Deleted ~p users: ~p~n", [N, UR]), + ?STATUS_SUCCESS; + +ctl_process(_Val, ["export2odbc", Server, Output]) -> + Tables = [ + {export_last, last}, + {export_offline, offline}, + {export_passwd, passwd}, + {export_private_storage, private_storage}, + {export_roster, roster}, + {export_vcard, vcard}, + {export_vcard_search, vcard_search}], + Export = fun({TableFun, Table}) -> + Filename = filename:join([Output, atom_to_list(Table)++".txt"]), + io:format("Trying to export Mnesia table '~p' on server '~s' to file '~s'~n", [Table, Server, Filename]), + Res = (catch ejd2odbc:TableFun(Server, Filename)), + io:format(" Result: ~p~n", [Res]) + end, + lists:foreach(Export, Tables), + ?STATUS_SUCCESS; + +ctl_process(_Val, ["set-password", User, Server, Password]) -> + ejabberd_auth:set_password(User, Server, Password), + ?STATUS_SUCCESS; + +ctl_process(_Val, ["vcard-get", User, Server, Data]) -> + {ok, Res} = vcard_get(User, Server, [Data]), + io:format("~s~n", [Res]), + ?STATUS_SUCCESS; + +ctl_process(_Val, ["vcard-get", User, Server, Data1, Data2]) -> + {ok, Res} = vcard_get(User, Server, [Data1, Data2]), + io:format("~s~n", [Res]), + ?STATUS_SUCCESS; + +ctl_process(_Val, ["vcard-set", User, Server, Data1, Content]) -> + {ok, Res} = vcard_set(User, Server, [Data1], Content), + io:format("~s~n", [Res]), + ?STATUS_SUCCESS; + +ctl_process(_Val, ["vcard-set", User, Server, Data1, Data2, Content]) -> + {ok, Res} = vcard_set(User, Server, [Data1, Data2], Content), + io:format("~s~n", [Res]), + ?STATUS_SUCCESS; + +ctl_process(_Val, ["compile", Module]) -> + compile:file(Module), + ?STATUS_SUCCESS; + +ctl_process(_Val, ["remove-node", Node]) -> + mnesia:del_table_copy(schema, list_to_atom(Node)), + ?STATUS_SUCCESS; + +%% The Display argument can be several groups separated with , +%% Example: ejabberdctl srg-create aa localhost Name Desc Display1,Display2,Display3 +ctl_process(_Val, ["srg-create" | Parameters]) -> + [Group, Host, Name, Description, Display] = group_parameters(Parameters, "'"), + DisplayList = string:tokens(Display, ","), + Opts = [{name, Name}, {displayed_groups, DisplayList}, {description, Description}], + {atomic, ok} = mod_shared_roster:create_group(Host, Group, Opts), + ?STATUS_SUCCESS; + +ctl_process(_Val, ["srg-delete", Group, Host]) -> + {atomic, ok} = mod_shared_roster:delete_group(Host, Group), + ?STATUS_SUCCESS; + +ctl_process(_Val, ["srg-user-add", User, Server, Group, Host]) -> + {atomic, ok} = mod_shared_roster:add_user_to_group(Host, {User, Server}, Group), + ?STATUS_SUCCESS; + +ctl_process(_Val, ["srg-user-del", User, Server, Group, Host]) -> + {atomic, ok} = mod_shared_roster:remove_user_from_group(Host, {User, Server}, Group), + ?STATUS_SUCCESS; + +ctl_process(_Val, ["srg-list-groups", Host]) -> + lists:foreach( + fun(SrgGroup) -> + io:format("~s~n",[SrgGroup]) + end, + lists:sort(mod_shared_roster:list_groups(Host))), + ?STATUS_SUCCESS; + +ctl_process(_Val, ["srg-get-info", Group, Host]) -> + Opts = mod_shared_roster:get_group_opts(Host,Group), + [io:format("~s: ~p~n", [Title, Value]) || {Title , Value} <- Opts], + + Members = mod_shared_roster:get_group_explicit_users(Host,Group), + Members_string = [ " " ++ jlib:jid_to_string(jlib:make_jid(MUser, MServer, "")) + || {MUser, MServer} <- Members], + io:format("members:~s~n", [Members_string]), + + ?STATUS_SUCCESS; + +ctl_process(_Val, ["add-rosteritem", LocalUser, LocalServer, RemoteUser, RemoteServer, Nick, Group, Subs]) -> + case add_rosteritem(LocalUser, LocalServer, RemoteUser, RemoteServer, Nick, Group, list_to_atom(Subs), []) of + {atomic, ok} -> + ?STATUS_SUCCESS; + {error, Reason} -> + io:format("Can't add ~p@~p to ~p@~p: ~p~n", + [RemoteUser, RemoteServer, LocalUser, LocalServer, Reason]), + ?STATUS_ERROR; + {badrpc, Reason} -> + io:format("Can't add roster item to user ~p: ~p~n", + [LocalUser, Reason]), + ?STATUS_BADRPC + end; + +ctl_process(_Val, ["rem-rosteritem", LocalUser, LocalServer, RemoteUser, RemoteServer]) -> + case rem_rosteritem(LocalUser, LocalServer, RemoteUser, RemoteServer) of + {atomic, ok} -> + ?STATUS_SUCCESS; + {error, Reason} -> + io:format("Can't remove ~p@~p to ~p@~p: ~p~n", + [RemoteUser, RemoteServer, LocalUser, LocalServer, Reason]), + ?STATUS_ERROR; + {badrpc, Reason} -> + io:format("Can't remove roster item to user ~p: ~p~n", + [LocalUser, Reason]), + ?STATUS_BADRPC + end; + +ctl_process(_Val, ["rosteritem-purge"]) -> + io:format("Rosteritems that match all the filtering will be removed.~n"), + io:format("Options for filtering:~n"), + io:format("~n"), + io:format(" -subs none|from|to|both~n"), + io:format(" Subscription type. By default all values~n"), + io:format("~n"), + io:format(" -ask none|out|in~n"), + io:format(" Pending subscription. By default all values~n"), + io:format("~n"), + io:format(" -user JID~n"), + io:format(" The JID of the local user.~n"), + io:format(" Can use these globs: *, ? and [...].~n"), + io:format(" By default it is: * *@*~n"), + io:format("~n"), + io:format(" -contact JID~n"), + io:format(" Similar to 'user', but for the contact JID.~n"), + io:format("~n"), + io:format("Example:~n"), + io:format(" ejabberdctl rosteritem-purge -subs none from to -ask out in -contact *@*icq*~n"), + io:format("~n"), + ?STATUS_SUCCESS; +ctl_process(_Val, ["rosteritem-purge" | Options_list]) -> + Options_prop_list = lists:foldl( + fun(O, R) -> + case O of + [$- | K] -> + [{K, []} | R]; + V -> + [{K, Vs} | RT] = R, + [{K, [V|Vs]} | RT] + end + end, + [], + Options_list), + + Subs = [list_to_atom(S) + || S <- proplists:get_value("subs", + Options_prop_list, + ["none", "from", "to", "both"])], + Asks = [list_to_atom(S) + || S <- + proplists:get_value("ask", + Options_prop_list, + ["none", "out", "in"])], + User = proplists:get_value("user", Options_prop_list, ["*", "*@*"]), + Contact = proplists:get_value("contact", Options_prop_list, ["*", "*@*"]), + + case rosteritem_purge({Subs, Asks, User, Contact}) of + {atomic, ok} -> + ?STATUS_SUCCESS; + {error, Reason} -> + io:format("Error purging rosteritems: ~p~n", + [Reason]), + ?STATUS_ERROR; + {badrpc, Reason} -> + io:format("BadRPC purging rosteritems: ~p~n", + [Reason]), + ?STATUS_BADRPC + end; + +ctl_process(_Val, ["pushroster", File, User, Server]) -> + case pushroster(File, User, Server) of + ok -> + ?STATUS_SUCCESS; + {error, Reason} -> + io:format("Can't push roster ~p to ~p@~p: ~p~n", + [File, User, Server, Reason]), + ?STATUS_ERROR; + {badrpc, Reason} -> + io:format("Can't push roster ~p: ~p~n", + [File, Reason]), + ?STATUS_BADRPC + end; + +ctl_process(_Val, ["pushroster-all", File]) -> + case pushroster_all([File]) of + ok -> + ?STATUS_SUCCESS; + {error, Reason} -> + io:format("Can't push roster ~p: ~p~n", + [File, Reason]), + ?STATUS_ERROR; + {badrpc, Reason} -> + io:format("Can't push roster ~p: ~p~n", + [File, Reason]), + ?STATUS_BADRPC + end; + +ctl_process(_Val, ["push-alltoall", Server, Group]) -> + case push_alltoall(Server, Group) of + ok -> + ?STATUS_SUCCESS; + {error, Reason} -> + io:format("Can't push all to all: ~p~n", + [Reason]), + ?STATUS_ERROR; + {badrpc, Reason} -> + io:format("Can't push all to all: ~p~n", + [Reason]), + ?STATUS_BADRPC + end; + +ctl_process(_Val, ["load-config", Path]) -> + case catch ejabberd_config:load_file(Path) of + ok -> + ?STATUS_SUCCESS; + {'EXIT', Reason} -> + io:format("Problem loading config file ~p: ~p~n", + [filename:absname(Path), Reason]), + ?STATUS_ERROR; + {badrpc, Reason} -> + io:format("Can't load config file ~p: ~p~n", + [filename:absname(Path), Reason]), + ?STATUS_BADRPC + end; + +ctl_process(_Val, ["stats", Stat]) -> + Res = case Stat of + "uptime-seconds" -> uptime_seconds(); + "registeredusers" -> length(ejabberd_auth:dirty_get_registered_users()); + "onlineusersnode" -> length(ejabberd_sm:dirty_get_my_sessions_list()); + "onlineusers" -> length(ejabberd_sm:dirty_get_sessions_list()) + end, + io:format("~p~n", [Res]), + ?STATUS_SUCCESS; + +ctl_process(_Val, ["status-num", Status_required]) -> + ctl_process(_Val, "all", ["status-num", Status_required]); + +ctl_process(_Val, ["status-list", Status_required]) -> + ctl_process(_Val, "all", ["status-list", Status_required]); + +ctl_process(_Val, ["get-cookie"]) -> + io:format("~s~n", [atom_to_list(erlang:get_cookie())]), + ?STATUS_SUCCESS; + +ctl_process(_Val, ["killsession", User, Server, Resource | Tail]) -> + kick_session(User, Server, Resource, prepare_reason(Tail)), + ?STATUS_SUCCESS; + +ctl_process(Val, _Args) -> + Val. + + +%%------------- +%% Commands vhost +%%------------- + +ctl_process(_Val, Host, ["num-active-users", Days]) -> + Number = num_active_users(Host, list_to_integer(Days)), + io:format("~p~n", [Number]), + ?STATUS_SUCCESS; + +ctl_process(_Val, Host, ["stats", Stat]) -> + Res = case Stat of + "registeredusers" -> length(ejabberd_auth:get_vh_registered_users(Host)); + "onlineusers" -> length(ejabberd_sm:get_vh_session_list(Host)) + end, + io:format("~p~n", [Res]), + ?STATUS_SUCCESS; + +ctl_process(_Val, Host, ["status-num", Status_required]) -> + Num = length(get_status_list(Host, Status_required)), + io:format("~p~n", [Num]), + ?STATUS_SUCCESS; + +ctl_process(_Val, Host, ["status-list", Status_required]) -> + Res = get_status_list(Host, Status_required), + [ io:format("~s@~s ~s ~p \"~s\"~n", [U, S, R, P, St]) || {U, S, R, P, St} <- Res], + ?STATUS_SUCCESS; + +ctl_process(_Val, Host, ["ban-account", User | Tail]) -> + ban_account(User, Host, prepare_reason(Tail)), + ?STATUS_SUCCESS; + +ctl_process(Val, _Host, _Args) -> + Val. + + +%%------------- +%% Utils +%%------------- + +uptime_seconds() -> + trunc(element(1, erlang:statistics(wall_clock))/1000). + +get_status_list(Host, Status_required) -> + %% Get list of all logged users + Sessions = ejabberd_sm:dirty_get_my_sessions_list(), + %% Reformat the list + Sessions2 = [ {Session#session.usr, Session#session.sid, Session#session.priority} || Session <- Sessions], + Fhost = case Host of + "all" -> + %% All hosts are requested, so dont filter at all + fun(_, _) -> true end; + _ -> + %% Filter the list, only Host is interesting + fun(A, B) -> A == B end + end, + Sessions3 = [ {Pid, Server, Priority} || {{_User, Server, _Resource}, {_, Pid}, Priority} <- Sessions2, apply(Fhost, [Server, Host])], + %% For each Pid, get its presence + Sessions4 = [ {ejabberd_c2s:get_presence(Pid), Server, Priority} || {Pid, Server, Priority} <- Sessions3], + %% Filter by status + Fstatus = case Status_required of + "all" -> + fun(_, _) -> true end; + _ -> + fun(A, B) -> A == B end + end, + [{User, Server, Resource, Priority, stringize(Status_text)} + || {{User, Resource, Status, Status_text}, Server, Priority} <- Sessions4, + apply(Fstatus, [Status, Status_required])]. + +%% Make string more print-friendly +stringize(String) -> + %% Replace newline characters with other code + element(2, regexp:gsub(String, "\n", "\\n")). + +add_rosteritem(LU, LS, RU, RS, Nick, Group, Subscription, Xattrs) -> + subscribe(LU, LS, RU, RS, Nick, Group, Subscription, Xattrs), + route_rosteritem(LU, LS, RU, RS, Nick, Group, Subscription), + {atomic, ok}. + +subscribe(LocalUser, LocalServer, RemoteUser, RemoteServer, Nick, Group, Subscription, Xattrs) -> + R = #roster{usj = {LocalUser,LocalServer,{RemoteUser,RemoteServer,[]}}, + us = {LocalUser,LocalServer}, + jid = {RemoteUser,RemoteServer,[]}, + name = Nick, + subscription = Subscription, % none, to=you see him, from=he sees you, both + ask = none, % out=send request, in=somebody requests you, none + groups = [Group], + askmessage = Xattrs, % example: [{"category","conference"}] + xs = []}, + mnesia:transaction(fun() -> mnesia:write(R) end). + +rem_rosteritem(LU, LS, RU, RS) -> + unsubscribe(LU, LS, RU, RS), + route_rosteritem(LU, LS, RU, RS, "", "", "remove"), + {atomic, ok}. + +unsubscribe(LocalUser, LocalServer, RemoteUser, RemoteServer) -> + Key = {{LocalUser,LocalServer,{RemoteUser,RemoteServer,[]}}, + {LocalUser,LocalServer}}, + mnesia:transaction(fun() -> mnesia:delete(roster, Key, write) end). + +route_rosteritem(LocalUser, LocalServer, RemoteUser, RemoteServer, Nick, Group, Subscription) -> + LJID = jlib:make_jid(LocalUser, LocalServer, ""), + RJID = jlib:make_jid(RemoteUser, RemoteServer, ""), + ToS = jlib:jid_to_string(LJID), + ItemJIDS = jlib:jid_to_string(RJID), + GroupXML = {xmlelement, "group", [], [{xmlcdata, Group}]}, + Item = {xmlelement, "item", + [{"jid", ItemJIDS}, + {"name", Nick}, + {"subscription", Subscription}], + [GroupXML]}, + Query = {xmlelement, "query", [{"xmlns", ?NS_ROSTER}], [Item]}, + Packet = {xmlelement, "iq", [{"type", "set"}, {"to", ToS}], [Query]}, + ejabberd_router:route(LJID, LJID, Packet). + + +%%----------------------------- +%% Ban user +%%----------------------------- + +ban_account(User, Server, Reason) -> + kick_sessions(User, Server, Reason), + set_random_password(User, Server, Reason). + +kick_sessions(User, Server, Reason) -> + lists:map( + fun(Resource) -> + kick_session(User, Server, Resource, Reason) + end, + get_resources(User, Server)). + +kick_session(User, Server, Resource, Reason) -> + ejabberd_router:route( + jlib:make_jid("", "", ""), + jlib:make_jid(User, Server, Resource), + {xmlelement, "broadcast", [], [{exit, Reason}]}). + +get_resources(User, Server) -> + lists:map( + fun(Session) -> + element(3, Session#session.usr) + end, + get_sessions(User, Server)). + +get_sessions(User, Server) -> + LUser = jlib:nodeprep(User), + LServer = jlib:nameprep(Server), + Sessions = mnesia:dirty_index_read(session, {LUser, LServer}, #session.us), + true = is_list(Sessions), + Sessions. + +set_random_password(User, Server, Reason) -> + NewPass = build_random_password(Reason), + set_password(User, Server, NewPass). + +build_random_password(Reason) -> + Date = jlib:timestamp_to_iso(calendar:universal_time()), + RandomString = randoms:get_string(), + "BANNED_ACCOUNT--" ++ Date ++ "--" ++ RandomString ++ "--" ++ Reason. + +set_password(User, Server, Password) -> + {atomic, ok} = ejabberd_auth:set_password(User, Server, Password). + +prepare_reason([]) -> + "Kicked by administrator"; +prepare_reason([Reason]) -> + Reason; +prepare_reason(StringList) -> + string:join(StringList, "_"). + + +%%----------------------------- +%% Purge roster items +%%----------------------------- + +rosteritem_purge(Options) -> + Num_rosteritems = mnesia:table_info(roster, size), + io:format("There are ~p roster items in total.~n", [Num_rosteritems]), + Key = mnesia:dirty_first(roster), + ok = rip(Key, Options, {0, Num_rosteritems, 0, 0}), + {atomic, ok}. + +rip('$end_of_table', _Options, Counters) -> + print_progress_line(Counters), + ok; +rip(Key, Options, {Pr, NT, NV, ND}) -> + Key_next = mnesia:dirty_next(roster, Key), + ND2 = case decide_rip(Key, Options) of + true -> + mnesia:dirty_delete(roster, Key), + ND+1; + false -> + ND + end, + NV2 = NV+1, + Pr2 = print_progress_line({Pr, NT, NV2, ND2}), + rip(Key_next, Options, {Pr2, NT, NV2, ND2}). + +print_progress_line({Pr, NT, NV, ND}) -> + Pr2 = trunc((NV/NT)*100), + case Pr == Pr2 of + true -> + ok; + false -> + io:format("Progress ~p% - visited ~p - deleted ~p~n", [Pr2, NV, ND]) + end, + Pr2. + +decide_rip(Key, {Subs, Asks, User, Contact}) -> + case catch mnesia:dirty_read(roster, Key) of + [RI] -> + lists:member(RI#roster.subscription, Subs) + andalso lists:member(RI#roster.ask, Asks) + andalso decide_rip_jid(RI#roster.us, User) + andalso decide_rip_jid(RI#roster.jid, Contact); + _ -> + false + end. + +%% Returns true if the server of the JID is included in the servers +decide_rip_jid({UName, UServer, _UResource}, Match_list) -> + decide_rip_jid({UName, UServer}, Match_list); +decide_rip_jid({UName, UServer}, Match_list) -> + lists:any( + fun(Match_string) -> + MJID = jlib:string_to_jid(Match_string), + MName = MJID#jid.luser, + MServer = MJID#jid.lserver, + Is_server = is_glob_match(UServer, MServer), + case MName of + [] when UName == [] -> + Is_server; + [] -> + false; + _ -> + Is_server + andalso is_glob_match(UName, MName) + end + end, + Match_list). + +%% Copied from ejabberd-2.0.0/src/acl.erl +is_regexp_match(String, RegExp) -> + case regexp:first_match(String, RegExp) of + nomatch -> + false; + {match, _, _} -> + true; + {error, ErrDesc} -> + io:format( + "Wrong regexp ~p in ACL: ~p", + [RegExp, lists:flatten(regexp:format_error(ErrDesc))]), + false + end. +is_glob_match(String, Glob) -> + is_regexp_match(String, regexp:sh_to_awk(Glob)). + + +%%----------------------------- +%% Push Roster from file +%%----------------------------- + +pushroster(File, User, Server) -> + {ok, [Roster]} = file:consult(File), + subscribe_roster({User, Server, "", User}, Roster). + +pushroster_all(File) -> + {ok, [Roster]} = file:consult(File), + subscribe_all(Roster). + +subscribe_all(Roster) -> + subscribe_all(Roster, Roster). +subscribe_all([], _) -> + ok; +subscribe_all([User1 | Users], Roster) -> + subscribe_roster(User1, Roster), + subscribe_all(Users, Roster). + +subscribe_roster(_, []) -> + ok; +%% Do not subscribe a user to itself +subscribe_roster({Name, Server, Group, Nick}, [{Name, Server, _, _} | Roster]) -> + subscribe_roster({Name, Server, Group, Nick}, Roster); +%% Subscribe Name2 to Name1 +subscribe_roster({Name1, Server1, Group1, Nick1}, [{Name2, Server2, Group2, Nick2} | Roster]) -> + subscribe(Name1, Server1, Name2, Server2, Nick2, Group2, both, []), + subscribe_roster({Name1, Server1, Group1, Nick1}, Roster). + +push_alltoall(S, G) -> + Users = ejabberd_auth:get_vh_registered_users(S), + Users2 = build_list_users(G, Users, []), + subscribe_all(Users2). + +build_list_users(_Group, [], Res) -> + Res; +build_list_users(Group, [{User, Server}|Users], Res) -> + build_list_users(Group, Users, [{User, Server, Group, User}|Res]). + +vcard_get(User, Server, Data) -> + [{_, Module, Function, _Opts}] = ets:lookup(sm_iqtable, {?NS_VCARD, Server}), + JID = jlib:make_jid(User, Server, ""), + IQ = #iq{type = get, xmlns = ?NS_VCARD}, + IQr = Module:Function(JID, JID, IQ), + Res = case IQr#iq.sub_el of + [A1] -> + case vcard_get(Data, A1) of + false -> no_value; + Elem -> xml:get_tag_cdata(Elem) + end; + [] -> + no_vcard + end, + {ok, Res}. + +vcard_get([Data1, Data2], A1) -> + case xml:get_subtag(A1, Data1) of + false -> false; + A2 -> vcard_get([Data2], A2) + end; + +vcard_get([Data], A1) -> + xml:get_subtag(A1, Data). + +vcard_set(User, Server, Data, Content) -> + [{_, Module, Function, _Opts}] = ets:lookup(sm_iqtable, {?NS_VCARD, Server}), + JID = jlib:make_jid(User, Server, ""), + IQ = #iq{type = get, xmlns = ?NS_VCARD}, + IQr = Module:Function(JID, JID, IQ), + + %% Get old vcard + A4 = case IQr#iq.sub_el of + [A1] -> + {_, _, _, A2} = A1, + update_vcard_els(Data, Content, A2); + [] -> + update_vcard_els(Data, Content, []) + end, + + %% Build new vcard + SubEl = {xmlelement, "vCard", [{"xmlns","vcard-temp"}], A4}, + IQ2 = #iq{type=set, sub_el = SubEl}, + + Module:Function(JID, JID, IQ2), + {ok, "done"}. + +update_vcard_els(Data, Content, Els1) -> + Els2 = lists:keysort(2, Els1), + [Data1 | Data2] = Data, + NewEl = case Data2 of + [] -> + {xmlelement, Data1, [], [{xmlcdata,Content}]}; + [D2] -> + OldEl = case lists:keysearch(Data1, 2, Els2) of + {value, A} -> A; + false -> {xmlelement, Data1, [], []} + end, + {xmlelement, _, _, ContentOld1} = OldEl, + Content2 = [{xmlelement, D2, [], [{xmlcdata,Content}]}], + ContentOld2 = lists:keysort(2, ContentOld1), + ContentOld3 = lists:keydelete(D2, 2, ContentOld2), + ContentNew = lists:keymerge(2, Content2, ContentOld3), + {xmlelement, Data1, [], ContentNew} + end, + Els3 = lists:keydelete(Data1, 2, Els2), + lists:keymerge(2, [NewEl], Els3). + +-record(last_activity, {us, timestamp, status}). + +delete_older_users(Days) -> + %% Get the list of registered users + Users = ejabberd_auth:dirty_get_registered_users(), + delete_older_users(Days, Users). + +delete_older_users_vhost(Host, Days) -> + %% Get the list of registered users + Users = ejabberd_auth:get_vh_registered_users(Host), + delete_older_users(Days, Users). + +delete_older_users(Days, Users) -> + %% Convert older time + SecOlder = Days*24*60*60, + + %% Get current time + {MegaSecs, Secs, _MicroSecs} = now(), + TimeStamp_now = MegaSecs * 1000000 + Secs, + + %% For a user, remove if required and answer true + F = fun({LUser, LServer}) -> + %% Check if the user is logged + case ejabberd_sm:get_user_resources(LUser, LServer) of + %% If it isnt + [] -> + %% Look for his last_activity + case mnesia:dirty_read(last_activity, {LUser, LServer}) of + %% If it is + %% existent: + [#last_activity{timestamp = TimeStamp}] -> + %% get his age + Sec = TimeStamp_now - TimeStamp, + %% If he is + if + %% younger than SecOlder: + Sec < SecOlder -> + %% do nothing + false; + %% older: + true -> + %% remove the user + ejabberd_auth:remove_user(LUser, LServer), + true + end; + %% nonexistent: + [] -> + %% remove the user + ejabberd_auth:remove_user(LUser, LServer), + true + end; + %% Else + _ -> + %% do nothing + false + end + end, + %% Apply the function to every user in the list + Users_removed = lists:filter(F, Users), + {removed, length(Users_removed), Users_removed}. + +num_active_users(Host, Days) -> + list_last_activity(Host, true, Days). + +%% Code based on ejabberd/src/web/ejabberd_web_admin.erl +list_last_activity(Host, Integral, Days) -> + {MegaSecs, Secs, _MicroSecs} = now(), + TimeStamp = MegaSecs * 1000000 + Secs, + TS = TimeStamp - Days * 86400, + case catch mnesia:dirty_select( + last_activity, [{{last_activity, {'_', Host}, '$1', '_'}, + [{'>', '$1', TS}], + [{'trunc', {'/', + {'-', TimeStamp, '$1'}, + 86400}}]}]) of + {'EXIT', _Reason} -> + []; + Vals -> + Hist = histogram(Vals, Integral), + if + Hist == [] -> + 0; + true -> + Left = if + Days == infinity -> + 0; + true -> + Days - length(Hist) + end, + Tail = if + Integral -> + lists:duplicate(Left, lists:last(Hist)); + true -> + lists:duplicate(Left, 0) + end, + lists:nth(Days, Hist ++ Tail) + end + end. +histogram(Values, Integral) -> + histogram(lists:sort(Values), Integral, 0, 0, []). +histogram([H | T], Integral, Current, Count, Hist) when Current == H -> + histogram(T, Integral, Current, Count + 1, Hist); +histogram([H | _] = Values, Integral, Current, Count, Hist) when Current < H -> + if + Integral -> + histogram(Values, Integral, Current + 1, Count, [Count | Hist]); + true -> + histogram(Values, Integral, Current + 1, 0, [Count | Hist]) + end; +histogram([], _Integral, _Current, Count, Hist) -> + if + Count > 0 -> + lists:reverse([Count | Hist]); + true -> + lists:reverse(Hist) + end. + +group_parameters(Ps, [Char]) -> + {none, Grouped_Ps} = lists:foldl( + fun(P, {State, Res}) -> + case State of + none -> + case P of + [Char | PTail]-> + {building, [PTail | Res]}; + _ -> + {none, [P | Res]} + end; + building -> + [ResHead | ResTail] = Res, + case lists:last(P) of + Char -> + P2 = lists:sublist(P, length(P)-1), + {none, [ResHead ++ " " ++ P2 | ResTail]}; + _ -> + {building, [ResHead ++ " " ++ P | ResTail]} + end + end + end, + {none, []}, + Ps), + lists:reverse(Grouped_Ps). diff --git a/mod_irc/Emakefile b/mod_irc/Emakefile new file mode 100644 index 0000000..4923d52 --- /dev/null +++ b/mod_irc/Emakefile @@ -0,0 +1,4 @@ +{'../ejabberd-dev/src/gen_mod', [{outdir, "../ejabberd-dev/ebin"},{i,"../ejabberd-dev/include"}]}. +{'src/iconv', [{outdir, "ebin"},{i,"../ejabberd-dev/include"}]}. +{'src/mod_irc', [{outdir, "ebin"},{i,"../ejabberd-dev/include"}]}. +{'src/mod_irc_connection', [{outdir, "ebin"},{i,"../ejabberd-dev/include"}]}. diff --git a/mod_irc/LICENSE.txt b/mod_irc/LICENSE.txt new file mode 100644 index 0000000..8b325f5 --- /dev/null +++ b/mod_irc/LICENSE.txt @@ -0,0 +1,343 @@ +As a special exception, the authors give permission to link this program +with the OpenSSL library and distribute the resulting binary. + + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Library General +Public License instead of this License. diff --git a/mod_irc/Makefile b/mod_irc/Makefile new file mode 100644 index 0000000..3681098 --- /dev/null +++ b/mod_irc/Makefile @@ -0,0 +1,20 @@ +# XXX: these paths shouldn't be hardcoded +CC = gcc +CFLAGS = -O2 -Wall +CPPFLAGS = -I/usr/pkg/include -I/usr/local/lib/erlang/lib/erl_interface-3.5.5/include -I/usr/local/lib/erlang/usr/include -I/usr/local/ssl/lib -I/usr/pkg/include +LDFLAGS = -L/usr/pkg/lib -L/usr/local/lib/erlang/lib/erl_interface-3.5.5/lib -lerl_interface -lei -liconv + +# Assume Linux-style dynamic library flags +DYNAMIC_LIB_CFLAGS = -fpic -shared +ifeq ($(shell uname),Darwin) + DYNAMIC_LIB_CFLAGS = -fPIC -bundle -flat_namespace -undefined suppress +endif +ifeq ($(shell uname),SunOs) + DYNAMIC_LIB_CFLAGS = -KPIC -G -z text +endif + +ebin/iconv_erl.so: src/iconv_erl.c + $(CC) $(CFLAGS) $(CPPFLAGS) $(LDFLAGS) $< -o $@ $(DYNAMIC_LIB_CFLAGS) + +clean: + rm -f src/iconv_erl.so diff --git a/mod_irc/README.txt b/mod_irc/README.txt new file mode 100644 index 0000000..e777ef7 --- /dev/null +++ b/mod_irc/README.txt @@ -0,0 +1,121 @@ + + mod_irc - IRC transport + + Author: Alexey Shchepin + Requires: GNU Iconv 1.8 or higher. Not needed on systems with GNU Libc. + http://www.ejabberd.im/mod_irc + + + DESCRIPTION + ----------- + +This module is an IRC transport that can be used to join channels on +IRC servers. + +This module was originally included in ejabberd releases. +It is not included since ejabberd 3.0.0. + + + INSTALL + ------- + +1. Compile the module + * On Windows: build.bat + * On other systems: ./build.sh; make + +2. Copy the beam files from ebin directory to your ejabberd ebin directory. + +3. Copy iconv_erl.so from ebin directory to your ejabberd binary +system libraries directory. +It may be something like /lib/ejabberd/priv/lib/ + +4. Edit ejabberd.cfg and add the module definition: +{modules, [ + {mod_irc, []}, + ... +]}. + +5. Restart ejabberd. +If problems appear, remember to always look first the ejabberd log files +ejabberd.log and sasl.log since they may provide some valuable information. + + + CONFIGURABLE PARAMETERS + ----------------------- + +host: This option defines the Jabber ID of the service. If the host + option is not specified, the Jabber ID will be the hostname of the + virtual host with the prefix ‘irc.’. The keyword "@HOST@" is + replaced at start time with the real virtual host name. + +access: This option can be used to specify who may use the IRC + transport (default value: all). + +default_encoding: Set the default IRC encoding (default value: + "koi8-r"). + + + EXAMPLE CONFIGURATION + --------------------- + + Example 1 + --------- + +In the first example, the IRC transport is available on (all) your +virtual host(s) with the prefix ‘irc.’. Furthermore, anyone is able to +use the transport. The default encoding is set to "iso8859-15". + +{modules, [ + {mod_irc, [{access, all}, + {default_encoding, "iso8859-15"}]}, + ... +]}. + + Example 2 + --------- + +In next example the IRC transport is available with JIDs with prefix +irc-t.net. Moreover, the transport is only accessible to two users of +example.org, and any user of example.com: + +{acl, paying_customers, {user, "customer1", "example.org"}}. +{acl, paying_customers, {user, "customer2", "example.org"}}. +{acl, paying_customers, {server, "example.com"}}. + +{access, irc_users, [{allow, paying_customers}, {deny, all}]}. + +{modules, [ + {mod_irc, [{access, irc_users}, + {host, "irc.example.net"}]}, + ... +]}. + + + USAGE + ----- + +How to use the IRC transport: + +* A Jabber client with ‘groupchat 1.0’ support or Multi-User Chat + support (XEP-0045) is necessary to join IRC channels. + +* An IRC channel can be joined in nearly the same way as joining a + Jabber Multi-User Chat room. The difference is that the room name + will be ‘channel%irc.example.org’ in case irc.example.org is the IRC + server hosting ‘channel’. And of course the host should point to the + IRC transport instead of the Multi-User Chat service. + +* You can register your nickame by sending ‘IDENTIFY password’ to + nickserver!irc.example.org@irc.jabberserver.org. + +* Entering your password is possible by sending ‘LOGIN nick password’ + to nickserver!irc.example.org@irc.jabberserver.org. + +* The IRC transport provides Ad-Hoc Commands (XEP-0050) to join a + channel, and to set custom IRC username and encoding. + +* When using a popular Jabber server, it can occur that no connection + can be achieved with some IRC servers because they limit the number + of conections from one IP. + + diff --git a/mod_irc/build.bat b/mod_irc/build.bat new file mode 100644 index 0000000..fff9b3b --- /dev/null +++ b/mod_irc/build.bat @@ -0,0 +1 @@ +erl -pa ../../ejabberd-dev/trunk/ebin -pa ebin -make diff --git a/mod_irc/build.sh b/mod_irc/build.sh new file mode 100755 index 0000000..f0a0cfa --- /dev/null +++ b/mod_irc/build.sh @@ -0,0 +1,2 @@ +#!/bin/sh +erl -pa ../ejabberd-dev/ebin -pz ebin -make diff --git a/mod_irc/src/iconv.erl b/mod_irc/src/iconv.erl new file mode 100644 index 0000000..8cccf2d --- /dev/null +++ b/mod_irc/src/iconv.erl @@ -0,0 +1,94 @@ +%%%---------------------------------------------------------------------- +%%% File : iconv.erl +%%% Author : Alexey Shchepin +%%% Purpose : Interface to libiconv +%%% Created : 16 Feb 2003 by Alexey Shchepin +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2009 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., 59 Temple Place, Suite 330, Boston, MA +%%% 02111-1307 USA +%%% +%%%---------------------------------------------------------------------- + +-module(iconv). +-author('alexey@process-one.net'). + +-behaviour(gen_server). + +-export([start/0, start_link/0, convert/3]). + +%% Internal exports, call-back functions. +-export([init/1, + handle_call/3, + handle_cast/2, + handle_info/2, + code_change/3, + terminate/2]). + + + +start() -> + gen_server:start({local, ?MODULE}, ?MODULE, [], []). + +start_link() -> + gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). + +init([]) -> + case erl_ddll:load_driver(ejabberd:get_so_path(), iconv_erl) of + ok -> ok; + {error, already_loaded} -> ok + end, + Port = open_port({spawn, iconv_erl}, []), + ets:new(iconv_table, [set, public, named_table]), + ets:insert(iconv_table, {port, Port}), + {ok, Port}. + + +%%% -------------------------------------------------------- +%%% The call-back functions. +%%% -------------------------------------------------------- + +handle_call(_, _, State) -> + {noreply, State}. + +handle_cast(_, State) -> + {noreply, State}. + +handle_info({'EXIT', Port, Reason}, Port) -> + {stop, {port_died, Reason}, Port}; +handle_info({'EXIT', _Pid, _Reason}, Port) -> + {noreply, Port}; +handle_info(_, State) -> + {noreply, State}. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +terminate(_Reason, Port) -> + Port ! {self, close}, + ok. + + + +convert(From, To, String) -> + [{port, Port} | _] = ets:lookup(iconv_table, port), + Bin = term_to_binary({From, To, String}), + BRes = port_control(Port, 1, Bin), + binary_to_list(BRes). + + + diff --git a/mod_irc/src/iconv_erl.c b/mod_irc/src/iconv_erl.c new file mode 100644 index 0000000..f301bd5 --- /dev/null +++ b/mod_irc/src/iconv_erl.c @@ -0,0 +1,155 @@ +/* + * ejabberd, Copyright (C) 2002-2009 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., 59 Temple Place, Suite 330, Boston, MA + * 02111-1307 USA + * + */ + +#include +#include +#include +#include +#include + +typedef struct { + ErlDrvPort port; + iconv_t cd; +} iconv_data; + + +static ErlDrvData iconv_erl_start(ErlDrvPort port, char *buff) +{ + iconv_data* d = (iconv_data*)driver_alloc(sizeof(iconv_data)); + d->port = port; + d->cd = NULL; + + set_port_control_flags(port, PORT_CONTROL_FLAG_BINARY); + + return (ErlDrvData)d; +} + +static void iconv_erl_stop(ErlDrvData handle) +{ + driver_free((char*)handle); +} + +static int iconv_erl_control(ErlDrvData drv_data, + unsigned int command, + char *buf, int len, + char **rbuf, int rlen) +{ + int i; + int size; + int index = 0; + int avail; + size_t inleft, outleft; + ErlDrvBinary *b; + char *from, *to, *string, *stmp, *rstring, *rtmp; + iconv_t cd; + int invalid_utf8_as_latin1 = 0; + + ei_decode_version(buf, &index, &i); + ei_decode_tuple_header(buf, &index, &i); + ei_get_type(buf, &index, &i, &size); + from = malloc(size + 1); + ei_decode_string(buf, &index, from); + + ei_get_type(buf, &index, &i, &size); + to = malloc(size + 1); + ei_decode_string(buf, &index, to); + + ei_get_type(buf, &index, &i, &size); + stmp = string = malloc(size + 1); + ei_decode_string(buf, &index, string); + + /* Special mode: parse as UTF-8 if possible; otherwise assume it's + Latin-1. Makes no difference when encoding. */ + if (strcmp(from, "utf-8+latin-1") == 0) { + from[5] = '\0'; + invalid_utf8_as_latin1 = 1; + } + if (strcmp(to, "utf-8+latin-1") == 0) { + to[5] = '\0'; + } + cd = iconv_open(to, from); + + if (cd == (iconv_t) -1) { + cd = iconv_open("ascii", "ascii"); + if (cd == (iconv_t) -1) { + *rbuf = (char*)(b = driver_alloc_binary(size)); + memcpy(b->orig_bytes, string, size); + + free(from); + free(to); + free(string); + + return size; + } + } + + outleft = avail = 4*size; + inleft = size; + rtmp = rstring = malloc(avail); + while (inleft > 0) { + if (iconv(cd, &stmp, &inleft, &rtmp, &outleft) == (size_t) -1) { + if (invalid_utf8_as_latin1 && (*stmp & 0x80) && outleft >= 2) { + /* Encode one byte of (assumed) Latin-1 into two bytes of UTF-8 */ + *rtmp++ = 0xc0 | ((*stmp & 0xc0) >> 6); + *rtmp++ = 0x80 | (*stmp & 0x3f); + outleft -= 2; + } + stmp++; + inleft--; + } + } + + size = rtmp - rstring; + + *rbuf = (char*)(b = driver_alloc_binary(size)); + memcpy(b->orig_bytes, rstring, size); + + free(from); + free(to); + free(string); + free(rstring); + iconv_close(cd); + + return size; +} + + + +ErlDrvEntry iconv_driver_entry = { + NULL, /* F_PTR init, N/A */ + iconv_erl_start, /* L_PTR start, called when port is opened */ + iconv_erl_stop, /* F_PTR stop, called when port is closed */ + NULL, /* F_PTR output, called when erlang has sent */ + NULL, /* F_PTR ready_input, called when input descriptor ready */ + NULL, /* F_PTR ready_output, called when output descriptor ready */ + "iconv_erl", /* char *driver_name, the argument to open_port */ + NULL, /* F_PTR finish, called when unloaded */ + NULL, /* handle */ + iconv_erl_control, /* F_PTR control, port_command callback */ + NULL, /* F_PTR timeout, reserved */ + NULL /* F_PTR outputv, reserved */ +}; + +DRIVER_INIT(iconv_erl) /* must match name in driver_entry */ +{ + return &iconv_driver_entry; +} + + diff --git a/mod_irc/src/mod_irc.erl b/mod_irc/src/mod_irc.erl new file mode 100644 index 0000000..4b8b246 --- /dev/null +++ b/mod_irc/src/mod_irc.erl @@ -0,0 +1,1016 @@ +%%%---------------------------------------------------------------------- +%%% File : mod_irc.erl +%%% Author : Alexey Shchepin +%%% Purpose : IRC transport +%%% Created : 15 Feb 2003 by Alexey Shchepin +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2009 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., 59 Temple Place, Suite 330, Boston, MA +%%% 02111-1307 USA +%%% +%%%---------------------------------------------------------------------- + +-module(mod_irc). +-author('alexey@process-one.net'). + +-behaviour(gen_server). +-behaviour(gen_mod). + +%% API +-export([start_link/2, + start/2, + stop/1, + closed_connection/3, + get_connection_params/3]). + +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). + +-include("ejabberd.hrl"). +-include("jlib.hrl"). +-include("adhoc.hrl"). + +-define(DEFAULT_IRC_ENCODING, "iso8859-1"). +-define(DEFAULT_IRC_PORT, 6667). +-define(POSSIBLE_ENCODINGS, ["koi8-r", "iso8859-1", "iso8859-2", "utf-8", "utf-8+latin-1"]). + +-record(irc_connection, {jid_server_host, pid}). +-record(irc_custom, {us_host, data}). + +-record(state, {host, server_host, access}). + +-define(PROCNAME, ejabberd_mod_irc). + +%%==================================================================== +%% API +%%==================================================================== +%%-------------------------------------------------------------------- +%% Function: start_link() -> {ok,Pid} | ignore | {error,Error} +%% Description: Starts the server +%%-------------------------------------------------------------------- +start_link(Host, Opts) -> + Proc = gen_mod:get_module_proc(Host, ?PROCNAME), + gen_server:start_link({local, Proc}, ?MODULE, [Host, Opts], []). + +start(Host, Opts) -> + start_supervisor(Host), + Proc = gen_mod:get_module_proc(Host, ?PROCNAME), + ChildSpec = + {Proc, + {?MODULE, start_link, [Host, Opts]}, + temporary, + 1000, + worker, + [?MODULE]}, + supervisor:start_child(ejabberd_sup, ChildSpec). + +stop(Host) -> + stop_supervisor(Host), + Proc = gen_mod:get_module_proc(Host, ?PROCNAME), + gen_server:call(Proc, stop), + supervisor:delete_child(ejabberd_sup, Proc). + +%%==================================================================== +%% gen_server callbacks +%%==================================================================== + +%%-------------------------------------------------------------------- +%% Function: init(Args) -> {ok, State} | +%% {ok, State, Timeout} | +%% ignore | +%% {stop, Reason} +%% Description: Initiates the server +%%-------------------------------------------------------------------- +init([Host, Opts]) -> + iconv:start(), + mnesia:create_table(irc_custom, + [{disc_copies, [node()]}, + {attributes, record_info(fields, irc_custom)}]), + MyHost = gen_mod:get_opt_host(Host, Opts, "irc.@HOST@"), + update_table(MyHost), + Access = gen_mod:get_opt(access, Opts, all), + catch ets:new(irc_connection, [named_table, + public, + {keypos, #irc_connection.jid_server_host}]), + ejabberd_router:register_route(MyHost), + {ok, #state{host = MyHost, + server_host = Host, + access = Access}}. + +%%-------------------------------------------------------------------- +%% Function: %% handle_call(Request, From, State) -> {reply, Reply, State} | +%% {reply, Reply, State, Timeout} | +%% {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, Reply, State} | +%% {stop, Reason, State} +%% Description: Handling call messages +%%-------------------------------------------------------------------- +handle_call(stop, _From, State) -> + {stop, normal, ok, State}. + +%%-------------------------------------------------------------------- +%% Function: handle_cast(Msg, State) -> {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, State} +%% Description: Handling cast messages +%%-------------------------------------------------------------------- +handle_cast(_Msg, State) -> + {noreply, State}. + +%%-------------------------------------------------------------------- +%% Function: handle_info(Info, State) -> {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, State} +%% Description: Handling all non call/cast messages +%%-------------------------------------------------------------------- +handle_info({route, From, To, Packet}, + #state{host = Host, + server_host = ServerHost, + access = Access} = State) -> + case catch do_route(Host, ServerHost, Access, From, To, Packet) of + {'EXIT', Reason} -> + ?ERROR_MSG("~p", [Reason]); + _ -> + ok + end, + {noreply, State}; +handle_info(_Info, State) -> + {noreply, State}. + +%%-------------------------------------------------------------------- +%% Function: terminate(Reason, State) -> void() +%% Description: This function is called by a gen_server when it is about to +%% terminate. It should be the opposite of Module:init/1 and do any necessary +%% cleaning up. When it returns, the gen_server terminates with Reason. +%% The return value is ignored. +%%-------------------------------------------------------------------- +terminate(_Reason, State) -> + ejabberd_router:unregister_route(State#state.host), + ok. + +%%-------------------------------------------------------------------- +%% Func: code_change(OldVsn, State, Extra) -> {ok, NewState} +%% Description: Convert process state when code is changed +%%-------------------------------------------------------------------- +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%-------------------------------------------------------------------- +%%% Internal functions +%%-------------------------------------------------------------------- +start_supervisor(Host) -> + Proc = gen_mod:get_module_proc(Host, ejabberd_mod_irc_sup), + ChildSpec = + {Proc, + {ejabberd_tmp_sup, start_link, + [Proc, mod_irc_connection]}, + permanent, + infinity, + supervisor, + [ejabberd_tmp_sup]}, + supervisor:start_child(ejabberd_sup, ChildSpec). + +stop_supervisor(Host) -> + Proc = gen_mod:get_module_proc(Host, ejabberd_mod_irc_sup), + supervisor:terminate_child(ejabberd_sup, Proc), + supervisor:delete_child(ejabberd_sup, Proc). + +do_route(Host, ServerHost, Access, From, To, Packet) -> + case acl:match_rule(ServerHost, Access, From) of + allow -> + do_route1(Host, ServerHost, From, To, Packet); + _ -> + {xmlelement, _Name, Attrs, _Els} = Packet, + Lang = xml:get_attr_s("xml:lang", Attrs), + ErrText = "Access denied by service policy", + Err = jlib:make_error_reply(Packet, + ?ERRT_FORBIDDEN(Lang, ErrText)), + ejabberd_router:route(To, From, Err) + end. + +do_route1(Host, ServerHost, From, To, Packet) -> + #jid{user = ChanServ, resource = Resource} = To, + {xmlelement, _Name, _Attrs, _Els} = Packet, + case ChanServ of + "" -> + case Resource of + "" -> + case jlib:iq_query_info(Packet) of + #iq{type = get, xmlns = ?NS_DISCO_INFO = XMLNS, + sub_el = SubEl, lang = Lang} = IQ -> + Node = xml:get_tag_attr_s("node", SubEl), + Info = ejabberd_hooks:run_fold( + disco_info, ServerHost, [], + [ServerHost, ?MODULE, "", ""]), + case iq_disco(Node, Lang) of + [] -> + Res = IQ#iq{type = result, + sub_el = [{xmlelement, "query", + [{"xmlns", XMLNS}], + []}]}, + ejabberd_router:route(To, + From, + jlib:iq_to_xml(Res)); + DiscoInfo -> + Res = IQ#iq{type = result, + sub_el = [{xmlelement, "query", + [{"xmlns", XMLNS}], + DiscoInfo ++ Info}]}, + ejabberd_router:route(To, + From, + jlib:iq_to_xml(Res)) + end; + #iq{type = get, xmlns = ?NS_DISCO_ITEMS = XMLNS, + sub_el = SubEl, lang = Lang} = IQ -> + Node = xml:get_tag_attr_s("node", SubEl), + case Node of + [] -> + ResIQ = IQ#iq{type = result, + sub_el = [{xmlelement, "query", + [{"xmlns", XMLNS}], + []}]}, + Res = jlib:iq_to_xml(ResIQ); + "join" -> + ResIQ = IQ#iq{type = result, + sub_el = [{xmlelement, "query", + [{"xmlns", XMLNS}], + []}]}, + Res = jlib:iq_to_xml(ResIQ); + "register" -> + ResIQ = IQ#iq{type = result, + sub_el = [{xmlelement, "query", + [{"xmlns", XMLNS}], + []}]}, + Res = jlib:iq_to_xml(ResIQ); + ?NS_COMMANDS -> + ResIQ = IQ#iq{type = result, + sub_el = [{xmlelement, "query", + [{"xmlns", XMLNS}, + {"node", Node}], + command_items(Host, Lang)}]}, + Res = jlib:iq_to_xml(ResIQ); + _ -> + Res = jlib:make_error_reply( + Packet, ?ERR_ITEM_NOT_FOUND) + end, + ejabberd_router:route(To, + From, + Res); + #iq{xmlns = ?NS_REGISTER} = IQ -> + process_register(Host, From, To, IQ); + #iq{type = get, xmlns = ?NS_VCARD = XMLNS, + lang = Lang} = IQ -> + Res = IQ#iq{type = result, + sub_el = + [{xmlelement, "vCard", + [{"xmlns", XMLNS}], + iq_get_vcard(Lang)}]}, + ejabberd_router:route(To, + From, + jlib:iq_to_xml(Res)); + #iq{type = set, xmlns = ?NS_COMMANDS, + lang = _Lang, sub_el = SubEl} = IQ -> + Request = adhoc:parse_request(IQ), + case lists:keysearch(Request#adhoc_request.node, 1, commands()) of + {value, {_, _, Function}} -> + case catch Function(From, To, Request) of + {'EXIT', Reason} -> + ?ERROR_MSG("~p~nfor ad-hoc handler of ~p", + [Reason, {From, To, IQ}]), + Res = IQ#iq{type = error, sub_el = [SubEl, + ?ERR_INTERNAL_SERVER_ERROR]}; + ignore -> + Res = ignore; + {error, Error} -> + Res = IQ#iq{type = error, sub_el = [SubEl, Error]}; + Command -> + Res = IQ#iq{type = result, sub_el = [Command]} + end, + if Res /= ignore -> + ejabberd_router:route(To, From, jlib:iq_to_xml(Res)); + true -> + ok + end; + _ -> + Err = jlib:make_error_reply( + Packet, ?ERR_ITEM_NOT_FOUND), + ejabberd_router:route(To, From, Err) + end; + #iq{} = _IQ -> + Err = jlib:make_error_reply( + Packet, ?ERR_FEATURE_NOT_IMPLEMENTED), + ejabberd_router:route(To, From, Err); + _ -> + ok + end; + _ -> + Err = jlib:make_error_reply(Packet, ?ERR_BAD_REQUEST), + ejabberd_router:route(To, From, Err) + end; + _ -> + case string:tokens(ChanServ, "%") of + [[_ | _] = Channel, [_ | _] = Server] -> + case ets:lookup(irc_connection, {From, Server, Host}) of + [] -> + ?DEBUG("open new connection~n", []), + {Username, Encoding, Port, Password} = get_connection_params( + Host, From, Server), + ConnectionUsername = + case Packet of + %% If the user tries to join a + %% chatroom, the packet for sure + %% contains the desired username. + {xmlelement, "presence", _, _} -> + Resource; + %% Otherwise, there is no firm + %% conclusion from the packet. + %% Better to use the configured + %% username (which defaults to the + %% username part of the JID). + _ -> + Username + end, + {ok, Pid} = mod_irc_connection:start( + From, Host, ServerHost, Server, + ConnectionUsername, Encoding, Port, Password), + ets:insert( + irc_connection, + #irc_connection{jid_server_host = {From, Server, Host}, + pid = Pid}), + mod_irc_connection:route_chan( + Pid, Channel, Resource, Packet), + ok; + [R] -> + Pid = R#irc_connection.pid, + ?DEBUG("send to process ~p~n", + [Pid]), + mod_irc_connection:route_chan( + Pid, Channel, Resource, Packet), + ok + end; + _ -> + case string:tokens(ChanServ, "!") of + [[_ | _] = Nick, [_ | _] = Server] -> + case ets:lookup(irc_connection, {From, Server, Host}) of + [] -> + Err = jlib:make_error_reply( + Packet, ?ERR_SERVICE_UNAVAILABLE), + ejabberd_router:route(To, From, Err); + [R] -> + Pid = R#irc_connection.pid, + ?DEBUG("send to process ~p~n", + [Pid]), + mod_irc_connection:route_nick( + Pid, Nick, Packet), + ok + end; + _ -> + Err = jlib:make_error_reply( + Packet, ?ERR_BAD_REQUEST), + ejabberd_router:route(To, From, Err) + end + end + end. + + +closed_connection(Host, From, Server) -> + ets:delete(irc_connection, {From, Server, Host}). + + +iq_disco([], Lang) -> + [{xmlelement, "identity", + [{"category", "conference"}, + {"type", "irc"}, + {"name", translate:translate(Lang, "IRC Transport")}], []}, + {xmlelement, "feature", [{"var", ?NS_DISCO_INFO}], []}, + {xmlelement, "feature", [{"var", ?NS_MUC}], []}, + {xmlelement, "feature", [{"var", ?NS_REGISTER}], []}, + {xmlelement, "feature", [{"var", ?NS_VCARD}], []}, + {xmlelement, "feature", [{"var", ?NS_COMMANDS}], []}]; +iq_disco(Node, Lang) -> + case lists:keysearch(Node, 1, commands()) of + {value, {_, Name, _}} -> + [{xmlelement, "identity", + [{"category", "automation"}, + {"type", "command-node"}, + {"name", translate:translate(Lang, Name)}], []}, + {xmlelement, "feature", + [{"var", ?NS_COMMANDS}], []}, + {xmlelement, "feature", + [{"var", ?NS_XDATA}], []}]; + _ -> + [] + end. + +iq_get_vcard(Lang) -> + [{xmlelement, "FN", [], + [{xmlcdata, "ejabberd/mod_irc"}]}, + {xmlelement, "URL", [], + [{xmlcdata, ?EJABBERD_URI}]}, + {xmlelement, "DESC", [], + [{xmlcdata, translate:translate(Lang, "ejabberd IRC module") ++ + "\nCopyright (c) 2003-2009 Alexey Shchepin"}]}]. + +command_items(Host, Lang) -> + lists:map(fun({Node, Name, _Function}) + -> {xmlelement, "item", + [{"jid", Host}, + {"node", Node}, + {"name", translate:translate(Lang, Name)}], []} + end, commands()). + +commands() -> + [{"join", "Join channel", fun adhoc_join/3}, + {"register", "Configure username, encoding, port and password", fun adhoc_register/3}]. + +process_register(Host, From, To, #iq{} = IQ) -> + case catch process_irc_register(Host, From, To, IQ) of + {'EXIT', Reason} -> + ?ERROR_MSG("~p", [Reason]); + ResIQ -> + if + ResIQ /= ignore -> + ejabberd_router:route(To, From, + jlib:iq_to_xml(ResIQ)); + true -> + ok + end + end. + +find_xdata_el({xmlelement, _Name, _Attrs, SubEls}) -> + find_xdata_el1(SubEls). + +find_xdata_el1([]) -> + false; + +find_xdata_el1([{xmlelement, Name, Attrs, SubEls} | Els]) -> + case xml:get_attr_s("xmlns", Attrs) of + ?NS_XDATA -> + {xmlelement, Name, Attrs, SubEls}; + _ -> + find_xdata_el1(Els) + end; + +find_xdata_el1([_ | Els]) -> + find_xdata_el1(Els). + +process_irc_register(Host, From, _To, + #iq{type = Type, xmlns = XMLNS, + lang = Lang, sub_el = SubEl} = IQ) -> + case Type of + set -> + XDataEl = find_xdata_el(SubEl), + case XDataEl of + false -> + IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ACCEPTABLE]}; + {xmlelement, _Name, Attrs, _SubEls} -> + case xml:get_attr_s("type", Attrs) of + "cancel" -> + IQ#iq{type = result, + sub_el = [{xmlelement, "query", + [{"xmlns", XMLNS}], []}]}; + "submit" -> + XData = jlib:parse_xdata_submit(XDataEl), + case XData of + invalid -> + IQ#iq{type = error, + sub_el = [SubEl, ?ERR_BAD_REQUEST]}; + _ -> + Node = string:tokens( + xml:get_tag_attr_s("node", SubEl), + "/"), + case set_form( + Host, From, Node, Lang, XData) of + {result, Res} -> + IQ#iq{type = result, + sub_el = [{xmlelement, "query", + [{"xmlns", XMLNS}], + Res + }]}; + {error, Error} -> + IQ#iq{type = error, + sub_el = [SubEl, Error]} + end + end; + _ -> + IQ#iq{type = error, + sub_el = [SubEl, ?ERR_BAD_REQUEST]} + end + end; + get -> + Node = + string:tokens(xml:get_tag_attr_s("node", SubEl), "/"), + case get_form(Host, From, Node, Lang) of + {result, Res} -> + IQ#iq{type = result, + sub_el = [{xmlelement, "query", + [{"xmlns", XMLNS}], + Res + }]}; + {error, Error} -> + IQ#iq{type = error, + sub_el = [SubEl, Error]} + end + end. + + + +get_form(Host, From, [], Lang) -> + #jid{user = User, server = Server, + luser = LUser, lserver = LServer} = From, + US = {LUser, LServer}, + Customs = + case catch mnesia:dirty_read({irc_custom, {US, Host}}) of + {'EXIT', _Reason} -> + {error, ?ERR_INTERNAL_SERVER_ERROR}; + [] -> + {User, []}; + [#irc_custom{data = Data}] -> + {xml:get_attr_s(username, Data), + xml:get_attr_s(connections_params, Data)} + end, + case Customs of + {error, _Error} -> + Customs; + {Username, ConnectionsParams} -> + {result, + [{xmlelement, "instructions", [], + [{xmlcdata, + translate:translate( + Lang, + "You need an x:data capable client " + "to configure mod_irc settings")}]}, + {xmlelement, "x", [{"xmlns", ?NS_XDATA}], + [{xmlelement, "title", [], + [{xmlcdata, + translate:translate( + Lang, + "Registration in mod_irc for ") ++ User ++ "@" ++ Server}]}, + {xmlelement, "instructions", [], + [{xmlcdata, + translate:translate( + Lang, + "Enter username, encodings, ports and passwords you wish to use for " + "connecting to IRC servers")}]}, + {xmlelement, "field", [{"type", "text-single"}, + {"label", + translate:translate( + Lang, "IRC Username")}, + {"var", "username"}], + [{xmlelement, "value", [], [{xmlcdata, Username}]}]}, + {xmlelement, "field", [{"type", "fixed"}], + [{xmlelement, "value", [], + [{xmlcdata, + lists:flatten( + io_lib:format( + translate:translate( + Lang, + "If you want to specify different ports, " + "passwords, encodings for IRC servers, fill " + "this list with values in format " + "'{\"irc server\", \"encoding\", port, \"password\"}'. " + "By default this service use \"~s\" encoding, port ~p, " + "empty password."), + [?DEFAULT_IRC_ENCODING, ?DEFAULT_IRC_PORT]))}]}]}, + {xmlelement, "field", [{"type", "fixed"}], + [{xmlelement, "value", [], + [{xmlcdata, + translate:translate( + Lang, + "Example: [{\"irc.lucky.net\", \"koi8-r\", 6667, \"secret\"}, " + "{\"vendetta.fef.net\", \"iso8859-1\", 7000}, {\"irc.sometestserver.net\", \"utf-8\"}]." + )}]}]}, + {xmlelement, "field", [{"type", "text-multi"}, + {"label", + translate:translate(Lang, "Connections parameters")}, + {"var", "connections_params"}], + lists:map( + fun(S) -> + {xmlelement, "value", [], [{xmlcdata, S}]} + end, + string:tokens( + lists:flatten( + io_lib:format("~p.", [ConnectionsParams])), + "\n")) + } + ]}]} + end; + +get_form(_Host, _, _, _Lang) -> + {error, ?ERR_SERVICE_UNAVAILABLE}. + + + + +set_form(Host, From, [], _Lang, XData) -> + {LUser, LServer, _} = jlib:jid_tolower(From), + US = {LUser, LServer}, + case {lists:keysearch("username", 1, XData), + lists:keysearch("connections_params", 1, XData)} of + {{value, {_, [Username]}}, {value, {_, Strings}}} -> + EncString = lists:foldl(fun(S, Res) -> + Res ++ S ++ "\n" + end, "", Strings), + case erl_scan:string(EncString) of + {ok, Tokens, _} -> + case erl_parse:parse_term(Tokens) of + {ok, ConnectionsParams} -> + case mnesia:transaction( + fun() -> + mnesia:write( + #irc_custom{us_host = + {US, Host}, + data = + [{username, + Username}, + {connections_params, + ConnectionsParams}]}) + end) of + {atomic, _} -> + {result, []}; + _ -> + {error, ?ERR_NOT_ACCEPTABLE} + end; + _ -> + {error, ?ERR_NOT_ACCEPTABLE} + end; + _ -> + {error, ?ERR_NOT_ACCEPTABLE} + end; + _ -> + {error, ?ERR_NOT_ACCEPTABLE} + end; + + +set_form(_Host, _, _, _Lang, _XData) -> + {error, ?ERR_SERVICE_UNAVAILABLE}. + + +get_connection_params(Host, From, IRCServer) -> + #jid{user = User, server = _Server, + luser = LUser, lserver = LServer} = From, + US = {LUser, LServer}, + case catch mnesia:dirty_read({irc_custom, {US, Host}}) of + {'EXIT', _Reason} -> + {User, ?DEFAULT_IRC_ENCODING, ?DEFAULT_IRC_PORT, ""}; + [] -> + {User, ?DEFAULT_IRC_ENCODING, ?DEFAULT_IRC_PORT, ""}; + [#irc_custom{data = Data}] -> + Username = xml:get_attr_s(username, Data), + {NewUsername, NewEncoding, NewPort, NewPassword} = + case lists:keysearch(IRCServer, 1, xml:get_attr_s(connections_params, Data)) of + {value, {_, Encoding, Port, Password}} -> + {Username, Encoding, Port, Password}; + {value, {_, Encoding, Port}} -> + {Username, Encoding, Port, ""}; + {value, {_, Encoding}} -> + {Username, Encoding, ?DEFAULT_IRC_PORT, ""}; + _ -> + {Username, ?DEFAULT_IRC_ENCODING, ?DEFAULT_IRC_PORT, ""} + end, + {NewUsername, + NewEncoding, + if + NewPort >= 0 andalso NewPort =< 65535 -> + NewPort; + true -> + ?DEFAULT_IRC_PORT + end, + NewPassword} + end. + +adhoc_join(_From, _To, #adhoc_request{action = "cancel"} = Request) -> + adhoc:produce_response(Request, + #adhoc_response{status = canceled}); +adhoc_join(From, To, #adhoc_request{lang = Lang, + node = _Node, + action = _Action, + xdata = XData} = Request) -> + %% Access control has already been taken care of in do_route. + if XData == false -> + Form = + {xmlelement, "x", + [{"xmlns", ?NS_XDATA}, + {"type", "form"}], + [{xmlelement, "title", [], [{xmlcdata, translate:translate(Lang, "Join IRC channel")}]}, + {xmlelement, "field", + [{"var", "channel"}, + {"type", "text-single"}, + {"label", translate:translate(Lang, "IRC channel (don't put the first #)")}], + [{xmlelement, "required", [], []}]}, + {xmlelement, "field", + [{"var", "server"}, + {"type", "text-single"}, + {"label", translate:translate(Lang, "IRC server")}], + [{xmlelement, "required", [], []}]}]}, + adhoc:produce_response(Request, + #adhoc_response{status = executing, + elements = [Form]}); + true -> + case jlib:parse_xdata_submit(XData) of + invalid -> + {error, ?ERR_BAD_REQUEST}; + Fields -> + Channel = case lists:keysearch("channel", 1, Fields) of + {value, {"channel", C}} -> + C; + _ -> + false + end, + Server = case lists:keysearch("server", 1, Fields) of + {value, {"server", S}} -> + S; + _ -> + false + end, + if Channel /= false, + Server /= false -> + RoomJID = Channel ++ "%" ++ Server ++ "@" ++ To#jid.server, + Invite = {xmlelement, "message", [], + [{xmlelement, "x", + [{"xmlns", ?NS_MUC_USER}], + [{xmlelement, "invite", + [{"from", jlib:jid_to_string(From)}], + [{xmlelement, "reason", [], + [{xmlcdata, + translate:translate(Lang, + "Join the IRC channel here.")}]}]}]}, + {xmlelement, "x", + [{"xmlns", ?NS_XCONFERENCE}], + [{xmlcdata, translate:translate(Lang, + "Join the IRC channel here.")}]}, + {xmlelement, "body", [], + [{xmlcdata, io_lib:format( + translate:translate(Lang, + "Join the IRC channel in this Jabber ID: ~s"), + [RoomJID])}]}]}, + ejabberd_router:route(jlib:string_to_jid(RoomJID), From, Invite), + adhoc:produce_response(Request, #adhoc_response{status = completed}); + true -> + {error, ?ERR_BAD_REQUEST} + end + end + end. + +adhoc_register(_From, _To, #adhoc_request{action = "cancel"} = Request) -> + adhoc:produce_response(Request, + #adhoc_response{status = canceled}); +adhoc_register(From, To, #adhoc_request{lang = Lang, + node = _Node, + xdata = XData, + action = Action} = Request) -> + #jid{user = User, luser = LUser, lserver = LServer} = From, + #jid{lserver = Host} = To, + US = {LUser, LServer}, + %% Generate form for setting username and encodings. If the user + %% hasn't begun to fill out the form, generate an initial form + %% based on current values. + if XData == false -> + case catch mnesia:dirty_read({irc_custom, {US, Host}}) of + {'EXIT', _Reason} -> + Username = User, + ConnectionsParams = []; + [] -> + Username = User, + ConnectionsParams = []; + [#irc_custom{data = Data}] -> + Username = xml:get_attr_s(username, Data), + ConnectionsParams = xml:get_attr_s(connections_params, Data) + end, + Error = false; + true -> + case jlib:parse_xdata_submit(XData) of + invalid -> + Error = {error, ?ERR_BAD_REQUEST}, + Username = false, + ConnectionsParams = false; + Fields -> + Username = case lists:keysearch("username", 1, Fields) of + {value, {"username", U}} -> + U; + _ -> + User + end, + ConnectionsParams = parse_connections_params(Fields), + Error = false + end + end, + + if Error /= false -> + Error; + Action == "complete" -> + case mnesia:transaction( + fun () -> + mnesia:write( + #irc_custom{us_host = + {US, Host}, + data = + [{username, + Username}, + {connections_params, + ConnectionsParams}]}) + end) of + {atomic, _} -> + adhoc:produce_response(Request, #adhoc_response{status = completed}); + _ -> + {error, ?ERR_INTERNAL_SERVER_ERROR} + end; + true -> + Form = generate_adhoc_register_form(Lang, Username, ConnectionsParams), + adhoc:produce_response(Request, + #adhoc_response{status = executing, + elements = [Form], + actions = ["next", "complete"]}) + end. + +generate_adhoc_register_form(Lang, Username, ConnectionsParams) -> + {xmlelement, "x", + [{"xmlns", ?NS_XDATA}, + {"type", "form"}], + [{xmlelement, "title", [], [{xmlcdata, translate:translate(Lang, "IRC settings")}]}, + {xmlelement, "instructions", [], + [{xmlcdata, + translate:translate( + Lang, + "Enter username and encodings you wish to use for " + "connecting to IRC servers. Press 'Next' to get more fields " + "to fill in. Press 'Complete' to save settings.")}]}, + {xmlelement, "field", + [{"var", "username"}, + {"type", "text-single"}, + {"label", translate:translate(Lang, "IRC username")}], + [{xmlelement, "required", [], []}, + {xmlelement, "value", [], [{xmlcdata, Username}]}]}] ++ + generate_connection_params_fields(Lang, ConnectionsParams, 1, [])}. + +generate_connection_params_fields(Lang, [], Number, Acc) -> + Field = generate_connection_params_field(Lang, "", "", -1, "", Number), + lists:reverse(Field ++ Acc); + +generate_connection_params_fields(Lang, [ConnectionParams | ConnectionsParams], Number, Acc) -> + case ConnectionParams of + {Server, Encoding, Port, Password} -> + Field = generate_connection_params_field(Lang, Server, Encoding, Port, Password, Number), + generate_connection_params_fields(Lang, ConnectionsParams, Number + 1, Field ++ Acc); + {Server, Encoding, Port} -> + Field = generate_connection_params_field(Lang, Server, Encoding, Port, [], Number), + generate_connection_params_fields(Lang, ConnectionsParams, Number + 1, Field ++ Acc); + {Server, Encoding} -> + Field = generate_connection_params_field(Lang, Server, Encoding, [], [], Number), + generate_connection_params_fields(Lang, ConnectionsParams, Number + 1, Field ++ Acc); + _ -> + [] + end. + +generate_connection_params_field(Lang, Server, Encoding, Port, Password, Number) -> + EncodingUsed = case Encoding of + [] -> + ?DEFAULT_IRC_ENCODING; + _ -> + Encoding + end, + PortUsedInt = if + Port >= 0 andalso Port =< 65535 -> + Port; + true -> + ?DEFAULT_IRC_PORT + end, + PortUsed = integer_to_list(PortUsedInt), + PasswordUsed = case Password of + [] -> + ""; + _ -> + Password + end, + NumberString = integer_to_list(Number), + %% Fields are in reverse order, as they will be reversed again later. + [{xmlelement, "field", + [{"var", "password" ++ NumberString}, + {"type", "text-single"}, + {"label", io_lib:format(translate:translate(Lang, "Password ~b"), [Number])}], + [{xmlelement, "value", [], [{xmlcdata, PasswordUsed}]}]}, + {xmlelement, "field", + [{"var", "port" ++ NumberString}, + {"type", "text-single"}, + {"label", io_lib:format(translate:translate(Lang, "Port ~b"), [Number])}], + [{xmlelement, "value", [], [{xmlcdata, PortUsed}]}]}, + {xmlelement, "field", + [{"var", "encoding" ++ NumberString}, + {"type", "list-single"}, + {"label", io_lib:format(translate:translate(Lang, "Encoding for server ~b"), [Number])}], + [{xmlelement, "value", [], [{xmlcdata, EncodingUsed}]} | + lists:map(fun(E) -> + {xmlelement, "option", [{"label", E}], + [{xmlelement, "value", [], [{xmlcdata, E}]}]} + end, ?POSSIBLE_ENCODINGS)]}, + {xmlelement, "field", + [{"var", "server" ++ NumberString}, + {"type", "text-single"}, + {"label", io_lib:format(translate:translate(Lang, "Server ~b"), [Number])}], + [{xmlelement, "value", [], [{xmlcdata, Server}]}]}]. + +parse_connections_params(Fields) -> + %% Find all fields staring with serverN, encodingN, portN and passwordN for any values + %% of N, and generate lists of {"N", Value}. + Servers = lists:sort( + [{lists:nthtail(6, Var), lists:flatten(Value)} || {Var, Value} <- Fields, + lists:prefix("server", Var)]), + Encodings = lists:sort( + [{lists:nthtail(8, Var), lists:flatten(Value)} || {Var, Value} <- Fields, + lists:prefix("encoding", Var)]), + + Ports = lists:sort( + [{lists:nthtail(4, Var), lists:flatten(Value)} || {Var, Value} <- Fields, + lists:prefix("port", Var)]), + + Passwords = lists:sort( + [{lists:nthtail(8, Var), lists:flatten(Value)} || {Var, Value} <- Fields, + lists:prefix("password", Var)]), + + %% Now sort the lists, and find the corresponding pairs. + parse_connections_params(Servers, Encodings, Ports, Passwords). + +retrieve_connections_params(ConnectionParams, ServerN) -> + case ConnectionParams of + [{ConnectionParamN, ConnectionParam} | ConnectionParamsTail] -> + if + ServerN == ConnectionParamN -> + {ConnectionParam, ConnectionParamsTail}; + ServerN < ConnectionParamN -> + {[], [{ConnectionParamN, ConnectionParam} | ConnectionParamsTail]}; + ServerN > ConnectionParamN -> + {[], ConnectionParamsTail} + end; + _ -> + {[], []} + end. + +parse_connections_params([], _, _, _) -> + []; +parse_connections_params(_, [], [], []) -> + []; + +parse_connections_params([{ServerN, Server} | Servers], Encodings, Ports, Passwords) -> + %% Try to match matches of servers, ports, passwords and encodings, no matter what fields + %% the client might have left out. + {NewEncoding, NewEncodings} = retrieve_connections_params(Encodings, ServerN), + {NewPort, NewPorts} = retrieve_connections_params(Ports, ServerN), + {NewPassword, NewPasswords} = retrieve_connections_params(Passwords, ServerN), + [{Server, NewEncoding, NewPort, NewPassword} | parse_connections_params(Servers, NewEncodings, NewPorts, NewPasswords)]. + +update_table(Host) -> + Fields = record_info(fields, irc_custom), + case mnesia:table_info(irc_custom, attributes) of + Fields -> + ok; + [userserver, data] -> + ?INFO_MSG("Converting irc_custom table from " + "{userserver, data} format", []), + {atomic, ok} = mnesia:create_table( + mod_irc_tmp_table, + [{disc_only_copies, [node()]}, + {type, bag}, + {local_content, true}, + {record_name, irc_custom}, + {attributes, record_info(fields, irc_custom)}]), + mnesia:transform_table(irc_custom, ignore, Fields), + F1 = fun() -> + mnesia:write_lock_table(mod_irc_tmp_table), + mnesia:foldl( + fun(#irc_custom{us_host = US} = R, _) -> + mnesia:dirty_write( + mod_irc_tmp_table, + R#irc_custom{us_host = {US, Host}}) + end, ok, irc_custom) + end, + mnesia:transaction(F1), + mnesia:clear_table(irc_custom), + F2 = fun() -> + mnesia:write_lock_table(irc_custom), + mnesia:foldl( + fun(R, _) -> + mnesia:dirty_write(R) + end, ok, mod_irc_tmp_table) + end, + mnesia:transaction(F2), + mnesia:delete_table(mod_irc_tmp_table); + _ -> + ?INFO_MSG("Recreating irc_custom table", []), + mnesia:transform_table(irc_custom, ignore, Fields) + end. diff --git a/mod_irc/src/mod_irc_connection.erl b/mod_irc/src/mod_irc_connection.erl new file mode 100644 index 0000000..252c425 --- /dev/null +++ b/mod_irc/src/mod_irc_connection.erl @@ -0,0 +1,1348 @@ +%%%---------------------------------------------------------------------- +%%% File : mod_irc_connection.erl +%%% Author : Alexey Shchepin +%%% Purpose : +%%% Created : 15 Feb 2003 by Alexey Shchepin +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2009 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., 59 Temple Place, Suite 330, Boston, MA +%%% 02111-1307 USA +%%% +%%%---------------------------------------------------------------------- + +-module(mod_irc_connection). +-author('alexey@process-one.net'). + +-behaviour(gen_fsm). + +%% External exports +-export([start_link/7, start/8, route_chan/4, route_nick/3]). + +%% gen_fsm callbacks +-export([init/1, + open_socket/2, + wait_for_registration/2, + stream_established/2, + handle_event/3, + handle_sync_event/4, + handle_info/3, + terminate/3, + code_change/4]). + +-include("ejabberd.hrl"). +-include("jlib.hrl"). + +-define(SETS, gb_sets). + +-record(state, {socket, encoding, port, password, + queue, user, host, server, nick, + channels = dict:new(), + nickchannel, + inbuf = "", outbuf = ""}). + +%-define(DBGFSM, true). + +-ifdef(DBGFSM). +-define(FSMOPTS, [{debug, [trace]}]). +-else. +-define(FSMOPTS, []). +-endif. + +%%%---------------------------------------------------------------------- +%%% API +%%%---------------------------------------------------------------------- +start(From, Host, ServerHost, Server, Username, Encoding, Port, Password) -> + Supervisor = gen_mod:get_module_proc(ServerHost, ejabberd_mod_irc_sup), + supervisor:start_child( + Supervisor, [From, Host, Server, Username, Encoding, Port, Password]). + +start_link(From, Host, Server, Username, Encoding, Port, Password) -> + gen_fsm:start_link(?MODULE, [From, Host, Server, Username, Encoding, Port, Password], + ?FSMOPTS). + +%%%---------------------------------------------------------------------- +%%% Callback functions from gen_fsm +%%%---------------------------------------------------------------------- + +%%---------------------------------------------------------------------- +%% Func: init/1 +%% Returns: {ok, StateName, StateData} | +%% {ok, StateName, StateData, Timeout} | +%% ignore | +%% {stop, StopReason} +%%---------------------------------------------------------------------- +init([From, Host, Server, Username, Encoding, Port, Password]) -> + gen_fsm:send_event(self(), init), + {ok, open_socket, #state{queue = queue:new(), + encoding = Encoding, + port = Port, + password = Password, + user = From, + nick = Username, + host = Host, + server = Server}}. + +%%---------------------------------------------------------------------- +%% Func: StateName/2 +%% Returns: {next_state, NextStateName, NextStateData} | +%% {next_state, NextStateName, NextStateData, Timeout} | +%% {stop, Reason, NewStateData} +%%---------------------------------------------------------------------- +open_socket(init, StateData) -> + Addr = StateData#state.server, + Port = StateData#state.port, + ?DEBUG("Connecting with IPv6 to ~s:~p", [Addr, Port]), + Connect6 = gen_tcp:connect(Addr, Port, [inet6, binary, {packet, 0}]), + Connect = case Connect6 of + {error, _} -> + ?DEBUG("Connection with IPv6 to ~s:~p failed. Now using IPv4.", [Addr, Port]), + gen_tcp:connect(Addr, Port, [inet, binary, {packet, 0}]); + _ -> + Connect6 + end, + case Connect of + {ok, Socket} -> + NewStateData = StateData#state{socket = Socket}, + if + StateData#state.password /= "" -> + send_text(NewStateData, + io_lib:format("PASS ~s\r\n", [StateData#state.password])); + true -> true + end, + send_text(NewStateData, + io_lib:format("NICK ~s\r\n", [StateData#state.nick])), + send_text(NewStateData, + io_lib:format( + "USER ~s ~s ~s :~s\r\n", + [StateData#state.nick, + StateData#state.nick, + StateData#state.host, + StateData#state.nick])), + send_text(NewStateData, + io_lib:format("CODEPAGE ~s\r\n", [StateData#state.encoding])), + {next_state, wait_for_registration, + NewStateData}; + {error, Reason} -> + ?DEBUG("connect return ~p~n", [Reason]), + Text = case Reason of + timeout -> "Server Connect Timeout"; + _ -> "Server Connect Failed" + end, + bounce_messages(Text), + {stop, normal, StateData} + end. + +wait_for_registration(closed, StateData) -> + {stop, normal, StateData}. + +stream_established({xmlstreamend, _Name}, StateData) -> + {stop, normal, StateData}; + +stream_established(timeout, StateData) -> + {stop, normal, StateData}; + +stream_established(closed, StateData) -> + {stop, normal, StateData}. + + + +%%---------------------------------------------------------------------- +%% Func: StateName/3 +%% Returns: {next_state, NextStateName, NextStateData} | +%% {next_state, NextStateName, NextStateData, Timeout} | +%% {reply, Reply, NextStateName, NextStateData} | +%% {reply, Reply, NextStateName, NextStateData, Timeout} | +%% {stop, Reason, NewStateData} | +%% {stop, Reason, Reply, NewStateData} +%%---------------------------------------------------------------------- +%state_name(Event, From, StateData) -> +% Reply = ok, +% {reply, Reply, state_name, StateData}. + +%%---------------------------------------------------------------------- +%% Func: handle_event/3 +%% Returns: {next_state, NextStateName, NextStateData} | +%% {next_state, NextStateName, NextStateData, Timeout} | +%% {stop, Reason, NewStateData} +%%---------------------------------------------------------------------- +handle_event(_Event, StateName, StateData) -> + {next_state, StateName, StateData}. + +%%---------------------------------------------------------------------- +%% Func: handle_sync_event/4 +%% Returns: {next_state, NextStateName, NextStateData} | +%% {next_state, NextStateName, NextStateData, Timeout} | +%% {reply, Reply, NextStateName, NextStateData} | +%% {reply, Reply, NextStateName, NextStateData, Timeout} | +%% {stop, Reason, NewStateData} | +%% {stop, Reason, Reply, NewStateData} +%%---------------------------------------------------------------------- +handle_sync_event(_Event, _From, StateName, StateData) -> + Reply = ok, + {reply, Reply, StateName, StateData}. + +code_change(_OldVsn, StateName, StateData, _Extra) -> + {ok, StateName, StateData}. + +-define(SEND(S), + if + StateName == stream_established -> + send_text(StateData, S), + StateData; + true -> + StateData#state{outbuf = StateData#state.outbuf ++ S} + end). + +%%---------------------------------------------------------------------- +%% Func: handle_info/3 +%% Returns: {next_state, NextStateName, NextStateData} | +%% {next_state, NextStateName, NextStateData, Timeout} | +%% {stop, Reason, NewStateData} +%%---------------------------------------------------------------------- +handle_info({route_chan, Channel, Resource, + {xmlelement, "presence", Attrs, _Els}}, + StateName, StateData) -> + NewStateData = + case xml:get_attr_s("type", Attrs) of + "unavailable" -> + send_stanza_unavailable(Channel, StateData), + S1 = ?SEND(io_lib:format("PART #~s\r\n", [Channel])), + S1#state{channels = + dict:erase(Channel, S1#state.channels)}; + "subscribe" -> StateData; + "subscribed" -> StateData; + "unsubscribe" -> StateData; + "unsubscribed" -> StateData; + "error" -> stop; + _ -> + Nick = case Resource of + "" -> + StateData#state.nick; + _ -> + Resource + end, + S1 = if + Nick /= StateData#state.nick -> + S11 = ?SEND(io_lib:format("NICK ~s\r\n", [Nick])), + % The server reply will change the copy of the + % nick in the state (or indicate a clash). + S11#state{nickchannel = Channel}; + true -> + StateData + end, + case dict:is_key(Channel, S1#state.channels) of + true -> + S1; + _ -> + S2 = ?SEND(io_lib:format("JOIN #~s\r\n", [Channel])), + S2#state{channels = + dict:store(Channel, ?SETS:new(), + S1#state.channels)} + end + end, + if + NewStateData == stop -> + {stop, normal, StateData}; + true -> + case dict:fetch_keys(NewStateData#state.channels) of + [] -> {stop, normal, NewStateData}; + _ -> {next_state, StateName, NewStateData} + end + end; + +handle_info({route_chan, Channel, Resource, + {xmlelement, "message", Attrs, _Els} = El}, + StateName, StateData) -> + NewStateData = + case xml:get_attr_s("type", Attrs) of + "groupchat" -> + case xml:get_path_s(El, [{elem, "subject"}, cdata]) of + "" -> + ejabberd_router:route( + jlib:make_jid( + lists:concat( + [Channel, "%", StateData#state.server]), + StateData#state.host, StateData#state.nick), + StateData#state.user, El), + Body = xml:get_path_s(El, [{elem, "body"}, cdata]), + case Body of + "/quote " ++ Rest -> + ?SEND(Rest ++ "\r\n"); + "/msg " ++ Rest -> + ?SEND("PRIVMSG " ++ Rest ++ "\r\n"); + "/me " ++ Rest -> + Strings = string:tokens(Rest, "\n"), + Res = lists:concat( + lists:map( + fun(S) -> + io_lib:format( + "PRIVMSG #~s :\001ACTION ~s\001\r\n", + [Channel, S]) + end, Strings)), + ?SEND(Res); + "/ctcp " ++ Rest -> + Words = string:tokens(Rest, " "), + case Words of + [CtcpDest | _] -> + CtcpCmd = + toupper( + string:substr( + Rest, + string:str(Rest, " ") + 1)), + Res = io_lib:format( + "PRIVMSG ~s :\001~s\001\r\n", + [CtcpDest, CtcpCmd]), + ?SEND(Res); + _ -> + ok + end; + _ -> + Strings = string:tokens(Body, "\n"), + Res = lists:concat( + lists:map( + fun(S) -> + io_lib:format( + "PRIVMSG #~s :~s\r\n", + [Channel, S]) + end, Strings)), + ?SEND(Res) + end; + Subject -> + Strings = string:tokens(Subject, "\n"), + Res = lists:concat( + lists:map( + fun(S) -> + io_lib:format("TOPIC #~s :~s\r\n", + [Channel, S]) + end, Strings)), + ?SEND(Res) + end; + Type when Type == "chat"; Type == ""; Type == "normal" -> + Body = xml:get_path_s(El, [{elem, "body"}, cdata]), + case Body of + "/quote " ++ Rest -> + ?SEND(Rest ++ "\r\n"); + "/msg " ++ Rest -> + ?SEND("PRIVMSG " ++ Rest ++ "\r\n"); + "/me " ++ Rest -> + Strings = string:tokens(Rest, "\n"), + Res = lists:concat( + lists:map( + fun(S) -> + io_lib:format( + "PRIVMSG ~s :\001ACTION ~s\001\r\n", + [Resource, S]) + end, Strings)), + ?SEND(Res); + "/ctcp " ++ Rest -> + Words = string:tokens(Rest, " "), + case Words of + [CtcpDest | _ ] -> + CtcpCmd = + toupper( + string:substr( + Rest, string:str(Rest, " ") + 1)), + Res = io_lib:format( + "PRIVMSG ~s :~s\r\n", + [CtcpDest, "\001" ++ CtcpCmd ++ "\001"]), + ?SEND(Res); + _ -> + ok + end; + _ -> + Strings = string:tokens(Body, "\n"), + Res = lists:concat( + lists:map( + fun(S) -> + io_lib:format("PRIVMSG ~s :~s\r\n", + [Resource, S]) + end, Strings)), + ?SEND(Res) + end; + "error" -> + stop; + _ -> + StateData + end, + if + NewStateData == stop -> + {stop, normal, StateData}; + true -> + {next_state, StateName, NewStateData} + end; + + +handle_info({route_chan, Channel, Resource, + {xmlelement, "iq", _Attrs, _Els} = El}, + StateName, StateData) -> + From = StateData#state.user, + To = jlib:make_jid(lists:concat([Channel, "%", StateData#state.server]), + StateData#state.host, StateData#state.nick), + _ = case jlib:iq_query_info(El) of + #iq{xmlns = ?NS_MUC_ADMIN} = IQ -> + iq_admin(StateData, Channel, From, To, IQ); + #iq{xmlns = ?NS_VERSION} -> + Res = io_lib:format("PRIVMSG ~s :\001VERSION\001\r\n", + [Resource]), + _ = ?SEND(Res), + Err = jlib:make_error_reply( + El, ?ERR_FEATURE_NOT_IMPLEMENTED), + ejabberd_router:route(To, From, Err); + #iq{xmlns = ?NS_TIME} -> + Res = io_lib:format("PRIVMSG ~s :\001TIME\001\r\n", + [Resource]), + _ = ?SEND(Res), + Err = jlib:make_error_reply( + El, ?ERR_FEATURE_NOT_IMPLEMENTED), + ejabberd_router:route(To, From, Err); + #iq{xmlns = ?NS_VCARD} -> + Res = io_lib:format("WHOIS ~s \r\n", + [Resource]), + _ = ?SEND(Res), + Err = jlib:make_error_reply( + El, ?ERR_FEATURE_NOT_IMPLEMENTED), + ejabberd_router:route(To, From, Err); + #iq{} -> + Err = jlib:make_error_reply( + El, ?ERR_FEATURE_NOT_IMPLEMENTED), + ejabberd_router:route(To, From, Err); + _ -> + ok + end, + {next_state, StateName, StateData}; + +handle_info({route_chan, _Channel, _Resource, _Packet}, StateName, StateData) -> + {next_state, StateName, StateData}; + + +handle_info({route_nick, Nick, + {xmlelement, "message", Attrs, _Els} = El}, + StateName, StateData) -> + NewStateData = + case xml:get_attr_s("type", Attrs) of + "chat" -> + Body = xml:get_path_s(El, [{elem, "body"}, cdata]), + case Body of + "/quote " ++ Rest -> + ?SEND(Rest ++ "\r\n"); + "/msg " ++ Rest -> + ?SEND("PRIVMSG " ++ Rest ++ "\r\n"); + "/me " ++ Rest -> + Strings = string:tokens(Rest, "\n"), + Res = lists:concat( + lists:map( + fun(S) -> + io_lib:format( + "PRIVMSG ~s :\001ACTION ~s\001\r\n", + [Nick, S]) + end, Strings)), + ?SEND(Res); + "/ctcp " ++ Rest -> + Words = string:tokens(Rest, " "), + case Words of + [CtcpDest | _ ] -> + CtcpCmd = toupper(string:substr(Rest, string:str(Rest, " ")+1 )), + Res = io_lib:format( + "PRIVMSG ~s :~s\r\n", + [CtcpDest, "\001" ++ CtcpCmd ++ "\001"]), + ?SEND(Res); + _ -> + ok + end; + _ -> + Strings = string:tokens(Body, "\n"), + Res = lists:concat( + lists:map( + fun(S) -> + io_lib:format("PRIVMSG ~s :~s\r\n", + [Nick, S]) + end, Strings)), + ?SEND(Res) + end; + "error" -> + stop; + _ -> + StateData + end, + if + NewStateData == stop -> + {stop, normal, StateData}; + true -> + {next_state, StateName, NewStateData} + end; + +handle_info({route_nick, _Nick, _Packet}, StateName, StateData) -> + {next_state, StateName, StateData}; + + +handle_info({ircstring, [$P, $I, $N, $G, $ | ID]}, StateName, StateData) -> + send_text(StateData, "PONG " ++ ID ++ "\r\n"), + {next_state, StateName, StateData}; + +handle_info({ircstring, [$: | String]}, wait_for_registration, StateData) -> + Words = string:tokens(String, " "), + {NewState, NewStateData} = + case Words of + [_, "001" | _] -> + {stream_established, StateData}; + [_, "433" | _] -> + {error, + {error, error_nick_in_use(StateData, String), StateData}}; + [_, [$4, _, _] | _] -> + {error, + {error, error_unknown_num(StateData, String, "cancel"), + StateData}}; + [_, [$5, _, _] | _] -> + {error, + {error, error_unknown_num(StateData, String, "cancel"), + StateData}}; + _ -> + ?DEBUG("unknown irc command '~s'~n", [String]), + {wait_for_registration, StateData} + end, + % Note that we don't send any data at this stage. + if + NewState == error -> + {stop, normal, NewStateData}; + true -> + {next_state, NewState, NewStateData} + end; + +handle_info({ircstring, [$: | String]}, _StateName, StateData) -> + Words = string:tokens(String, " "), + NewStateData = + case Words of + [_, "353" | Items] -> + process_channel_list(StateData, Items); + [_, "332", _Nick, [$# | Chan] | _] -> + process_channel_topic(StateData, Chan, String), + StateData; + [_, "333", _Nick, [$# | Chan] | _] -> + process_channel_topic_who(StateData, Chan, String), + StateData; + [_, "318", _, Nick | _] -> + process_endofwhois(StateData, String, Nick), + StateData; + [_, "311", _, Nick, Ident, Irchost | _ ] -> + process_whois311(StateData, String, Nick, Ident, Irchost), + StateData; + [_, "312", _, Nick, Ircserver | _ ] -> + process_whois312(StateData, String, Nick, Ircserver), + StateData; + [_, "319", _, Nick | _ ] -> + process_whois319(StateData, String, Nick), + StateData; + [_, "433" | _] -> + process_nick_in_use(StateData, String); + % CODEPAGE isn't standard, so don't complain if it's not there. + [_, "421", _, "CODEPAGE" | _] -> + StateData; + [_, [$4, _, _] | _] -> + process_num_error(StateData, String); + [_, [$5, _, _] | _] -> + process_num_error(StateData, String); + [From, "PRIVMSG", [$# | Chan] | _] -> + process_chanprivmsg(StateData, Chan, From, String), + StateData; + [From, "NOTICE", [$# | Chan] | _] -> + process_channotice(StateData, Chan, From, String), + StateData; + [From, "PRIVMSG", Nick, ":\001VERSION\001" | _] -> + process_version(StateData, Nick, From), + StateData; + [From, "PRIVMSG", Nick, ":\001USERINFO\001" | _] -> + process_userinfo(StateData, Nick, From), + StateData; + [From, "PRIVMSG", Nick | _] -> + process_privmsg(StateData, Nick, From, String), + StateData; + [From, "NOTICE", Nick | _] -> + process_notice(StateData, Nick, From, String), + StateData; + [From, "TOPIC", [$# | Chan] | _] -> + process_topic(StateData, Chan, From, String), + StateData; + [From, "PART", [$# | Chan] | _] -> + process_part(StateData, Chan, From, String); + [From, "QUIT" | _] -> + process_quit(StateData, From, String); + [From, "JOIN", Chan | _] -> + process_join(StateData, Chan, From, String); + [From, "MODE", [$# | Chan], "+o", Nick | _] -> + process_mode_o(StateData, Chan, From, Nick, + "admin", "moderator"), + StateData; + [From, "MODE", [$# | Chan], "-o", Nick | _] -> + process_mode_o(StateData, Chan, From, Nick, + "member", "participant"), + StateData; + [From, "KICK", [$# | Chan], Nick | _] -> + process_kick(StateData, Chan, From, Nick, String), + StateData; + [From, "NICK", Nick | _] -> + process_nick(StateData, From, Nick); + _ -> + ?DEBUG("unknown irc command '~s'~n", [String]), + StateData + end, + NewStateData1 = + case StateData#state.outbuf of + "" -> + NewStateData; + Data -> + send_text(NewStateData, Data), + NewStateData#state{outbuf = ""} + end, + {next_state, stream_established, NewStateData1}; + +handle_info({ircstring, [$E, $R, $R, $O, $R | _] = String}, + StateName, StateData) -> + process_error(StateData, String), + {next_state, StateName, StateData}; + + +handle_info({ircstring, String}, StateName, StateData) -> + ?DEBUG("unknown irc command '~s'~n", [String]), + {next_state, StateName, StateData}; + + +handle_info({send_text, Text}, StateName, StateData) -> + send_text(StateData, Text), + {next_state, StateName, StateData}; +handle_info({tcp, _Socket, Data}, StateName, StateData) -> + Buf = StateData#state.inbuf ++ binary_to_list(Data), + {ok, Strings} = regexp:split([C || C <- Buf, C /= $\r], "\n"), + ?DEBUG("strings=~p~n", [Strings]), + NewBuf = process_lines(StateData#state.encoding, Strings), + {next_state, StateName, StateData#state{inbuf = NewBuf}}; +handle_info({tcp_closed, _Socket}, StateName, StateData) -> + gen_fsm:send_event(self(), closed), + {next_state, StateName, StateData}; +handle_info({tcp_error, _Socket, _Reason}, StateName, StateData) -> + gen_fsm:send_event(self(), closed), + {next_state, StateName, StateData}. + +%%---------------------------------------------------------------------- +%% Func: terminate/3 +%% Purpose: Shutdown the fsm +%% Returns: any +%%---------------------------------------------------------------------- +terminate(_Reason, _StateName, FullStateData) -> + % Extract error message if there was one. + {Error, StateData} = case FullStateData of + {error, SError, SStateData} -> + {SError, SStateData}; + _ -> + {{xmlelement, "error", [{"code", "502"}], + [{xmlcdata, "Server Connect Failed"}]}, + FullStateData} + end, + mod_irc:closed_connection(StateData#state.host, + StateData#state.user, + StateData#state.server), + bounce_messages("Server Connect Failed"), + lists:foreach( + fun(Chan) -> + Stanza = {xmlelement, "presence", [{"type", "error"}], + [Error]}, + send_stanza(Chan, StateData, Stanza) + end, dict:fetch_keys(StateData#state.channels)), + case StateData#state.socket of + undefined -> + ok; + Socket -> + gen_tcp:close(Socket) + end, + ok. + +send_stanza(Chan, StateData, Stanza) -> + ejabberd_router:route( + jlib:make_jid( + lists:concat([Chan, "%", StateData#state.server]), + StateData#state.host, StateData#state.nick), + StateData#state.user, + Stanza). + +send_stanza_unavailable(Chan, StateData) -> + Affiliation = "member", % this is a simplification + Role = "none", + Stanza = + {xmlelement, "presence", [{"type", "unavailable"}], + [{xmlelement, "x", [{"xmlns", ?NS_MUC_USER}], + [{xmlelement, "item", + [{"affiliation", Affiliation}, + {"role", Role}], + []}, + {xmlelement, "status", + [{"code", "110"}], + []} + ]}]}, + send_stanza(Chan, StateData, Stanza). + +%%%---------------------------------------------------------------------- +%%% Internal functions +%%%---------------------------------------------------------------------- + +send_text(#state{socket = Socket, encoding = Encoding}, Text) -> + CText = iconv:convert("utf-8", Encoding, lists:flatten(Text)), + %?DEBUG("IRC OUTu: ~s~nIRC OUTk: ~s~n", [Text, CText]), + gen_tcp:send(Socket, CText). + + +%send_queue(Socket, Q) -> +% case queue:out(Q) of +% {{value, El}, Q1} -> +% send_element(Socket, El), +% send_queue(Socket, Q1); +% {empty, Q1} -> +% ok +% end. + +bounce_messages(Reason) -> + receive + {send_element, El} -> + {xmlelement, _Name, Attrs, _SubTags} = El, + case xml:get_attr_s("type", Attrs) of + "error" -> + ok; + _ -> + Err = jlib:make_error_reply(El, + "502", Reason), + From = jlib:string_to_jid(xml:get_attr_s("from", Attrs)), + To = jlib:string_to_jid(xml:get_attr_s("to", Attrs)), + ejabberd_router:route(To, From, Err) + end, + bounce_messages(Reason) + after 0 -> + ok + end. + + +route_chan(Pid, Channel, Resource, Packet) -> + Pid ! {route_chan, Channel, Resource, Packet}. + +route_nick(Pid, Nick, Packet) -> + Pid ! {route_nick, Nick, Packet}. + + +process_lines(_Encoding, [S]) -> + S; +process_lines(Encoding, [S | Ss]) -> + self() ! {ircstring, iconv:convert(Encoding, "utf-8", S)}, + process_lines(Encoding, Ss). + +process_channel_list(StateData, Items) -> + process_channel_list_find_chan(StateData, Items). + +process_channel_list_find_chan(StateData, []) -> + StateData; +process_channel_list_find_chan(StateData, [[$# | Chan] | Items]) -> + process_channel_list_users(StateData, Chan, Items); +process_channel_list_find_chan(StateData, [_ | Items]) -> + process_channel_list_find_chan(StateData, Items). + +process_channel_list_users(StateData, _Chan, []) -> + StateData; +process_channel_list_users(StateData, Chan, [User | Items]) -> + NewStateData = process_channel_list_user(StateData, Chan, User), + process_channel_list_users(NewStateData, Chan, Items). + +process_channel_list_user(StateData, Chan, User) -> + User1 = case User of + [$: | U1] -> U1; + _ -> User + end, + {User2, Affiliation, Role} = + case User1 of + [$@ | U2] -> {U2, "admin", "moderator"}; + [$+ | U2] -> {U2, "member", "participant"}; + [$\% | U2] -> {U2, "admin", "moderator"}; + [$& | U2] -> {U2, "admin", "moderator"}; + [$~ | U2] -> {U2, "admin", "moderator"}; + _ -> {User1, "member", "participant"} + end, + ejabberd_router:route( + jlib:make_jid(lists:concat([Chan, "%", StateData#state.server]), + StateData#state.host, User2), + StateData#state.user, + {xmlelement, "presence", [], + [{xmlelement, "x", [{"xmlns", ?NS_MUC_USER}], + [{xmlelement, "item", + [{"affiliation", Affiliation}, + {"role", Role}], + []}]}]}), + case catch dict:update(Chan, + fun(Ps) -> + ?SETS:add_element(User2, Ps) + end, StateData#state.channels) of + {'EXIT', _} -> + StateData; + NS -> + StateData#state{channels = NS} + end. + + +process_channel_topic(StateData, Chan, String) -> + {ok, Msg, _} = regexp:sub(String, ".*332[^:]*:", ""), + Msg1 = filter_message(Msg), + ejabberd_router:route( + jlib:make_jid( + lists:concat([Chan, "%", StateData#state.server]), + StateData#state.host, ""), + StateData#state.user, + {xmlelement, "message", [{"type", "groupchat"}], + [{xmlelement, "subject", [], [{xmlcdata, Msg1}]}, + {xmlelement, "body", [], [{xmlcdata, "Topic for #" ++ Chan ++ ": " ++ Msg1}]} + ]}). + +process_channel_topic_who(StateData, Chan, String) -> + Words = string:tokens(String, " "), + Msg1 = case Words of + [_, "333", _, _Chan, Whoset , Timeset] -> + case string:to_integer(Timeset) of + {Unixtimeset, _Rest} -> + "Topic for #" ++ Chan ++ " set by " ++ Whoset ++ + " at " ++ unixtime2string(Unixtimeset); + _-> + "Topic for #" ++ Chan ++ " set by " ++ Whoset + end; + [_, "333", _, _Chan, Whoset | _] -> + "Topic for #" ++ Chan ++ " set by " ++ Whoset; + _ -> + String + end, + Msg2 = filter_message(Msg1), + ejabberd_router:route( + jlib:make_jid(lists:concat([Chan, "%", StateData#state.server]), + StateData#state.host, ""), + StateData#state.user, + {xmlelement, "message", [{"type", "groupchat"}], + [{xmlelement, "body", [], [{xmlcdata, Msg2}]}]}). + + +error_nick_in_use(_StateData, String) -> + {ok, Msg, _} = regexp:sub(String, ".*433 +[^ ]* +", ""), + Msg1 = filter_message(Msg), + {xmlelement, "error", [{"code", "409"}, {"type", "cancel"}], + [{xmlelement, "conflict", [{"xmlns", ?NS_STANZAS}], []}, + {xmlelement, "text", [{"xmlns", ?NS_STANZAS}], + [{xmlcdata, Msg1}]}]}. + +process_nick_in_use(StateData, String) -> + % We can't use the jlib macro because we don't know the language of the + % message. + Error = error_nick_in_use(StateData, String), + case StateData#state.nickchannel of + undefined -> + % Shouldn't happen with a well behaved server + StateData; + Chan -> + ejabberd_router:route( + jlib:make_jid( + lists:concat([Chan, "%", StateData#state.server]), + StateData#state.host, StateData#state.nick), + StateData#state.user, + {xmlelement, "presence", [{"type", "error"}], [Error]}), + StateData#state{nickchannel = undefined} + end. + +process_num_error(StateData, String) -> + Error = error_unknown_num(StateData, String, "continue"), + lists:foreach( + fun(Chan) -> + ejabberd_router:route( + jlib:make_jid( + lists:concat([Chan, "%", StateData#state.server]), + StateData#state.host, StateData#state.nick), + StateData#state.user, + {xmlelement, "message", [{"type", "error"}], + [Error]}) + end, dict:fetch_keys(StateData#state.channels)), + StateData. + +process_endofwhois(StateData, _String, Nick) -> + ejabberd_router:route( + jlib:make_jid(lists:concat([Nick, "!", StateData#state.server]), + StateData#state.host, ""), + StateData#state.user, + {xmlelement, "message", [{"type", "chat"}], + [{xmlelement, "body", [], [{xmlcdata, "End of WHOIS"}]}]}). + +process_whois311(StateData, String, Nick, Ident, Irchost) -> + {ok, Fullname, _} = regexp:sub(String, ".*311[^:]*:", ""), + ejabberd_router:route( + jlib:make_jid(lists:concat([Nick, "!", StateData#state.server]), + StateData#state.host, ""), + StateData#state.user, + {xmlelement, "message", [{"type", "chat"}], + [{xmlelement, "body", [], + [{xmlcdata, lists:concat( + ["WHOIS: ", Nick, " is ", + Ident, "@" , Irchost, " : " , Fullname])}]}]}). + +process_whois312(StateData, String, Nick, Ircserver) -> + {ok, Ircserverdesc, _} = regexp:sub(String, ".*312[^:]*:", ""), + ejabberd_router:route( + jlib:make_jid(lists:concat([Nick, "!", StateData#state.server]), + StateData#state.host, ""), + StateData#state.user, + {xmlelement, "message", [{"type", "chat"}], + [{xmlelement, "body", [], + [{xmlcdata, lists:concat(["WHOIS: ", Nick, " use ", + Ircserver, " : ", Ircserverdesc])}]}]}). + +process_whois319(StateData, String, Nick) -> + {ok, Chanlist, _} = regexp:sub(String, ".*319[^:]*:", ""), + ejabberd_router:route( + jlib:make_jid(lists:concat([Nick, "!", StateData#state.server]), + StateData#state.host, ""), + StateData#state.user, + {xmlelement, "message", [{"type", "chat"}], + [{xmlelement, "body", [], + [{xmlcdata, lists:concat(["WHOIS: ", Nick, " is on ", + Chanlist])}]}]}). + + + +process_chanprivmsg(StateData, Chan, From, String) -> + [FromUser | _] = string:tokens(From, "!"), + {ok, Msg, _} = regexp:sub(String, ".*PRIVMSG[^:]*:", ""), + Msg1 = case Msg of + [1, $A, $C, $T, $I, $O, $N, $ | Rest] -> + "/me " ++ Rest; + _ -> + Msg + end, + Msg2 = filter_message(Msg1), + ejabberd_router:route( + jlib:make_jid(lists:concat([Chan, "%", StateData#state.server]), + StateData#state.host, FromUser), + StateData#state.user, + {xmlelement, "message", [{"type", "groupchat"}], + [{xmlelement, "body", [], [{xmlcdata, Msg2}]}]}). + + + +process_channotice(StateData, Chan, From, String) -> + [FromUser | _] = string:tokens(From, "!"), + {ok, Msg, _} = regexp:sub(String, ".*NOTICE[^:]*:", ""), + Msg1 = case Msg of + [1, $A, $C, $T, $I, $O, $N, $ | Rest] -> + "/me " ++ Rest; + _ -> + "/me NOTICE: " ++ Msg + end, + Msg2 = filter_message(Msg1), + ejabberd_router:route( + jlib:make_jid(lists:concat([Chan, "%", StateData#state.server]), + StateData#state.host, FromUser), + StateData#state.user, + {xmlelement, "message", [{"type", "groupchat"}], + [{xmlelement, "body", [], [{xmlcdata, Msg2}]}]}). + + + + +process_privmsg(StateData, _Nick, From, String) -> + [FromUser | _] = string:tokens(From, "!"), + {ok, Msg, _} = regexp:sub(String, ".*PRIVMSG[^:]*:", ""), + Msg1 = case Msg of + [1, $A, $C, $T, $I, $O, $N, $ | Rest] -> + "/me " ++ Rest; + _ -> + Msg + end, + Msg2 = filter_message(Msg1), + ejabberd_router:route( + jlib:make_jid(lists:concat([FromUser, "!", StateData#state.server]), + StateData#state.host, ""), + StateData#state.user, + {xmlelement, "message", [{"type", "chat"}], + [{xmlelement, "body", [], [{xmlcdata, Msg2}]}]}). + + +process_notice(StateData, _Nick, From, String) -> + [FromUser | _] = string:tokens(From, "!"), + {ok, Msg, _} = regexp:sub(String, ".*NOTICE[^:]*:", ""), + Msg1 = case Msg of + [1, $A, $C, $T, $I, $O, $N, $ | Rest] -> + "/me " ++ Rest; + _ -> + "/me NOTICE: " ++ Msg + end, + Msg2 = filter_message(Msg1), + ejabberd_router:route( + jlib:make_jid(lists:concat([FromUser, "!", StateData#state.server]), + StateData#state.host, ""), + StateData#state.user, + {xmlelement, "message", [{"type", "chat"}], + [{xmlelement, "body", [], [{xmlcdata, Msg2}]}]}). + + +process_version(StateData, _Nick, From) -> + [FromUser | _] = string:tokens(From, "!"), + send_text( + StateData, + io_lib:format("NOTICE ~s :\001VERSION " + "ejabberd IRC transport ~s (c) Alexey Shchepin" + "\001\r\n", + [FromUser, ?VERSION]) ++ + io_lib:format("NOTICE ~s :\001VERSION " + "http://ejabberd.jabberstudio.org/" + "\001\r\n", + [FromUser])). + + +process_userinfo(StateData, _Nick, From) -> + [FromUser | _] = string:tokens(From, "!"), + send_text( + StateData, + io_lib:format("NOTICE ~s :\001USERINFO " + "xmpp:~s" + "\001\r\n", + [FromUser, + jlib:jid_to_string(StateData#state.user)])). + + +process_topic(StateData, Chan, From, String) -> + [FromUser | _] = string:tokens(From, "!"), + {ok, Msg, _} = regexp:sub(String, ".*TOPIC[^:]*:", ""), + Msg1 = filter_message(Msg), + ejabberd_router:route( + jlib:make_jid(lists:concat([Chan, "%", StateData#state.server]), + StateData#state.host, FromUser), + StateData#state.user, + {xmlelement, "message", [{"type", "groupchat"}], + [{xmlelement, "subject", [], [{xmlcdata, Msg1}]}, + {xmlelement, "body", [], + [{xmlcdata, "/me has changed the subject to: " ++ + Msg1}]}]}). + +process_part(StateData, Chan, From, String) -> + [FromUser | FromIdent] = string:tokens(From, "!"), + {ok, Msg, _} = regexp:sub(String, ".*PART[^:]*:", ""), + Msg1 = filter_message(Msg), + ejabberd_router:route( + jlib:make_jid(lists:concat([Chan, "%", StateData#state.server]), + StateData#state.host, FromUser), + StateData#state.user, + {xmlelement, "presence", [{"type", "unavailable"}], + [{xmlelement, "x", [{"xmlns", ?NS_MUC_USER}], + [{xmlelement, "item", + [{"affiliation", "member"}, + {"role", "none"}], + []}]}, + {xmlelement, "status", [], + [{xmlcdata, Msg1 ++ " (" ++ FromIdent ++ ")"}]}] + }), + case catch dict:update(Chan, + fun(Ps) -> + remove_element(FromUser, Ps) + end, StateData#state.channels) of + {'EXIT', _} -> + StateData; + NS -> + StateData#state{channels = NS} + end. + + +process_quit(StateData, From, String) -> + [FromUser | FromIdent] = string:tokens(From, "!"), + + {ok, Msg, _} = regexp:sub(String, ".*QUIT[^:]*:", ""), + Msg1 = filter_message(Msg), + %%NewChans = + dict:map( + fun(Chan, Ps) -> + case ?SETS:is_member(FromUser, Ps) of + true -> + ejabberd_router:route( + jlib:make_jid( + lists:concat([Chan, "%", StateData#state.server]), + StateData#state.host, FromUser), + StateData#state.user, + {xmlelement, "presence", [{"type", "unavailable"}], + [{xmlelement, "x", [{"xmlns", ?NS_MUC_USER}], + [{xmlelement, "item", + [{"affiliation", "member"}, + {"role", "none"}], + []}]}, + {xmlelement, "status", [], + [{xmlcdata, Msg1 ++ " (" ++ FromIdent ++ ")"}]} + ]}), + remove_element(FromUser, Ps); + _ -> + Ps + end + end, StateData#state.channels), + StateData. + + +process_join(StateData, Channel, From, _String) -> + [FromUser | FromIdent] = string:tokens(From, "!"), + Chan = lists:subtract(Channel, ":#"), + ejabberd_router:route( + jlib:make_jid(lists:concat([Chan, "%", StateData#state.server]), + StateData#state.host, FromUser), + StateData#state.user, + {xmlelement, "presence", [], + [{xmlelement, "x", [{"xmlns", ?NS_MUC_USER}], + [{xmlelement, "item", + [{"affiliation", "member"}, + {"role", "participant"}], + []}]}, + {xmlelement, "status", [], + [{xmlcdata, FromIdent}]}]}), + + case catch dict:update(Chan, + fun(Ps) -> + ?SETS:add_element(FromUser, Ps) + end, StateData#state.channels) of + {'EXIT', _} -> + StateData; + NS -> + StateData#state{channels = NS} + end. + + + +process_mode_o(StateData, Chan, _From, Nick, Affiliation, Role) -> + %Msg = lists:last(string:tokens(String, ":")), + ejabberd_router:route( + jlib:make_jid(lists:concat([Chan, "%", StateData#state.server]), + StateData#state.host, Nick), + StateData#state.user, + {xmlelement, "presence", [], + [{xmlelement, "x", [{"xmlns", ?NS_MUC_USER}], + [{xmlelement, "item", + [{"affiliation", Affiliation}, + {"role", Role}], + []}]}]}). + +process_kick(StateData, Chan, From, Nick, String) -> + Msg = lists:last(string:tokens(String, ":")), + Msg2 = Nick ++ " kicked by " ++ From ++ " (" ++ filter_message(Msg) ++ ")", + ejabberd_router:route( + jlib:make_jid(lists:concat([Chan, "%", StateData#state.server]), + StateData#state.host, ""), + StateData#state.user, + {xmlelement, "message", [{"type", "groupchat"}], + [{xmlelement, "body", [], [{xmlcdata, Msg2}]}]}), + ejabberd_router:route( + jlib:make_jid(lists:concat([Chan, "%", StateData#state.server]), + StateData#state.host, Nick), + StateData#state.user, + {xmlelement, "presence", [{"type", "unavailable"}], + [{xmlelement, "x", [{"xmlns", ?NS_MUC_USER}], + [{xmlelement, "item", + [{"affiliation", "none"}, + {"role", "none"}], + []}, + {xmlelement, "status", [{"code", "307"}], []} + ]}]}). + +process_nick(StateData, From, NewNick) -> + [FromUser | _] = string:tokens(From, "!"), + Nick = lists:subtract(NewNick, ":"), + NewChans = + dict:map( + fun(Chan, Ps) -> + case ?SETS:is_member(FromUser, Ps) of + true -> + ejabberd_router:route( + jlib:make_jid( + lists:concat([Chan, "%", StateData#state.server]), + StateData#state.host, FromUser), + StateData#state.user, + {xmlelement, "presence", [{"type", "unavailable"}], + [{xmlelement, "x", [{"xmlns", ?NS_MUC_USER}], + [{xmlelement, "item", + [{"affiliation", "member"}, + {"role", "participant"}, + {"nick", Nick}], + []}, + {xmlelement, "status", [{"code", "303"}], []} + ]}]}), + ejabberd_router:route( + jlib:make_jid( + lists:concat([Chan, "%", StateData#state.server]), + StateData#state.host, Nick), + StateData#state.user, + {xmlelement, "presence", [], + [{xmlelement, "x", [{"xmlns", ?NS_MUC_USER}], + [{xmlelement, "item", + [{"affiliation", "member"}, + {"role", "participant"}], + []} + ]}]}), + ?SETS:add_element(Nick, + remove_element(FromUser, Ps)); + _ -> + Ps + end + end, StateData#state.channels), + if + FromUser == StateData#state.nick -> + StateData#state{nick = Nick, + nickchannel = undefined, + channels = NewChans}; + true -> + StateData#state{channels = NewChans} + end. + + +process_error(StateData, String) -> + lists:foreach( + fun(Chan) -> + ejabberd_router:route( + jlib:make_jid( + lists:concat([Chan, "%", StateData#state.server]), + StateData#state.host, StateData#state.nick), + StateData#state.user, + {xmlelement, "presence", [{"type", "error"}], + [{xmlelement, "error", [{"code", "502"}], + [{xmlcdata, String}]}]}) + end, dict:fetch_keys(StateData#state.channels)). + +error_unknown_num(_StateData, String, Type) -> + {ok, Msg, _} = regexp:sub(String, ".*[45][0-9][0-9] +[^ ]* +", ""), + Msg1 = filter_message(Msg), + {xmlelement, "error", [{"code", "500"}, {"type", Type}], + [{xmlelement, "undefined-condition", [{"xmlns", ?NS_STANZAS}], []}, + {xmlelement, "text", [{"xmlns", ?NS_STANZAS}], + [{xmlcdata, Msg1}]}]}. + + + +remove_element(E, Set) -> + case ?SETS:is_element(E, Set) of + true -> + ?SETS:del_element(E, Set); + _ -> + Set + end. + + + +iq_admin(StateData, Channel, From, To, + #iq{type = Type, xmlns = XMLNS, sub_el = SubEl} = IQ) -> + case catch process_iq_admin(StateData, Channel, Type, SubEl) of + {'EXIT', Reason} -> + ?ERROR_MSG("~p", [Reason]); + Res -> + if + Res /= ignore -> + ResIQ = case Res of + {result, ResEls} -> + IQ#iq{type = result, + sub_el = [{xmlelement, "query", + [{"xmlns", XMLNS}], + ResEls + }]}; + {error, Error} -> + IQ#iq{type = error, + sub_el = [SubEl, Error]} + end, + ejabberd_router:route(To, From, + jlib:iq_to_xml(ResIQ)); + true -> + ok + end + end. + + +process_iq_admin(StateData, Channel, set, SubEl) -> + case xml:get_subtag(SubEl, "item") of + false -> + {error, ?ERR_BAD_REQUEST}; + ItemEl -> + Nick = xml:get_tag_attr_s("nick", ItemEl), + Affiliation = xml:get_tag_attr_s("affiliation", ItemEl), + Role = xml:get_tag_attr_s("role", ItemEl), + Reason = xml:get_path_s(ItemEl, [{elem, "reason"}, cdata]), + process_admin(StateData, Channel, Nick, Affiliation, Role, Reason) + end; +process_iq_admin(_StateData, _Channel, get, _SubEl) -> + {error, ?ERR_FEATURE_NOT_IMPLEMENTED}. + + + +process_admin(_StateData, _Channel, "", _Affiliation, _Role, _Reason) -> + {error, ?ERR_FEATURE_NOT_IMPLEMENTED}; + +process_admin(StateData, Channel, Nick, _Affiliation, "none", Reason) -> + case Reason of + "" -> + send_text(StateData, + io_lib:format("KICK #~s ~s\r\n", + [Channel, Nick])); + _ -> + send_text(StateData, + io_lib:format("KICK #~s ~s :~s\r\n", + [Channel, Nick, Reason])) + end, + {result, []}; + + + +process_admin(_StateData, _Channel, _Nick, _Affiliation, _Role, _Reason) -> + {error, ?ERR_FEATURE_NOT_IMPLEMENTED}. + + + +filter_message(Msg) -> + lists:filter( + fun(C) -> + if (C < 32) and + (C /= 9) and + (C /= 10) and + (C /= 13) -> + false; + true -> true + end + end, filter_mirc_colors(Msg)). + +filter_mirc_colors(Msg) -> + case regexp:gsub(Msg, "(\\003[0-9]+)(,[0-9]+)?", "") of + {ok, Msg2, _} -> + Msg2; + _ -> + Msg + end. + +unixtime2string(Unixtime) -> + Secs = Unixtime + calendar:datetime_to_gregorian_seconds( + {{1970, 1, 1}, {0,0,0}}), + case calendar:universal_time_to_local_time( + calendar:gregorian_seconds_to_datetime(Secs)) of + {{Year, Month, Day}, {Hour, Minute, Second}} -> + lists:flatten( + io_lib:format("~4..0w-~2..0w-~2..0w ~2..0w:~2..0w:~2..0w", + [Year, Month, Day, Hour, Minute, Second])); + _-> + "0000-00-00 00:00:00" + end. + +toupper([C | Cs]) -> + if + C >= $a, C =< $z -> + [C - 32 | toupper(Cs)]; + true -> + [C | toupper(Cs)] + end; +toupper([]) -> + []. diff --git a/mod_log_chat/Emakefile b/mod_log_chat/Emakefile new file mode 100644 index 0000000..1c2813c --- /dev/null +++ b/mod_log_chat/Emakefile @@ -0,0 +1,3 @@ +{'../ejabberd-dev/src/gen_mod', + [{outdir, "../ejabberd-dev/ebin"},{i,"../ejabberd-dev/include"}]}. +{'src/*', [{outdir, "ebin"},{i,"../ejabberd-dev/include"}]}. diff --git a/mod_log_chat/LICENSE.txt b/mod_log_chat/LICENSE.txt new file mode 100644 index 0000000..2d2d780 --- /dev/null +++ b/mod_log_chat/LICENSE.txt @@ -0,0 +1,510 @@ + + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations +below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it +becomes a de-facto standard. To achieve this, non-free programs must +be allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control +compilation and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at least + three years, to give the same user the materials specified in + Subsection 6a, above, for a charge no more than the cost of + performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply, and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License +may add an explicit geographical distribution limitation excluding those +countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms +of the ordinary General Public License). + + To apply these terms, attach the following notices to the library. +It is safest to attach them to the start of each source file to most +effectively convey the exclusion of warranty; and each file should +have at least the "copyright" line and a pointer to where the full +notice is found. + + + + Copyright (C) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or +your school, if any, to sign a "copyright disclaimer" for the library, +if necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James + Random Hacker. + + , 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! + + diff --git a/mod_log_chat/Makefile b/mod_log_chat/Makefile new file mode 100644 index 0000000..dc7e133 --- /dev/null +++ b/mod_log_chat/Makefile @@ -0,0 +1,14 @@ + +# Set the ERL environnement variable if you want to use a specific erl + +ERL ?= $(shell which erl) +EJABBERD_DEV ?= ../../ejabberd-dev/trunk + +all: + $(ERL) -pa $(EJABBERD_DEV)/ebin -make + +clean: + rm -f ebin/*.beam + +dist-clean: clean + find . \( -name \*~ -o -name *.swp \) -exec rm -f {} \; diff --git a/mod_log_chat/README.txt b/mod_log_chat/README.txt new file mode 100644 index 0000000..bd97d3b --- /dev/null +++ b/mod_log_chat/README.txt @@ -0,0 +1,29 @@ +mod_log_chat +============ + +mod_log_chat is a ejabberd module aimed at logging chat messages in +text files. mod_log_chat create one file per couple of chatters and +per day (it doesn't log muc messages, use mod_muc_log for this). + +It can store messages in plain text or HTML format. + +Compilation and installation +---------------------------- + +You need to have Erlang installed as well as the ejabberd-dev module +(checkout it in the same directory than mod_log_chat is). + +- Run + erl -pa ../../ejabberd-dev/trunk/ebin -make + in the trunk directory of mod_log_chat. + +- Copy generated mod_log_chat.beam file from the ebin directory to the + directory where your ejabberd .beam files are. + +- Edit the "modules" section of your ejabberd.cfg configuration file to + suit your needs (see conf/ejabberd.conf.sample for examples). + +- Be sure that the directories where you want to create your log + files exists and are writable by you ejabberd user. + +- Restart ejabberd. diff --git a/mod_log_chat/TODO b/mod_log_chat/TODO new file mode 100644 index 0000000..fd82f88 --- /dev/null +++ b/mod_log_chat/TODO @@ -0,0 +1,3 @@ + +- Custom templates and CSS in the configuration file +- XML format \ No newline at end of file diff --git a/mod_log_chat/build.bat b/mod_log_chat/build.bat new file mode 100644 index 0000000..fff9b3b --- /dev/null +++ b/mod_log_chat/build.bat @@ -0,0 +1 @@ +erl -pa ../../ejabberd-dev/trunk/ebin -pa ebin -make diff --git a/mod_log_chat/build.sh b/mod_log_chat/build.sh new file mode 100755 index 0000000..f0a0cfa --- /dev/null +++ b/mod_log_chat/build.sh @@ -0,0 +1,2 @@ +#!/bin/sh +erl -pa ../ejabberd-dev/ebin -pz ebin -make diff --git a/mod_log_chat/conf/ejabberd.conf.sample b/mod_log_chat/conf/ejabberd.conf.sample new file mode 100644 index 0000000..b67bb92 --- /dev/null +++ b/mod_log_chat/conf/ejabberd.conf.sample @@ -0,0 +1,28 @@ +% Copy one of this examples into the modules section of your +% ejabberd.cfg configuration file and edit it to fit your needs + + + % log chat messages for all vhosts in current directory in text format + {mod_log_chat, []}, + + + % log all vhosts chats in /var/log/ejabberd/chat directory in format + {mod_log_chat, [{path, "/var/log/ejabberd/chat"}, {format, html}]}, + + + % log only vh1.myjabberserver.net vhost in /var/log/ejabberd/vh1.myjabberserver.net directory + % in HTML format + {mod_log_chat, [{host_config, + [{"vh1.myjabberserver.net", [{path, "/var/log/ejabberd/vh1.myjabberserver.net"}, + {format, html}]}]}]}, + + + % log only vh1.myjabberserver.net vhost in /var/log/ejabberd/vh1.myjabberserver.net directory + % in HTML format and vh2.myjabberserver.net vhost in /var/log/ejabberd/vh2.myjabberserver.net directory + % in text format + {mod_log_chat, [{host_config, + [{"vh1.myjabberserver.net", [{path, "/var/log/ejabberd/vh1.myjabberserver.net"}, + {format, html}]}, + {"vh2.myjabberserver.net", [{path, "/var/log/ejabberd/vh2.myjabberserver.net"}, + {format, text}]}]} + ]}, diff --git a/mod_log_chat/src/mod_log_chat.erl b/mod_log_chat/src/mod_log_chat.erl new file mode 100644 index 0000000..ffcf40b --- /dev/null +++ b/mod_log_chat/src/mod_log_chat.erl @@ -0,0 +1,261 @@ +%%%---------------------------------------------------------------------- +%%% File : mod_log_chat.erl +%%% Author : Jérôme Sautret +%%% Purpose : Log 2 ways chat messages in files +%%% Id : $Id: mod_log_chat.erl 412 2007-11-15 10:10:09Z mremond $ +%%%---------------------------------------------------------------------- + +-module(mod_log_chat). +-author('jerome.sautret@process-one.net'). + +-behaviour(gen_mod). + +-export([start/2, + init/1, + stop/1, + log_packet_send/3, + log_packet_receive/4]). + +%-define(ejabberd_debug, true). + +-include("ejabberd.hrl"). +-include("jlib.hrl"). + +-define(PROCNAME, ?MODULE). +-define(DEFAULT_PATH, "."). +-define(DEFAULT_FORMAT, text). + +-record(config, {path=?DEFAULT_PATH, format=?DEFAULT_FORMAT}). + +start(Host, Opts) -> + ?DEBUG(" ~p ~p~n", [Host, Opts]), + case gen_mod:get_opt(host_config, Opts, []) of + [] -> + start_vh(Host, Opts); + HostConfig -> + start_vhs(Host, HostConfig) + end. + +start_vhs(_, []) -> + ok; +start_vhs(Host, [{Host, Opts}| Tail]) -> + ?DEBUG("start_vhs ~p ~p~n", [Host, [{Host, Opts}| Tail]]), + start_vh(Host, Opts), + start_vhs(Host, Tail); +start_vhs(Host, [{_VHost, _Opts}| Tail]) -> + ?DEBUG("start_vhs ~p ~p~n", [Host, [{_VHost, _Opts}| Tail]]), + start_vhs(Host, Tail). +start_vh(Host, Opts) -> + Path = gen_mod:get_opt(path, Opts, ?DEFAULT_PATH), + Format = gen_mod:get_opt(format, Opts, ?DEFAULT_FORMAT), + ejabberd_hooks:add(user_send_packet, Host, ?MODULE, log_packet_send, 55), + ejabberd_hooks:add(user_receive_packet, Host, ?MODULE, log_packet_receive, 55), + register(gen_mod:get_module_proc(Host, ?PROCNAME), + spawn(?MODULE, init, [#config{path=Path, format=Format}])). + +init(Config)-> + ?DEBUG("Starting ~p with config ~p~n", [?MODULE, Config]), + loop(Config). + +loop(Config) -> + receive + {call, Caller, get_config} -> + Caller ! {config, Config}, + loop(Config); + stop -> + exit(normal) + end. + +stop(Host) -> + ejabberd_hooks:delete(user_send_packet, Host, + ?MODULE, log_packet_send, 55), + ejabberd_hooks:delete(user_receive_packet, Host, + ?MODULE, log_packet_receive, 55), + gen_mod:get_module_proc(Host, ?PROCNAME) ! stop, + ok. + +log_packet_send(From, To, Packet) -> + log_packet(From, To, Packet, From#jid.lserver). + +log_packet_receive(_JID, From, To, _Packet) when From#jid.lserver == To#jid.lserver-> + ok; % only log at send time if the message is local to the server +log_packet_receive(_JID, From, To, Packet) -> + log_packet(From, To, Packet, To#jid.lserver). + +log_packet(From, To, Packet = {xmlelement, "message", Attrs, _Els}, Host) -> + case xml:get_attr_s("type", Attrs) of + "groupchat" -> %% mod_muc_log already does it + ?DEBUG("dropping groupchat: ~s", [xml:element_to_string(Packet)]), + ok; + "error" -> %% we don't log errors + ?DEBUG("dropping error: ~s", [xml:element_to_string(Packet)]), + ok; + _ -> + write_packet(From, To, Packet, Host) + end; +log_packet(_From, _To, _Packet, _Host) -> + ok. + +write_packet(From, To, Packet, Host) -> + gen_mod:get_module_proc(Host, ?PROCNAME) ! {call, self(), get_config}, + Config = receive + {config, Result} -> + Result + end, + Format = Config#config.format, + {Subject, Body} = {case xml:get_path_s(Packet, [{elem, "subject"}, cdata]) of + false -> + ""; + Text -> + escape(Format, Text) + end, + escape(Format, xml:get_path_s(Packet, [{elem, "body"}, cdata]))}, + case Subject ++ Body of + "" -> %% don't log empty messages + ?DEBUG("not logging empty message from ~s",[jlib:jid_to_string(From)]), + ok; + _ -> + Path = Config#config.path, + FromJid = From#jid.luser++"@"++From#jid.lserver, + ToJid = To#jid.luser++"@"++To#jid.lserver, + {FilenameTemplate, DateString, Header, MessageType} = + case calendar:local_time() of + {{Y, M, D}, {H, Min, S}} -> + SortedJid = lists:sort([FromJid, ToJid]), + Title = io_lib:format(template(Format, title), [FromJid, ToJid, Y, M, D]), + {lists:flatten(io_lib:format("~s/~~p-~~2.2.0w-~~2.2.0w ~s - ~s~s", + [Path | SortedJid]++[template(Format, extension)])), + io_lib:format(template(Format, date), [Y, M, D, H, Min, S]), + + io_lib:format(template(Format, header), + lists:duplicate(count(template(Format, header), "~s"), + Title) + ), + case hd(SortedJid) of + FromJid -> + message1; + ToJid -> + message2 + end + } + end, + ?DEBUG("FilenameTemplate ~p~n",[FilenameTemplate]), + Filename = io_lib:format(FilenameTemplate, [Y, M, D]), + ?DEBUG("logging message from ~s into ~s~n",[jlib:jid_to_string(From), Filename]), + File = case file:read_file_info(Filename) of + {ok, _} -> + open_logfile(Filename); + {error, enoent} -> + close_previous_logfile(FilenameTemplate, Format, {Y, M, D}), + NewFile = open_logfile(Filename), + io:format(NewFile, Header, []), + NewFile + end, + MessageText = case Subject of + "" -> + Body; + _ -> + io_lib:format(template(Format, subject), [Subject])++Body + end, + ?DEBUG("MessageTemplate ~s~n",[template(Format, MessageType)]), + io:format(File, lists:flatten(template(Format, MessageType)), [DateString, FromJid, From#jid.lresource, ToJid, + To#jid.lresource, MessageText]), + file:close(File) + end. + +open_logfile(Filename) -> + case file:open(Filename, [append]) of + {ok, File} -> + File; + {error, Reason} -> + ?ERROR_MSG("Cannot write into file ~s: ~p~n", [Filename, Reason]) + end. + +close_previous_logfile(FilenameTemplate, Format, Date) -> + Yesterday = calendar:gregorian_days_to_date(calendar:date_to_gregorian_days(Date) - 1), + Filename = io_lib:format(FilenameTemplate, tuple_to_list(Yesterday)), + case file:read_file_info(Filename) of + {ok, _} -> + File = open_logfile(Filename), + io:format(File, template(Format, footer), []), + file:close(File); + {error, enoent} -> + ok + end. + +escape(text, Text) -> + Text; +escape(_, "") -> + ""; +escape(html, [$< | Text]) -> + "<" ++ escape(html, Text); +escape(html, [$& | Text]) -> + "&" ++ escape(html, Text); +escape(html, [Char | Text]) -> + [Char | escape(html, Text)]. + + +% return the number of occurence of Word in String +count(String, Word) -> + case string:str(String, Word) of + 0 -> + 0; + N -> + 1+count(string:substr(String, N+length(Word)), Word) + end. + + + +template(text, extension) -> + ".log"; +template(text, title) -> + "Messages log between ~s and ~s on ~p-~2.2.0w-~2.2.0w"; +template(text, header) -> + "~s~n-----------------------------------------------------------------------~n"; +template(text, subject) -> + "Subject: ~s~n"; +template(text, message) -> + "~~s ~~s/~~s -> ~~s/~~s~n~s~~s~n"; +template(text, message1) -> + io_lib:format(template(text, message), ["> "]); +template(text, message2) -> + io_lib:format(template(text, message), ["< "]); +template(text, date) -> + "~p-~2.2.0w-~2.2.0w ~2.2.0w:~2.2.0w:~2.2.0w"; +template(text, footer) -> + "---- End ----~n"; + +template(html, extension) -> + ".html"; +template(html, title) -> + template(text, title); +template(html, header) -> + "~n"++ + "~s"++ + css()++ + "~n

    ~s

    ~n"; +template(html, subject) -> + "
    Subject: ~s
    "; +template(html, message) -> + "
    ~~s ~~s/~~s -> ~~s/~~s~~n~~s
    ~~n"; +template(html, message1) -> + io_lib:format(template(html, message), [1]); +template(html, message2) -> + io_lib:format(template(html, message), [2]); +template(html, date) -> + "~p-~2.2.0w-~2.2.0w ~2.2.0w:~2.2.0w:~2.2.0w"; +template(html, footer) -> + "". + +css() -> + "~n". diff --git a/mod_logsession/COPYING b/mod_logsession/COPYING new file mode 100644 index 0000000..8b325f5 --- /dev/null +++ b/mod_logsession/COPYING @@ -0,0 +1,343 @@ +As a special exception, the authors give permission to link this program +with the OpenSSL library and distribute the resulting binary. + + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Library General +Public License instead of this License. diff --git a/mod_logsession/ChangeLog b/mod_logsession/ChangeLog new file mode 100644 index 0000000..cf58282 --- /dev/null +++ b/mod_logsession/ChangeLog @@ -0,0 +1,10 @@ +2008-12-12 Badlop + + * src/mod_logsession.erl: Replace the ejabberdctl command with an + ejabberd command; requires ejabberd trunk SVN + * README.txt: Documented the change and the requirement + +2008-04-25 Badlop + + * mod_logsession: New module to log session connections + diff --git a/mod_logsession/Emakefile b/mod_logsession/Emakefile new file mode 100644 index 0000000..a0458cb --- /dev/null +++ b/mod_logsession/Emakefile @@ -0,0 +1,2 @@ +{'../ejabberd-dev/src/gen_mod', [{outdir, "../ejabberd-dev/ebin"},{i,"../ejabberd-dev/include"}]}. +{'src/mod_logsession', [{outdir, "ebin"},{i,"../ejabberd-dev/include"}]}. diff --git a/mod_logsession/README.txt b/mod_logsession/README.txt new file mode 100644 index 0000000..750456b --- /dev/null +++ b/mod_logsession/README.txt @@ -0,0 +1,81 @@ + + mod_logsession - Log session connections to file + + Homepage: http://www.ejabberd.im/mod_logsession + Author: Badlop + Requirements: ejabberd trunk SVN 1635 or newer + + + DESCRIPTION + ----------- + +This module is intended to log in a text file the session connections. +Right now it only logs the forbidden connection attempts and the +failed authentication attempts. +Each vhost is logged in a different file. + +Note: to log the failed authentication attempts, you need to patch ejabberd. + + + INSTALL + ------- + +1 Copy this file to ejabberd/src/mod_logsession.erl +2 Recompile ejabberd +3 Add to ejabberd.cfg, 'modules' section the basic configuration: + {mod_logsession, []}, +4 With this configuration, the log files are: + /tmp/ejabberd_logsession_@HOST@.log + + + CONFIGURABLE PARAMETERS + ----------------------- + +sessionlog: + Define the name of log files. + The keyword @HOST@ will be substituted with the name of each vhost. + Default value: "/tmp/ejabberd_logsession_@HOST@.log" + + + EXAMPLE CONFIGURATION + --------------------- + +{modules, [ + ... + {mod_logsession, [ + {sessionlog, "/var/log/ejabberd/session_@HOST@.log"} + ]}, + ... +]}. + +With that configuration, if the server has three vhosts: + "localhost", "example.org" and "example.net", +then the forbidden accesses will be logged in the files: + /var/log/ejabberd/session_localhost.log + /var/log/ejabberd/session_example.org.log + /var/log/ejabberd/session_example.net.log + + + FORMAT OF LOG + ------------- + +The content of the file is the date and time of the attempted login +and the JID of the denied user. + +For example: +2008-01-08 12:20:50 Forbidden session for tron@localhost/teeest +2008-01-08 12:36:01 Forbidden session for baduser@localhost/aaa22 +2010-04-02 17:21:37 Failed legacy authentication for someuser@localhost from 127.0.0.1 port 58973 +2010-04-02 17:25:20 Failed sasl_resp authentication for badlop@localhost from 127.0.0.1 port 45842 + + + REOPEN LOG FILES + ---------------- + +This module provides an ejabberd command to reopen the log file +of a host where the module is enabled. + +Example usage: + ejabberdctl reopen-seslog localhost + ejabberdctl reopen-seslog jabber.example.org + diff --git a/mod_logsession/build.bat b/mod_logsession/build.bat new file mode 100644 index 0000000..fff9b3b --- /dev/null +++ b/mod_logsession/build.bat @@ -0,0 +1 @@ +erl -pa ../../ejabberd-dev/trunk/ebin -pa ebin -make diff --git a/mod_logsession/build.sh b/mod_logsession/build.sh new file mode 100755 index 0000000..f0a0cfa --- /dev/null +++ b/mod_logsession/build.sh @@ -0,0 +1,2 @@ +#!/bin/sh +erl -pa ../ejabberd-dev/ebin -pz ebin -make diff --git a/mod_logsession/src/failed_auth.patch b/mod_logsession/src/failed_auth.patch new file mode 100644 index 0000000..0699cd7 --- /dev/null +++ b/mod_logsession/src/failed_auth.patch @@ -0,0 +1,44 @@ +--- ejabberd_c2s.erl ++++ ejabberd_c2s.erl +@@ -496,6 +496,11 @@ wait_for_auth({xmlstreamelement, El}, StateData) -> + fsm_next_state_pack(session_established, + NewStateData); + _ -> ++ ejabberd_hooks:run(failed_auth_hook, ++ StateData#state.server, ++ [legacy, U, ++ StateData#state.server, ++ StateData#state.ip]), + ?INFO_MSG( + "(~w) Failed legacy authentication for ~s", + [StateData#state.socket, +@@ -586,6 +591,11 @@ wait_for_feature_request({xmlstreamelement, El}, StateData) -> + StateData#state{ + sasl_state = NewSASLState}); + {error, Error, Username} -> ++ ejabberd_hooks:run(failed_auth_hook, ++ StateData#state.server, ++ [sasl_auth, Username, ++ StateData#state.server, ++ StateData#state.ip]), + ?INFO_MSG( + "(~w) Failed authentication for ~s@~s", + [StateData#state.socket, +@@ -721,10 +731,15 @@ wait_for_sasl_response({xmlstreamelement, El}, StateData) -> + fsm_next_state(wait_for_sasl_response, + StateData#state{sasl_state = NewSASLState}); + {error, Error, Username} -> ++ ejabberd_hooks:run(failed_auth_hook, ++ StateData#state.server, ++ [sasl_resp, Username, ++ StateData#state.server, ++ StateData#state.ip]), + ?INFO_MSG( +- "(~w) Failed authentication for ~s@~s", ++ "(~w) Failed authentication for ~s@~s ~n~p", + [StateData#state.socket, +- Username, StateData#state.server]), ++ Username, StateData#state.server, StateData]), + send_element(StateData, + {xmlelement, "failure", + [{"xmlns", ?NS_SASL}], diff --git a/mod_logsession/src/mod_logsession.erl b/mod_logsession/src/mod_logsession.erl new file mode 100644 index 0000000..a67931a --- /dev/null +++ b/mod_logsession/src/mod_logsession.erl @@ -0,0 +1,153 @@ +%%%---------------------------------------------------------------------- +%%% File : mod_logsession.erl +%%% Author : Badlop +%%% Purpose : Log session connections to file +%%% Created : 8 Jan 2008 by Badlop +%%% +%%% +%%% ejabberd, Copyright (C) 2008 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., 59 Temple Place, Suite 330, Boston, MA +%%% 02111-1307 USA +%%% +%%%---------------------------------------------------------------------- + + +-module(mod_logsession). +-author('badlop@process-one.net'). + +-behaviour(gen_mod). + +-export([ + start/2, + stop/1, + loop/3, + reopen_log/1, + failed_auth/4, + forbidden/1 + ]). + +-include("ejabberd.hrl"). +-include("jlib.hrl"). +-include("ejabberd_commands.hrl"). + +-define(PROCNAME, ejabberd_logsession). + +%%%---------------------------------------------------------------------- +%%% BEHAVIOUR CALLBACKS +%%%---------------------------------------------------------------------- + +start(Host, Opts) -> + ejabberd_hooks:add(reopen_log_hook, Host, ?MODULE, reopen_log, 50), + ejabberd_hooks:add(forbidden_session_hook, Host, ?MODULE, forbidden, 50), + ejabberd_hooks:add(failed_auth_hook, Host, ?MODULE, failed_auth, 50), + ejabberd_commands:register_commands(commands()), + Filename1 = gen_mod:get_opt( + sessionlog, + Opts, + "/tmp/ejabberd_logsession_@HOST@.log"), + Filename = replace_host(Host, Filename1), + File = open_file(Filename), + register(get_process_name(Host), spawn(?MODULE, loop, [Filename, File, Host])). + +stop(Host) -> + ejabberd_hooks:delete(reopen_log_hook, Host, ?MODULE, reopen_log, 50), + ejabberd_hooks:delete(forbidden_session_hook, Host, ?MODULE, forbidden, 50), + ejabberd_commands:unregister_commands(commands()), + Proc = get_process_name(Host), + exit(whereis(Proc), stop), + {wait, Proc}. + +%%%---------------------------------------------------------------------- +%%% REQUEST HANDLERS +%%%---------------------------------------------------------------------- + +reopen_log(Host) -> + get_process_name(Host) ! reopenlog. + +forbidden(JID) -> + Host = JID#jid.lserver, + get_process_name(Host) ! {log, forbidden, JID}. + +failed_auth(AuthType, U, Host, IPPT) -> + get_process_name(Host) ! {log, failed_auth, {AuthType, U, IPPT}}. + +commands() -> + [#ejabberd_commands{name = reopen_seslog, tags = [logs, server], + desc = "Reopen mod_logsession log file", + module = ?MODULE, function = reopen_log, + args = [{host, string}], + result = {res, rescode}}]. + +%%%---------------------------------------------------------------------- +%%% LOOP +%%%---------------------------------------------------------------------- + +loop(Filename, File, Host) -> + receive + {log, Type, Data} -> + log(File, Host, Type, Data), + loop(Filename, File, Host); + reopenlog -> + File2 = reopen_file(File, Filename), + loop(Filename, File2, Host); + stop -> + close_file(File) + end. + +%%%---------------------------------------------------------------------- +%%% UTILITIES +%%%---------------------------------------------------------------------- + +get_process_name(Host) -> + gen_mod:get_module_proc(Host, ?PROCNAME). + +replace_host(Host, Filename) -> + element(2, regexp:gsub(Filename, "@HOST@", Host)). + +open_file(Filename) -> + {ok, File} = file:open(Filename, [append]), + File. + +close_file(File) -> + file:close(File). + +reopen_file(File, Filename) -> + close_file(File), + open_file(Filename). + +log(File, Host, Type, Data) -> + DateString = make_date(calendar:local_time()), + MessageString = make_message(Host, Type, Data), + io:format(File, "~s ~s~n", [DateString, MessageString]). + +make_date(Date) -> + {{Y, Mo, D}, {H, Mi, S}} = Date, + %% Combined format: + %%io_lib:format("[~p/~p/~p:~p:~p:~p]", [D, Mo, Y, H, Mi, S]). + %% Erlang format: + io_lib:format("~w-~.2.0w-~.2.0w ~.2.0w:~.2.0w:~.2.0w", + [Y, Mo, D, H, Mi, S]). + +make_message(Host, Type, {AuthType, Username, {IPTuple, IPPort}}) -> + String = get_string(Type), + IPString = inet_parse:ntoa(IPTuple), + io_lib:format(String, [AuthType, Username, Host, IPString, IPPort]); +make_message(_Host, Type, JID) -> + String = get_string(Type), + io_lib:format(String, [jlib:jid_to_string(JID)]). + +get_string(failed_auth) -> "Failed ~p authentication for ~s@~s from ~s port ~p"; +get_string(forbidden) -> "Forbidden session for ~s". diff --git a/mod_logxml/COPYING b/mod_logxml/COPYING new file mode 100644 index 0000000..8b325f5 --- /dev/null +++ b/mod_logxml/COPYING @@ -0,0 +1,343 @@ +As a special exception, the authors give permission to link this program +with the OpenSSL library and distribute the resulting binary. + + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Library General +Public License instead of this License. diff --git a/mod_logxml/ChangeLog b/mod_logxml/ChangeLog new file mode 100644 index 0000000..fc12180 --- /dev/null +++ b/mod_logxml/ChangeLog @@ -0,0 +1,37 @@ +2008-03-18 Badlop + + * README.txt: Disable show_ip in example configuration because it + requires ejabberd 2.0.0 + + * src/mod_logxml.erl: Fixed indentation + + * ChangeLog.txt: New file to track changes + +2007-07-20 Badlop + + * mod_logxml.erl: Added new option: show_ip + +2007-03-20 Badlop + + * mod_logxml.erl: The file name respects the timezone option + +2007-02-03 Badlop + + * mod_logxml.erl: Added new option: timezone + +2006-08-08 Badlop + + * mod_logxml.erl: Fixed small bug on start/2 + +2006-03-08 Badlop + + * mod_logxml.erl: Changed some configuration options: rotate_days, + rotate_mages and rotate_kpackets can now be set independently + + * mod_logxml.erl: New format of XML logs: now XMPP packets are + enclosed in a 'packet' element with attributes: or, ljid, ts + +2005-11-11 Badlop + + * mod_logxml.erl: Initial version + diff --git a/mod_logxml/Emakefile b/mod_logxml/Emakefile new file mode 100644 index 0000000..72300f6 --- /dev/null +++ b/mod_logxml/Emakefile @@ -0,0 +1,2 @@ +{'../ejabberd-dev/src/gen_mod', [{outdir, "../ejabberd-dev/ebin"},{i,"../ejabberd-dev/include"}]}. +{'src/mod_logxml', [{outdir, "ebin"},{i,"../ejabberd-dev/include"}]}. diff --git a/mod_logxml/README.txt b/mod_logxml/README.txt new file mode 100644 index 0000000..96da9ec --- /dev/null +++ b/mod_logxml/README.txt @@ -0,0 +1,95 @@ + + mod_logxml - Log XMPP packets to XML file + + Homepage: http://www.ejabberd.im/mod_logxml + Author: Badlop + Module for ejabberd 0.7.5 or newer + + + DESCRIPTION + ----------- + +This module sniffs all the XMPP traffic send and received by ejabberd, +both internally and externally transmitted. It logs the XMPP packets +to a XML formatted file. It's posible to filter transmitted packets +by orientation, stanza and direction. It's possible to configure the +file rotation rules and intervals. + +This module reuses code from mod_log_forensic, mod_stats2file, mod_muc_log + + + CONFIGURATION + ------------- + +stanza: + Log packets only when stanza matches + Default value: [iq, message, presence, other] +direction: + Log packets only when direction matches + Default value: [internal, vhosts, external] +orientation: + Log packets only when orientation matches + Default value: [send, revc] +logdir: + Base filename, including absolute path + Default value: "/tmp/jabberlogs/" +timezone: + The time zone for the logs is configurable with this option. + Allowed values are 'local' and 'universal'. + With the first value, the local time, + as reported to Erlang by the operating system, will be used. + With the latter, GMT/UTC time will be used. + Default value: local +show_ip: + If the IP address of the local user should be logged to file. + This option requires ejabberd 2.0.0 or newer, specifically SVN r772 (2007-05-21) + Default value: false +rotate_days: + Rotate logs every X days + Put 'no' to disable this limit. + Default value: 1 +rotate_megs: + Rotate when the logfile size is higher than this, in megabytes. + Put 'no' to disable this limit. + Default value: 10 +rotate_kpackets: + Rotate every *1000 XMPP packets logged + Put 'no' to disable this limit. + Default value: 10 +check_rotate_kpackets: + Check rotation every *1000 packets + Default value: 1 + + + + EXAMPLE CONFIGURATION + --------------------- + +In ejabberd.cfg, in the modules section, add the module. For example: + +{modules, [ + ... + {mod_logxml, [ + {stanza, [message, other]}, + {direction, [external]}, + {orientation, [send, recv]}, + {logdir, "/var/jabber/logs/"}, + {timezone, universal}, + {show_ip, false}, % To enable this option you need ejabberd 2.0.0 or newer + {rotate_days, 1}, + {rotate_megs, 100}, + {rotate_kpackets, no}, + {check_rotate_kpackets, 1} + ]}, + ... +]}. + + + + FORMAT OF XML + ------------- + +XMPP packets are enclosed in , with attributes: + or: orientation of the packet, either 'send' or 'recv' + ljid: local JID of the sender or receiver, depending on the orientation + ts: timestamp when the packet was logged diff --git a/mod_logxml/build.bat b/mod_logxml/build.bat new file mode 100644 index 0000000..fff9b3b --- /dev/null +++ b/mod_logxml/build.bat @@ -0,0 +1 @@ +erl -pa ../../ejabberd-dev/trunk/ebin -pa ebin -make diff --git a/mod_logxml/build.sh b/mod_logxml/build.sh new file mode 100755 index 0000000..f0a0cfa --- /dev/null +++ b/mod_logxml/build.sh @@ -0,0 +1,2 @@ +#!/bin/sh +erl -pa ../ejabberd-dev/ebin -pz ebin -make diff --git a/mod_logxml/src/mod_logxml.erl b/mod_logxml/src/mod_logxml.erl new file mode 100644 index 0000000..c550a21 --- /dev/null +++ b/mod_logxml/src/mod_logxml.erl @@ -0,0 +1,266 @@ +%%%---------------------------------------------------------------------- +%%% File : mod_logxml.erl +%%% Author : Badlop +%%% Purpose : Log XMPP packets to XML file +%%% Created : +%%% Id : +%%%---------------------------------------------------------------------- + +-module(mod_logxml). +-author('badlop@ono.com'). + +-behaviour(gen_mod). + +-export([start/2, init/7, stop/1, + send_packet/3, receive_packet/4]). + +-include("ejabberd.hrl"). +-include("jlib.hrl"). + +-define(PROCNAME, ejabberd_mod_logxml). + +%% ------------------- +%% Module control +%% ------------------- + +start(Host, Opts) -> + Logdir = gen_mod:get_opt(logdir, Opts, "/tmp/jabberlogs/"), + + Rd = gen_mod:get_opt(rotate_days, Opts, 1), + Rf = case gen_mod:get_opt(rotate_megs, Opts, 10) of + no -> no; + Rf1 -> Rf1*1024*1024 + end, + Rp = case gen_mod:get_opt(rotate_kpackets, Opts, 10) of + no -> no; + Rp1 -> Rp1*1000 + end, + RotateO = {Rd, Rf, Rp}, + CheckRKP = gen_mod:get_opt(check_rotate_kpackets, Opts, 1), + + Timezone = gen_mod:get_opt(timezone, Opts, local), + + Orientation = gen_mod:get_opt(orientation, Opts, [send, recv]), + Stanza = gen_mod:get_opt(stanza, Opts, [iq, message, presence, other]), + Direction = gen_mod:get_opt(direction, Opts, [internal, vhosts, external]), + FilterO = { + {orientation, Orientation}, + {stanza, Stanza}, + {direction, Direction}}, + ShowIP = gen_mod:get_opt(show_ip, Opts, false), + + ejabberd_hooks:add(user_send_packet, Host, ?MODULE, send_packet, 90), + ejabberd_hooks:add(user_receive_packet, Host, ?MODULE, receive_packet, 90), + register(gen_mod:get_module_proc(Host, ?PROCNAME), + spawn(?MODULE, init, [Host, Logdir, RotateO, CheckRKP, + Timezone, ShowIP, FilterO])). + +stop(Host) -> + ejabberd_hooks:delete(user_send_packet, Host, ?MODULE, send_packet, 90), + ejabberd_hooks:delete(user_receive_packet, Host, ?MODULE, receive_packet, 90), + Proc = gen_mod:get_module_proc(Host, ?PROCNAME), + Proc ! stop, + {wait, Proc}. + +init(Host, Logdir, RotateO, CheckRKP, Timezone, ShowIP, FilterO) -> + {IoDevice, Filename, Gregorian_day} = open_file(Logdir, Host, Timezone), + loop(Host, IoDevice, Filename, Logdir, CheckRKP, RotateO, 0, Gregorian_day, + Timezone, ShowIP, FilterO). + +%% ------------------- +%% Main +%% ------------------- + +manage_rotate(Host, IoDevice, Filename, Logdir, RotateO, PacketC, + Gregorian_day_log, Timezone) -> + {RO_days, RO_size, RO_packets} = RotateO, + + Rotate1 = case RO_packets of + no -> false; + PacketC -> true; + _ -> false + end, + + Filesize = filelib:file_size(Filename), + Rotate2 = if + RO_size == no -> false; + Filesize >= RO_size -> true; + true -> false + end, + + Gregorian_day_today = get_gregorian_day(), + Rotate3 = if + RO_days == no -> false; + (Gregorian_day_today - Gregorian_day_log) >= RO_days -> + true; + true -> false + end, + + case lists:any(fun(E) -> E end, [Rotate1, Rotate2, Rotate3]) of + true -> + {IoDevice2, Filename2, Gregorian_day2} = + rotate_log(IoDevice, Logdir, Host, Timezone), + {IoDevice2, Filename2, Gregorian_day2, 0}; + false -> + {IoDevice, Filename, Gregorian_day_log, PacketC+1} + end. + +filter(FilterO, E) -> + {{orientation, OrientationO},{stanza, StanzaO},{direction, DirectionO}} = + FilterO, + {Orientation, From, To, Packet} = E, + + {xmlelement, Stanza_str, _Attrs, _Els} = Packet, + Stanza = list_to_atom(Stanza_str), + + Hosts_all = ejabberd_config:get_global_option(hosts), + {Host_local, Host_remote} = case Orientation of + send -> {From#jid.lserver, To#jid.lserver}; + recv -> {To#jid.lserver, From#jid.lserver} + end, + Direction = case Host_remote of + Host_local -> internal; + _ -> + case lists:member(Host_remote, Hosts_all) of + true -> vhosts; + false -> external + end + end, + + {lists:all(fun(O) -> O end, + [lists:member(Orientation, OrientationO), + lists:member(Stanza, StanzaO), + lists:member(Direction, DirectionO)]), + {Orientation, Stanza, Direction}}. + +loop(Host, IoDevice, Filename, Logdir, CheckRKP, RotateO, PacketC, + Gregorian_day, Timezone, ShowIP, FilterO) -> + receive + {addlog, E} -> + {IoDevice3, Filename3, Gregorian_day3, PacketC3} = + case filter(FilterO, E) of + {true, OSD} -> + Div = calc_div(PacketC, CheckRKP), + {IoDevice2, Filename2, Gregorian_day2, PacketC2} = + case Div==round(Div) of + true -> + manage_rotate(Host, IoDevice, Filename, + Logdir, RotateO, PacketC, + Gregorian_day, Timezone); + false -> + {IoDevice, Filename, Gregorian_day, + PacketC+1} + end, + add_log(IoDevice2, Timezone, ShowIP, E, OSD), + {IoDevice2, Filename2, Gregorian_day2, PacketC2}; + _ -> + {IoDevice, Filename, Gregorian_day, PacketC} + end, + loop(Host, IoDevice3, Filename3, Logdir, CheckRKP, RotateO, + PacketC3, Gregorian_day3, Timezone, ShowIP, FilterO); + stop -> + close_file(IoDevice), + ok; + _ -> + loop(Host, IoDevice, Filename, Logdir, CheckRKP, RotateO, PacketC, + Gregorian_day, Timezone, ShowIP, FilterO) + end. + +send_packet(FromJID, ToJID, P) -> + Host = FromJID#jid.lserver, + Proc = gen_mod:get_module_proc(Host, ?PROCNAME), + Proc ! {addlog, {send, FromJID, ToJID, P}}. + +receive_packet(_JID, From, To, P) -> + Host = To#jid.lserver, + Proc = gen_mod:get_module_proc(Host, ?PROCNAME), + Proc ! {addlog, {recv, From, To, P}}. + +add_log(Io, Timezone, ShowIP, {Orientation, From, To, Packet}, _OSD) -> + %%{Orientation, Stanza, Direction} = OSD, + LocalJID = case Orientation of + send -> From; + recv -> To + end, + LocalIPS = case ShowIP of + true -> + {UserIP, _Port} = ejabberd_sm:get_user_ip( + LocalJID#jid.user, + LocalJID#jid.server, + LocalJID#jid.resource), + io_lib:format("lip=\"~s\" ", [inet_parse:ntoa(UserIP)]); + false -> "" + end, + TimestampISO = get_now_iso(Timezone), + io:fwrite(Io, "~s~n", + [Orientation, jlib:jid_to_string(LocalJID), LocalIPS, + TimestampISO, xml:element_to_string(Packet)]). + +%% ------------------- +%% File +%% ------------------- + +open_file(Logdir, Host, Timezone) -> + TimeStamp = get_now_iso(Timezone), + Year = string:substr(TimeStamp, 1, 4), + Month = string:substr(TimeStamp, 5, 2), + Day = string:substr(TimeStamp, 7, 2), + Hour = string:substr(TimeStamp, 10, 2), + Min = string:substr(TimeStamp, 13, 2), + Sec = string:substr(TimeStamp, 16, 2), + S = "-", + Logname = lists:flatten([Host,S,Year,S,Month,S,Day,S,Hour,S,Min,S,Sec, + ".xml"]), + Filename = filename:join([Logdir, Logname]), + + Gregorian_day = get_gregorian_day(), + + %% Open file, create if it does not exist, create parent dirs if needed + case file:read_file_info(Filename) of + {ok, _} -> + {ok, IoDevice} = file:open(Filename, [append]); + {error, enoent} -> + make_dir_rec(Logdir), + {ok, IoDevice} = file:open(Filename, [append]), + io:fwrite(IoDevice, "~s~n", [""]), + io:fwrite(IoDevice, "~s~n", [""]), + io:fwrite(IoDevice, "~s~n", [""]) + end, + {IoDevice, Filename, Gregorian_day}. + +close_file(IoDevice) -> + io:fwrite(IoDevice, "~s~n", [""]), + file:close(IoDevice). + +rotate_log(IoDevice, Logdir, Host, Timezone) -> + close_file(IoDevice), + open_file(Logdir, Host, Timezone). + +make_dir_rec(Dir) -> + case file:read_file_info(Dir) of + {ok, _} -> + ok; + {error, enoent} -> + DirS = filename:split(Dir), + DirR = lists:sublist(DirS, length(DirS)-1), + make_dir_rec(filename:join(DirR)), + file:make_dir(Dir) + end. + +%% ------------------- +%% Utils +%% ------------------- + +get_gregorian_day() -> calendar:date_to_gregorian_days(date()). + +get_now_iso(Timezone) -> + TimeStamp = case Timezone of + local -> calendar:now_to_local_time(now()); + universal -> calendar:now_to_universal_time(now()) + end, + jlib:timestamp_to_iso(TimeStamp). + +calc_div(A, B) when is_integer(A) and is_integer(B) and B =/= 0 -> + A/B; +calc_div(_A, _B) -> + 0.5. %% This ensures that no rotation is performed diff --git a/mod_muc_admin/COPYING b/mod_muc_admin/COPYING new file mode 100644 index 0000000..8b325f5 --- /dev/null +++ b/mod_muc_admin/COPYING @@ -0,0 +1,343 @@ +As a special exception, the authors give permission to link this program +with the OpenSSL library and distribute the resulting binary. + + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Library General +Public License instead of this License. diff --git a/mod_muc_admin/ChangeLog b/mod_muc_admin/ChangeLog new file mode 100644 index 0000000..5ebbea3 --- /dev/null +++ b/mod_muc_admin/ChangeLog @@ -0,0 +1,80 @@ +2009-07-01 Badlop + + * src/mod_muc_admin.erl: Fix change_room_option + +2009-06-02 Badlop + + * src/mod_muc_admin.erl: Destroy the room before forgetting it. + +2008-12-02 Badlop + + * src/mod_muc_admin.erl: Include mod_muc_room.hrl + * README.txt: Likewise + +2008-11-17 Badlop + + * src/mod_muc_admin.erl: Fix include of web hrl (thanks to Johnny) + +2008-10-12 Badlop + + * src/mod_muc_admin.erl: Update from ctl to commands (EJAB-694) + +2008-09-30 Badlop + + * src/mod_muc_admin.erl: Update record definitions to ejabberd + trunk SVN + +2008-05-16 Badlop + + * src/mod_muc_admin.erl: New exported function + set_affiliation/4 (thanks to Darren Ferguson) + +2008-05-12 Badlop + + * src/mod_muc_admin.erl: Added new function: change_room_option/4 + +2008-05-06 Badlop + + * src/mod_muc_admin.erl: New exported functions create_room and + destroy_room (thanks to Darren Ferguson) + +2008-04-04 Badlop + + * src/mod_muc_admin.erl: New command muc-create-file to create the + rooms indicated in the file + +2008-03-29 Badlop + + * src/mod_muc_admin.erl: Improvements in the list of rooms of web + admin: show timestamp of last message, sorting by any column. + +2008-03-19 Badlop + + * src/mod_muc_admin.erl (join): Copied the new function + string:join/2 introduced in Erlang R12 + + * README.txt: Clarified requirements + + * src/mod_muc_admin.erl (split_roomjid): Small fix + +2008-02-01 Badlop + + * src/mod_muc_admin.erl: New command: muc-unregister-nick. + +2007-12-26 Badlop + + * src/mod_muc_admin.erl: Translate menu items of webadmin hooks in + each module (EJAB-485) + +2007-11-15 Badlop + + * src/mod_muc_admin.erl: New command: muc-destroy-file. + +2007-09-09 Badlop + + * src/mod_muc_admin.erl: Added Web Admin and List rooms. + +2007-09-08 Badlop + + * mod_muc_admin: Initial commit. + diff --git a/mod_muc_admin/Emakefile b/mod_muc_admin/Emakefile new file mode 100644 index 0000000..07dd65c --- /dev/null +++ b/mod_muc_admin/Emakefile @@ -0,0 +1,2 @@ +{'../ejabberd-dev/src/gen_mod', [{outdir, "../ejabberd-dev/ebin"},{i,"../ejabberd-dev/include"}]}. +{'src/mod_muc_admin', [{outdir, "ebin"},{i,"../ejabberd-dev/include"}]}. diff --git a/mod_muc_admin/README.txt b/mod_muc_admin/README.txt new file mode 100644 index 0000000..c70fbf8 --- /dev/null +++ b/mod_muc_admin/README.txt @@ -0,0 +1,46 @@ + + + mod_muc_admin - Administrative features for MUC + + Homepage: http://www.ejabberd.im/mod_muc_admin + Author: Badlop + Requirements: ejabberd trunk SVN 1699 or newer + + +This module implements several ejabberd commands that can be +executed using ejabberdctl. + +It also implements Web Admin pages to view the list of existing +rooms. + + + CONFIGURATION + ============= + +Add the module to your ejabberd.cfg, on the modules section: +{modules, [ + ... + {mod_muc_admin, []}, + ... +]}. + + + EJABBERD COMMANDS + ================= + +Description of some commands: + + - muc-unusued-* + Those commands related to MUC require an ejabberd version newer than 1.1.x. + The room characteristics used to decide if a room is unusued: + - Days since the last message or subject change: + greater or equal to the command argument + - Number of participants: 0 + - Persistent: not important + - Has history: not important + - Days since last join, leave, room config or affiliation edit: + not important + - Just created: no + Note that ejabberd does not keep room history after a module restart, so + the history of all rooms is emtpy after a module or server start. + diff --git a/mod_muc_admin/build.bat b/mod_muc_admin/build.bat new file mode 100644 index 0000000..fff9b3b --- /dev/null +++ b/mod_muc_admin/build.bat @@ -0,0 +1 @@ +erl -pa ../../ejabberd-dev/trunk/ebin -pa ebin -make diff --git a/mod_muc_admin/build.sh b/mod_muc_admin/build.sh new file mode 100755 index 0000000..f0a0cfa --- /dev/null +++ b/mod_muc_admin/build.sh @@ -0,0 +1,2 @@ +#!/bin/sh +erl -pa ../ejabberd-dev/ebin -pz ebin -make diff --git a/mod_muc_admin/src/mod_muc_admin.erl b/mod_muc_admin/src/mod_muc_admin.erl new file mode 100644 index 0000000..141fb6f --- /dev/null +++ b/mod_muc_admin/src/mod_muc_admin.erl @@ -0,0 +1,881 @@ +%%%---------------------------------------------------------------------- +%%% File : mod_muc_admin.erl +%%% Author : Badlop +%%% Purpose : Tools for additional MUC administration +%%% Created : 8 Sep 2007 by Badlop +%%% Id : $Id: mod_muc_admin.erl 1133 2012-10-17 22:13:06Z badlop $ +%%%---------------------------------------------------------------------- + +-module(mod_muc_admin). +-author('badlop@ono.com'). + +-behaviour(gen_mod). + +-export([ + start/2, stop/1, % gen_mod API + muc_online_rooms/1, + muc_unregister_nick/1, + create_room/3, destroy_room/3, + create_rooms_file/1, destroy_rooms_file/1, + rooms_unused_list/2, rooms_unused_destroy/2, + get_room_occupants/2, + get_room_occupants_number/2, + send_direct_invitation/4, + change_room_option/4, + set_room_affiliation/4, + get_room_affiliations/2, + web_menu_main/2, web_page_main/2, % Web Admin API + web_menu_host/3, web_page_host/3 + ]). + +-include("ejabberd.hrl"). +-include("jlib.hrl"). +-include("mod_muc/mod_muc_room.hrl"). +-include("web/ejabberd_http.hrl"). +-include("web/ejabberd_web_admin.hrl"). +-include("ejabberd_commands.hrl"). + +%% Copied from mod_muc/mod_muc.erl +-record(muc_online_room, {name_host, pid}). + +%%---------------------------- +%% gen_mod +%%---------------------------- + +start(Host, _Opts) -> + ejabberd_commands:register_commands(commands()), + ejabberd_hooks:add(webadmin_menu_main, ?MODULE, web_menu_main, 50), + ejabberd_hooks:add(webadmin_menu_host, Host, ?MODULE, web_menu_host, 50), + ejabberd_hooks:add(webadmin_page_main, ?MODULE, web_page_main, 50), + ejabberd_hooks:add(webadmin_page_host, Host, ?MODULE, web_page_host, 50). + +stop(Host) -> + ejabberd_commands:unregister_commands(commands()), + ejabberd_hooks:delete(webadmin_menu_main, ?MODULE, web_menu_main, 50), + ejabberd_hooks:delete(webadmin_menu_host, Host, ?MODULE, web_menu_host, 50), + ejabberd_hooks:delete(webadmin_page_main, ?MODULE, web_page_main, 50), + ejabberd_hooks:delete(webadmin_page_host, Host, ?MODULE, web_page_host, 50), + ejabberd_hooks:delete(webadmin_user, Host, ?MODULE, web_user, 50). + +%%% +%%% Register commands +%%% + +commands() -> + [ + #ejabberd_commands{name = muc_online_rooms, tags = [muc], + desc = "List existing rooms ('global' to get all vhosts)", + module = ?MODULE, function = muc_online_rooms, + args = [{host, string}], + result = {rooms, {list, {room, string}}}}, + #ejabberd_commands{name = muc_unregister_nick, tags = [muc], + desc = "Unregister the nick in the MUC service", + module = ?MODULE, function = muc_unregister_nick, + args = [{nick, string}], + result = {res, rescode}}, + + #ejabberd_commands{name = create_room, tags = [muc_room], + desc = "Create a MUC room name@service in host", + module = ?MODULE, function = create_room, + args = [{name, string}, {service, string}, + {host, string}], + result = {res, rescode}}, + #ejabberd_commands{name = destroy_room, tags = [muc_room], + desc = "Destroy a MUC room", + module = ?MODULE, function = destroy_room, + args = [{name, string}, {service, string}, + {host, string}], + result = {res, rescode}}, + #ejabberd_commands{name = create_rooms_file, tags = [muc], + desc = "Create the rooms indicated in file", + module = ?MODULE, function = create_rooms_file, + args = [{file, string}], + result = {res, rescode}}, + #ejabberd_commands{name = destroy_rooms_file, tags = [muc], + desc = "Destroy the rooms indicated in file", + module = ?MODULE, function = destroy_rooms_file, + args = [{file, string}], + result = {res, rescode}}, + #ejabberd_commands{name = rooms_unused_list, tags = [muc], + desc = "List the rooms that are unused for many days in host", + module = ?MODULE, function = rooms_unused_list, + args = [{host, string}, {days, integer}], + result = {rooms, {list, {room, string}}}}, + #ejabberd_commands{name = rooms_unused_destroy, tags = [muc], + desc = "Destroy the rooms that are unused for many days in host", + module = ?MODULE, function = rooms_unused_destroy, + args = [{host, string}, {days, integer}], + result = {rooms, {list, {room, string}}}}, + + #ejabberd_commands{name = get_room_occupants, tags = [muc_room], + desc = "Get the list of occupants of a MUC room", + module = ?MODULE, function = get_room_occupants, + args = [{name, string}, {service, string}], + result = {occupants, {list, + {occupant, {tuple, + [{jid, string}, + {nick, string}, + {role, string} + ]}} + }}}, + + #ejabberd_commands{name = get_room_occupants_number, tags = [muc_room], + desc = "Get the number of occupants of a MUC room", + module = ?MODULE, function = get_room_occupants_number, + args = [{name, string}, {service, string}], + result = {occupants, integer}}, + + #ejabberd_commands{name = send_direct_invitation, tags = [muc_room], + desc = "Send a direct invitation to several destinations", + longdesc = "Password and Message can also be: none. Users JIDs are separated with : ", + module = ?MODULE, function = send_direct_invitation, + args = [{room, string}, {password, string}, {reason, string}, {users, string}], + result = {res, rescode}}, + + #ejabberd_commands{name = change_room_option, tags = [muc_room], + desc = "Change an option in a MUC room", + module = ?MODULE, function = change_room_option, + args = [{name, string}, {service, string}, + {option, string}, {value, string}], + result = {res, rescode}}, + + #ejabberd_commands{name = set_room_affiliation, tags = [muc_room], + desc = "Change an affiliation in a MUC room", + module = ?MODULE, function = set_room_affiliation, + args = [{name, string}, {service, string}, + {jid, string}, {affiliation, string}], + result = {res, rescode}}, + #ejabberd_commands{name = get_room_affiliations, tags = [muc_room], + desc = "Get the list of affiliations of a MUC room", + module = ?MODULE, function = get_room_affiliations, + args = [{name, string}, {service, string}], + result = {affiliations, {list, + {affiliation, {tuple, + [{username, string}, + {domain, string}, + {affiliation, atom}, + {reason, string} + ]}} + }}} + ]. + + +%%% +%%% ejabberd commands +%%% + +muc_online_rooms(ServerHost) -> + MUCHost = find_host(ServerHost), + Rooms = ets:tab2list(muc_online_room), + lists:foldl( + fun({_, {Roomname, Host}, _}, Results) -> + case MUCHost of + global -> + [Roomname ++ "@" ++ Host | Results]; + Host -> + [Roomname ++ "@" ++ Host | Results]; + _ -> + Results + end + end, + [], + Rooms). + +muc_unregister_nick(Nick) -> + F2 = fun(N) -> + [{_,Key,_}] = mnesia:index_read(muc_registered, N, 3), + mnesia:delete({muc_registered, Key}) + end, + case mnesia:transaction(F2, [Nick], 1) of + {atomic, ok} -> + ok; + {aborted, _Error} -> + error + end. + + +%%---------------------------- +%% Ad-hoc commands +%%---------------------------- + + +%%---------------------------- +%% Web Admin +%%---------------------------- + +%%--------------- +%% Web Admin Menu + +web_menu_main(Acc, Lang) -> + Acc ++ [{"muc", ?T("Multi-User Chat")}]. + +web_menu_host(Acc, _Host, Lang) -> + Acc ++ [{"muc", ?T("Multi-User Chat")}]. + + +%%--------------- +%% Web Admin Page + +-define(TDTD(L, N), + ?XE("tr", [?XCT("td", L), + ?XC("td", integer_to_list(N)) + ])). + +web_page_main(_, #request{path=["muc"], lang = Lang} = _Request) -> + Res = [?XC("h1", "Multi-User Chat"), + ?XC("h3", "Statistics"), + ?XAE("table", [], + [?XE("tbody", [?TDTD("Total rooms", ets:info(muc_online_room, size)), + ?TDTD("Permanent rooms", mnesia:table_info(muc_room, size)), + ?TDTD("Registered nicknames", mnesia:table_info(muc_registered, size)) + ]) + ]), + ?XE("ul", [?LI([?ACT("rooms", "List of rooms")])]) + ], + {stop, Res}; + +web_page_main(_, #request{path=["muc", "rooms"], q = Q, lang = Lang} = _Request) -> + Sort_query = get_sort_query(Q), + Res = make_rooms_page(global, Lang, Sort_query), + {stop, Res}; + +web_page_main(Acc, _) -> Acc. + +web_page_host(_, Host, + #request{path = ["muc"], + q = Q, + lang = Lang} = _Request) -> + Sort_query = get_sort_query(Q), + Res = make_rooms_page(find_host(Host), Lang, Sort_query), + {stop, Res}; +web_page_host(Acc, _, _) -> Acc. + + +%% Returns: {normal | reverse, Integer} +get_sort_query(Q) -> + case catch get_sort_query2(Q) of + {ok, Res} -> Res; + _ -> {normal, 1} + end. + +get_sort_query2(Q) -> + {value, {_, String}} = lists:keysearch("sort", 1, Q), + Integer = list_to_integer(String), + case Integer >= 0 of + true -> {ok, {normal, Integer}}; + false -> {ok, {reverse, abs(Integer)}} + end. + +make_rooms_page(Host, Lang, {Sort_direction, Sort_column}) -> + Rooms_names = get_rooms(Host), + Rooms_infos = build_info_rooms(Rooms_names), + Rooms_sorted = sort_rooms(Sort_direction, Sort_column, Rooms_infos), + Rooms_prepared = prepare_rooms_infos(Rooms_sorted), + TList = lists:map( + fun(Room) -> + ?XE("tr", [?XC("td", E) || E <- Room]) + end, Rooms_prepared), + Titles = ["Jabber ID", + "# participants", + "Last message", + "Public", + "Persistent", + "Logging", + "Just created", + "Title"], + {Titles_TR, _} = + lists:mapfoldl( + fun(Title, Num_column) -> + NCS = integer_to_list(Num_column), + TD = ?XE("td", [?CT(Title), + ?C(" "), + ?ACT("?sort="++NCS, "<"), + ?C(" "), + ?ACT("?sort=-"++NCS, ">")]), + {TD, Num_column+1} + end, + 1, + Titles), + [?XC("h1", "Multi-User Chat"), + ?XC("h2", "Rooms"), + ?XE("table", + [?XE("thead", + [?XE("tr", Titles_TR)] + ), + ?XE("tbody", TList) + ] + ) + ]. + +sort_rooms(Direction, Column, Rooms) -> + Rooms2 = lists:keysort(Column, Rooms), + case Direction of + normal -> Rooms2; + reverse -> lists:reverse(Rooms2) + end. + +build_info_rooms(Rooms) -> + [build_info_room(Room) || Room <- Rooms]. + +build_info_room({Name, Host, Pid}) -> + C = get_room_config(Pid), + Title = C#config.title, + Public = C#config.public, + Persistent = C#config.persistent, + Logging = C#config.logging, + + S = get_room_state(Pid), + Just_created = S#state.just_created, + Num_participants = length(dict:fetch_keys(S#state.users)), + + History = (S#state.history)#lqueue.queue, + Ts_last_message = + case queue:is_empty(History) of + true -> + "A long time ago"; + false -> + Last_message1 = queue:last(History), + {_, _, _, Ts_last, _} = Last_message1, + jlib:timestamp_to_iso(Ts_last) + end, + + {Name++"@"++Host, + Num_participants, + Ts_last_message, + Public, + Persistent, + Logging, + Just_created, + Title}. + +prepare_rooms_infos(Rooms) -> + [prepare_room_info(Room) || Room <- Rooms]. +prepare_room_info(Room_info) -> + {NameHost, + Num_participants, + Ts_last_message, + Public, + Persistent, + Logging, + Just_created, + Title} = Room_info, + [NameHost, + integer_to_list(Num_participants), + Ts_last_message, + atom_to_list(Public), + atom_to_list(Persistent), + atom_to_list(Logging), + atom_to_list(Just_created), + Title]. + + +%%---------------------------- +%% Create/Delete Room +%%---------------------------- + +%% @spec (Name::string(), Host::string(), ServerHost::string()) -> +%% ok | error +%% @doc Create a room immediately with the default options. +create_room(Name, Host, ServerHost) -> + + %% Get the default room options from the muc configuration + DefRoomOpts = gen_mod:get_module_opt(ServerHost, mod_muc, + default_room_options, []), + + %% Store the room on the server, it is not started yet though at this point + mod_muc:store_room(ServerHost, Host, Name, DefRoomOpts), + + %% Get all remaining mod_muc parameters that might be utilized + Access = gen_mod:get_module_opt(ServerHost, mod_muc, access, all), + AcCreate = gen_mod:get_module_opt(ServerHost, mod_muc, access_create, all), + AcAdmin = gen_mod:get_module_opt(ServerHost, mod_muc, access_admin, none), + AcPer = gen_mod:get_module_opt(ServerHost, mod_muc, access_persistent, all), + HistorySize = gen_mod:get_module_opt(ServerHost, mod_muc, history_size, 20), + RoomShaper = gen_mod:get_module_opt(ServerHost, mod_muc, room_shaper, none), + + %% If the room does not exist yet in the muc_online_room + case mnesia:dirty_read(muc_online_room, {Name, Host}) of + [] -> + %% Start the room + {ok, Pid} = mod_muc_room:start( + Host, + ServerHost, + {Access, AcCreate, AcAdmin, AcPer}, + Name, + HistorySize, + RoomShaper, + DefRoomOpts), + {atomic, ok} = register_room(Host, Name, Pid), + ok; + _ -> + error + end. + +register_room(Host, Name, Pid) -> + F = fun() -> + mnesia:write(#muc_online_room{name_host = {Name, Host}, + pid = Pid}) + end, + mnesia:transaction(F). + +%% Create the room only in the database. +%% It is required to restart the MUC service for the room to appear. +muc_create_room(ServerHost, {Name, Host, _}, DefRoomOpts) -> + io:format("Creating room ~s@~s~n", [Name, Host]), + mod_muc:store_room(ServerHost, Host, Name, DefRoomOpts). + +%% @spec (Name::string(), Host::string(), ServerHost::string()) -> +%% ok | {error, room_not_exists} +%% @doc Destroy the room immediately. +%% If the room has participants, they are not notified that the room was destroyed; +%% they will notice when they try to chat and receive an error that the room doesn't exist. +destroy_room(Name, Service, _Server) -> + case mnesia:dirty_read(muc_online_room, {Name, Service}) of + [R] -> + Pid = R#muc_online_room.pid, + gen_fsm:send_all_state_event(Pid, destroy), + ok; + [] -> + error + end. + +destroy_room({N, H, SH}) -> + io:format("Destroying room: ~s@~s - vhost: ~s~n", [N, H, SH]), + destroy_room(N, H, SH). + + +%%---------------------------- +%% Destroy Rooms in File +%%---------------------------- + +%% The format of the file is: one chatroom JID per line +%% The file encoding must be UTF-8 + +destroy_rooms_file(Filename) -> + {ok, F} = file:open(Filename, [read]), + RJID = read_room(F), + Rooms = read_rooms(F, RJID, []), + file:close(F), + [destroy_room(A) || A <- Rooms], + ok. + +read_rooms(_F, eof, L) -> + L; + +read_rooms(F, RJID, L) -> + RJID2 = read_room(F), + read_rooms(F, RJID2, [RJID | L]). + +read_room(F) -> + case io:get_line(F, "") of + eof -> eof; + String -> + case io_lib:fread("~s", String) of + {ok, [RoomJID], _} -> split_roomjid(RoomJID); + {error, What} -> + io:format("Parse error: what: ~p~non the line: ~p~n~n", [What, String]) + end + end. + +%% This function is quite rudimentary +%% and may not be accurate +split_roomjid(RoomJID) -> + [Name, Host] = string:tokens(RoomJID, "@"), + [_MUC_service_name | ServerHostList] = string:tokens(Host, "."), + ServerHost = join(ServerHostList, "."), + {Name, Host, ServerHost}. + +%% This function is copied from string:join/2 in Erlang/OTP R12B-1 +%% Note that string:join/2 is not implemented in Erlang/OTP R11B +join([H|T], Sep) -> + H ++ lists:concat([Sep ++ X || X <- T]). + + +%%---------------------------- +%% Create Rooms in File +%%---------------------------- + +create_rooms_file(Filename) -> + {ok, F} = file:open(Filename, [read]), + RJID = read_room(F), + Rooms = read_rooms(F, RJID, []), + file:close(F), + %% Read the default room options defined for the first virtual host + DefRoomOpts = gen_mod:get_module_opt(?MYNAME, mod_muc, + default_room_options, []), + [muc_create_room(?MYNAME, A, DefRoomOpts) || A <- Rooms], + ok. + + +%%---------------------------- +%% List/Delete Unused Rooms +%%---------------------------- + +%%--------------- +%% Control + +rooms_unused_list(Host, Days) -> + rooms_unused_report(list, Host, Days). +rooms_unused_destroy(Host, Days) -> + rooms_unused_report(destroy, Host, Days). + +rooms_unused_report(Action, Host, Days) -> + {NA, NP, RP} = muc_unused(Action, Host, Days), + io:format("Unused rooms: ~p out of ~p~n", [NP, NA]), + [R ++ "@" ++ H || {R, H, _P} <- RP]. + +muc_unused(Action, ServerHost, Days) -> + Host = find_host(ServerHost), + muc_unused2(Action, ServerHost, Host, Days). + +muc_unused2(Action, ServerHost, Host, Last_allowed) -> + %% Get all required info about all existing rooms + Rooms_all = get_rooms(Host), + + %% Decide which ones pass the requirements + Rooms_pass = decide_rooms(Rooms_all, Last_allowed), + + Num_rooms_all = length(Rooms_all), + Num_rooms_pass = length(Rooms_pass), + + %% Perform the desired action for matching rooms + act_on_rooms(Action, Rooms_pass, ServerHost), + + {Num_rooms_all, Num_rooms_pass, Rooms_pass}. + +%%--------------- +%% Get info + +get_rooms(Host) -> + Get_room_names = fun(Room_reg, Names) -> + Pid = Room_reg#muc_online_room.pid, + case {Host, Room_reg#muc_online_room.name_host} of + {Host, {Name1, Host}} -> + [{Name1, Host, Pid} | Names]; + {global, {Name1, Host1}} -> + [{Name1, Host1, Pid} | Names]; + _ -> + Names + end + end, + ets:foldr(Get_room_names, [], muc_online_room). + +get_room_config(Room_pid) -> + {ok, R} = gen_fsm:sync_send_all_state_event(Room_pid, get_config), + R. + +get_room_state(Room_pid) -> + {ok, R} = gen_fsm:sync_send_all_state_event(Room_pid, get_state), + R. + +%%--------------- +%% Decide + +decide_rooms(Rooms, Last_allowed) -> + Decide = fun(R) -> decide_room(R, Last_allowed) end, + lists:filter(Decide, Rooms). + +decide_room({_Room_name, _Host, Room_pid}, Last_allowed) -> + C = get_room_config(Room_pid), + Persistent = C#config.persistent, + + S = get_room_state(Room_pid), + Just_created = S#state.just_created, + + Room_users = S#state.users, + Num_users = length(?DICT:to_list(Room_users)), + + History = (S#state.history)#lqueue.queue, + Ts_now = calendar:now_to_universal_time(now()), + Ts_uptime = uptime_seconds(), + {Has_hist, Last} = case queue:is_empty(History) of + true -> + {false, Ts_uptime}; + false -> + Last_message = queue:last(History), + {_, _, _, Ts_last, _} = Last_message, + Ts_diff = + calendar:datetime_to_gregorian_seconds(Ts_now) + - calendar:datetime_to_gregorian_seconds(Ts_last), + {true, Ts_diff} + end, + + case {Persistent, Just_created, Num_users, Has_hist, seconds_to_days(Last)} of + {_true, false, 0, _, Last_days} + when Last_days >= Last_allowed -> + true; + _ -> + false + end. + +seconds_to_days(S) -> + S div (60*60*24). + +%%--------------- +%% Act + +act_on_rooms(Action, Rooms, ServerHost) -> + ServerHosts = [ {A, find_host(A)} || A <- ?MYHOSTS ], + Delete = fun({_N, H, _Pid} = Room) -> + SH = case ServerHost of + global -> find_serverhost(H, ServerHosts); + O -> O + end, + + act_on_room(Action, Room, SH) + end, + lists:foreach(Delete, Rooms). + +find_serverhost(Host, ServerHosts) -> + {value, {ServerHost, Host}} = lists:keysearch(Host, 2, ServerHosts), + ServerHost. + +act_on_room(destroy, {N, H, Pid}, SH) -> + gen_fsm:send_all_state_event( + Pid, {destroy, "Room destroyed by rooms_unused_destroy."}), + mod_muc:room_destroyed(H, N, Pid, SH), + mod_muc:forget_room(H, N); + +act_on_room(list, _, _) -> + ok. + + +%%---------------------------- +%% Change Room Option +%%---------------------------- + +get_room_occupants(Room, Host) -> + case get_room_pid(Room, Host) of + room_not_found -> throw({error, room_not_found}); + Pid -> get_room_occupants(Pid) + end. + +get_room_occupants(Pid) -> + S = get_room_state(Pid), + lists:map( + fun({_LJID, Info}) -> + {jlib:jid_to_string(Info#user.jid), + Info#user.nick, + atom_to_list(Info#user.role)} + end, + dict:to_list(S#state.users)). + +get_room_occupants_number(Room, Host) -> + length(get_room_occupants(Room, Host)). + +%%---------------------------- +%% Send Direct Invitation +%%---------------------------- +%% http://xmpp.org/extensions/xep-0249.html + +send_direct_invitation(RoomString, Password, Reason, UsersString) -> + RoomJid = jlib:string_to_jid(RoomString), + XmlEl = build_invitation(Password, Reason, RoomString), + UsersStrings = get_users_to_invite(RoomJid, UsersString), + [send_direct_invitation(RoomJid, jlib:string_to_jid(UserStrings), XmlEl) + || UserStrings <- UsersStrings], + timer:sleep(1000), + ok. + +get_users_to_invite(RoomJid, UsersString) -> + UsersStrings = string:tokens(UsersString, ":"), + OccupantsTuples = get_room_occupants(RoomJid#jid.luser, + RoomJid#jid.lserver), + OccupantsJids = [jlib:string_to_jid(JidString) + || {JidString, _Nick, _} <- OccupantsTuples], + lists:filter( + fun(UserString) -> + UserJid = jlib:string_to_jid(UserString), + %% [{"badlop@localhost/work","badlop","moderator"}] + lists:all(fun(OccupantJid) -> + UserJid#jid.luser /= OccupantJid#jid.luser + orelse UserJid#jid.lserver /= OccupantJid#jid.lserver + end, + OccupantsJids) + end, + UsersStrings). + +build_invitation(Password, Reason, RoomString) -> + PasswordAttrList = case Password of + "none" -> []; + _ -> [{"password", Password}] + end, + ReasonAttrList = case Reason of + "none" -> []; + _ -> [{"reason", Reason}] + end, + XAttrs = [{"xmlns", ?NS_XCONFERENCE}, + {"jid", RoomString}] + ++ PasswordAttrList + ++ ReasonAttrList, + XEl = {xmlelement, "x", XAttrs, []}, + {xmlelement, "message", [], [XEl]}. + +send_direct_invitation(FromJid, UserJid, XmlEl) -> + ejabberd_router:route(FromJid, UserJid, XmlEl). + +%%---------------------------- +%% Change Room Option +%%---------------------------- + +%% @spec(Name::string(), Service::string(), Option::string(), Value) -> ok +%% Value = atom() | integer() | string() +%% @doc Change an option in an existing room. +%% Requires the name of the room, the MUC service where it exists, +%% 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(Name, Service, OptionString, ValueString) -> + Option = list_to_atom(OptionString), + Value = case Option of + title -> ValueString; + description -> ValueString; + password -> ValueString; + max_users -> list_to_integer(ValueString); + _ -> list_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}). + +%% @doc Get the Pid of an existing MUC room, or 'room_not_found'. +get_room_pid(Name, Service) -> + case mnesia:dirty_read(muc_online_room, {Name, Service}) of + [] -> + room_not_found; + [Room] -> + Room#muc_online_room.pid + end. + +%% It is required to put explicitely all the options because +%% the record elements are replaced at compile time. +%% So, this can't be parametrized. +change_option(Option, Value, Config) -> + case Option of + allow_change_subj -> Config#config{allow_change_subj = Value}; + allow_private_messages -> Config#config{allow_private_messages = Value}; + allow_query_users -> Config#config{allow_query_users = Value}; + allow_user_invites -> Config#config{allow_user_invites = Value}; + anonymous -> Config#config{anonymous = Value}; + logging -> Config#config{logging = Value}; + max_users -> Config#config{max_users = Value}; + members_by_default -> Config#config{members_by_default = Value}; + members_only -> Config#config{members_only = Value}; + moderated -> Config#config{moderated = Value}; + password -> Config#config{password = Value}; + password_protected -> Config#config{password_protected = Value}; + persistent -> Config#config{persistent = Value}; + public -> Config#config{public = Value}; + public_list -> Config#config{public_list = Value}; + title -> Config#config{title = Value} + end. + + +%%---------------------------- +%% Get Room Affiliations +%%---------------------------- + +%% @spec(Name::string(), Service::string()) -> +%% [{JID::string(), Domain::string(), Role::string(), Reason::string()}] +%% @doc Get the affiliations of the room Name@Service. +get_room_affiliations(Name, Service) -> + case mnesia:dirty_read(muc_online_room, {Name, Service}) of + [R] -> + %% Get the PID of the online room, then request its state + Pid = R#muc_online_room.pid, + {ok, StateData} = gen_fsm:sync_send_all_state_event(Pid, get_state), + Affiliations = ?DICT:to_list(StateData#state.affiliations), + lists:map( + fun({{Uname, Domain, _Res}, {Aff, Reason}}) when is_atom(Aff)-> + {Uname, Domain, Aff, Reason}; + ({{Uname, Domain, _Res}, Aff}) when is_atom(Aff)-> + {Uname, Domain, Aff, ""} + end, Affiliations); + [] -> + throw({error, "The room does not exist."}) + end. + +%%---------------------------- +%% Change Room Affiliation +%%---------------------------- + +%% @spec(Name, Service, JID, AffiliationString) -> ok | {error, Error} +%% Name = string() +%% Service = string() +%% JID = string() +%% AffiliationString = "outcast" | "none" | "member" | "admin" | "owner" +%% @doc Set the affiliation of JID in the room Name@Service. +%% If the affiliation is 'none', the action is to remove, +%% In any other case the action will be to create the affiliation. +set_room_affiliation(Name, Service, JID, AffiliationString) -> + Affiliation = list_to_atom(AffiliationString), + case mnesia:dirty_read(muc_online_room, {Name, Service}) of + [R] -> + %% Get the PID for the online room so we can get the state of the room + Pid = R#muc_online_room.pid, + {ok, StateData} = gen_fsm:sync_send_all_state_event(Pid, get_state), + SJID = jlib:string_to_jid(JID), + LJID = jlib:jid_remove_resource(jlib:jid_tolower(SJID)), + Affiliations = change_affiliation(Affiliation, LJID, StateData#state.affiliations), + Res = StateData#state{affiliations = Affiliations}, + {ok, _State} = gen_fsm:sync_send_all_state_event(Pid, {change_state, Res}), + mod_muc:store_room(Res#state.server_host, Res#state.host, Res#state.room, make_opts(Res)), + ok; + [] -> + error + end. + +change_affiliation(none, LJID, Affiliations) -> + ?DICT:erase(LJID, Affiliations); +change_affiliation(Affiliation, LJID, Affiliations) -> + ?DICT:store(LJID, Affiliation, Affiliations). + +-define(MAKE_CONFIG_OPT(Opt), {Opt, Config#config.Opt}). + +make_opts(StateData) -> + Config = StateData#state.config, + [ + ?MAKE_CONFIG_OPT(title), + ?MAKE_CONFIG_OPT(allow_change_subj), + ?MAKE_CONFIG_OPT(allow_query_users), + ?MAKE_CONFIG_OPT(allow_private_messages), + ?MAKE_CONFIG_OPT(public), + ?MAKE_CONFIG_OPT(public_list), + ?MAKE_CONFIG_OPT(persistent), + ?MAKE_CONFIG_OPT(moderated), + ?MAKE_CONFIG_OPT(members_by_default), + ?MAKE_CONFIG_OPT(members_only), + ?MAKE_CONFIG_OPT(allow_user_invites), + ?MAKE_CONFIG_OPT(password_protected), + ?MAKE_CONFIG_OPT(password), + ?MAKE_CONFIG_OPT(anonymous), + ?MAKE_CONFIG_OPT(logging), + ?MAKE_CONFIG_OPT(max_users), + {affiliations, ?DICT:to_list(StateData#state.affiliations)}, + {subject, StateData#state.subject}, + {subject_author, StateData#state.subject_author} + ]. + + +%%---------------------------- +%% Utils +%%---------------------------- + +uptime_seconds() -> + trunc(element(1, erlang:statistics(wall_clock))/1000). + +find_host(global) -> + global; +find_host("global") -> + global; +find_host(ServerHost) -> + gen_mod:get_module_opt_host(ServerHost, mod_muc, "conference.@HOST@"). + diff --git a/mod_muc_log_http/COPYING b/mod_muc_log_http/COPYING new file mode 100644 index 0000000..8b325f5 --- /dev/null +++ b/mod_muc_log_http/COPYING @@ -0,0 +1,343 @@ +As a special exception, the authors give permission to link this program +with the OpenSSL library and distribute the resulting binary. + + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Library General +Public License instead of this License. diff --git a/mod_muc_log_http/ChangeLog b/mod_muc_log_http/ChangeLog new file mode 100644 index 0000000..d35edea --- /dev/null +++ b/mod_muc_log_http/ChangeLog @@ -0,0 +1,41 @@ +2009-06-14 Badlop + + * src/mod_muc_log_http.erl: Include mod_muc_room.hrl + +2009-03-11 Badlop + + * src/mod_muc_log_http.erl: Fix capitalization of HTTP headers + +2009-02-28 Badlop + + * src/mod_muc_log_http.erl: Add support to run in several vhosts + * README.txt: Explain that it requires recent ejabberd SVN + +2008-09-30 Badlop + + * src/mod_muc_log_http.erl: Update record definitions to ejabberd + trunk SVN + +2008-05-17 Badlop + + * src/mod_muc_log_http.erl: Use httpd_util:to_lower when not + compiling with Erlang R12 (EJAB-628) + * Emakefile: Assumes that Erlang/OTP R11B-4 or newer is used + * README.txt: Explain how to compile in older versions + +2008-04-12 Badlop + + * src/mod_muc_log_http.erl: Include Last-Modified HTTP + header in responses to allow caching (EJAB-546) + +2008-03-12 Badlop + + * README.txt: Updated text formatting. Require ejabberd 2.0.0 + + * mod_muc_log_http.erl: Updated to ejabberd 2.0.0 + + * ChangeLog: New file to log changes + +2007-04-13 Badlop + + * mod_muc_log_http: New module to serve MUC HTML logs over HTTP diff --git a/mod_muc_log_http/Emakefile b/mod_muc_log_http/Emakefile new file mode 100644 index 0000000..6b816f4 --- /dev/null +++ b/mod_muc_log_http/Emakefile @@ -0,0 +1,2 @@ +{'../ejabberd-dev/src/gen_mod', [{outdir, "../ejabberd-dev/ebin"},{i,"../ejabberd-dev/include"}]}. +{'src/mod_muc_log_http', [{d,'SSL39'},{outdir, "ebin"},{i,"../ejabberd-dev/include"}]}. diff --git a/mod_muc_log_http/README.txt b/mod_muc_log_http/README.txt new file mode 100644 index 0000000..69d30f4 --- /dev/null +++ b/mod_muc_log_http/README.txt @@ -0,0 +1,56 @@ + + + mod_muc_log_http - Serve MUC logs on the web + + Homepage: http://ejabberd.jabber.ru/mod_muc_log_http + Author: Badlop + Requirement: ejabberd SVN r1561, or ejabbed 2.1.0 when released + + + DESCRIPTION + =========== + +This module serves the directory containing MUC logs already +configured on mod_muc_log. So, there's no need to setup a web server +to allow your users to view the MUC logs. It is a small modification +of mod_http_fileserver, customized for log serving. + + + CONFIGURATION + ============= + +If you want to compile this module with Erlang/OTP R11B-3 or older, +edit Emakefile and remove this: + {d, 'SSL39'}, + +Sample ejabberd.cfg options. The directory to serve is already defined +on mod_muc_log. + +{listen, + ... + {5280, ejabberd_http, [http_poll, web_admin, + {request_handlers, [ + {["pub", "muclogs"], mod_muc_log_http} + ] + } + ] + } + ... +]}. + +{modules, + [ + ... + {mod_muc_log, [ + {outdir, "/var/www/ejabberdlogs"} + ]}, + {mod_muc_log_http, []}, + ... +]}. + + + USAGE + ===== + +With the example options, open your web browser at: +http://server:5280/pub/muclogs/ diff --git a/mod_muc_log_http/build.bat b/mod_muc_log_http/build.bat new file mode 100644 index 0000000..fff9b3b --- /dev/null +++ b/mod_muc_log_http/build.bat @@ -0,0 +1 @@ +erl -pa ../../ejabberd-dev/trunk/ebin -pa ebin -make diff --git a/mod_muc_log_http/build.sh b/mod_muc_log_http/build.sh new file mode 100755 index 0000000..f0a0cfa --- /dev/null +++ b/mod_muc_log_http/build.sh @@ -0,0 +1,2 @@ +#!/bin/sh +erl -pa ../ejabberd-dev/ebin -pz ebin -make diff --git a/mod_muc_log_http/src/mod_muc_log_http.erl b/mod_muc_log_http/src/mod_muc_log_http.erl new file mode 100644 index 0000000..f6b52ee --- /dev/null +++ b/mod_muc_log_http/src/mod_muc_log_http.erl @@ -0,0 +1,256 @@ +%%%---------------------------------------------------------------------- +%%% File : mod_muc_log_http.erl +%%% Author : Badlop, Massimiliano Mirra +%%% Purpose : MUC logs simple file server +%%% Created : +%%% Id : +%%%---------------------------------------------------------------------- + +-module(mod_muc_log_http). + +-behaviour(gen_mod). + +-export([ + start/2, + stop/1, + loop/1, + process/2 + ]). + +-include("ejabberd.hrl"). +-include("jlib.hrl"). +-include("web/ejabberd_http.hrl"). +-include("mod_muc/mod_muc_room.hrl"). +-include_lib("kernel/include/file.hrl"). + +-define(PROCNAME, mod_muc_log_http). + +-ifdef(SSL39). +-define(STRING2LOWER, string). +-else. +-define(STRING2LOWER, httpd_util). +-endif. + +% TODO: +% - If chatroom is password protected, ask password +% - If chatroom is only for members, ask for username and password + +%% Copied from mod_muc/mod_muc.erl +-record(muc_online_room, {name_host, pid}). + + +%%%---------------------------------------------------------------------- +%%% REQUEST HANDLERS +%%%---------------------------------------------------------------------- + +process(LocalPath, Request) -> + serve(LocalPath, Request). + +serve(LocalPath, #request{host = Host} = Request) -> + Proc = gen_mod:get_module_proc(Host, ?PROCNAME), + Proc ! {get_docroot, self()}, + receive DocRoot -> ok end, + FileName = filename:join(filename:split(DocRoot) ++ LocalPath), + case file:read_file(FileName) of + {ok, FileContents} -> + ?DEBUG("Delivering content.", []), + {200, + [{"Server", "ejabberd"}, + {"Last-Modified", last_modified(FileName)}, + {"Content-Type", content_type(FileName)}], + FileContents}; + {error, eisdir} -> + FileNameIndex = FileName ++ "/index.html", + case file:read_file_info(FileNameIndex) of + {ok, _FileInfo} -> serve(LocalPath ++ ["index.html"], Request); + {error, _Error} -> show_dir_listing(FileName, LocalPath) + end; + {error, Error} -> + ?DEBUG("Delivering error: ~p", [Error]), + case Error of + eacces -> {403, [], "Forbidden"}; + enoent -> {404, [], "Not found"}; + _Else -> {404, [], atom_to_list(Error)} + end + end. + + +%%%---------------------------------------------------------------------- +%%% Dir listing +%%%---------------------------------------------------------------------- + +build_datetimelist(DateTime) -> + {{Ye, Mo, Da}, {Ho, Mi, Se}} = DateTime, + Nums = [Mo, Da, Ho, Mi, Se], + Nums2 = [fill(Num) || Num <- Nums], + [integer_to_list(Ye)] ++ Nums2. + +fill(Num) when Num < 10 -> io_lib:format("0~p", [Num]); +fill(Num) -> io_lib:format("~p", [Num]). + + +%%%---------------------------------------------------------------------- +%%% MUC INFO +%%%---------------------------------------------------------------------- + +get_room_pid(Name, Host) -> + case ets:lookup(muc_online_room, {Name, Host}) of + [] -> unknown; + [Room_ets] -> Room_ets#muc_online_room.pid + end. + +get_room_config(Room_pid) -> + {ok, C} = gen_fsm:sync_send_all_state_event(Room_pid, get_config), + C. + + +%%%---------------------------------------------------------------------- +%%% MUC LIST +%%%---------------------------------------------------------------------- + +show_dir_listing(DirName, LocalPath) -> + Header = io_lib:format("Name Last modified Size Description~n", []), + Address = io_lib:format("
    ejabberd/~s Server
    ", [?VERSION]), + + {ok, Listing} = file:list_dir(DirName), + Listing2 = lists:sort(Listing), + + Listing3 = case LocalPath of + [] -> + lists:filter( + fun(RoomFull) -> + case string:tokens(RoomFull, "@") of + [Room, Host] -> + case get_room_pid(Room, Host) of + unknown -> true; + Room_pid -> (get_room_config(Room_pid))#config.public + end; + _ -> false % Don't show files that are not rooms + end + end, + Listing2); + _ -> + Listing2 + end, + + Listing4 = lists:map( + fun(RoomFull) -> + Desc = case string:tokens(RoomFull, "@") of + [Room, Host] -> + case get_room_pid(Room, Host) of + unknown -> "-"; + Room_pid -> (get_room_config(Room_pid))#config.title + end; + _ -> "-" + end, + {RoomFull, Desc} + end, + Listing3), + + DirNest = ["Chatroom logs" | LocalPath], + {_, Indexof1} = lists:foldl( + fun(E, {N, Res}) -> + D = string:copies("../", N), + Res2 = Res + ++ io_lib:format("~s", [D, E]) + ++ " > ", + {N-1, Res2} + end, + {length(DirNest)-1, ""}, + DirNest), + + Title1 = lists:foldl( + fun(E, Res) -> + Res ++ E ++ " > " + end, + "", + DirNest), + + Title = io_lib:format("~s", [Title1]), + Indexof = io_lib:format("

    ~s

    ", [Indexof1]), + + Files = lists:foldl( + fun({Filename, Description}, Res) -> + {ok, Fi} = file:read_file_info(DirName ++ "/" ++ Filename), + + {Filename2, Size} = case Fi#file_info.type of + directory -> {Filename ++ "/", "-"}; + _ -> {Filename, integer_to_list(Fi#file_info.size)} + end, + + DateTimeList = build_datetimelist(Fi#file_info.ctime), + Time = io_lib:format("~s-~s-~s ~s:~s:~s", DateTimeList), + + FillSpace = lists:flatten(lists:duplicate(50-length(Filename2), " ")), + + FileString = io_lib:format("~s~s ~s ~10s ~s~n", + [Filename2, Filename2, FillSpace, Time, Size, Description]), + Res ++ FileString + end, + "", + Listing4), + + Content = "" + ++ Title + ++ "
    "
    +		++ Indexof
    +		++ Header
    +		++ "
    " + ++ Files + ++ "
    " + ++ Address + ++ "", + + {200, + [{"Server", "ejabberd"}, + {"Content-Type", "text/html"}], + Content}. + + +%%%---------------------------------------------------------------------- +%%% UTILITIES +%%%---------------------------------------------------------------------- + +content_type(Filename) -> + case ?STRING2LOWER:to_lower(filename:extension(Filename)) of + ".jpg" -> "image/jpeg"; + ".jpeg" -> "image/jpeg"; + ".gif" -> "image/gif"; + ".png" -> "image/png"; + ".html" -> "text/html"; + ".css" -> "text/css"; + ".txt" -> "text/plain"; + ".xul" -> "application/vnd.mozilla.xul+xml"; + ".jar" -> "application/java-archive"; + ".xpi" -> "application/x-xpinstall"; + _Else -> "application/octet-stream" + end. + +last_modified(FileName) -> + {ok, FileInfo} = file:read_file_info(FileName), + Then = FileInfo#file_info.mtime, + httpd_util:rfc1123_date(Then). + +loop(DocRoot) -> + receive + {get_docroot, Pid} -> + Pid ! DocRoot, + loop(DocRoot); + stop -> + ok + end. + +%%%---------------------------------------------------------------------- +%%% BEHAVIOUR CALLBACKS +%%%---------------------------------------------------------------------- + +start(Host, _Opts) -> + DocRoot = gen_mod:get_module_opt(Host, mod_muc_log, outdir, "www/muc"), + Proc = gen_mod:get_module_proc(Host, ?PROCNAME), + catch register(Proc, spawn(?MODULE, loop, [DocRoot])), + ok. + +stop(Host) -> + Proc = gen_mod:get_module_proc(Host, ?PROCNAME), + exit(whereis(Proc), stop), + {wait, Proc}. diff --git a/mod_multicast/COPYING b/mod_multicast/COPYING new file mode 100644 index 0000000..8b325f5 --- /dev/null +++ b/mod_multicast/COPYING @@ -0,0 +1,343 @@ +As a special exception, the authors give permission to link this program +with the OpenSSL library and distribute the resulting binary. + + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Library General +Public License instead of this License. diff --git a/mod_multicast/ChangeLog b/mod_multicast/ChangeLog new file mode 100644 index 0000000..923bac8 --- /dev/null +++ b/mod_multicast/ChangeLog @@ -0,0 +1,72 @@ +2007-10-14 Badlop + + * src/ejabberd_c2s.erl, src/mod_muc_room.erl: Updated to ejabberd trunk. + +2007-09-30 Badlop + + * src/mod_multicast.erl: Huge performance optimization on route_* functions. + +2007-09-29 Badlop + + * src/mod_multicast.erl: Huge reorganization of do_route* functions. + +2007-08-18 Badlop + + * src/mod_multicast.erl: Removed 'unrestricted' type of limits because it's unusued, as reported by Dialyzer. + +2007-08-12 Badlop + + * src/mod_multicast.erl: Local services are not inspected for ACL and Limits. Usage of stj() and jts() functions. + +2007-08-11 Badlop + + * src/ejabberd_route_multicast.erl: New router for multicast packets. + * src/ejabberd_route.erl: Removed. + * Emakefile: Updated. + * src/ejabberd.app: Updated. + * src/ejabberd_sup.erl: Updated. + * README.txt: Updated. + + * src/mod_multicast.erl: Use the new multicast router. + * src/ejabberd_c2s.erl: Idem. + * src/mod_muc_room.erl: Idem. + + * src/ejabberd.app: Added from ejabberd trunk. + * src/ejabberd_sup.erl: Idem. + * Emakefile: Updated. + +2007-08-10 Badlop + + * src/mod_muc_room.erl, src/ejabberd_router.erl, + src/ejabberd_c2s.erl: Updated to ejabberd trunk without patches. + + * src/mod_multicast.erl: Implement XEP33 limits: 4) store limits, use them when sending to remote service. 5) Force service limits to incoming packets. + +2007-08-09 Badlop + + * src/mod_multicast.erl: Implement XEP33 limits: 3) receive remote limits. + +2007-08-07 Badlop + + * src/mod_multicast.erl: Fixed indentation. No differentiation + between user and server limits. In disco#info responses, report + only limits of interest. + +2007-08-06 Badlop + + * src/mod_multicast.erl: Implement XEP33 limits: 2) report custom limits. + +2007-08-05 Badlop + + * src/mod_multicast.erl: Implement XEP33 limits: 1) read options. + +2007-08-04 Badlop + + * ChangeLog: New file to track changes. + * README.txt: Removed Changelog section. + + * src/ejabberd_c2s.erl, + src/ejabberd_router.erl, + src/mod_muc_room.erl, + src/mod_multicast.erl: Update + indentation using Emacs' erlang-mode. diff --git a/mod_multicast/Emakefile b/mod_multicast/Emakefile new file mode 100644 index 0000000..ef1a8cf --- /dev/null +++ b/mod_multicast/Emakefile @@ -0,0 +1,6 @@ +{'../ejabberd-dev/src/gen_mod', [{outdir, "../ejabberd-dev/ebin"},{i,"../ejabberd-dev/include"}]}. +{'src/mod_multicast', [{outdir, "ebin"},{i,"../ejabberd-dev/include"}]}. +{'src/ejabberd_router_multicast', [{outdir, "ebin"},{i,"../ejabberd-dev/include"}]}. +{'src/ejabberd_c2s', [{outdir, "ebin"},{i,"../ejabberd-dev/include"}]}. +{'src/mod_muc_room', [{outdir, "ebin"},{i,"../ejabberd-dev/include"}]}. +{'src/ejabberd_sup', [{outdir, "ebin"},{i,"../ejabberd-dev/include"}]}. diff --git a/mod_multicast/README.txt b/mod_multicast/README.txt new file mode 100644 index 0000000..3e54e86 --- /dev/null +++ b/mod_multicast/README.txt @@ -0,0 +1,99 @@ + + + mod_multicast - Extended Stanza Addressing (XEP-0033) support + + Homepage: http://ejabberd.jabber.ru/mod_multicast + Author: Badlop + Module for ejabberd SVN + + +***************************************** +** +** IMPORTANT!! +** +** This code is now hosted and developed in this Git repository and branch: +** https://git.process-one.net/~badlop/ejabberd/badlop-ejabberd/commits/multicast-2.1.x +** +***************************************** + + + DESCRIPTION + ----------- + +This module implements Extended Stanza Addressing (XEP-0033). + +The development of this module is included on a Google Summer of Code 2007 project. + + + INSTALL + ------- + +1. Compile the module. +2. Copy the binary files to ejabberd ebin directory. +3. Edit ejabberd.cfg and add the module to the list of modules: + {mod_multicast, []}, +4. Start ejabberd. + + + CONFIGURABLE PARAMETERS + ----------------------- + +host + Define the hostname of the service. + Default value: "multicast.SERVER" +access: + Specify who can send packets to the multicast service. + Default value: all +limits: + Specify a list of custom limits which override the default ones defined + in XEP-0033. + Limits are defined with this syntax: {Sender_type, Stanza_type, Number} + Where: + Sender_type can have values: local or remote. + Stanza_type can have values: message or presence. + Number can be a positive integer or the key word infinite. + Default value: [] + + + EXAMPLE CONFIGURATION + --------------------- + +% Only admins can send packets to multicast service +{access, multicast, [{allow, admin}, {deny, all}]}. + +% If you want to allow all your users: +%{access, multicast, [{allow, all}]}. + +% This allows both admins and remote users to send packets, +% but does not allow local users +%{acl, allservers, {server_glob, "*"}}. +%{access, multicast, [{allow, admin}, {deny, local}, {allow, allservers}]}. + + +{modules, [ + ... + {mod_multicast, [ + %{host, "multicast.example.org"}, + {access, multicast}, + {limits, [ + {local, message, 40}, + {local, presence, infinite}, + {remote, message, 150} + ]} + ]}, + ... +]}. + + + TO DO + ----- + +Tasks to do: + - Consider anti-spam requirements + +Feature requests: + - GUI with FORMS to allow users of non-capable clients to write XEP-33 packets easily + +Could use mod_multicast somehow: + - mod_pubsub/mod_pep + - mod_irc diff --git a/mod_multicast/build.bat b/mod_multicast/build.bat new file mode 100644 index 0000000..fff9b3b --- /dev/null +++ b/mod_multicast/build.bat @@ -0,0 +1 @@ +erl -pa ../../ejabberd-dev/trunk/ebin -pa ebin -make diff --git a/mod_multicast/build.sh b/mod_multicast/build.sh new file mode 100755 index 0000000..f0a0cfa --- /dev/null +++ b/mod_multicast/build.sh @@ -0,0 +1,2 @@ +#!/bin/sh +erl -pa ../ejabberd-dev/ebin -pz ebin -make diff --git a/mod_multicast/src/ejabberd.app b/mod_multicast/src/ejabberd.app new file mode 100644 index 0000000..cdc44ca --- /dev/null +++ b/mod_multicast/src/ejabberd.app @@ -0,0 +1,81 @@ +% $Id: ejabberd.app 733 2007-02-19 15:02:28Z mremond $ + +{application, ejabberd, + [{description, "ejabberd"}, + {vsn, "1.1.3"}, + {modules, [acl, + configure, + cyrsasl, + cyrsasl_digest, + cyrsasl_plain, + ejabberd, + ejabberd_app, + ejabberd_auth, + ejabberd_c2s, + ejabberd_config, + ejabberd_listener, + ejabberd_logger_h, + ejabberd_local, + ejabberd_router, + ejabberd_router_multicast, + ejabberd_s2s, + ejabberd_s2s_in, + ejabberd_s2s_out, + ejabberd_service, + ejabberd_sm, + ejabberd_sup, + ejabberd_tmp_sup, + gen_iq_handler, + gen_mod, + jd2ejd, + jlib, + mod_configure, + mod_disco, + mod_echo, + mod_last, + mod_offline, + mod_private, + mod_register, + mod_roster, + mod_stats, + mod_time, + mod_vcard, + mod_version, + randoms, + sha, + shaper, + translate, + xml, + xml_stream + ]}, + {registered, [ejabberd, + ejabberd_sup, + ejabberd_auth, + ejabberd_router, + ejabberd_router_multicast, + ejabberd_sm, + ejabberd_s2s, + ejabberd_local, + ejabberd_listeners, + ejabberd_iq_sup, + ejabberd_service_sup, + ejabberd_s2s_out_sup, + ejabberd_s2s_in_sup, + ejabberd_c2s_sup, + ejabberd_mod_roster, + ejabberd_mod_echo, + ejabberd_mod_pubsub, + ejabberd_mod_irc, + ejabberd_mod_muc, + ejabberd_offline, + random_generator + ]}, + {applications, [kernel, stdlib]}, + {env, []}, + {mod, {ejabberd_app, []}}]}. + + + +% Local Variables: +% mode: erlang +% End: diff --git a/mod_multicast/src/ejabberd_c2s.erl b/mod_multicast/src/ejabberd_c2s.erl new file mode 100644 index 0000000..433b75b --- /dev/null +++ b/mod_multicast/src/ejabberd_c2s.erl @@ -0,0 +1,1863 @@ +%%%---------------------------------------------------------------------- +%%% File : ejabberd_c2s.erl +%%% Author : Alexey Shchepin +%%% Purpose : Serve C2S connection +%%% Created : 16 Nov 2002 by Alexey Shchepin +%%% Id : $Id: ejabberd_c2s.erl 952 2009-05-06 17:29:39Z badlop $ +%%%---------------------------------------------------------------------- + +-module(ejabberd_c2s). +-author('alexey@sevcom.net'). +-update_info({update, 0}). + +-behaviour(gen_fsm). + +%% External exports +-export([start/2, + start_link/2, + send_text/2, + send_element/2, + socket_type/0, + get_presence/1]). + +%% gen_fsm callbacks +-export([init/1, + wait_for_stream/2, + wait_for_auth/2, + wait_for_feature_request/2, + wait_for_bind/2, + wait_for_session/2, + wait_for_sasl_response/2, + session_established/2, + handle_event/3, + handle_sync_event/4, + code_change/4, + handle_info/3, + terminate/3]). + +-include("ejabberd.hrl"). +-include("jlib.hrl"). + +-define(SETS, gb_sets). + +-record(state, {socket, + sockmod, + socket_monitor, + streamid, + sasl_state, + access, + shaper, + zlib = false, + tls = false, + tls_required = false, + tls_enabled = false, + tls_options = [], + authenticated = false, + jid, + user = "", server = ?MYNAME, resource = "", + sid, + pres_t = ?SETS:new(), + pres_f = ?SETS:new(), + pres_a = ?SETS:new(), + pres_i = ?SETS:new(), + pres_last, pres_pri, + pres_timestamp, + pres_invis = false, + privacy_list = none, + ip, + lang}). + +%-define(DBGFSM, true). + +-ifdef(DBGFSM). +-define(FSMOPTS, [{debug, [trace]}]). +-else. +-define(FSMOPTS, []). +-endif. + +%% Module start with or without supervisor: +-ifdef(NO_TRANSIENT_SUPERVISORS). +-define(SUPERVISOR_START, gen_fsm:start(ejabberd_c2s, [SockData, Opts], + ?FSMOPTS)). +-else. +-define(SUPERVISOR_START, supervisor:start_child(ejabberd_c2s_sup, + [SockData, Opts])). +-endif. + +%% This is the timeout to apply between event when starting a new +%% session: +-define(C2S_OPEN_TIMEOUT, 5000). +-define(C2S_HIBERNATE_TIMEOUT, 90000). + +-define(STREAM_HEADER, + "" + "" + ). + +-define(STREAM_TRAILER, ""). + +-define(INVALID_NS_ERR, + xml:element_to_string(?SERR_INVALID_NAMESPACE)). +-define(INVALID_XML_ERR, + xml:element_to_string(?SERR_XML_NOT_WELL_FORMED)). +-define(HOST_UNKNOWN_ERR, + xml:element_to_string(?SERR_HOST_UNKNOWN)). +-define(POLICY_VIOLATION_ERR(Lang, Text), + xml:element_to_string(?SERRT_POLICY_VIOLATION(Lang, Text))). + +%%%---------------------------------------------------------------------- +%%% API +%%%---------------------------------------------------------------------- +start(SockData, Opts) -> + ?SUPERVISOR_START. + +start_link(SockData, Opts) -> + gen_fsm:start_link(ejabberd_c2s, [SockData, Opts], ?FSMOPTS). + +socket_type() -> + xml_stream. + +%% Return Username, Resource and presence information +get_presence(FsmRef) -> + gen_fsm:sync_send_all_state_event(FsmRef, {get_presence}, 1000). + +%%%---------------------------------------------------------------------- +%%% Callback functions from gen_fsm +%%%---------------------------------------------------------------------- + +%%---------------------------------------------------------------------- +%% Func: init/1 +%% Returns: {ok, StateName, StateData} | +%% {ok, StateName, StateData, Timeout} | +%% ignore | +%% {stop, StopReason} +%%---------------------------------------------------------------------- +init([{SockMod, Socket}, Opts]) -> + Access = case lists:keysearch(access, 1, Opts) of + {value, {_, A}} -> A; + _ -> all + end, + Shaper = case lists:keysearch(shaper, 1, Opts) of + {value, {_, S}} -> S; + _ -> none + end, + Zlib = lists:member(zlib, Opts), + StartTLS = lists:member(starttls, Opts), + StartTLSRequired = lists:member(starttls_required, Opts), + TLSEnabled = lists:member(tls, Opts), + TLS = StartTLS orelse StartTLSRequired orelse TLSEnabled, + TLSOpts = lists:filter(fun({certfile, _}) -> true; + (_) -> false + end, Opts), + IP = peerip(SockMod, Socket), + Socket1 = + if + TLSEnabled -> + SockMod:starttls(Socket, TLSOpts); + true -> + Socket + end, + SocketMonitor = SockMod:monitor(Socket1), + {ok, wait_for_stream, #state{socket = Socket1, + sockmod = SockMod, + socket_monitor = SocketMonitor, + zlib = Zlib, + tls = TLS, + tls_required = StartTLSRequired, + tls_enabled = TLSEnabled, + tls_options = TLSOpts, + streamid = new_id(), + access = Access, + shaper = Shaper, + ip = IP}, ?C2S_OPEN_TIMEOUT}. + + +%%---------------------------------------------------------------------- +%% Func: StateName/2 +%% Returns: {next_state, NextStateName, NextStateData} | +%% {next_state, NextStateName, NextStateData, Timeout} | +%% {stop, Reason, NewStateData} +%%---------------------------------------------------------------------- + +wait_for_stream({xmlstreamstart, _Name, Attrs}, StateData) -> + DefaultLang = case ?MYLANG of + undefined -> + " xml:lang='en'"; + DL -> + " xml:lang='" ++ DL ++ "'" + end, + case xml:get_attr_s("xmlns:stream", Attrs) of + ?NS_STREAM -> + Server = jlib:nameprep(xml:get_attr_s("to", Attrs)), + case lists:member(Server, ?MYHOSTS) of + true -> + Lang = xml:get_attr_s("xml:lang", Attrs), + change_shaper(StateData, jlib:make_jid("", Server, "")), + case xml:get_attr_s("version", Attrs) of + "1.0" -> + Header = io_lib:format(?STREAM_HEADER, + [StateData#state.streamid, + Server, + " version='1.0'", + DefaultLang]), + send_text(StateData, Header), + case StateData#state.authenticated of + false -> + SASLState = + cyrsasl:server_new( + "jabber", Server, "", [], + fun(U) -> + ejabberd_auth:get_password( + U, Server) + end, + fun(U, P) -> + ejabberd_auth:check_password( + U, Server, P) + end), + Mechs = lists:map( + fun(S) -> + {xmlelement, "mechanism", [], + [{xmlcdata, S}]} + end, cyrsasl:listmech(Server)), + SockMod = + (StateData#state.sockmod):get_sockmod( + StateData#state.socket), + Zlib = StateData#state.zlib, + CompressFeature = + case Zlib andalso + (SockMod == gen_tcp) of + true -> + [{xmlelement, "compression", + [{"xmlns", ?NS_FEATURE_COMPRESS}], + [{xmlelement, "method", + [], [{xmlcdata, "zlib"}]}]}]; + _ -> + [] + end, + TLS = StateData#state.tls, + TLSEnabled = StateData#state.tls_enabled, + TLSRequired = StateData#state.tls_required, + TLSFeature = + case (TLS == true) andalso + (TLSEnabled == false) andalso + (SockMod == gen_tcp) of + true -> + case TLSRequired of + true -> + [{xmlelement, "starttls", + [{"xmlns", ?NS_TLS}], + [{xmlelement, "required", + [], []}]}]; + _ -> + [{xmlelement, "starttls", + [{"xmlns", ?NS_TLS}], []}] + end; + false -> + [] + end, + send_element(StateData, + {xmlelement, "stream:features", [], + TLSFeature ++ CompressFeature ++ + [{xmlelement, "mechanisms", + [{"xmlns", ?NS_SASL}], + Mechs}] ++ + ejabberd_hooks:run_fold( + c2s_stream_features, + Server, + [], [])}), + fsm_next_state(wait_for_feature_request, + StateData#state{ + server = Server, + sasl_state = SASLState, + lang = Lang}); + _ -> + case StateData#state.resource of + "" -> + send_element( + StateData, + {xmlelement, "stream:features", [], + [{xmlelement, "bind", + [{"xmlns", ?NS_BIND}], []}, + {xmlelement, "session", + [{"xmlns", ?NS_SESSION}], []}]}), + fsm_next_state(wait_for_bind, + StateData#state{ + server = Server, + lang = Lang}); + _ -> + send_element( + StateData, + {xmlelement, "stream:features", [], []}), + fsm_next_state(wait_for_session, + StateData#state{ + server = Server, + lang = Lang}) + end + end; + _ -> + Header = io_lib:format( + ?STREAM_HEADER, + [StateData#state.streamid, Server, "", + DefaultLang]), + if + (not StateData#state.tls_enabled) and + StateData#state.tls_required -> + send_text(StateData, + Header ++ + ?POLICY_VIOLATION_ERR( + Lang, + "Use of STARTTLS required") ++ + ?STREAM_TRAILER), + {stop, normal, StateData}; + true -> + send_text(StateData, Header), + fsm_next_state(wait_for_auth, + StateData#state{ + server = Server, + lang = Lang}) + end + end; + _ -> + Header = io_lib:format( + ?STREAM_HEADER, + [StateData#state.streamid, ?MYNAME, "", + DefaultLang]), + send_text(StateData, + Header ++ ?HOST_UNKNOWN_ERR ++ ?STREAM_TRAILER), + {stop, normal, StateData} + end; + _ -> + Header = io_lib:format( + ?STREAM_HEADER, + [StateData#state.streamid, ?MYNAME, "", DefaultLang]), + send_text(StateData, + Header ++ ?INVALID_NS_ERR ++ ?STREAM_TRAILER), + {stop, normal, StateData} + end; + +wait_for_stream(timeout, StateData) -> + {stop, normal, StateData}; + +wait_for_stream({xmlstreamelement, _}, StateData) -> + send_text(StateData, ?INVALID_XML_ERR ++ ?STREAM_TRAILER), + {stop, normal, StateData}; + +wait_for_stream({xmlstreamend, _}, StateData) -> + send_text(StateData, ?INVALID_XML_ERR ++ ?STREAM_TRAILER), + {stop, normal, StateData}; + +wait_for_stream({xmlstreamerror, _}, StateData) -> + Header = io_lib:format(?STREAM_HEADER, + ["none", ?MYNAME, " version='1.0'", ""]), + send_text(StateData, + Header ++ ?INVALID_XML_ERR ++ ?STREAM_TRAILER), + {stop, normal, StateData}; + +wait_for_stream(closed, StateData) -> + {stop, normal, StateData}. + + +wait_for_auth({xmlstreamelement, El}, StateData) -> + case is_auth_packet(El) of + {auth, _ID, get, {U, _, _, _}} -> + {xmlelement, Name, Attrs, _Els} = jlib:make_result_iq_reply(El), + case U of + "" -> + UCdata = []; + _ -> + UCdata = [{xmlcdata, U}] + end, + Res = case ejabberd_auth:plain_password_required( + StateData#state.server) of + false -> + {xmlelement, Name, Attrs, + [{xmlelement, "query", [{"xmlns", ?NS_AUTH}], + [{xmlelement, "username", [], UCdata}, + {xmlelement, "password", [], []}, + {xmlelement, "digest", [], []}, + {xmlelement, "resource", [], []} + ]}]}; + true -> + {xmlelement, Name, Attrs, + [{xmlelement, "query", [{"xmlns", ?NS_AUTH}], + [{xmlelement, "username", [], UCdata}, + {xmlelement, "password", [], []}, + {xmlelement, "resource", [], []} + ]}]} + end, + send_element(StateData, Res), + fsm_next_state(wait_for_auth, StateData); + {auth, _ID, set, {_U, _P, _D, ""}} -> + Err = jlib:make_error_reply( + El, + ?ERR_AUTH_NO_RESOURCE_PROVIDED(StateData#state.lang)), + send_element(StateData, Err), + fsm_next_state(wait_for_auth, StateData); + {auth, _ID, set, {U, P, D, R}} -> + JID = jlib:make_jid(U, StateData#state.server, R), + case (JID /= error) andalso + (acl:match_rule(StateData#state.server, + StateData#state.access, JID) == allow) of + true -> + case ejabberd_auth:check_password( + U, StateData#state.server, P, + StateData#state.streamid, D) of + true -> + ?INFO_MSG( + "(~w) Accepted legacy authentication for ~s", + [StateData#state.socket, + jlib:jid_to_string(JID)]), + SID = {now(), self()}, + IP = StateData#state.ip, + ejabberd_sm:open_session( + SID, U, StateData#state.server, R, IP), + Res1 = jlib:make_result_iq_reply(El), + Res = setelement(4, Res1, []), + send_element(StateData, Res), + change_shaper(StateData, JID), + {Fs, Ts} = ejabberd_hooks:run_fold( + roster_get_subscription_lists, + StateData#state.server, + {[], []}, + [U, StateData#state.server]), + LJID = jlib:jid_tolower( + jlib:jid_remove_resource(JID)), + Fs1 = [LJID | Fs], + Ts1 = [LJID | Ts], + PrivList = + ejabberd_hooks:run_fold( + privacy_get_user_list, StateData#state.server, + none, + [U, StateData#state.server]), + fsm_next_state(session_established, + StateData#state{ + user = U, + resource = R, + jid = JID, + sid = SID, + pres_f = ?SETS:from_list(Fs1), + pres_t = ?SETS:from_list(Ts1), + privacy_list = PrivList}); + _ -> + ?INFO_MSG( + "(~w) Failed legacy authentication for ~s", + [StateData#state.socket, + jlib:jid_to_string(JID)]), + Err = jlib:make_error_reply( + El, ?ERR_NOT_AUTHORIZED), + send_element(StateData, Err), + fsm_next_state(wait_for_auth, StateData) + end; + _ -> + if + JID == error -> + ?INFO_MSG( + "(~w) Forbidden legacy authentication for " + "username '~s' with resource '~s'", + [StateData#state.socket, U, R]), + Err = jlib:make_error_reply(El, ?ERR_JID_MALFORMED), + send_element(StateData, Err), + fsm_next_state(wait_for_auth, StateData); + true -> + ?INFO_MSG( + "(~w) Forbidden legacy authentication for ~s", + [StateData#state.socket, + jlib:jid_to_string(JID)]), + Err = jlib:make_error_reply(El, ?ERR_NOT_ALLOWED), + send_element(StateData, Err), + fsm_next_state(wait_for_auth, StateData) + end + end; + _ -> + process_unauthenticated_stanza(StateData, El), + fsm_next_state(wait_for_auth, StateData) + end; + +wait_for_auth(timeout, StateData) -> + {stop, normal, StateData}; + +wait_for_auth({xmlstreamend, _Name}, StateData) -> + send_text(StateData, ?STREAM_TRAILER), + {stop, normal, StateData}; + +wait_for_auth({xmlstreamerror, _}, StateData) -> + send_text(StateData, ?INVALID_XML_ERR ++ ?STREAM_TRAILER), + {stop, normal, StateData}; + +wait_for_auth(closed, StateData) -> + {stop, normal, StateData}. + + +wait_for_feature_request({xmlstreamelement, El}, StateData) -> + {xmlelement, Name, Attrs, Els} = El, + Zlib = StateData#state.zlib, + TLS = StateData#state.tls, + TLSEnabled = StateData#state.tls_enabled, + TLSRequired = StateData#state.tls_required, + SockMod = (StateData#state.sockmod):get_sockmod(StateData#state.socket), + case {xml:get_attr_s("xmlns", Attrs), Name} of + {?NS_SASL, "auth"} when not ((SockMod == gen_tcp) and TLSRequired) -> + Mech = xml:get_attr_s("mechanism", Attrs), + ClientIn = jlib:decode_base64(xml:get_cdata(Els)), + case cyrsasl:server_start(StateData#state.sasl_state, + Mech, + ClientIn) of + {ok, Props} -> + (StateData#state.sockmod):reset_stream( + StateData#state.socket), + send_element(StateData, + {xmlelement, "success", + [{"xmlns", ?NS_SASL}], []}), + U = xml:get_attr_s(username, Props), + ?INFO_MSG("(~w) Accepted authentication for ~s", + [StateData#state.socket, U]), + fsm_next_state(wait_for_stream, + StateData#state{ + streamid = new_id(), + authenticated = true, + user = U }); + {continue, ServerOut, NewSASLState} -> + send_element(StateData, + {xmlelement, "challenge", + [{"xmlns", ?NS_SASL}], + [{xmlcdata, + jlib:encode_base64(ServerOut)}]}), + fsm_next_state(wait_for_sasl_response, + StateData#state{ + sasl_state = NewSASLState}); + {error, Error, Username} -> + ?INFO_MSG( + "(~w) Failed authentication for ~s@~s", + [StateData#state.socket, + Username, StateData#state.server]), + send_element(StateData, + {xmlelement, "failure", + [{"xmlns", ?NS_SASL}], + [{xmlelement, Error, [], []}]}), + {next_state, wait_for_feature_request, StateData, + ?C2S_OPEN_TIMEOUT}; + {error, Error} -> + send_element(StateData, + {xmlelement, "failure", + [{"xmlns", ?NS_SASL}], + [{xmlelement, Error, [], []}]}), + fsm_next_state(wait_for_feature_request, StateData) + end; + {?NS_TLS, "starttls"} when TLS == true, + TLSEnabled == false, + SockMod == gen_tcp -> + TLSOpts = case ejabberd_config:get_local_option( + {domain_certfile, StateData#state.server}) of + undefined -> + StateData#state.tls_options; + CertFile -> + [{certfile, CertFile} | + lists:keydelete( + certfile, 1, StateData#state.tls_options)] + end, + Socket = StateData#state.socket, + TLSSocket = (StateData#state.sockmod):starttls( + Socket, TLSOpts, + xml:element_to_string( + {xmlelement, "proceed", [{"xmlns", ?NS_TLS}], []})), + fsm_next_state(wait_for_stream, + StateData#state{socket = TLSSocket, + streamid = new_id(), + tls_enabled = true + }); + {?NS_COMPRESS, "compress"} when Zlib == true, + SockMod == gen_tcp -> + case xml:get_subtag(El, "method") of + false -> + send_element(StateData, + {xmlelement, "failure", + [{"xmlns", ?NS_COMPRESS}], + [{xmlelement, "setup-failed", [], []}]}), + fsm_next_state(wait_for_feature_request, StateData); + Method -> + case xml:get_tag_cdata(Method) of + "zlib" -> + Socket = StateData#state.socket, + ZlibSocket = (StateData#state.sockmod):compress( + Socket, + xml:element_to_string( + {xmlelement, "compressed", + [{"xmlns", ?NS_COMPRESS}], []})), + fsm_next_state(wait_for_stream, + StateData#state{socket = ZlibSocket, + streamid = new_id() + }); + _ -> + send_element(StateData, + {xmlelement, "failure", + [{"xmlns", ?NS_COMPRESS}], + [{xmlelement, "unsupported-method", + [], []}]}), + fsm_next_state(wait_for_feature_request, + StateData) + end + end; + _ -> + if + (SockMod == gen_tcp) and TLSRequired -> + Lang = StateData#state.lang, + send_text(StateData, ?POLICY_VIOLATION_ERR( + Lang, + "Use of STARTTLS required") ++ + ?STREAM_TRAILER), + {stop, normal, StateData}; + true -> + process_unauthenticated_stanza(StateData, El), + fsm_next_state(wait_for_feature_request, StateData) + end + end; + +wait_for_feature_request(timeout, StateData) -> + {stop, normal, StateData}; + +wait_for_feature_request({xmlstreamend, _Name}, StateData) -> + send_text(StateData, ?STREAM_TRAILER), + {stop, normal, StateData}; + +wait_for_feature_request({xmlstreamerror, _}, StateData) -> + send_text(StateData, ?INVALID_XML_ERR ++ ?STREAM_TRAILER), + {stop, normal, StateData}; + +wait_for_feature_request(closed, StateData) -> + {stop, normal, StateData}. + + +wait_for_sasl_response({xmlstreamelement, El}, StateData) -> + {xmlelement, Name, Attrs, Els} = El, + case {xml:get_attr_s("xmlns", Attrs), Name} of + {?NS_SASL, "response"} -> + ClientIn = jlib:decode_base64(xml:get_cdata(Els)), + case cyrsasl:server_step(StateData#state.sasl_state, + ClientIn) of + {ok, Props} -> + (StateData#state.sockmod):reset_stream( + StateData#state.socket), + send_element(StateData, + {xmlelement, "success", + [{"xmlns", ?NS_SASL}], []}), + U = xml:get_attr_s(username, Props), + ?INFO_MSG("(~w) Accepted authentication for ~s", + [StateData#state.socket, U]), + fsm_next_state(wait_for_stream, + StateData#state{ + streamid = new_id(), + authenticated = true, + user = U}); + {continue, ServerOut, NewSASLState} -> + send_element(StateData, + {xmlelement, "challenge", + [{"xmlns", ?NS_SASL}], + [{xmlcdata, + jlib:encode_base64(ServerOut)}]}), + fsm_next_state(wait_for_sasl_response, + StateData#state{sasl_state = NewSASLState}); + {error, Error, Username} -> + ?INFO_MSG( + "(~w) Failed authentication for ~s@~s", + [StateData#state.socket, + Username, StateData#state.server]), + send_element(StateData, + {xmlelement, "failure", + [{"xmlns", ?NS_SASL}], + [{xmlelement, Error, [], []}]}), + fsm_next_state(wait_for_feature_request, StateData); + {error, Error} -> + send_element(StateData, + {xmlelement, "failure", + [{"xmlns", ?NS_SASL}], + [{xmlelement, Error, [], []}]}), + fsm_next_state(wait_for_feature_request, StateData) + end; + _ -> + process_unauthenticated_stanza(StateData, El), + fsm_next_state(wait_for_feature_request, StateData) + end; + +wait_for_sasl_response(timeout, StateData) -> + {stop, normal, StateData}; + +wait_for_sasl_response({xmlstreamend, _Name}, StateData) -> + send_text(StateData, ?STREAM_TRAILER), + {stop, normal, StateData}; + +wait_for_sasl_response({xmlstreamerror, _}, StateData) -> + send_text(StateData, ?INVALID_XML_ERR ++ ?STREAM_TRAILER), + {stop, normal, StateData}; + +wait_for_sasl_response(closed, StateData) -> + {stop, normal, StateData}. + + + +wait_for_bind({xmlstreamelement, El}, StateData) -> + case jlib:iq_query_info(El) of + #iq{type = set, xmlns = ?NS_BIND, sub_el = SubEl} = IQ -> + U = StateData#state.user, + R1 = xml:get_path_s(SubEl, [{elem, "resource"}, cdata]), + R = case jlib:resourceprep(R1) of + error -> error; + "" -> + lists:concat( + [randoms:get_string() | tuple_to_list(now())]); + Resource -> Resource + end, + case R of + error -> + Err = jlib:make_error_reply(El, ?ERR_BAD_REQUEST), + send_element(StateData, Err), + fsm_next_state(wait_for_bind, StateData); + _ -> + JID = jlib:make_jid(U, StateData#state.server, R), + Res = IQ#iq{type = result, + sub_el = [{xmlelement, "bind", + [{"xmlns", ?NS_BIND}], + [{xmlelement, "jid", [], + [{xmlcdata, + jlib:jid_to_string(JID)}]}]}]}, + send_element(StateData, jlib:iq_to_xml(Res)), + fsm_next_state(wait_for_session, + StateData#state{resource = R, jid = JID}) + end; + _ -> + fsm_next_state(wait_for_bind, StateData) + end; + +wait_for_bind(timeout, StateData) -> + {stop, normal, StateData}; + +wait_for_bind({xmlstreamend, _Name}, StateData) -> + send_text(StateData, ?STREAM_TRAILER), + {stop, normal, StateData}; + +wait_for_bind({xmlstreamerror, _}, StateData) -> + send_text(StateData, ?INVALID_XML_ERR ++ ?STREAM_TRAILER), + {stop, normal, StateData}; + +wait_for_bind(closed, StateData) -> + {stop, normal, StateData}. + + + +wait_for_session({xmlstreamelement, El}, StateData) -> + case jlib:iq_query_info(El) of + #iq{type = set, xmlns = ?NS_SESSION} -> + U = StateData#state.user, + R = StateData#state.resource, + JID = StateData#state.jid, + case acl:match_rule(StateData#state.server, + StateData#state.access, JID) of + allow -> + ?INFO_MSG("(~w) Opened session for ~s", + [StateData#state.socket, + jlib:jid_to_string(JID)]), + SID = {now(), self()}, + IP = StateData#state.ip, + ejabberd_sm:open_session( + SID, U, StateData#state.server, R, IP), + Res = jlib:make_result_iq_reply(El), + send_element(StateData, Res), + change_shaper(StateData, JID), + {Fs, Ts} = ejabberd_hooks:run_fold( + roster_get_subscription_lists, + StateData#state.server, + {[], []}, + [U, StateData#state.server]), + LJID = jlib:jid_tolower(jlib:jid_remove_resource(JID)), + Fs1 = [LJID | Fs], + Ts1 = [LJID | Ts], + PrivList = + ejabberd_hooks:run_fold( + privacy_get_user_list, StateData#state.server, + none, + [U, StateData#state.server]), + fsm_next_state(session_established, + StateData#state{ + sid = SID, + pres_f = ?SETS:from_list(Fs1), + pres_t = ?SETS:from_list(Ts1), + privacy_list = PrivList}); + _ -> + ?INFO_MSG("(~w) Forbidden session for ~s", + [StateData#state.socket, + jlib:jid_to_string(JID)]), + Err = jlib:make_error_reply(El, ?ERR_NOT_ALLOWED), + send_element(StateData, Err), + fsm_next_state(wait_for_session, StateData) + end; + _ -> + fsm_next_state(wait_for_session, StateData) + end; + +wait_for_session(timeout, StateData) -> + {stop, normal, StateData}; + +wait_for_session({xmlstreamend, _Name}, StateData) -> + send_text(StateData, ?STREAM_TRAILER), + {stop, normal, StateData}; + +wait_for_session({xmlstreamerror, _}, StateData) -> + send_text(StateData, ?INVALID_XML_ERR ++ ?STREAM_TRAILER), + {stop, normal, StateData}; + +wait_for_session(closed, StateData) -> + {stop, normal, StateData}. + + + + +session_established({xmlstreamelement, El}, StateData) -> + {xmlelement, Name, Attrs, _Els} = El, + User = StateData#state.user, + Server = StateData#state.server, + % TODO: check 'from' attribute in stanza + FromJID = StateData#state.jid, + To = xml:get_attr_s("to", Attrs), + ToJID = case To of + "" -> + jlib:make_jid(User, Server, ""); + _ -> + jlib:string_to_jid(To) + end, + NewEl1 = jlib:remove_attr("xmlns", El), + NewEl = case xml:get_attr_s("xml:lang", Attrs) of + "" -> + case StateData#state.lang of + "" -> NewEl1; + Lang -> + xml:replace_tag_attr("xml:lang", Lang, NewEl1) + end; + _ -> + NewEl1 + end, + NewState = + case ToJID of + error -> + case xml:get_attr_s("type", Attrs) of + "error" -> StateData; + "result" -> StateData; + _ -> + Err = jlib:make_error_reply(NewEl, ?ERR_JID_MALFORMED), + send_element(StateData, Err), + StateData + end; + _ -> + case Name of + "presence" -> + PresenceEl = ejabberd_hooks:run_fold( + c2s_update_presence, + Server, + NewEl, + [User, Server]), + case ToJID of + #jid{user = User, + server = Server, + resource = ""} -> + ?DEBUG("presence_update(~p,~n\t~p,~n\t~p)", + [FromJID, PresenceEl, StateData]), + presence_update(FromJID, PresenceEl, + StateData); + _ -> + presence_track(FromJID, ToJID, PresenceEl, + StateData) + end; + "iq" -> + case StateData#state.privacy_list of + none -> + ejabberd_router:route(FromJID, ToJID, NewEl), + StateData; + _PrivList -> + case jlib:iq_query_info(NewEl) of + #iq{xmlns = ?NS_PRIVACY} = IQ -> + process_privacy_iq( + FromJID, ToJID, IQ, StateData); + _ -> + ejabberd_hooks:run( + user_send_packet, + Server, + [FromJID, ToJID, NewEl]), + ejabberd_router:route( + FromJID, ToJID, NewEl), + StateData + end + end; + "message" -> + ejabberd_hooks:run(user_send_packet, + Server, + [FromJID, ToJID, NewEl]), + ejabberd_router:route(FromJID, ToJID, NewEl), + StateData; + _ -> + StateData + end + end, + ejabberd_hooks:run(c2s_loop_debug, [{xmlstreamelement, El}]), + fsm_next_state(session_established, NewState); + +%% We hibernate the process to reduce memory consumption after a +%% configurable activity timeout +session_established(timeout, StateData) -> + %% TODO: Options must be stored in state: + Options = [], + proc_lib:hibernate(gen_fsm, enter_loop, + [?MODULE, Options, session_established, StateData]), + fsm_next_state(session_established, StateData); + +session_established({xmlstreamend, _Name}, StateData) -> + send_text(StateData, ?STREAM_TRAILER), + {stop, normal, StateData}; + +session_established({xmlstreamerror, _}, StateData) -> + send_text(StateData, ?INVALID_XML_ERR ++ ?STREAM_TRAILER), + {stop, normal, StateData}; + +session_established(closed, StateData) -> + {stop, normal, StateData}. + + + +%%---------------------------------------------------------------------- +%% Func: StateName/3 +%% Returns: {next_state, NextStateName, NextStateData} | +%% {next_state, NextStateName, NextStateData, Timeout} | +%% {reply, Reply, NextStateName, NextStateData} | +%% {reply, Reply, NextStateName, NextStateData, Timeout} | +%% {stop, Reason, NewStateData} | +%% {stop, Reason, Reply, NewStateData} +%%---------------------------------------------------------------------- +%state_name(Event, From, StateData) -> +% Reply = ok, +% {reply, Reply, state_name, StateData}. + +%%---------------------------------------------------------------------- +%% Func: handle_event/3 +%% Returns: {next_state, NextStateName, NextStateData} | +%% {next_state, NextStateName, NextStateData, Timeout} | +%% {stop, Reason, NewStateData} +%%---------------------------------------------------------------------- +handle_event(_Event, StateName, StateData) -> + fsm_next_state(StateName, StateData). + +%%---------------------------------------------------------------------- +%% Func: handle_sync_event/4 +%% Returns: {next_state, NextStateName, NextStateData} | +%% {next_state, NextStateName, NextStateData, Timeout} | +%% {reply, Reply, NextStateName, NextStateData} | +%% {reply, Reply, NextStateName, NextStateData, Timeout} | +%% {stop, Reason, NewStateData} | +%% {stop, Reason, Reply, NewStateData} +%%---------------------------------------------------------------------- +handle_sync_event({get_presence}, _From, StateName, StateData) -> + User = StateData#state.user, + PresLast = StateData#state.pres_last, + + Show = get_showtag(PresLast), + Status = get_statustag(PresLast), + Resource = StateData#state.resource, + + Reply = {User, Resource, Show, Status}, + fsm_reply(Reply, StateName, StateData); + +handle_sync_event(_Event, _From, StateName, StateData) -> + Reply = ok, + fsm_reply(Reply, StateName, StateData). + +code_change(_OldVsn, StateName, StateData, _Extra) -> + {ok, StateName, StateData}. + +%%---------------------------------------------------------------------- +%% Func: handle_info/3 +%% Returns: {next_state, NextStateName, NextStateData} | +%% {next_state, NextStateName, NextStateData, Timeout} | +%% {stop, Reason, NewStateData} +%%---------------------------------------------------------------------- +handle_info({send_text, Text}, StateName, StateData) -> + send_text(StateData, Text), + ejabberd_hooks:run(c2s_loop_debug, [Text]), + + fsm_next_state(StateName, StateData); +handle_info(replaced, _StateName, StateData) -> + Lang = StateData#state.lang, + send_text(StateData, + xml:element_to_string( + ?SERRT_CONFLICT(Lang, "Replaced by new connection")) + ++ ?STREAM_TRAILER), + {stop, normal, StateData#state{authenticated = replaced}}; +handle_info({route, From, To, Packet}, StateName, StateData) -> + {xmlelement, Name, Attrs, Els} = Packet, + {Pass, NewAttrs, NewState} = + case Name of + "presence" -> + case xml:get_attr_s("type", Attrs) of + "probe" -> + LFrom = jlib:jid_tolower(From), + LBFrom = jlib:jid_remove_resource(LFrom), + NewStateData = + case ?SETS:is_element( + LFrom, StateData#state.pres_a) orelse + ?SETS:is_element( + LBFrom, StateData#state.pres_a) of + true -> + StateData; + false -> + case ?SETS:is_element( + LFrom, StateData#state.pres_f) of + true -> + A = ?SETS:add_element( + LFrom, + StateData#state.pres_a), + StateData#state{pres_a = A}; + false -> + case ?SETS:is_element( + LBFrom, StateData#state.pres_f) of + true -> + A = ?SETS:add_element( + LBFrom, + StateData#state.pres_a), + StateData#state{pres_a = A}; + false -> + StateData + end + end + end, + process_presence_probe(From, To, NewStateData), + {false, Attrs, NewStateData}; + "error" -> + NewA = remove_element(jlib:jid_tolower(From), + StateData#state.pres_a), + {true, Attrs, StateData#state{pres_a = NewA}}; + "invisible" -> + Attrs1 = lists:keydelete("type", 1, Attrs), + {true, [{"type", "unavailable"} | Attrs1], StateData}; + "subscribe" -> + {true, Attrs, StateData}; + "subscribed" -> + {true, Attrs, StateData}; + "unsubscribe" -> + {true, Attrs, StateData}; + "unsubscribed" -> + {true, Attrs, StateData}; + _ -> + case ejabberd_hooks:run_fold( + privacy_check_packet, StateData#state.server, + allow, + [StateData#state.user, + StateData#state.server, + StateData#state.privacy_list, + {From, To, Packet}, + in]) of + allow -> + LFrom = jlib:jid_tolower(From), + LBFrom = jlib:jid_remove_resource(LFrom), + case ?SETS:is_element( + LFrom, StateData#state.pres_a) orelse + ?SETS:is_element( + LBFrom, StateData#state.pres_a) of + true -> + {true, Attrs, StateData}; + false -> + case ?SETS:is_element( + LFrom, StateData#state.pres_f) of + true -> + A = ?SETS:add_element( + LFrom, + StateData#state.pres_a), + {true, Attrs, + StateData#state{pres_a = A}}; + false -> + case ?SETS:is_element( + LBFrom, StateData#state.pres_f) of + true -> + A = ?SETS:add_element( + LBFrom, + StateData#state.pres_a), + {true, Attrs, + StateData#state{pres_a = A}}; + false -> + {true, Attrs, StateData} + end + end + end; + deny -> + {false, Attrs, StateData} + end + end; + "broadcast" -> + ?DEBUG("broadcast~n~p~n", [Els]), + case Els of + [{item, IJID, ISubscription}] -> + {false, Attrs, + roster_change(IJID, ISubscription, + StateData)}; + [{exit, Reason}] -> + {exit, Attrs, Reason}; + [{privacy_list, PrivList, PrivListName}] -> + case ejabberd_hooks:run_fold( + privacy_updated_list, StateData#state.server, + false, + [StateData#state.privacy_list, + PrivList]) of + false -> + {false, Attrs, StateData}; + NewPL -> + PrivPushIQ = + #iq{type = set, xmlns = ?NS_PRIVACY, + id = "push", + sub_el = [{xmlelement, "query", + [{"xmlns", ?NS_PRIVACY}], + [{xmlelement, "list", + [{"name", PrivListName}], + []}]}]}, + PrivPushEl = + jlib:replace_from_to( + jlib:jid_remove_resource( + StateData#state.jid), + StateData#state.jid, + jlib:iq_to_xml(PrivPushIQ)), + send_element(StateData, PrivPushEl), + {false, Attrs, StateData#state{privacy_list = NewPL}} + end; + _ -> + {false, Attrs, StateData} + end; + "iq" -> + IQ = jlib:iq_query_info(Packet), + case IQ of + #iq{xmlns = ?NS_VCARD} -> + Host = StateData#state.server, + case ets:lookup(sm_iqtable, {?NS_VCARD, Host}) of + [{_, Module, Function, Opts}] -> + gen_iq_handler:handle(Host, Module, Function, Opts, + From, To, IQ); + [] -> + Err = jlib:make_error_reply( + Packet, ?ERR_FEATURE_NOT_IMPLEMENTED), + ejabberd_router:route(To, From, Err) + end, + {false, Attrs, StateData}; + #iq{} -> + case ejabberd_hooks:run_fold( + privacy_check_packet, StateData#state.server, + allow, + [StateData#state.user, + StateData#state.server, + StateData#state.privacy_list, + {From, To, Packet}, + in]) of + allow -> + {true, Attrs, StateData}; + deny -> + Err = jlib:make_error_reply( + Packet, ?ERR_FEATURE_NOT_IMPLEMENTED), + ejabberd_router:route(To, From, Err), + {false, Attrs, StateData} + end; + _ -> + {true, Attrs, StateData} + end; + "message" -> + case ejabberd_hooks:run_fold( + privacy_check_packet, StateData#state.server, + allow, + [StateData#state.user, + StateData#state.server, + StateData#state.privacy_list, + {From, To, Packet}, + in]) of + allow -> + {true, Attrs, StateData}; + deny -> + {false, Attrs, StateData} + end; + _ -> + {true, Attrs, StateData} + end, + if + Pass == exit -> + catch send_text(StateData, ?STREAM_TRAILER), + {stop, normal, StateData}; + Pass -> + Attrs2 = jlib:replace_from_to_attrs(jlib:jid_to_string(From), + jlib:jid_to_string(To), + NewAttrs), + FixedPacket = {xmlelement, Name, Attrs2, Els}, + Text = xml:element_to_string(FixedPacket), + send_text(StateData, Text), + ejabberd_hooks:run(user_receive_packet, + StateData#state.server, + [StateData#state.jid, From, To, FixedPacket]), + ejabberd_hooks:run(c2s_loop_debug, [{route, From, To, Packet}]), + fsm_next_state(StateName, NewState); + true -> + ejabberd_hooks:run(c2s_loop_debug, [{route, From, To, Packet}]), + fsm_next_state(StateName, NewState) + end; +handle_info({'DOWN', Monitor, _Type, _Object, _Info}, _StateName, StateData) + when Monitor == StateData#state.socket_monitor -> + {stop, normal, StateData}; +handle_info(Info, StateName, StateData) -> + ?ERROR_MSG("Unexpected info: ~p", [Info]), + fsm_next_state(StateName, StateData). + +%%---------------------------------------------------------------------- +%% Func: terminate/3 +%% Purpose: Shutdown the fsm +%% Returns: any +%%---------------------------------------------------------------------- +terminate(_Reason, StateName, StateData) -> + case StateName of + session_established -> + case StateData#state.authenticated of + replaced -> + ?INFO_MSG("(~w) Replaced session for ~s", + [StateData#state.socket, + jlib:jid_to_string(StateData#state.jid)]), + From = StateData#state.jid, + Packet = {xmlelement, "presence", + [{"type", "unavailable"}], + [{xmlelement, "status", [], + [{xmlcdata, "Replaced by new connection"}]}]}, + ejabberd_sm:close_session_unset_presence( + StateData#state.sid, + StateData#state.user, + StateData#state.server, + StateData#state.resource, + "Replaced by new connection"), + presence_broadcast( + StateData, From, StateData#state.pres_a, Packet), + presence_broadcast( + StateData, From, StateData#state.pres_i, Packet); + _ -> + ?INFO_MSG("(~w) Close session for ~s", + [StateData#state.socket, + jlib:jid_to_string(StateData#state.jid)]), + + EmptySet = ?SETS:new(), + case StateData of + #state{pres_last = undefined, + pres_a = EmptySet, + pres_i = EmptySet, + pres_invis = false} -> + ejabberd_sm:close_session(StateData#state.sid, + StateData#state.user, + StateData#state.server, + StateData#state.resource); + _ -> + From = StateData#state.jid, + Packet = {xmlelement, "presence", + [{"type", "unavailable"}], []}, + ejabberd_sm:close_session_unset_presence( + StateData#state.sid, + StateData#state.user, + StateData#state.server, + StateData#state.resource, + ""), + presence_broadcast( + StateData, From, StateData#state.pres_a, Packet), + presence_broadcast( + StateData, From, StateData#state.pres_i, Packet) + end + end; + _ -> + ok + end, + (StateData#state.sockmod):close(StateData#state.socket), + ok. + +%%%---------------------------------------------------------------------- +%%% Internal functions +%%%---------------------------------------------------------------------- + +change_shaper(StateData, JID) -> + Shaper = acl:match_rule(StateData#state.server, + StateData#state.shaper, JID), + (StateData#state.sockmod):change_shaper(StateData#state.socket, Shaper). + +send_text(StateData, Text) -> + ?DEBUG("Send XML on stream = ~p", [lists:flatten(Text)]), + (StateData#state.sockmod):send(StateData#state.socket, Text). + +send_element(StateData, El) -> + send_text(StateData, xml:element_to_string(El)). + + +new_id() -> + randoms:get_string(). + + +is_auth_packet(El) -> + case jlib:iq_query_info(El) of + #iq{id = ID, type = Type, xmlns = ?NS_AUTH, sub_el = SubEl} -> + {xmlelement, _, _, Els} = SubEl, + {auth, ID, Type, + get_auth_tags(Els, "", "", "", "")}; + _ -> + false + end. + + +get_auth_tags([{xmlelement, Name, _Attrs, Els}| L], U, P, D, R) -> + CData = xml:get_cdata(Els), + case Name of + "username" -> + get_auth_tags(L, CData, P, D, R); + "password" -> + get_auth_tags(L, U, CData, D, R); + "digest" -> + get_auth_tags(L, U, P, CData, R); + "resource" -> + get_auth_tags(L, U, P, D, CData); + _ -> + get_auth_tags(L, U, P, D, R) + end; +get_auth_tags([_ | L], U, P, D, R) -> + get_auth_tags(L, U, P, D, R); +get_auth_tags([], U, P, D, R) -> + {U, P, D, R}. + + +process_presence_probe(From, To, StateData) -> + LFrom = jlib:jid_tolower(From), + LBFrom = setelement(3, LFrom, ""), + case StateData#state.pres_last of + undefined -> + ok; + _ -> + Cond1 = (not StateData#state.pres_invis) + andalso (?SETS:is_element(LFrom, StateData#state.pres_f) + orelse + ((LFrom /= LBFrom) andalso + ?SETS:is_element(LBFrom, StateData#state.pres_f))) + andalso (not + (?SETS:is_element(LFrom, StateData#state.pres_i) + orelse + ((LFrom /= LBFrom) andalso + ?SETS:is_element(LBFrom, StateData#state.pres_i)))), + Cond2 = StateData#state.pres_invis + andalso ?SETS:is_element(LFrom, StateData#state.pres_f) + andalso ?SETS:is_element(LFrom, StateData#state.pres_a), + if + Cond1 -> + Packet = StateData#state.pres_last, + case ejabberd_hooks:run_fold( + privacy_check_packet, StateData#state.server, + allow, + [StateData#state.user, + StateData#state.server, + StateData#state.privacy_list, + {To, From, Packet}, + out]) of + deny -> + ok; + allow -> + ejabberd_router:route(To, From, Packet) + end; + Cond2 -> + ejabberd_router:route(To, From, + {xmlelement, "presence", + [], + []}); + true -> + ok + end + end. + +presence_update(From, Packet, StateData) -> + {xmlelement, _Name, Attrs, _Els} = Packet, + case xml:get_attr_s("type", Attrs) of + "unavailable" -> + Status = case xml:get_subtag(Packet, "status") of + false -> + ""; + StatusTag -> + xml:get_tag_cdata(StatusTag) + end, + ejabberd_sm:unset_presence(StateData#state.sid, + StateData#state.user, + StateData#state.server, + StateData#state.resource, + Status, + StateData#state.ip), + presence_broadcast(StateData, From, StateData#state.pres_a, Packet), + presence_broadcast(StateData, From, StateData#state.pres_i, Packet), + StateData#state{pres_last = undefined, + pres_a = ?SETS:new(), + pres_i = ?SETS:new(), + pres_invis = false}; + "invisible" -> + NewState = + if + not StateData#state.pres_invis -> + presence_broadcast(StateData, From, + StateData#state.pres_a, + Packet), + presence_broadcast(StateData, From, + StateData#state.pres_i, + Packet), + S1 = StateData#state{pres_last = undefined, + pres_a = ?SETS:new(), + pres_i = ?SETS:new(), + pres_invis = true}, + presence_broadcast_first(From, S1, Packet); + true -> + StateData + end, + NewState; + "error" -> + StateData; + "probe" -> + StateData; + "subscribe" -> + StateData; + "subscribed" -> + StateData; + "unsubscribe" -> + StateData; + "unsubscribed" -> + StateData; + _ -> + OldPriority = case StateData#state.pres_last of + undefined -> + 0; + OldPresence -> + get_priority_from_presence(OldPresence) + end, + NewPriority = get_priority_from_presence(Packet), + update_priority(NewPriority, Packet, StateData), + FromUnavail = (StateData#state.pres_last == undefined) or + StateData#state.pres_invis, + ?DEBUG("from unavail = ~p~n", [FromUnavail]), + NewState = + if + FromUnavail -> + ejabberd_hooks:run(user_available_hook, + StateData#state.server, + [StateData#state.jid]), + if NewPriority >= 0 -> + resend_offline_messages(StateData), + resend_subscription_requests(StateData); + true -> + ok + end, + presence_broadcast_first( + From, StateData#state{pres_last = Packet, + pres_invis = false + }, Packet); + true -> + presence_broadcast_to_trusted(StateData, + From, + StateData#state.pres_f, + StateData#state.pres_a, + Packet), + if OldPriority < 0, NewPriority >= 0 -> + resend_offline_messages(StateData); + true -> + ok + end, + StateData#state{pres_last = Packet, + pres_invis = false + } + end, + NewState + end. + +presence_track(From, To, Packet, StateData) -> + {xmlelement, _Name, Attrs, _Els} = Packet, + LTo = jlib:jid_tolower(To), + User = StateData#state.user, + Server = StateData#state.server, + case xml:get_attr_s("type", Attrs) of + "unavailable" -> + ejabberd_router:route(From, To, Packet), + I = remove_element(LTo, StateData#state.pres_i), + A = remove_element(LTo, StateData#state.pres_a), + StateData#state{pres_i = I, + pres_a = A}; + "invisible" -> + ejabberd_router:route(From, To, Packet), + I = ?SETS:add_element(LTo, StateData#state.pres_i), + A = remove_element(LTo, StateData#state.pres_a), + StateData#state{pres_i = I, + pres_a = A}; + "subscribe" -> + ejabberd_hooks:run(roster_out_subscription, + Server, + [User, Server, To, subscribe]), + ejabberd_router:route(jlib:jid_remove_resource(From), To, Packet), + StateData; + "subscribed" -> + ejabberd_hooks:run(roster_out_subscription, + Server, + [User, Server, To, subscribed]), + ejabberd_router:route(jlib:jid_remove_resource(From), To, Packet), + StateData; + "unsubscribe" -> + ejabberd_hooks:run(roster_out_subscription, + Server, + [User, Server, To, unsubscribe]), + ejabberd_router:route(jlib:jid_remove_resource(From), To, Packet), + StateData; + "unsubscribed" -> + ejabberd_hooks:run(roster_out_subscription, + Server, + [User, Server, To, unsubscribed]), + ejabberd_router:route(jlib:jid_remove_resource(From), To, Packet), + StateData; + "error" -> + ejabberd_router:route(From, To, Packet), + StateData; + "probe" -> + ejabberd_router:route(From, To, Packet), + StateData; + _ -> + case ejabberd_hooks:run_fold( + privacy_check_packet, StateData#state.server, + allow, + [StateData#state.user, + StateData#state.server, + StateData#state.privacy_list, + {From, To, Packet}, + out]) of + deny -> + ok; + allow -> + ejabberd_router:route(From, To, Packet) + end, + I = remove_element(LTo, StateData#state.pres_i), + A = ?SETS:add_element(LTo, StateData#state.pres_a), + StateData#state{pres_i = I, + pres_a = A} + end. + +%% Send presence when disconnecting +presence_broadcast(StateData, From, JIDSet, Packet) -> + JIDs = ?SETS:to_list(JIDSet), + JIDs2 = format_and_check_privacy(From, StateData, Packet, JIDs), + Server = StateData#state.server, + send_multiple(From, Server, JIDs2, Packet). + +%% Send presence when updating presence +presence_broadcast_to_trusted(StateData, From, Trusted, JIDSet, Packet) -> + JIDs = ?SETS:to_list(JIDSet), + JIDs_trusted = [JID || JID <- JIDs, ?SETS:is_element(JID, Trusted)], + JIDs2 = format_and_check_privacy(From, StateData, Packet, JIDs_trusted), + Server = StateData#state.server, + send_multiple(From, Server, JIDs2, Packet). + + +%% Send presence when connecting +presence_broadcast_first(From, StateData, Packet) -> + JIDsProbe = + ?SETS:fold( + fun(JID, L) -> [JID | L] end, + [], + StateData#state.pres_t), + PacketProbe = {xmlelement, "presence", [{"type", "probe"}], []}, + JIDs2Probe = format_and_check_privacy(From, StateData, Packet, JIDsProbe), + Server = StateData#state.server, + send_multiple(From, Server, JIDs2Probe, PacketProbe), + if + StateData#state.pres_invis -> + StateData; + true -> + {As, JIDs} = + ?SETS:fold( + fun(JID, {A, JID_list}) -> + {?SETS:add_element(JID, A), JID_list++[JID]} + end, + {StateData#state.pres_a, []}, + StateData#state.pres_f), + JIDs2 = format_and_check_privacy(From, StateData, Packet, JIDs), + Server = StateData#state.server, + send_multiple(From, Server, JIDs2, Packet), + StateData#state{pres_a = As} + end. + +format_and_check_privacy(From, StateData, Packet, JIDs) -> + FJIDs = [jlib:make_jid(JID) || JID <- JIDs], + lists:filter( + fun(FJID) -> + case ejabberd_hooks:run_fold( + privacy_check_packet, StateData#state.server, + allow, + [StateData#state.user, + StateData#state.server, + StateData#state.privacy_list, + {From, FJID, Packet}, + out]) of + deny -> false; + allow -> true + end + end, + FJIDs). + +send_multiple(From, Server, JIDs, Packet) -> + ejabberd_router_multicast:route_multicast(From, Server, JIDs, Packet). + +remove_element(E, Set) -> + case ?SETS:is_element(E, Set) of + true -> + ?SETS:del_element(E, Set); + _ -> + Set + end. + + +roster_change(IJID, ISubscription, StateData) -> + LIJID = jlib:jid_tolower(IJID), + IsFrom = (ISubscription == both) or (ISubscription == from), + IsTo = (ISubscription == both) or (ISubscription == to), + OldIsFrom = ?SETS:is_element(LIJID, StateData#state.pres_f), + FSet = if + IsFrom -> + ?SETS:add_element(LIJID, StateData#state.pres_f); + true -> + remove_element(LIJID, StateData#state.pres_f) + end, + TSet = if + IsTo -> + ?SETS:add_element(LIJID, StateData#state.pres_t); + true -> + remove_element(LIJID, StateData#state.pres_t) + end, + case StateData#state.pres_last of + undefined -> + StateData#state{pres_f = FSet, pres_t = TSet}; + P -> + ?DEBUG("roster changed for ~p~n", [StateData#state.user]), + From = StateData#state.jid, + To = jlib:make_jid(IJID), + Cond1 = (not StateData#state.pres_invis) and IsFrom + and (not OldIsFrom), + Cond2 = (not IsFrom) and OldIsFrom + and (?SETS:is_element(LIJID, StateData#state.pres_a) or + ?SETS:is_element(LIJID, StateData#state.pres_i)), + if + Cond1 -> + ?DEBUG("C1: ~p~n", [LIJID]), + case ejabberd_hooks:run_fold( + privacy_check_packet, StateData#state.server, + allow, + [StateData#state.user, + StateData#state.server, + StateData#state.privacy_list, + {From, To, P}, + out]) of + deny -> + ok; + allow -> + ejabberd_router:route(From, To, P) + end, + A = ?SETS:add_element(LIJID, + StateData#state.pres_a), + StateData#state{pres_a = A, + pres_f = FSet, + pres_t = TSet}; + Cond2 -> + ?DEBUG("C2: ~p~n", [LIJID]), + PU = {xmlelement, "presence", + [{"type", "unavailable"}], []}, + case ejabberd_hooks:run_fold( + privacy_check_packet, StateData#state.server, + allow, + [StateData#state.user, + StateData#state.server, + StateData#state.privacy_list, + {From, To, PU}, + out]) of + deny -> + ok; + allow -> + ejabberd_router:route(From, To, PU) + end, + I = remove_element(LIJID, + StateData#state.pres_i), + A = remove_element(LIJID, + StateData#state.pres_a), + StateData#state{pres_i = I, + pres_a = A, + pres_f = FSet, + pres_t = TSet}; + true -> + StateData#state{pres_f = FSet, pres_t = TSet} + end + end. + + +update_priority(Priority, Packet, StateData) -> + ejabberd_sm:set_presence(StateData#state.sid, + StateData#state.user, + StateData#state.server, + StateData#state.resource, + Priority, + Packet, + StateData#state.ip). + +get_priority_from_presence(PresencePacket) -> + case xml:get_subtag(PresencePacket, "priority") of + false -> + 0; + SubEl -> + case catch list_to_integer(xml:get_tag_cdata(SubEl)) of + P when is_integer(P) -> + P; + _ -> + 0 + end + end. + +process_privacy_iq(From, To, + #iq{type = Type, sub_el = SubEl} = IQ, + StateData) -> + {Res, NewStateData} = + case Type of + get -> + R = ejabberd_hooks:run_fold( + privacy_iq_get, StateData#state.server, + {error, ?ERR_FEATURE_NOT_IMPLEMENTED}, + [From, To, IQ, StateData#state.privacy_list]), + {R, StateData}; + set -> + case ejabberd_hooks:run_fold( + privacy_iq_set, StateData#state.server, + {error, ?ERR_FEATURE_NOT_IMPLEMENTED}, + [From, To, IQ]) of + {result, R, NewPrivList} -> + {{result, R}, + StateData#state{privacy_list = NewPrivList}}; + R -> {R, StateData} + end + end, + IQRes = + case Res of + {result, Result} -> + IQ#iq{type = result, sub_el = Result}; + {error, Error} -> + IQ#iq{type = error, sub_el = [SubEl, Error]} + end, + ejabberd_router:route( + To, From, jlib:iq_to_xml(IQRes)), + NewStateData. + + +resend_offline_messages(#state{user = User, + server = Server, + privacy_list = PrivList} = StateData) -> + case ejabberd_hooks:run_fold(resend_offline_messages_hook, + Server, + [], + [User, Server]) of + Rs when is_list(Rs) -> + lists:foreach( + fun({route, + From, To, {xmlelement, Name, Attrs, Els} = Packet}) -> + Pass = case ejabberd_hooks:run_fold( + privacy_check_packet, Server, + allow, + [User, + Server, + PrivList, + {From, To, Packet}, + in]) of + allow -> + true; + deny -> + false + end, + if + Pass -> + Attrs2 = jlib:replace_from_to_attrs( + jlib:jid_to_string(From), + jlib:jid_to_string(To), + Attrs), + send_element(StateData, + {xmlelement, Name, Attrs2, Els}); + true -> + ok + end + end, Rs) + end. + +resend_subscription_requests(#state{user = User, + server = Server} = StateData) -> + PendingSubscriptions = ejabberd_hooks:run_fold( + resend_subscription_requests_hook, + Server, + [], + [User, Server]), + lists:foreach(fun(XMLPacket) -> + send_element(StateData, + XMLPacket) + end, + PendingSubscriptions). + +get_showtag(undefined) -> + "unavailable"; +get_showtag(Presence) -> + case xml:get_path_s(Presence, [{elem, "show"}, cdata]) of + "" -> "available"; + ShowTag -> ShowTag + end. + +get_statustag(undefined) -> + ""; +get_statustag(Presence) -> + case xml:get_path_s(Presence, [{elem, "status"}, cdata]) of + ShowTag -> ShowTag + end. + +process_unauthenticated_stanza(StateData, El) -> + case jlib:iq_query_info(El) of + #iq{} = IQ -> + Res = ejabberd_hooks:run_fold(c2s_unauthenticated_iq, + StateData#state.server, + empty, + [StateData#state.server, IQ]), + case Res of + empty -> + % The only reasonable IQ's here are auth and register IQ's + % They contain secrets, so don't include subelements to response + ResIQ = IQ#iq{type = error, + sub_el = [?ERR_SERVICE_UNAVAILABLE]}, + Res1 = jlib:replace_from_to( + jlib:make_jid("", StateData#state.server, ""), + jlib:make_jid("", "", ""), + jlib:iq_to_xml(ResIQ)), + send_element(StateData, jlib:remove_attr("to", Res1)); + _ -> + send_element(StateData, Res) + end; + _ -> + % Drop any stanza, which isn't IQ stanza + ok + end. + +peerip(SockMod, Socket) -> + IP = case SockMod of + gen_tcp -> inet:peername(Socket); + _ -> SockMod:peername(Socket) + end, + case IP of + {ok, IPOK} -> IPOK; + _ -> undefined + end. + +%% fsm_next_state: Generate the next_state FSM tuple with different +%% timeout, depending on the future state +fsm_next_state(session_established, StateData) -> + {next_state, session_established, StateData, ?C2S_HIBERNATE_TIMEOUT}; +fsm_next_state(StateName, StateData) -> + {next_state, StateName, StateData, ?C2S_OPEN_TIMEOUT}. + +%% fsm_reply: Generate the reply FSM tuple with different timeout, +%% depending on the future state +fsm_reply(Reply, session_established, StateData) -> + {reply, Reply, session_established, StateData, ?C2S_HIBERNATE_TIMEOUT}; +fsm_reply(Reply, StateName, StateData) -> + {reply, Reply, StateName, StateData, ?C2S_OPEN_TIMEOUT}. diff --git a/mod_multicast/src/ejabberd_router_multicast.erl b/mod_multicast/src/ejabberd_router_multicast.erl new file mode 100644 index 0000000..5f6d967 --- /dev/null +++ b/mod_multicast/src/ejabberd_router_multicast.erl @@ -0,0 +1,210 @@ +%%%---------------------------------------------------------------------- +%%% File : ejabberd_router_multicast.erl +%%% Author : Badlop +%%% Purpose : Multicast router +%%% Created : 11 Aug 2007 by Badlop +%%% Id : $Id: ejabberd_router_multicast.erl 440 2007-12-06 22:36:21Z badlop $ +%%%---------------------------------------------------------------------- + +-module(ejabberd_router_multicast). +-author('alexey@sevcom.net'). +-author('badlop@ono.com'). + +-behaviour(gen_server). + +%% API +-export([route_multicast/4, + register_route/1, + unregister_route/1 + ]). + +-export([start_link/0]). + +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). + +-include("ejabberd.hrl"). +-include("jlib.hrl"). + +-record(route_multicast, {domain, pid}). +-record(state, {}). + +%%==================================================================== +%% API +%%==================================================================== +%%-------------------------------------------------------------------- +%% Function: start_link() -> {ok,Pid} | ignore | {error,Error} +%% Description: Starts the server +%%-------------------------------------------------------------------- +start_link() -> + gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). + + +route_multicast(From, Domain, Destinations, Packet) -> + case catch do_route(From, Domain, Destinations, Packet) of + {'EXIT', Reason} -> + ?ERROR_MSG("~p~nwhen processing: ~p", + [Reason, {From, Domain, Destinations, Packet}]); + _ -> + ok + end. + +register_route(Domain) -> + case jlib:nameprep(Domain) of + error -> + erlang:error({invalid_domain, Domain}); + LDomain -> + Pid = self(), + F = fun() -> + mnesia:write(#route_multicast{domain = LDomain, + pid = Pid}) + end, + mnesia:transaction(F) + end. + +unregister_route(Domain) -> + case jlib:nameprep(Domain) of + error -> + erlang:error({invalid_domain, Domain}); + LDomain -> + Pid = self(), + F = fun() -> + case mnesia:match(#route_multicast{domain = LDomain, + pid = Pid}) of + [R] -> mnesia:delete_object(R); + _ -> ok + end + end, + mnesia:transaction(F) + end. + + +%%==================================================================== +%% gen_server callbacks +%%==================================================================== + +%%-------------------------------------------------------------------- +%% Function: init(Args) -> {ok, State} | +%% {ok, State, Timeout} | +%% ignore | +%% {stop, Reason} +%% Description: Initiates the server +%%-------------------------------------------------------------------- +init([]) -> + mnesia:create_table(route_multicast, + [{ram_copies, [node()]}, + {type, bag}, + {attributes, + record_info(fields, route_multicast)}]), + mnesia:add_table_copy(route_multicast, node(), ram_copies), + mnesia:subscribe({table, route_multicast, simple}), + lists:foreach( + fun(Pid) -> + erlang:monitor(process, Pid) + end, + mnesia:dirty_select(route_multicast, [{{route_multicast, '_', '$1'}, [], ['$1']}])), + {ok, #state{}}. + +%%-------------------------------------------------------------------- +%% Function: %% handle_call(Request, From, State) -> {reply, Reply, State} | +%% {reply, Reply, State, Timeout} | +%% {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, Reply, State} | +%% {stop, Reason, State} +%% Description: Handling call messages +%%-------------------------------------------------------------------- +handle_call(_Request, _From, State) -> + Reply = ok, + {reply, Reply, State}. + +%%-------------------------------------------------------------------- +%% Function: handle_cast(Msg, State) -> {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, State} +%% Description: Handling cast messages +%%-------------------------------------------------------------------- +handle_cast(_Msg, State) -> + {noreply, State}. + +%%-------------------------------------------------------------------- +%% Function: handle_info(Info, State) -> {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, State} +%% Description: Handling all non call/cast messages +%%-------------------------------------------------------------------- +handle_info({route_multicast, From, Domain, Destinations, Packet}, State) -> + case catch do_route(From, Domain, Destinations, Packet) of + {'EXIT', Reason} -> + ?ERROR_MSG("~p~nwhen processing: ~p", + [Reason, {From, Domain, Destinations, Packet}]); + _ -> + ok + end, + {noreply, State}; +handle_info({mnesia_table_event, {write, #route_multicast{pid = Pid}, _ActivityId}}, + State) -> + erlang:monitor(process, Pid), + {noreply, State}; +handle_info({'DOWN', _Ref, _Type, Pid, _Info}, State) -> + F = fun() -> + Es = mnesia:select( + route_multicast, + [{#route_multicast{pid = Pid, _ = '_'}, + [], + ['$_']}]), + lists:foreach( + fun(E) -> + mnesia:delete_object(E) + end, Es) + end, + mnesia:transaction(F), + {noreply, State}; +handle_info(_Info, State) -> + {noreply, State}. + +%%-------------------------------------------------------------------- +%% Function: terminate(Reason, State) -> void() +%% Description: This function is called by a gen_server when it is about to +%% terminate. It should be the opposite of Module:init/1 and do any necessary +%% cleaning up. When it returns, the gen_server terminates with Reason. +%% The return value is ignored. +%%-------------------------------------------------------------------- +terminate(_Reason, _State) -> + ok. + +%%-------------------------------------------------------------------- +%% Func: code_change(OldVsn, State, Extra) -> {ok, NewState} +%% Description: Convert process state when code is changed +%%-------------------------------------------------------------------- +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%-------------------------------------------------------------------- +%%% Internal functions +%%-------------------------------------------------------------------- +%% From = #jid +%% Destinations = [#jid] +do_route(From, Domain, Destinations, Packet) -> + + ?DEBUG("route_multicast~n\tfrom ~p~n\tdomain ~p~n\tdestinations ~p~n\tpacket ~p~n", + [From, Domain, Destinations, Packet]), + + %% Try to find an appropriate multicast service + case mnesia:dirty_read(route_multicast, Domain) of + + %% If no multicast service is available in this server, send manually + [] -> do_route_normal(From, Destinations, Packet); + + %% If available, send the packet using multicast service + [R] -> + case R#route_multicast.pid of + Pid when is_pid(Pid) -> + Pid ! {route_trusted, From, Destinations, Packet}; + _ -> do_route_normal(From, Destinations, Packet) + end + end. + +do_route_normal(From, Destinations, Packet) -> + [ejabberd_router:route(From, To, Packet) || To <- Destinations]. diff --git a/mod_multicast/src/ejabberd_sup.erl b/mod_multicast/src/ejabberd_sup.erl new file mode 100644 index 0000000..abbde74 --- /dev/null +++ b/mod_multicast/src/ejabberd_sup.erl @@ -0,0 +1,174 @@ +%%%---------------------------------------------------------------------- +%%% File : ejabberd_sup.erl +%%% Author : Alexey Shchepin +%%% Purpose : +%%% Created : 31 Jan 2003 by Alexey Shchepin +%%% Id : $Id: ejabberd_sup.erl 440 2007-12-06 22:36:21Z badlop $ +%%%---------------------------------------------------------------------- + +-module(ejabberd_sup). +-author('alexey@sevcom.net'). + +-behaviour(supervisor). + +-export([start_link/0, init/1]). + +start_link() -> + supervisor:start_link({local, ?MODULE}, ?MODULE, []). + +init([]) -> + Hooks = + {ejabberd_hooks, + {ejabberd_hooks, start_link, []}, + permanent, + brutal_kill, + worker, + [ejabberd_hooks]}, + NodeGroups = + {ejabberd_node_groups, + {ejabberd_node_groups, start_link, []}, + permanent, + brutal_kill, + worker, + [ejabberd_node_groups]}, + SystemMonitor = + {ejabberd_system_monitor, + {ejabberd_system_monitor, start_link, []}, + permanent, + brutal_kill, + worker, + [ejabberd_system_monitor]}, + Router = + {ejabberd_router, + {ejabberd_router, start_link, []}, + permanent, + brutal_kill, + worker, + [ejabberd_router]}, + Router_multicast = + {ejabberd_router_multicast, + {ejabberd_router_multicast, start_link, []}, + permanent, + brutal_kill, + worker, + [ejabberd_router_multicast]}, + SM = + {ejabberd_sm, + {ejabberd_sm, start_link, []}, + permanent, + brutal_kill, + worker, + [ejabberd_sm]}, + S2S = + {ejabberd_s2s, + {ejabberd_s2s, start_link, []}, + permanent, + brutal_kill, + worker, + [ejabberd_s2s]}, + Local = + {ejabberd_local, + {ejabberd_local, start_link, []}, + permanent, + brutal_kill, + worker, + [ejabberd_local]}, + Listener = + {ejabberd_listener, + {ejabberd_listener, start_link, []}, + permanent, + infinity, + supervisor, + [ejabberd_listener]}, + ReceiverSupervisor = + {ejabberd_receiver_sup, + {ejabberd_tmp_sup, start_link, + [ejabberd_receiver_sup, ejabberd_receiver]}, + permanent, + infinity, + supervisor, + [ejabberd_tmp_sup]}, + C2SSupervisor = + {ejabberd_c2s_sup, + {ejabberd_tmp_sup, start_link, [ejabberd_c2s_sup, ejabberd_c2s]}, + permanent, + infinity, + supervisor, + [ejabberd_tmp_sup]}, + S2SInSupervisor = + {ejabberd_s2s_in_sup, + {ejabberd_tmp_sup, start_link, + [ejabberd_s2s_in_sup, ejabberd_s2s_in]}, + permanent, + infinity, + supervisor, + [ejabberd_tmp_sup]}, + S2SOutSupervisor = + {ejabberd_s2s_out_sup, + {ejabberd_tmp_sup, start_link, + [ejabberd_s2s_out_sup, ejabberd_s2s_out]}, + permanent, + infinity, + supervisor, + [ejabberd_tmp_sup]}, + ServiceSupervisor = + {ejabberd_service_sup, + {ejabberd_tmp_sup, start_link, + [ejabberd_service_sup, ejabberd_service]}, + permanent, + infinity, + supervisor, + [ejabberd_tmp_sup]}, + HTTPSupervisor = + {ejabberd_http_sup, + {ejabberd_tmp_sup, start_link, + [ejabberd_http_sup, ejabberd_http]}, + permanent, + infinity, + supervisor, + [ejabberd_tmp_sup]}, + HTTPPollSupervisor = + {ejabberd_http_poll_sup, + {ejabberd_tmp_sup, start_link, + [ejabberd_http_poll_sup, ejabberd_http_poll]}, + permanent, + infinity, + supervisor, + [ejabberd_tmp_sup]}, + FrontendSocketSupervisor = + {ejabberd_frontend_socket_sup, + {ejabberd_tmp_sup, start_link, + [ejabberd_frontend_socket_sup, ejabberd_frontend_socket]}, + permanent, + infinity, + supervisor, + [ejabberd_tmp_sup]}, + IQSupervisor = + {ejabberd_iq_sup, + {ejabberd_tmp_sup, start_link, + [ejabberd_iq_sup, gen_iq_handler]}, + permanent, + infinity, + supervisor, + [ejabberd_tmp_sup]}, + {ok, {{one_for_one, 10, 1}, + [Hooks, + NodeGroups, + SystemMonitor, + Router, + Router_multicast, + SM, + S2S, + Local, + ReceiverSupervisor, + C2SSupervisor, + S2SInSupervisor, + S2SOutSupervisor, + ServiceSupervisor, + HTTPSupervisor, + HTTPPollSupervisor, + IQSupervisor, + FrontendSocketSupervisor, + Listener]}}. + + diff --git a/mod_multicast/src/mod_muc_room.erl b/mod_multicast/src/mod_muc_room.erl new file mode 100644 index 0000000..1dab48c --- /dev/null +++ b/mod_multicast/src/mod_muc_room.erl @@ -0,0 +1,3005 @@ +%%%---------------------------------------------------------------------- +%%% File : mod_muc_room.erl +%%% Author : Alexey Shchepin +%%% Purpose : MUC room stuff +%%% Created : 19 Mar 2003 by Alexey Shchepin +%%%---------------------------------------------------------------------- + +-module(mod_muc_room). +-author('alexey@sevcom.net'). + +-behaviour(gen_fsm). + + +%% External exports +-export([start_link/9, + start_link/7, + start/9, + start/7, + route/4]). + +%% gen_fsm callbacks +-export([init/1, + normal_state/2, + handle_event/3, + handle_sync_event/4, + handle_info/3, + terminate/3, + code_change/4]). + +-include("ejabberd.hrl"). +-include("jlib.hrl"). + +-define(MAX_USERS_DEFAULT, 200). +-define(MAX_USERS_DEFAULT_LIST, + [5, 10, 20, 30, 50, 100, 200, 500, 1000, 2000, 5000]). + +-define(SETS, gb_sets). +-define(DICT, dict). + +-record(lqueue, {queue, len, max}). + +-record(config, {title = "", + allow_change_subj = true, + allow_query_users = true, + allow_private_messages = true, + public = true, + public_list = true, + persistent = false, + moderated = false, % TODO + members_by_default = true, + members_only = false, + allow_user_invites = false, + password_protected = false, + password = "", + anonymous = true, + max_users = ?MAX_USERS_DEFAULT, + logging = false + }). + +-record(user, {jid, + nick, + role, + last_presence}). + +-record(activity, {message_time = 0, + presence_time = 0, + message_shaper, + presence_shaper, + message, + presence}). + +-record(state, {room, + host, + server_host, + access, + jid, + config = #config{}, + users = ?DICT:new(), + affiliations = ?DICT:new(), + history = lqueue_new(20), + subject = "", + subject_author = "", + just_created = false, + activity = ?DICT:new(), + room_shaper, + room_queue = queue:new()}). + + +%-define(DBGFSM, true). + +-ifdef(DBGFSM). +-define(FSMOPTS, [{debug, [trace]}]). +-else. +-define(FSMOPTS, []). +-endif. + +%% Module start with or without supervisor: +-ifdef(NO_TRANSIENT_SUPERVISORS). +-define(SUPERVISOR_START, + gen_fsm:start(?MODULE, [Host, ServerHost, Access, Room, HistorySize, + RoomShaper, Creator, Nick, DefRoomOpts], + ?FSMOPTS)). +-else. +-define(SUPERVISOR_START, + Supervisor = gen_mod:get_module_proc(ServerHost, ejabberd_mod_muc_sup), + supervisor:start_child( + Supervisor, [Host, ServerHost, Access, Room, HistorySize, RoomShaper, + Creator, Nick, DefRoomOpts])). +-endif. + +%%%---------------------------------------------------------------------- +%%% API +%%%---------------------------------------------------------------------- +start(Host, ServerHost, Access, Room, HistorySize, RoomShaper, + Creator, Nick, DefRoomOpts) -> + ?SUPERVISOR_START. + +start(Host, ServerHost, Access, Room, HistorySize, RoomShaper, Opts) -> + Supervisor = gen_mod:get_module_proc(ServerHost, ejabberd_mod_muc_sup), + supervisor:start_child( + Supervisor, [Host, ServerHost, Access, Room, HistorySize, RoomShaper, + Opts]). + +start_link(Host, ServerHost, Access, Room, HistorySize, RoomShaper, + Creator, Nick, DefRoomOpts) -> + gen_fsm:start_link(?MODULE, [Host, ServerHost, Access, Room, HistorySize, + RoomShaper, Creator, Nick, DefRoomOpts], + ?FSMOPTS). + +start_link(Host, ServerHost, Access, Room, HistorySize, RoomShaper, Opts) -> + gen_fsm:start_link(?MODULE, [Host, ServerHost, Access, Room, HistorySize, + RoomShaper, Opts], + ?FSMOPTS). + +%%%---------------------------------------------------------------------- +%%% Callback functions from gen_fsm +%%%---------------------------------------------------------------------- + +%%---------------------------------------------------------------------- +%% Func: init/1 +%% Returns: {ok, StateName, StateData} | +%% {ok, StateName, StateData, Timeout} | +%% ignore | +%% {stop, StopReason} +%%---------------------------------------------------------------------- +init([Host, ServerHost, Access, Room, HistorySize, RoomShaper, Creator, _Nick, DefRoomOpts]) -> + Shaper = shaper:new(RoomShaper), + State = set_affiliation(Creator, owner, + #state{host = Host, + server_host = ServerHost, + access = Access, + room = Room, + history = lqueue_new(HistorySize), + jid = jlib:make_jid(Room, Host, ""), + just_created = true, + room_shaper = Shaper}), + State1 = set_opts(DefRoomOpts, State), + {ok, normal_state, State1}; +init([Host, ServerHost, Access, Room, HistorySize, RoomShaper, Opts]) -> + Shaper = shaper:new(RoomShaper), + State = set_opts(Opts, #state{host = Host, + server_host = ServerHost, + access = Access, + room = Room, + history = lqueue_new(HistorySize), + jid = jlib:make_jid(Room, Host, ""), + room_shaper = Shaper}), + {ok, normal_state, State}. + +%%---------------------------------------------------------------------- +%% Func: StateName/2 +%% Returns: {next_state, NextStateName, NextStateData} | +%% {next_state, NextStateName, NextStateData, Timeout} | +%% {stop, Reason, NewStateData} +%%---------------------------------------------------------------------- +normal_state({route, From, "", + {xmlelement, "message", Attrs, Els} = Packet}, + StateData) -> + Lang = xml:get_attr_s("xml:lang", Attrs), + case is_user_online(From, StateData) of + true -> + case xml:get_attr_s("type", Attrs) of + "groupchat" -> + Activity = get_user_activity(From, StateData), + Now = now_to_usec(now()), + MinMessageInterval = + trunc(gen_mod:get_module_opt( + StateData#state.server_host, + mod_muc, min_message_interval, 0) * 1000000), + Size = lists:flatlength(xml:element_to_string(Packet)), + {MessageShaper, MessageShaperInterval} = + shaper:update(Activity#activity.message_shaper, Size), + if + Activity#activity.message /= undefined -> + ErrText = "Traffic rate limit is exceeded", + Err = jlib:make_error_reply( + Packet, ?ERRT_RESOURCE_CONSTRAINT(Lang, ErrText)), + ejabberd_router:route( + StateData#state.jid, + From, Err), + {next_state, normal_state, StateData}; + Now >= Activity#activity.message_time + MinMessageInterval, + MessageShaperInterval == 0 -> + {RoomShaper, RoomShaperInterval} = + shaper:update(StateData#state.room_shaper, Size), + RoomQueueEmpty = queue:is_empty( + StateData#state.room_queue), + if + RoomShaperInterval == 0, + RoomQueueEmpty -> + NewActivity = Activity#activity{ + message_time = Now, + message_shaper = MessageShaper}, + StateData1 = + StateData#state{ + activity = ?DICT:store( + jlib:jid_tolower(From), + NewActivity, + StateData#state.activity), + room_shaper = RoomShaper}, + process_groupchat_message(From, Packet, StateData1); + true -> + StateData1 = + if + RoomQueueEmpty -> + erlang:send_after( + RoomShaperInterval, self(), + process_room_queue), + StateData#state{ + room_shaper = RoomShaper}; + true -> + StateData + end, + NewActivity = Activity#activity{ + message_time = Now, + message_shaper = MessageShaper, + message = Packet}, + RoomQueue = queue:in( + {message, From}, + StateData#state.room_queue), + StateData2 = + StateData1#state{ + activity = ?DICT:store( + jlib:jid_tolower(From), + NewActivity, + StateData#state.activity), + room_queue = RoomQueue}, + {next_state, normal_state, StateData2} + end; + true -> + MessageInterval = + (Activity#activity.message_time + + MinMessageInterval - Now) div 1000, + Interval = lists:max([MessageInterval, + MessageShaperInterval]), + erlang:send_after( + Interval, self(), {process_user_message, From}), + NewActivity = Activity#activity{ + message = Packet, + message_shaper = MessageShaper}, + StateData1 = + StateData#state{ + activity = ?DICT:store( + jlib:jid_tolower(From), + NewActivity, + StateData#state.activity)}, + {next_state, normal_state, StateData1} + end; + "error" -> + case is_user_online(From, StateData) of + true -> + NewState = + add_user_presence_un( + From, + {xmlelement, "presence", + [{"type", "unavailable"}], []}, + StateData), + send_new_presence(From, NewState), + {next_state, normal_state, + remove_online_user(From, NewState)}; + _ -> + {next_state, normal_state, StateData} + end; + "chat" -> + ErrText = "It is not allowed to send private messages to the conference", + Err = jlib:make_error_reply( + Packet, ?ERRT_NOT_ACCEPTABLE(Lang, ErrText)), + ejabberd_router:route( + StateData#state.jid, + From, Err), + {next_state, normal_state, StateData}; + Type when (Type == "") or (Type == "normal") -> + case catch check_invitation(From, Els, Lang, StateData) of + {error, Error} -> + Err = jlib:make_error_reply( + Packet, Error), + ejabberd_router:route( + StateData#state.jid, + From, Err), + {next_state, normal_state, StateData}; + IJID -> + Config = StateData#state.config, + case Config#config.members_only of + true -> + case get_affiliation(IJID, StateData) of + none -> + NSD = set_affiliation( + IJID, + member, + StateData), + case (NSD#state.config)#config.persistent of + true -> + mod_muc:store_room( + NSD#state.host, + NSD#state.room, + make_opts(NSD)); + _ -> + ok + end, + {next_state, normal_state, NSD}; + _ -> + {next_state, normal_state, + StateData} + end; + false -> + {next_state, normal_state, StateData} + end + end; + _ -> + ErrText = "Improper message type", + Err = jlib:make_error_reply( + Packet, ?ERRT_NOT_ACCEPTABLE(Lang, ErrText)), + ejabberd_router:route( + StateData#state.jid, + From, Err), + {next_state, normal_state, StateData} + end; + _ -> + case xml:get_attr_s("type", Attrs) of + "error" -> + ok; + _ -> + ErrText = "Only occupants are allowed to send messages to the conference", + Err = jlib:make_error_reply( + Packet, ?ERRT_NOT_ACCEPTABLE(Lang, ErrText)), + ejabberd_router:route(StateData#state.jid, From, Err) + end, + {next_state, normal_state, StateData} + end; + +normal_state({route, From, "", + {xmlelement, "iq", _Attrs, _Els} = Packet}, + StateData) -> + case jlib:iq_query_info(Packet) of + #iq{type = Type, xmlns = XMLNS, lang = Lang, sub_el = SubEl} = IQ when + (XMLNS == ?NS_MUC_ADMIN) or + (XMLNS == ?NS_MUC_OWNER) or + (XMLNS == ?NS_DISCO_INFO) or + (XMLNS == ?NS_DISCO_ITEMS) -> + Res1 = case XMLNS of + ?NS_MUC_ADMIN -> + process_iq_admin(From, Type, Lang, SubEl, StateData); + ?NS_MUC_OWNER -> + process_iq_owner(From, Type, Lang, SubEl, StateData); + ?NS_DISCO_INFO -> + process_iq_disco_info(From, Type, Lang, StateData); + ?NS_DISCO_ITEMS -> + process_iq_disco_items(From, Type, Lang, StateData) + end, + {IQRes, NewStateData} = + case Res1 of + {result, Res, SD} -> + {IQ#iq{type = result, + sub_el = [{xmlelement, "query", + [{"xmlns", XMLNS}], + Res + }]}, + SD}; + {error, Error} -> + {IQ#iq{type = error, + sub_el = [SubEl, Error]}, + StateData} + end, + ejabberd_router:route(StateData#state.jid, + From, + jlib:iq_to_xml(IQRes)), + case NewStateData of + stop -> + {stop, normal, StateData}; + _ -> + {next_state, normal_state, NewStateData} + end; + reply -> + {next_state, normal_state, StateData}; + _ -> + Err = jlib:make_error_reply( + Packet, ?ERR_FEATURE_NOT_IMPLEMENTED), + ejabberd_router:route(StateData#state.jid, From, Err), + {next_state, normal_state, StateData} + end; + +normal_state({route, From, Nick, + {xmlelement, "presence", _Attrs, _Els} = Packet}, + StateData) -> + Activity = get_user_activity(From, StateData), + Now = now_to_usec(now()), + MinPresenceInterval = + trunc(gen_mod:get_module_opt( + StateData#state.server_host, + mod_muc, min_presence_interval, 0) * 1000000), + if + (Now >= Activity#activity.presence_time + MinPresenceInterval) and + (Activity#activity.presence == undefined) -> + NewActivity = Activity#activity{presence_time = Now}, + StateData1 = + StateData#state{ + activity = ?DICT:store( + jlib:jid_tolower(From), + NewActivity, + StateData#state.activity)}, + process_presence(From, Nick, Packet, StateData1); + true -> + if + Activity#activity.presence == undefined -> + Interval = (Activity#activity.presence_time + + MinPresenceInterval - Now) div 1000, + erlang:send_after( + Interval, self(), {process_user_presence, From}); + true -> + ok + end, + NewActivity = Activity#activity{presence = {Nick, Packet}}, + StateData1 = + StateData#state{ + activity = ?DICT:store( + jlib:jid_tolower(From), + NewActivity, + StateData#state.activity)}, + {next_state, normal_state, StateData1} + end; + +normal_state({route, From, ToNick, + {xmlelement, "message", Attrs, _Els} = Packet}, + StateData) -> + Type = xml:get_attr_s("type", Attrs), + Lang = xml:get_attr_s("xml:lang", Attrs), + case Type of + "error" -> + case is_user_online(From, StateData) of + true -> + NewState = + add_user_presence_un( + From, + {xmlelement, "presence", + [{"type", "unavailable"}], []}, + StateData), + send_new_presence(From, NewState), + {next_state, normal_state, + remove_online_user(From, NewState)}; + _ -> + {next_state, normal_state, StateData} + end; + _ -> + case (StateData#state.config)#config.allow_private_messages + andalso is_user_online(From, StateData) of + true -> + case Type of + "groupchat" -> + ErrText = "It is not allowed to send private " + "messages of type \"groupchat\"", + Err = jlib:make_error_reply( + Packet, ?ERRT_BAD_REQUEST(Lang, ErrText)), + ejabberd_router:route( + jlib:jid_replace_resource( + StateData#state.jid, + ToNick), + From, Err); + _ -> + case find_jid_by_nick(ToNick, StateData) of + false -> + ErrText = "Recipient is not in the conference room", + Err = jlib:make_error_reply( + Packet, ?ERRT_ITEM_NOT_FOUND(Lang, ErrText)), + ejabberd_router:route( + jlib:jid_replace_resource( + StateData#state.jid, + ToNick), + From, Err); + ToJID -> + {ok, #user{nick = FromNick}} = + ?DICT:find(jlib:jid_tolower(From), + StateData#state.users), + ejabberd_router:route( + jlib:jid_replace_resource( + StateData#state.jid, + FromNick), + ToJID, Packet) + end + end; + _ -> + ErrText = "Only occupants are allowed to send messages to the conference", + Err = jlib:make_error_reply( + Packet, ?ERRT_NOT_ACCEPTABLE(Lang, ErrText)), + ejabberd_router:route( + jlib:jid_replace_resource( + StateData#state.jid, + ToNick), + From, Err) + end, + {next_state, normal_state, StateData} + end; + +normal_state({route, From, ToNick, + {xmlelement, "iq", Attrs, _Els} = Packet}, + StateData) -> + Lang = xml:get_attr_s("xml:lang", Attrs), + case {(StateData#state.config)#config.allow_query_users, + is_user_online(From, StateData)} of + {true, true} -> + case find_jid_by_nick(ToNick, StateData) of + false -> + case jlib:iq_query_info(Packet) of + reply -> + ok; + _ -> + ErrText = "Recipient is not in the conference room", + Err = jlib:make_error_reply( + Packet, ?ERRT_ITEM_NOT_FOUND(Lang, ErrText)), + ejabberd_router:route( + jlib:jid_replace_resource( + StateData#state.jid, ToNick), + From, Err) + end; + ToJID -> + {ok, #user{nick = FromNick}} = + ?DICT:find(jlib:jid_tolower(From), + StateData#state.users), + ejabberd_router:route( + jlib:jid_replace_resource(StateData#state.jid, FromNick), + ToJID, Packet) + end; + {_, false} -> + case jlib:iq_query_info(Packet) of + reply -> + ok; + _ -> + ErrText = "Only occupants are allowed to send queries to the conference", + Err = jlib:make_error_reply( + Packet, ?ERRT_NOT_ACCEPTABLE(Lang, ErrText)), + ejabberd_router:route( + jlib:jid_replace_resource(StateData#state.jid, ToNick), + From, Err) + end; + _ -> + case jlib:iq_query_info(Packet) of + reply -> + ok; + _ -> + ErrText = "Queries to the conference members are not allowed in this room", + Err = jlib:make_error_reply( + Packet, ?ERRT_NOT_ALLOWED(Lang, ErrText)), + ejabberd_router:route( + jlib:jid_replace_resource(StateData#state.jid, ToNick), + From, Err) + end + end, + {next_state, normal_state, StateData}; + +normal_state(_Event, StateData) -> + {next_state, normal_state, StateData}. + + + +%%---------------------------------------------------------------------- +%% Func: handle_event/3 +%% Returns: {next_state, NextStateName, NextStateData} | +%% {next_state, NextStateName, NextStateData, Timeout} | +%% {stop, Reason, NewStateData} +%%---------------------------------------------------------------------- +handle_event({service_message, Msg}, _StateName, StateData) -> + MessagePkt = {xmlelement, "message", + [{"type", "groupchat"}], + [{xmlelement, "body", [], [{xmlcdata, Msg}]}]}, + send_multiple( + StateData#state.jid, + StateData#state.server_host, + StateData#state.users, + MessagePkt), + NSD = add_message_to_history("", + MessagePkt, + StateData), + {next_state, normal_state, NSD}; + +handle_event({destroy, Reason}, _StateName, StateData) -> + {result, [], stop} = + destroy_room( + {xmlelement, "destroy", + [{"xmlns", ?NS_MUC_OWNER}], + case Reason of + none -> []; + _Else -> + [{xmlelement, "reason", + [], [{xmlcdata, Reason}]}] + end}, StateData), + {stop, normal, StateData}; +handle_event(destroy, StateName, StateData) -> + handle_event({destroy, none}, StateName, StateData); + +handle_event({set_affiliations, Affiliations}, StateName, StateData) -> + {next_state, StateName, StateData#state{affiliations = Affiliations}}; + +handle_event(_Event, StateName, StateData) -> + {next_state, StateName, StateData}. + +%%---------------------------------------------------------------------- +%% Func: handle_sync_event/4 +%% Returns: {next_state, NextStateName, NextStateData} | +%% {next_state, NextStateName, NextStateData, Timeout} | +%% {reply, Reply, NextStateName, NextStateData} | +%% {reply, Reply, NextStateName, NextStateData, Timeout} | +%% {stop, Reason, NewStateData} | +%% {stop, Reason, Reply, NewStateData} +%%---------------------------------------------------------------------- +handle_sync_event({get_disco_item, JID, Lang}, _From, StateName, StateData) -> + FAffiliation = get_affiliation(JID, StateData), + FRole = get_role(JID, StateData), + Tail = + case ((StateData#state.config)#config.public_list == true) orelse + (FRole /= none) orelse + (FAffiliation == admin) orelse + (FAffiliation == owner) of + true -> + Desc = case (StateData#state.config)#config.public of + true -> + ""; + _ -> + translate:translate(Lang, "private, ") + end, + Len = ?DICT:fold(fun(_, _, Acc) -> Acc + 1 end, 0, + StateData#state.users), + " (" ++ Desc ++ integer_to_list(Len) ++ ")"; + _ -> + "" + end, + Reply = case ((StateData#state.config)#config.public == true) orelse + (FRole /= none) orelse + (FAffiliation == admin) orelse + (FAffiliation == owner) of + true -> + {item, get_title(StateData) ++ Tail}; + _ -> + false + end, + {reply, Reply, StateName, StateData}; +handle_sync_event(get_config, _From, StateName, StateData) -> + {reply, {ok, StateData#state.config}, StateName, StateData}; +handle_sync_event(get_state, _From, StateName, StateData) -> + {reply, {ok, StateData}, StateName, StateData}; +handle_sync_event({change_config, Config}, _From, StateName, StateData) -> + {result, [], NSD} = change_config(Config, StateData), + {reply, {ok, NSD#state.config}, StateName, NSD}; +handle_sync_event(_Event, _From, StateName, StateData) -> + Reply = ok, + {reply, Reply, StateName, StateData}. + +code_change(_OldVsn, StateName, StateData, _Extra) -> + {ok, StateName, StateData}. + +%%---------------------------------------------------------------------- +%% Func: handle_info/3 +%% Returns: {next_state, NextStateName, NextStateData} | +%% {next_state, NextStateName, NextStateData, Timeout} | +%% {stop, Reason, NewStateData} +%%---------------------------------------------------------------------- +handle_info({process_user_presence, From}, normal_state = _StateName, StateData) -> + Activity = get_user_activity(From, StateData), + Now = now_to_usec(now()), + {Nick, Packet} = Activity#activity.presence, + NewActivity = Activity#activity{presence_time = Now, + presence = undefined}, + StateData1 = + StateData#state{ + activity = ?DICT:store( + jlib:jid_tolower(From), + NewActivity, + StateData#state.activity)}, + process_presence(From, Nick, Packet, StateData1); +handle_info({process_user_message, From}, normal_state = _StateName, StateData) -> + Activity = get_user_activity(From, StateData), + Now = now_to_usec(now()), + Packet = Activity#activity.message, + NewActivity = Activity#activity{message_time = Now, + message = undefined}, + StateData1 = + StateData#state{ + activity = ?DICT:store( + jlib:jid_tolower(From), + NewActivity, + StateData#state.activity)}, + process_groupchat_message(From, Packet, StateData1); +handle_info(process_room_queue, normal_state = StateName, StateData) -> + case queue:out(StateData#state.room_queue) of + {{value, {message, From}}, RoomQueue} -> + Activity = get_user_activity(From, StateData), + Packet = Activity#activity.message, + NewActivity = Activity#activity{message = undefined}, + StateData1 = + StateData#state{ + activity = ?DICT:store( + jlib:jid_tolower(From), + NewActivity, + StateData#state.activity), + room_queue = RoomQueue}, + StateData2 = prepare_room_queue(StateData1), + process_groupchat_message(From, Packet, StateData2); + {{value, {presence, From}}, RoomQueue} -> + Activity = get_user_activity(From, StateData), + {Nick, Packet} = Activity#activity.presence, + NewActivity = Activity#activity{presence = undefined}, + StateData1 = + StateData#state{ + activity = ?DICT:store( + jlib:jid_tolower(From), + NewActivity, + StateData#state.activity), + room_queue = RoomQueue}, + StateData2 = prepare_room_queue(StateData1), + process_presence(From, Nick, Packet, StateData2); + {empty, _} -> + {next_state, StateName, StateData} + end; +handle_info(_Info, StateName, StateData) -> + {next_state, StateName, StateData}. + +%%---------------------------------------------------------------------- +%% Func: terminate/3 +%% Purpose: Shutdown the fsm +%% Returns: any +%%---------------------------------------------------------------------- +terminate(_Reason, _StateName, StateData) -> + mod_muc:room_destroyed(StateData#state.host, StateData#state.room, self(), + StateData#state.server_host), + ok. + +%%%---------------------------------------------------------------------- +%%% Internal functions +%%%---------------------------------------------------------------------- + +route(Pid, From, ToNick, Packet) -> + gen_fsm:send_event(Pid, {route, From, ToNick, Packet}). + +process_groupchat_message(From, {xmlelement, "message", Attrs, _Els} = Packet, + StateData) -> + Lang = xml:get_attr_s("xml:lang", Attrs), + case is_user_online(From, StateData) of + true -> + {ok, #user{nick = FromNick, role = Role}} = + ?DICT:find(jlib:jid_tolower(From), + StateData#state.users), + if + (Role == moderator) or (Role == participant) -> + {NewStateData1, IsAllowed} = + case check_subject(Packet) of + false -> + {StateData, true}; + Subject -> + case can_change_subject(Role, + StateData) of + true -> + NSD = + StateData#state{ + subject = Subject, + subject_author = + FromNick}, + case (NSD#state.config)#config.persistent of + true -> + mod_muc:store_room( + NSD#state.host, + NSD#state.room, + make_opts(NSD)); + _ -> + ok + end, + {NSD, true}; + _ -> + {StateData, false} + end + end, + case IsAllowed of + true -> + send_multiple( + jlib:jid_replace_resource(StateData#state.jid, FromNick), + StateData#state.server_host, + StateData#state.users, + Packet), + NewStateData2 = + add_message_to_history(FromNick, + Packet, + NewStateData1), + {next_state, normal_state, NewStateData2}; + _ -> + Err = + case (StateData#state.config)#config.allow_change_subj of + true -> + ?ERRT_FORBIDDEN( + Lang, + "Only moderators and participants " + "are allowed to change subject in this room"); + _ -> + ?ERRT_FORBIDDEN( + Lang, + "Only moderators " + "are allowed to change subject in this room") + end, + ejabberd_router:route( + StateData#state.jid, + From, + jlib:make_error_reply(Packet, Err)), + {next_state, normal_state, StateData} + end; + true -> + ErrText = "Visitors are not allowed to send messages to all occupants", + Err = jlib:make_error_reply( + Packet, ?ERRT_FORBIDDEN(Lang, ErrText)), + ejabberd_router:route( + StateData#state.jid, + From, Err), + {next_state, normal_state, StateData} + end; + false -> + ErrText = "Only occupants are allowed to send messages to the conference", + Err = jlib:make_error_reply( + Packet, ?ERRT_NOT_ACCEPTABLE(Lang, ErrText)), + ejabberd_router:route(StateData#state.jid, From, Err), + {next_state, normal_state, StateData} + end. + +process_presence(From, Nick, {xmlelement, "presence", Attrs, _Els} = Packet, + StateData) -> + Type = xml:get_attr_s("type", Attrs), + Lang = xml:get_attr_s("xml:lang", Attrs), + StateData1 = + case Type of + "unavailable" -> + case is_user_online(From, StateData) of + true -> + NewState = + add_user_presence_un(From, Packet, StateData), + send_new_presence(From, NewState), + Reason = case xml:get_subtag(Packet, "status") of + false -> ""; + Status_el -> xml:get_tag_cdata(Status_el) + end, + remove_online_user(From, NewState, Reason); + _ -> + StateData + end; + "error" -> + case is_user_online(From, StateData) of + true -> + NewState = + add_user_presence_un( + From, + {xmlelement, "presence", + [{"type", "unavailable"}], []}, + StateData), + send_new_presence(From, NewState), + remove_online_user(From, NewState); + _ -> + StateData + end; + "" -> + case is_user_online(From, StateData) of + true -> + case is_nick_change(From, Nick, StateData) of + true -> + case {is_nick_exists(Nick, StateData), + mod_muc:can_use_nick( + StateData#state.host, From, Nick)} of + {true, _} -> + Lang = xml:get_attr_s("xml:lang", Attrs), + ErrText = "Nickname is already in use by another occupant", + Err = jlib:make_error_reply( + Packet, + ?ERRT_CONFLICT(Lang, ErrText)), + ejabberd_router:route( + jlib:jid_replace_resource( + StateData#state.jid, + Nick), % TODO: s/Nick/""/ + From, Err), + StateData; + {_, false} -> + ErrText = "Nickname is registered by another person", + Err = jlib:make_error_reply( + Packet, + ?ERRT_CONFLICT(Lang, ErrText)), + ejabberd_router:route( + % TODO: s/Nick/""/ + jlib:jid_replace_resource( + StateData#state.jid, + Nick), + From, Err), + StateData; + _ -> + change_nick(From, Nick, StateData) + end; + _ -> + NewState = + add_user_presence(From, Packet, StateData), + send_new_presence(From, NewState), + NewState + end; + _ -> + add_new_user(From, Nick, Packet, StateData) + end; + _ -> + StateData + end, + case (not (StateData1#state.config)#config.persistent) andalso + (?DICT:to_list(StateData1#state.users) == []) of + true -> + {stop, normal, StateData1}; + _ -> + {next_state, normal_state, StateData1} + end. + +is_user_online(JID, StateData) -> + LJID = jlib:jid_tolower(JID), + ?DICT:is_key(LJID, StateData#state.users). + +role_to_list(Role) -> + case Role of + moderator -> "moderator"; + participant -> "participant"; + visitor -> "visitor"; + none -> "none" + end. + +affiliation_to_list(Affiliation) -> + case Affiliation of + owner -> "owner"; + admin -> "admin"; + member -> "member"; + outcast -> "outcast"; + none -> "none" + end. + +list_to_role(Role) -> + case Role of + "moderator" -> moderator; + "participant" -> participant; + "visitor" -> visitor; + "none" -> none + end. + +list_to_affiliation(Affiliation) -> + case Affiliation of + "owner" -> owner; + "admin" -> admin; + "member" -> member; + "outcast" -> outcast; + "none" -> none + end. + + + +set_affiliation(JID, Affiliation, StateData) -> + LJID = jlib:jid_remove_resource(jlib:jid_tolower(JID)), + Affiliations = case Affiliation of + none -> + ?DICT:erase(LJID, + StateData#state.affiliations); + _ -> + ?DICT:store(LJID, + Affiliation, + StateData#state.affiliations) + end, + StateData#state{affiliations = Affiliations}. + +set_affiliation_and_reason(JID, Affiliation, Reason, StateData) -> + LJID = jlib:jid_remove_resource(jlib:jid_tolower(JID)), + Affiliations = case Affiliation of + none -> + ?DICT:erase(LJID, + StateData#state.affiliations); + _ -> + ?DICT:store(LJID, + {Affiliation, Reason}, + StateData#state.affiliations) + end, + StateData#state{affiliations = Affiliations}. + +get_affiliation(JID, StateData) -> + {_AccessRoute, _AccessCreate, AccessAdmin, _AccessPersistent} = StateData#state.access, + Res = + case acl:match_rule(StateData#state.server_host, AccessAdmin, JID) of + allow -> + owner; + _ -> + LJID = jlib:jid_tolower(JID), + case ?DICT:find(LJID, StateData#state.affiliations) of + {ok, Affiliation} -> + Affiliation; + _ -> + LJID1 = jlib:jid_remove_resource(LJID), + case ?DICT:find(LJID1, StateData#state.affiliations) of + {ok, Affiliation} -> + Affiliation; + _ -> + LJID2 = setelement(1, LJID, ""), + case ?DICT:find(LJID2, StateData#state.affiliations) of + {ok, Affiliation} -> + Affiliation; + _ -> + LJID3 = jlib:jid_remove_resource(LJID2), + case ?DICT:find(LJID3, StateData#state.affiliations) of + {ok, Affiliation} -> + Affiliation; + _ -> + none + end + end + end + end + end, + case Res of + {A, _Reason} -> + A; + _ -> + Res + end. + +get_service_affiliation(JID, StateData) -> + {_AccessRoute, _AccessCreate, AccessAdmin, _AccessPersistent} = + StateData#state.access, + case acl:match_rule(StateData#state.server_host, AccessAdmin, JID) of + allow -> + owner; + _ -> + none + end. + +set_role(JID, Role, StateData) -> + LJID = jlib:jid_tolower(JID), + LJIDs = case LJID of + {U, S, ""} -> + ?DICT:fold( + fun(J, _, Js) -> + case J of + {U, S, _} -> + [J | Js]; + _ -> + Js + end + end, [], StateData#state.users); + _ -> + case ?DICT:is_key(LJID, StateData#state.users) of + true -> + [LJID]; + _ -> + [] + end + end, + Users = case Role of + none -> + lists:foldl(fun(J, Us) -> + ?DICT:erase(J, + Us) + end, StateData#state.users, LJIDs); + _ -> + lists:foldl(fun(J, Us) -> + {ok, User} = ?DICT:find(J, Us), + ?DICT:store(J, + User#user{role = Role}, + Us) + end, StateData#state.users, LJIDs) + end, + StateData#state{users = Users}. + +get_role(JID, StateData) -> + LJID = jlib:jid_tolower(JID), + case ?DICT:find(LJID, StateData#state.users) of + {ok, #user{role = Role}} -> + Role; + _ -> + none + end. + +get_default_role(Affiliation, StateData) -> + case Affiliation of + owner -> moderator; + admin -> moderator; + member -> participant; + outcast -> none; + none -> + case (StateData#state.config)#config.members_only of + true -> + none; + _ -> + case (StateData#state.config)#config.members_by_default of + true -> + participant; + _ -> + visitor + end + end + end. + +get_max_users(StateData) -> + MaxUsers = (StateData#state.config)#config.max_users, + ServiceMaxUsers = get_service_max_users(StateData), + if + MaxUsers =< ServiceMaxUsers -> MaxUsers; + true -> ServiceMaxUsers + end. + +get_service_max_users(StateData) -> + gen_mod:get_module_opt(StateData#state.server_host, + mod_muc, max_users, ?MAX_USERS_DEFAULT). + +get_max_users_admin_threshold(StateData) -> + gen_mod:get_module_opt(StateData#state.server_host, + mod_muc, max_users_admin_threshold, 5). + +get_user_activity(JID, StateData) -> + case ?DICT:find(jlib:jid_tolower(JID), + StateData#state.activity) of + {ok, A} -> A; + error -> + MessageShaper = + shaper:new(gen_mod:get_module_opt( + StateData#state.server_host, + mod_muc, user_message_shaper, none)), + PresenceShaper = + shaper:new(gen_mod:get_module_opt( + StateData#state.server_host, + mod_muc, user_presence_shaper, none)), + #activity{message_shaper = MessageShaper, + presence_shaper = PresenceShaper} + end. + +prepare_room_queue(StateData) -> + case queue:out(StateData#state.room_queue) of + {{value, {message, From}}, _RoomQueue} -> + Activity = get_user_activity(From, StateData), + Packet = Activity#activity.message, + Size = lists:flatlength(xml:element_to_string(Packet)), + {RoomShaper, RoomShaperInterval} = + shaper:update(StateData#state.room_shaper, Size), + erlang:send_after( + RoomShaperInterval, self(), + process_room_queue), + StateData#state{ + room_shaper = RoomShaper}; + {{value, {presence, From}}, _RoomQueue} -> + Activity = get_user_activity(From, StateData), + {_Nick, Packet} = Activity#activity.presence, + Size = lists:flatlength(xml:element_to_string(Packet)), + {RoomShaper, RoomShaperInterval} = + shaper:update(StateData#state.room_shaper, Size), + erlang:send_after( + RoomShaperInterval, self(), + process_room_queue), + StateData#state{ + room_shaper = RoomShaper}; + {empty, _} -> + StateData + end. + + +add_online_user(JID, Nick, Role, StateData) -> + LJID = jlib:jid_tolower(JID), + Users = ?DICT:store(LJID, + #user{jid = JID, + nick = Nick, + role = Role}, + StateData#state.users), + add_to_log(join, Nick, StateData), + StateData#state{users = Users}. + +remove_online_user(JID, StateData) -> + remove_online_user(JID, StateData, ""). + +remove_online_user(JID, StateData, Reason) -> + LJID = jlib:jid_tolower(JID), + {ok, #user{nick = Nick}} = + ?DICT:find(LJID, StateData#state.users), + add_to_log(leave, {Nick, Reason}, StateData), + Users = ?DICT:erase(LJID, StateData#state.users), + StateData#state{users = Users}. + + +filter_presence({xmlelement, "presence", Attrs, Els}) -> + FEls = lists:filter( + fun(El) -> + case El of + {xmlcdata, _} -> + false; + {xmlelement, _Name1, Attrs1, _Els1} -> + XMLNS = xml:get_attr_s("xmlns", Attrs1), + case XMLNS of + ?NS_MUC ++ _ -> + false; + _ -> + true + end + end + end, Els), + {xmlelement, "presence", Attrs, FEls}. + + +add_user_presence(JID, Presence, StateData) -> + LJID = jlib:jid_tolower(JID), + FPresence = filter_presence(Presence), + Users = + ?DICT:update( + LJID, + fun(#user{} = User) -> + User#user{last_presence = FPresence} + end, StateData#state.users), + StateData#state{users = Users}. + +add_user_presence_un(JID, Presence, StateData) -> + LJID = jlib:jid_tolower(JID), + FPresence = filter_presence(Presence), + Users = + ?DICT:update( + LJID, + fun(#user{} = User) -> + User#user{last_presence = FPresence, + role = none} + end, StateData#state.users), + StateData#state{users = Users}. + + +is_nick_exists(Nick, StateData) -> + ?DICT:fold(fun(_, #user{nick = N}, B) -> + B orelse (N == Nick) + end, false, StateData#state.users). + +find_jid_by_nick(Nick, StateData) -> + ?DICT:fold(fun(_, #user{jid = JID, nick = N}, R) -> + case Nick of + N -> JID; + _ -> R + end + end, false, StateData#state.users). + +is_nick_change(JID, Nick, StateData) -> + LJID = jlib:jid_tolower(JID), + case Nick of + "" -> + false; + _ -> + {ok, #user{nick = OldNick}} = + ?DICT:find(LJID, StateData#state.users), + Nick /= OldNick + end. + +add_new_user(From, Nick, {xmlelement, _, Attrs, Els} = Packet, StateData) -> + Lang = xml:get_attr_s("xml:lang", Attrs), + MaxUsers = get_max_users(StateData), + MaxAdminUsers = MaxUsers + get_max_users_admin_threshold(StateData), + NUsers = dict:fold(fun(_, _, Acc) -> Acc + 1 end, 0, + StateData#state.users), + Affiliation = get_affiliation(From, StateData), + ServiceAffiliation = get_service_affiliation(From, StateData), + case {ServiceAffiliation == owner orelse + MaxUsers == none orelse + ((Affiliation == admin orelse Affiliation == owner) andalso + NUsers < MaxAdminUsers) orelse + NUsers < MaxUsers, + is_nick_exists(Nick, StateData), + mod_muc:can_use_nick(StateData#state.host, From, Nick), + get_default_role(Affiliation, StateData)} of + {false, _, _, _} -> + % max user reached and user is not admin or owner + Err = jlib:make_error_reply( + Packet, + ?ERR_SERVICE_UNAVAILABLE), + ejabberd_router:route( % TODO: s/Nick/""/ + jlib:jid_replace_resource(StateData#state.jid, Nick), + From, Err), + StateData; + {_, _, _, none} -> + Err = jlib:make_error_reply( + Packet, + case Affiliation of + outcast -> + ErrText = "You have been banned from this room", + ?ERRT_FORBIDDEN(Lang, ErrText); + _ -> + ErrText = "Membership required to enter this room", + ?ERRT_REGISTRATION_REQUIRED(Lang, ErrText) + end), + ejabberd_router:route( % TODO: s/Nick/""/ + jlib:jid_replace_resource(StateData#state.jid, Nick), + From, Err), + StateData; + {_, true, _, _} -> + ErrText = "Nickname is already in use by another occupant", + Err = jlib:make_error_reply(Packet, ?ERRT_CONFLICT(Lang, ErrText)), + ejabberd_router:route( + % TODO: s/Nick/""/ + jlib:jid_replace_resource(StateData#state.jid, Nick), + From, Err), + StateData; + {_, _, false, _} -> + ErrText = "Nickname is registered by another person", + Err = jlib:make_error_reply(Packet, ?ERRT_CONFLICT(Lang, ErrText)), + ejabberd_router:route( + % TODO: s/Nick/""/ + jlib:jid_replace_resource(StateData#state.jid, Nick), + From, Err), + StateData; + {_, _, _, Role} -> + case check_password(Affiliation, Els, StateData) of + true -> + NewState = + add_user_presence( + From, Packet, + add_online_user(From, Nick, Role, StateData)), + if not (NewState#state.config)#config.anonymous -> + WPacket = {xmlelement, "message", [{"type", "groupchat"}], + [{xmlelement, "body", [], + [{xmlcdata, translate:translate( + Lang, + "This room is not anonymous")}]}, + {xmlelement, "x", [{"xmlns", ?NS_MUC_USER}], + [{xmlelement, "status", [{"code", "100"}], []}]}]}, + ejabberd_router:route( + StateData#state.jid, + From, WPacket); + true -> + ok + end, + send_existing_presences(From, NewState), + send_new_presence(From, NewState), + Shift = count_stanza_shift(Nick, Els, NewState), + case send_history(From, Shift, NewState) of + true -> + ok; + _ -> + send_subject(From, Lang, StateData) + end, + case NewState#state.just_created of + true -> + NewState#state{just_created = false}; + false -> + NewState + end; + nopass -> + ErrText = "Password required to enter this room", + Err = jlib:make_error_reply( + Packet, ?ERRT_NOT_AUTHORIZED(Lang, ErrText)), + ejabberd_router:route( % TODO: s/Nick/""/ + jlib:jid_replace_resource( + StateData#state.jid, Nick), + From, Err), + StateData; + _ -> + ErrText = "Incorrect password", + Err = jlib:make_error_reply( + Packet, ?ERRT_NOT_AUTHORIZED(Lang, ErrText)), + ejabberd_router:route( % TODO: s/Nick/""/ + jlib:jid_replace_resource( + StateData#state.jid, Nick), + From, Err), + StateData + end + end. + +check_password(owner, _Els, _StateData) -> + true; +check_password(_Affiliation, Els, StateData) -> + case (StateData#state.config)#config.password_protected of + false -> + true; + true -> + Pass = extract_password(Els), + case Pass of + false -> + nopass; + _ -> + case (StateData#state.config)#config.password of + Pass -> + true; + _ -> + false + end + end + end. + +extract_password([]) -> + false; +extract_password([{xmlelement, _Name, Attrs, _SubEls} = El | Els]) -> + case xml:get_attr_s("xmlns", Attrs) of + ?NS_MUC -> + case xml:get_subtag(El, "password") of + false -> + false; + SubEl -> + xml:get_tag_cdata(SubEl) + end; + _ -> + extract_password(Els) + end; +extract_password([_ | Els]) -> + extract_password(Els). + +count_stanza_shift(Nick, Els, StateData) -> + HL = lqueue_to_list(StateData#state.history), + Since = extract_history(Els, "since"), + Shift0 = case Since of + false -> + 0; + _ -> + Sin = calendar:datetime_to_gregorian_seconds(Since), + count_seconds_shift(Sin, HL) + end, + Seconds = extract_history(Els, "seconds"), + Shift1 = case Seconds of + false -> + 0; + _ -> + Sec = calendar:datetime_to_gregorian_seconds( + calendar:now_to_universal_time(now())) - Seconds, + count_seconds_shift(Sec, HL) + end, + MaxStanzas = extract_history(Els, "maxstanzas"), + Shift2 = case MaxStanzas of + false -> + 0; + _ -> + count_maxstanzas_shift(MaxStanzas, HL) + end, + MaxChars = extract_history(Els, "maxchars"), + Shift3 = case MaxChars of + false -> + 0; + _ -> + count_maxchars_shift(Nick, MaxChars, HL) + end, + lists:max([Shift0, Shift1, Shift2, Shift3]). + +count_seconds_shift(Seconds, HistoryList) -> + lists:sum( + lists:map( + fun({_Nick, _Packet, _HaveSubject, TimeStamp, _Size}) -> + T = calendar:datetime_to_gregorian_seconds(TimeStamp), + if + T < Seconds -> + 1; + true -> + 0 + end + end, HistoryList)). + +count_maxstanzas_shift(MaxStanzas, HistoryList) -> + S = length(HistoryList) - MaxStanzas, + if + S =< 0 -> + 0; + true -> + S + end. + +count_maxchars_shift(Nick, MaxSize, HistoryList) -> + NLen = string:len(Nick) + 1, + Sizes = lists:map( + fun({_Nick, _Packet, _HaveSubject, _TimeStamp, Size}) -> + Size + NLen + end, HistoryList), + calc_shift(MaxSize, Sizes). + +calc_shift(MaxSize, Sizes) -> + Total = lists:sum(Sizes), + calc_shift(MaxSize, Total, 0, Sizes). + +calc_shift(_MaxSize, _Size, Shift, []) -> + Shift; +calc_shift(MaxSize, Size, Shift, [S | TSizes]) -> + if + MaxSize >= Size -> + Shift; + true -> + calc_shift(MaxSize, Size - S, Shift + 1, TSizes) + end. + +extract_history([], _Type) -> + false; +extract_history([{xmlelement, _Name, Attrs, _SubEls} = El | Els], Type) -> + case xml:get_attr_s("xmlns", Attrs) of + ?NS_MUC -> + AttrVal = xml:get_path_s(El, + [{elem, "history"}, {attr, Type}]), + case Type of + "since" -> + case jlib:datetime_string_to_timestamp(AttrVal) of + undefined -> + false; + TS -> + calendar:now_to_universal_time(TS) + end; + _ -> + case catch list_to_integer(AttrVal) of + IntVal when is_integer(IntVal) and (IntVal >= 0) -> + IntVal; + _ -> + false + end + end; + _ -> + extract_history(Els, Type) + end; +extract_history([_ | Els], Type) -> + extract_history(Els, Type). + + +send_update_presence(JID, StateData) -> + LJID = jlib:jid_tolower(JID), + LJIDs = case LJID of + {U, S, ""} -> + ?DICT:fold( + fun(J, _, Js) -> + case J of + {U, S, _} -> + [J | Js]; + _ -> + Js + end + end, [], StateData#state.users); + _ -> + case ?DICT:is_key(LJID, StateData#state.users) of + true -> + [LJID]; + _ -> + [] + end + end, + lists:foreach(fun(J) -> + send_new_presence(J, StateData) + end, LJIDs). + +send_new_presence(NJID, StateData) -> + {ok, #user{jid = RealJID, + nick = Nick, + role = Role, + last_presence = Presence}} = + ?DICT:find(jlib:jid_tolower(NJID), StateData#state.users), + Affiliation = get_affiliation(NJID, StateData), + SAffiliation = affiliation_to_list(Affiliation), + SRole = role_to_list(Role), + lists:foreach( + fun({_LJID, Info}) -> + ItemAttrs = + case (Info#user.role == moderator) orelse + ((StateData#state.config)#config.anonymous == false) of + true -> + [{"jid", jlib:jid_to_string(RealJID)}, + {"affiliation", SAffiliation}, + {"role", SRole}]; + _ -> + [{"affiliation", SAffiliation}, + {"role", SRole}] + end, + Status = case StateData#state.just_created of + true -> + [{xmlelement, "status", [{"code", "201"}], []}]; + false -> + [] + end, + Packet = append_subtags( + Presence, + [{xmlelement, "x", [{"xmlns", ?NS_MUC_USER}], + [{xmlelement, "item", ItemAttrs, []} | Status]}]), + ejabberd_router:route( + jlib:jid_replace_resource(StateData#state.jid, Nick), + Info#user.jid, + Packet) + end, ?DICT:to_list(StateData#state.users)). + + +send_existing_presences(ToJID, StateData) -> + LToJID = jlib:jid_tolower(ToJID), + {ok, #user{jid = RealToJID, + role = Role}} = + ?DICT:find(LToJID, StateData#state.users), + lists:foreach( + fun({LJID, #user{jid = FromJID, + nick = FromNick, + role = FromRole, + last_presence = Presence + }}) -> + case RealToJID of + FromJID -> + ok; + _ -> + FromAffiliation = get_affiliation(LJID, StateData), + ItemAttrs = + case (Role == moderator) orelse + ((StateData#state.config)#config.anonymous == + false) of + true -> + [{"jid", jlib:jid_to_string(FromJID)}, + {"affiliation", + affiliation_to_list(FromAffiliation)}, + {"role", role_to_list(FromRole)}]; + _ -> + [{"affiliation", + affiliation_to_list(FromAffiliation)}, + {"role", role_to_list(FromRole)}] + end, + Packet = append_subtags( + Presence, + [{xmlelement, "x", [{"xmlns", ?NS_MUC_USER}], + [{xmlelement, "item", ItemAttrs, []}]}]), + ejabberd_router:route( + jlib:jid_replace_resource( + StateData#state.jid, FromNick), + RealToJID, + Packet) + end + end, ?DICT:to_list(StateData#state.users)). + + +append_subtags({xmlelement, Name, Attrs, SubTags1}, SubTags2) -> + {xmlelement, Name, Attrs, SubTags1 ++ SubTags2}. + + +now_to_usec({MSec, Sec, USec}) -> + (MSec*1000000 + Sec)*1000000 + USec. + + +change_nick(JID, Nick, StateData) -> + LJID = jlib:jid_tolower(JID), + {ok, #user{nick = OldNick}} = + ?DICT:find(LJID, StateData#state.users), + Users = + ?DICT:update( + LJID, + fun(#user{} = User) -> + User#user{nick = Nick} + end, StateData#state.users), + NewStateData = StateData#state{users = Users}, + send_nick_changing(JID, OldNick, NewStateData), + add_to_log(nickchange, {OldNick, Nick}, StateData), + NewStateData. + +send_nick_changing(JID, OldNick, StateData) -> + {ok, #user{jid = RealJID, + nick = Nick, + role = Role, + last_presence = Presence}} = + ?DICT:find(jlib:jid_tolower(JID), StateData#state.users), + Affiliation = get_affiliation(JID, StateData), + SAffiliation = affiliation_to_list(Affiliation), + SRole = role_to_list(Role), + lists:foreach( + fun({_LJID, Info}) -> + ItemAttrs1 = + case (Info#user.role == moderator) orelse + ((StateData#state.config)#config.anonymous == false) of + true -> + [{"jid", jlib:jid_to_string(RealJID)}, + {"affiliation", SAffiliation}, + {"role", SRole}, + {"nick", Nick}]; + _ -> + [{"affiliation", SAffiliation}, + {"role", SRole}, + {"nick", Nick}] + end, + ItemAttrs2 = + case (Info#user.role == moderator) orelse + ((StateData#state.config)#config.anonymous == false) of + true -> + [{"jid", jlib:jid_to_string(RealJID)}, + {"affiliation", SAffiliation}, + {"role", SRole}]; + _ -> + [{"affiliation", SAffiliation}, + {"role", SRole}] + end, + Packet1 = + {xmlelement, "presence", [{"type", "unavailable"}], + [{xmlelement, "x", [{"xmlns", ?NS_MUC_USER}], + [{xmlelement, "item", ItemAttrs1, []}, + {xmlelement, "status", [{"code", "303"}], []}]}]}, + Packet2 = append_subtags( + Presence, + [{xmlelement, "x", [{"xmlns", ?NS_MUC_USER}], + [{xmlelement, "item", ItemAttrs2, []}]}]), + ejabberd_router:route( + jlib:jid_replace_resource(StateData#state.jid, OldNick), + Info#user.jid, + Packet1), + ejabberd_router:route( + jlib:jid_replace_resource(StateData#state.jid, Nick), + Info#user.jid, + Packet2) + end, ?DICT:to_list(StateData#state.users)). + + +lqueue_new(Max) -> + #lqueue{queue = queue:new(), + len = 0, + max = Max}. + +%% If the message queue limit is set to 0, do not store messages. +lqueue_in(_Item, LQ = #lqueue{max = 0}) -> + LQ; +%% Otherwise, rotate messages in the queue store. +lqueue_in(Item, #lqueue{queue = Q1, len = Len, max = Max}) -> + Q2 = queue:in(Item, Q1), + if + Len >= Max -> + Q3 = lqueue_cut(Q2, Len - Max + 1), + #lqueue{queue = Q3, len = Max, max = Max}; + true -> + #lqueue{queue = Q2, len = Len + 1, max = Max} + end. + +lqueue_cut(Q, 0) -> + Q; +lqueue_cut(Q, N) -> + {_, Q1} = queue:out(Q), + lqueue_cut(Q1, N - 1). + +lqueue_to_list(#lqueue{queue = Q1}) -> + queue:to_list(Q1). + + +add_message_to_history(FromNick, Packet, StateData) -> + HaveSubject = case xml:get_subtag(Packet, "subject") of + false -> + false; + _ -> + true + end, + TimeStamp = calendar:now_to_universal_time(now()), + TSPacket = append_subtags(Packet, + [jlib:timestamp_to_xml(TimeStamp)]), + SPacket = jlib:replace_from_to( + jlib:jid_replace_resource(StateData#state.jid, FromNick), + StateData#state.jid, + TSPacket), + Size = lists:flatlength(xml:element_to_string(SPacket)), + Q1 = lqueue_in({FromNick, TSPacket, HaveSubject, TimeStamp, Size}, + StateData#state.history), + add_to_log(text, {FromNick, Packet}, StateData), + StateData#state{history = Q1}. + +send_history(JID, Shift, StateData) -> + lists:foldl( + fun({Nick, Packet, HaveSubject, _TimeStamp, _Size}, B) -> + ejabberd_router:route( + jlib:jid_replace_resource(StateData#state.jid, Nick), + JID, + Packet), + B or HaveSubject + end, false, lists:nthtail(Shift, lqueue_to_list(StateData#state.history))). + + +send_subject(JID, Lang, StateData) -> + case StateData#state.subject_author of + "" -> + ok; + Nick -> + Subject = StateData#state.subject, + Packet = {xmlelement, "message", [{"type", "groupchat"}], + [{xmlelement, "subject", [], [{xmlcdata, Subject}]}, + {xmlelement, "body", [], + [{xmlcdata, + Nick ++ + translate:translate(Lang, + " has set the subject to: ") ++ + Subject}]}]}, + ejabberd_router:route( + StateData#state.jid, + JID, + Packet) + end. + +check_subject(Packet) -> + case xml:get_subtag(Packet, "subject") of + false -> + false; + SubjEl -> + xml:get_tag_cdata(SubjEl) + end. + +can_change_subject(Role, StateData) -> + case (StateData#state.config)#config.allow_change_subj of + true -> + (Role == moderator) orelse (Role == participant); + _ -> + Role == moderator + end. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% Admin stuff + +process_iq_admin(From, set, Lang, SubEl, StateData) -> + {xmlelement, _, _, Items} = SubEl, + process_admin_items_set(From, Items, Lang, StateData); + +process_iq_admin(From, get, Lang, SubEl, StateData) -> + case xml:get_subtag(SubEl, "item") of + false -> + {error, ?ERR_BAD_REQUEST}; + Item -> + FAffiliation = get_affiliation(From, StateData), + FRole = get_role(From, StateData), + case xml:get_tag_attr("role", Item) of + false -> + case xml:get_tag_attr("affiliation", Item) of + false -> + {error, ?ERR_BAD_REQUEST}; + {value, StrAffiliation} -> + case catch list_to_affiliation(StrAffiliation) of + {'EXIT', _} -> + {error, ?ERR_BAD_REQUEST}; + SAffiliation -> + if + (FAffiliation == owner) or + (FAffiliation == admin) -> + Items = items_with_affiliation( + SAffiliation, StateData), + {result, Items, StateData}; + true -> + ErrText = "Administrator privileges required", + {error, ?ERRT_FORBIDDEN(Lang, ErrText)} + end + end + end; + {value, StrRole} -> + case catch list_to_role(StrRole) of + {'EXIT', _} -> + {error, ?ERR_BAD_REQUEST}; + SRole -> + if + FRole == moderator -> + Items = items_with_role(SRole, StateData), + {result, Items, StateData}; + true -> + ErrText = "Moderator privileges required", + {error, ?ERRT_FORBIDDEN(Lang, ErrText)} + end + end + end + end. + + +items_with_role(SRole, StateData) -> + lists:map( + fun({_, U}) -> + user_to_item(U, StateData) + end, search_role(SRole, StateData)). + +items_with_affiliation(SAffiliation, StateData) -> + lists:map( + fun({JID, {Affiliation, Reason}}) -> + {xmlelement, "item", + [{"affiliation", affiliation_to_list(Affiliation)}, + {"jid", jlib:jid_to_string(JID)}], + [{xmlelement, "reason", [], [{xmlcdata, Reason}]}]}; + ({JID, Affiliation}) -> + {xmlelement, "item", + [{"affiliation", affiliation_to_list(Affiliation)}, + {"jid", jlib:jid_to_string(JID)}], + []} + end, search_affiliation(SAffiliation, StateData)). + +user_to_item(#user{role = Role, + nick = Nick, + jid = JID + }, StateData) -> + Affiliation = get_affiliation(JID, StateData), + {xmlelement, "item", + [{"role", role_to_list(Role)}, + {"affiliation", affiliation_to_list(Affiliation)}, + {"nick", Nick}, + {"jid", jlib:jid_to_string(JID)}], + []}. + +search_role(Role, StateData) -> + lists:filter( + fun({_, #user{role = R}}) -> + Role == R + end, ?DICT:to_list(StateData#state.users)). + +search_affiliation(Affiliation, StateData) -> + lists:filter( + fun({_, A}) -> + case A of + {A1, _Reason} -> + Affiliation == A1; + _ -> + Affiliation == A + end + end, ?DICT:to_list(StateData#state.affiliations)). + + +process_admin_items_set(UJID, Items, Lang, StateData) -> + UAffiliation = get_affiliation(UJID, StateData), + URole = get_role(UJID, StateData), + case find_changed_items(UJID, UAffiliation, URole, Items, Lang, StateData, []) of + {result, Res} -> + ?INFO_MSG("Processing MUC admin query from ~s:~n ~p~n", + [jlib:jid_to_string(UJID), Res]), + NSD = + lists:foldl( + fun(E, SD) -> + case catch ( + case E of + {JID, role, none, Reason} -> + catch send_kickban_presence( + JID, Reason, "307", SD), + set_role(JID, none, SD); + {JID, affiliation, none, Reason} -> + case (SD#state.config)#config.members_only of + true -> + catch send_kickban_presence( + JID, Reason, "321", SD), + SD1 = set_affiliation(JID, none, SD), + set_role(JID, none, SD1); + _ -> + SD1 = set_affiliation(JID, none, SD), + send_update_presence(JID, SD1), + SD1 + end; + {JID, affiliation, outcast, Reason} -> + catch send_kickban_presence( + JID, Reason, "301", SD), + set_affiliation_and_reason( + JID, outcast, Reason, + set_role(JID, none, SD)); + {JID, affiliation, A, _Reason} when + (A == admin) or (A == owner) -> + SD1 = set_affiliation(JID, A, SD), + SD2 = set_role(JID, moderator, SD1), + send_update_presence(JID, SD2), + SD2; + {JID, affiliation, member, _Reason} -> + SD1 = set_affiliation( + JID, member, SD), + SD2 = set_role(JID, participant, SD1), + send_update_presence(JID, SD2), + SD2; + {JID, role, R, _Reason} -> + SD1 = set_role(JID, R, SD), + catch send_new_presence(JID, SD1), + SD1; + {JID, affiliation, A, _Reason} -> + SD1 = set_affiliation(JID, A, SD), + send_update_presence(JID, SD1), + SD1 + end + ) of + {'EXIT', ErrReason} -> + ?ERROR_MSG("MUC ITEMS SET ERR: ~p~n", + [ErrReason]), + SD; + NSD -> + NSD + end + end, StateData, Res), + case (NSD#state.config)#config.persistent of + true -> + mod_muc:store_room(NSD#state.host, NSD#state.room, + make_opts(NSD)); + _ -> + ok + end, + {result, [], NSD}; + Err -> + Err + end. + + +find_changed_items(_UJID, _UAffiliation, _URole, [], _Lang, _StateData, Res) -> + {result, Res}; +find_changed_items(UJID, UAffiliation, URole, [{xmlcdata, _} | Items], + Lang, StateData, Res) -> + find_changed_items(UJID, UAffiliation, URole, Items, Lang, StateData, Res); +find_changed_items(UJID, UAffiliation, URole, + [{xmlelement, "item", Attrs, _Els} = Item | Items], + Lang, StateData, Res) -> + TJID = case xml:get_attr("jid", Attrs) of + {value, S} -> + case jlib:string_to_jid(S) of + error -> + ErrText = io_lib:format( + translate:translate( + Lang, + "JID ~s is invalid"), [S]), + {error, ?ERRT_NOT_ACCEPTABLE(Lang, ErrText)}; + J -> + {value, J} + end; + _ -> + case xml:get_attr("nick", Attrs) of + {value, N} -> + case find_jid_by_nick(N, StateData) of + false -> + ErrText = + io_lib:format( + translate:translate( + Lang, + "Nickname ~s does not exist in the room"), + [N]), + {error, ?ERRT_NOT_ACCEPTABLE(Lang, ErrText)}; + J -> + {value, J} + end; + _ -> + {error, ?ERR_BAD_REQUEST} + end + end, + case TJID of + {value, JID} -> + TAffiliation = get_affiliation(JID, StateData), + TRole = get_role(JID, StateData), + case xml:get_attr("role", Attrs) of + false -> + case xml:get_attr("affiliation", Attrs) of + false -> + {error, ?ERR_BAD_REQUEST}; + {value, StrAffiliation} -> + case catch list_to_affiliation(StrAffiliation) of + {'EXIT', _} -> + ErrText1 = + io_lib:format( + translate:translate( + Lang, + "Invalid affiliation: ~s"), + [StrAffiliation]), + {error, ?ERRT_NOT_ACCEPTABLE(Lang, ErrText1)}; + SAffiliation -> + CanChangeRA = + case can_change_ra( + UAffiliation, URole, + TAffiliation, TRole, + affiliation, SAffiliation) of + nothing -> + nothing; + true -> + true; + check_owner -> + case search_affiliation( + owner, StateData) of + [{OJID, _}] -> + jlib:jid_remove_resource(OJID) /= + jlib:jid_tolower(jlib:jid_remove_resource(UJID)); + _ -> + true + end; + _ -> + false + end, + case CanChangeRA of + nothing -> + find_changed_items( + UJID, + UAffiliation, URole, + Items, Lang, StateData, + Res); + true -> + find_changed_items( + UJID, + UAffiliation, URole, + Items, Lang, StateData, + [{jlib:jid_remove_resource(JID), + affiliation, + SAffiliation, + xml:get_path_s( + Item, [{elem, "reason"}, + cdata])} | Res]); + false -> + {error, ?ERR_NOT_ALLOWED} + end + end + end; + {value, StrRole} -> + case catch list_to_role(StrRole) of + {'EXIT', _} -> + ErrText1 = + io_lib:format( + translate:translate( + Lang, + "Invalid role: ~s"), + [StrRole]), + {error, ?ERRT_BAD_REQUEST(Lang, ErrText1)}; + SRole -> + CanChangeRA = + case can_change_ra( + UAffiliation, URole, + TAffiliation, TRole, + role, SRole) of + nothing -> + nothing; + true -> + true; + check_owner -> + case search_affiliation( + owner, StateData) of + [{OJID, _}] -> + jlib:jid_remove_resource(OJID) /= + jlib:jid_tolower(jlib:jid_remove_resource(UJID)); + _ -> + true + end; + _ -> + false + end, + case CanChangeRA of + nothing -> + find_changed_items( + UJID, + UAffiliation, URole, + Items, Lang, StateData, + Res); + true -> + find_changed_items( + UJID, + UAffiliation, URole, + Items, Lang, StateData, + [{JID, role, SRole, + xml:get_path_s( + Item, [{elem, "reason"}, + cdata])} | Res]); + _ -> + {error, ?ERR_NOT_ALLOWED} + end + end + end; + Err -> + Err + end; +find_changed_items(_UJID, _UAffiliation, _URole, _Items, + _Lang, _StateData, _Res) -> + {error, ?ERR_BAD_REQUEST}. + + +can_change_ra(_FAffiliation, _FRole, + TAffiliation, _TRole, + affiliation, Value) + when (TAffiliation == Value) -> + nothing; +can_change_ra(_FAffiliation, _FRole, + _TAffiliation, TRole, + role, Value) + when (TRole == Value) -> + nothing; +can_change_ra(FAffiliation, _FRole, + outcast, _TRole, + affiliation, none) + when (FAffiliation == owner) or (FAffiliation == admin) -> + true; +can_change_ra(FAffiliation, _FRole, + outcast, _TRole, + affiliation, member) + when (FAffiliation == owner) or (FAffiliation == admin) -> + true; +can_change_ra(owner, _FRole, + outcast, _TRole, + affiliation, admin) -> + true; +can_change_ra(owner, _FRole, + outcast, _TRole, + affiliation, owner) -> + true; +can_change_ra(FAffiliation, _FRole, + none, _TRole, + affiliation, outcast) + when (FAffiliation == owner) or (FAffiliation == admin) -> + true; +can_change_ra(FAffiliation, _FRole, + none, _TRole, + affiliation, member) + when (FAffiliation == owner) or (FAffiliation == admin) -> + true; +can_change_ra(owner, _FRole, + none, _TRole, + affiliation, admin) -> + true; +can_change_ra(owner, _FRole, + none, _TRole, + affiliation, owner) -> + true; +can_change_ra(FAffiliation, _FRole, + member, _TRole, + affiliation, outcast) + when (FAffiliation == owner) or (FAffiliation == admin) -> + true; +can_change_ra(FAffiliation, _FRole, + member, _TRole, + affiliation, none) + when (FAffiliation == owner) or (FAffiliation == admin) -> + true; +can_change_ra(owner, _FRole, + member, _TRole, + affiliation, admin) -> + true; +can_change_ra(owner, _FRole, + member, _TRole, + affiliation, owner) -> + true; +can_change_ra(owner, _FRole, + admin, _TRole, + affiliation, _Affiliation) -> + true; +can_change_ra(owner, _FRole, + owner, _TRole, + affiliation, _Affiliation) -> + check_owner; +can_change_ra(_FAffiliation, _FRole, + _TAffiliation, _TRole, + affiliation, _Value) -> + false; +can_change_ra(_FAffiliation, moderator, + _TAffiliation, visitor, + role, none) -> + true; +can_change_ra(_FAffiliation, moderator, + _TAffiliation, visitor, + role, participant) -> + true; +can_change_ra(FAffiliation, _FRole, + _TAffiliation, visitor, + role, moderator) + when (FAffiliation == owner) or (FAffiliation == admin) -> + true; +can_change_ra(_FAffiliation, moderator, + _TAffiliation, participant, + role, none) -> + true; +can_change_ra(_FAffiliation, moderator, + _TAffiliation, participant, + role, visitor) -> + true; +can_change_ra(FAffiliation, _FRole, + _TAffiliation, participant, + role, moderator) + when (FAffiliation == owner) or (FAffiliation == admin) -> + true; +can_change_ra(_FAffiliation, _FRole, + owner, moderator, + role, visitor) -> + false; +can_change_ra(owner, _FRole, + _TAffiliation, moderator, + role, visitor) -> + true; +can_change_ra(_FAffiliation, _FRole, + admin, moderator, + role, visitor) -> + false; +can_change_ra(admin, _FRole, + _TAffiliation, moderator, + role, visitor) -> + true; +can_change_ra(_FAffiliation, _FRole, + owner, moderator, + role, participant) -> + false; +can_change_ra(owner, _FRole, + _TAffiliation, moderator, + role, participant) -> + true; +can_change_ra(_FAffiliation, _FRole, + admin, moderator, + role, participant) -> + false; +can_change_ra(admin, _FRole, + _TAffiliation, moderator, + role, participant) -> + true; +can_change_ra(_FAffiliation, _FRole, + _TAffiliation, _TRole, + role, _Value) -> + false. + + +send_kickban_presence(JID, Reason, Code, StateData) -> + LJID = jlib:jid_tolower(JID), + LJIDs = case LJID of + {U, S, ""} -> + ?DICT:fold( + fun(J, _, Js) -> + case J of + {U, S, _} -> + [J | Js]; + _ -> + Js + end + end, [], StateData#state.users); + _ -> + case ?DICT:is_key(LJID, StateData#state.users) of + true -> + [LJID]; + _ -> + [] + end + end, + lists:foreach(fun(J) -> + {ok, #user{nick = Nick}} = + ?DICT:find(J, StateData#state.users), + add_to_log(kickban, {Nick, Reason, Code}, StateData), + send_kickban_presence1(J, Reason, Code, StateData) + end, LJIDs). + +send_kickban_presence1(UJID, Reason, Code, StateData) -> + {ok, #user{jid = _RealJID, + nick = Nick}} = + ?DICT:find(jlib:jid_tolower(UJID), StateData#state.users), + Affiliation = get_affiliation(UJID, StateData), + SAffiliation = affiliation_to_list(Affiliation), + lists:foreach( + fun({_LJID, Info}) -> + ItemAttrs = [{"affiliation", SAffiliation}, + {"role", "none"}], + ItemEls = case Reason of + "" -> + []; + _ -> + [{xmlelement, "reason", [], + [{xmlcdata, Reason}]}] + end, + Packet = {xmlelement, "presence", [{"type", "unavailable"}], + [{xmlelement, "x", [{"xmlns", ?NS_MUC_USER}], + [{xmlelement, "item", ItemAttrs, ItemEls}, + {xmlelement, "status", [{"code", Code}], []}]}]}, + ejabberd_router:route( + jlib:jid_replace_resource(StateData#state.jid, Nick), + Info#user.jid, + Packet) + end, ?DICT:to_list(StateData#state.users)). + + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% Owner stuff + +process_iq_owner(From, set, Lang, SubEl, StateData) -> + FAffiliation = get_affiliation(From, StateData), + case FAffiliation of + owner -> + {xmlelement, _Name, _Attrs, Els} = SubEl, + case xml:remove_cdata(Els) of + [{xmlelement, "x", _Attrs1, _Els1} = XEl] -> + case {xml:get_tag_attr_s("xmlns", XEl), + xml:get_tag_attr_s("type", XEl)} of + {?NS_XDATA, "cancel"} -> + {result, [], StateData}; + {?NS_XDATA, "submit"} -> + case {check_allowed_log_change(XEl, StateData, From), + check_allowed_persistent_change(XEl, StateData, From)} of + {allow, allow} -> set_config(XEl, StateData); + _ -> {error, ?ERR_BAD_REQUEST} + end; + _ -> + {error, ?ERR_BAD_REQUEST} + end; + [{xmlelement, "destroy", _Attrs1, _Els1} = SubEl1] -> + destroy_room(SubEl1, StateData); + Items -> + process_admin_items_set(From, Items, Lang, StateData) + end; + _ -> + ErrText = "Owner privileges required", + {error, ?ERRT_FORBIDDEN(Lang, ErrText)} + end; + +process_iq_owner(From, get, Lang, SubEl, StateData) -> + FAffiliation = get_affiliation(From, StateData), + case FAffiliation of + owner -> + {xmlelement, _Name, _Attrs, Els} = SubEl, + case xml:remove_cdata(Els) of + [] -> + get_config(Lang, StateData, From); + [Item] -> + case xml:get_tag_attr("affiliation", Item) of + false -> + {error, ?ERR_BAD_REQUEST}; + {value, StrAffiliation} -> + case catch list_to_affiliation(StrAffiliation) of + {'EXIT', _} -> + ErrText = + io_lib:format( + translate:translate( + Lang, + "Invalid affiliation: ~s"), + [StrAffiliation]), + {error, ?ERRT_NOT_ACCEPTABLE(Lang, ErrText)}; + SAffiliation -> + Items = items_with_affiliation( + SAffiliation, StateData), + {result, Items, StateData} + end + end; + _ -> + {error, ?ERR_FEATURE_NOT_IMPLEMENTED} + end; + _ -> + ErrText = "Owner privileges required", + {error, ?ERRT_FORBIDDEN(Lang, ErrText)} + end. + +check_allowed_log_change(XEl, StateData, From) -> + case lists:keymember("muc#roomconfig_enablelogging", 1, + jlib:parse_xdata_submit(XEl)) of + false -> + allow; + true -> + mod_muc_log:check_access_log( + StateData#state.server_host, From) + end. + +check_allowed_persistent_change(XEl, StateData, From) -> + case lists:keymember("muc#roomconfig_persistentroom", 1, + jlib:parse_xdata_submit(XEl)) of + false -> + allow; + true -> + {_AccessRoute, _AccessCreate, _AccessAdmin, AccessPersistent} = StateData#state.access, + acl:match_rule(StateData#state.server_host, AccessPersistent, From) + end. + +-define(XFIELD(Type, Label, Var, Val), + {xmlelement, "field", [{"type", Type}, + {"label", translate:translate(Lang, Label)}, + {"var", Var}], + [{xmlelement, "value", [], [{xmlcdata, Val}]}]}). + +-define(BOOLXFIELD(Label, Var, Val), + ?XFIELD("boolean", Label, Var, + case Val of + true -> "1"; + _ -> "0" + end)). + +-define(STRINGXFIELD(Label, Var, Val), + ?XFIELD("text-single", Label, Var, Val)). + +-define(PRIVATEXFIELD(Label, Var, Val), + ?XFIELD("text-private", Label, Var, Val)). + + +get_config(Lang, StateData, From) -> + {_AccessRoute, _AccessCreate, _AccessAdmin, AccessPersistent} = StateData#state.access, + ServiceMaxUsers = get_service_max_users(StateData), + Config = StateData#state.config, + Res = + [{xmlelement, "title", [], + [{xmlcdata, translate:translate(Lang, "Configuration for ") ++ + jlib:jid_to_string(StateData#state.jid)}]}, + {xmlelement, "field", [{"type", "hidden"}, + {"var", "FORM_TYPE"}], + [{xmlelement, "value", [], + [{xmlcdata, "http://jabber.org/protocol/muc#roomconfig"}]}]}, + ?STRINGXFIELD("Room title", + "muc#roomconfig_roomname", + Config#config.title) + ] ++ + case acl:match_rule(StateData#state.server_host, AccessPersistent, From) of + allow -> + [?BOOLXFIELD( + "Make room persistent", + "muc#roomconfig_persistentroom", + Config#config.persistent)]; + _ -> [] + end ++ [ + ?BOOLXFIELD("Make room public searchable", + "muc#roomconfig_publicroom", + Config#config.public), + ?BOOLXFIELD("Make participants list public", + "public_list", + Config#config.public_list), + ?BOOLXFIELD("Make room password protected", + "muc#roomconfig_passwordprotectedroom", + Config#config.password_protected), + ?PRIVATEXFIELD("Password", + "muc#roomconfig_roomsecret", + case Config#config.password_protected of + true -> Config#config.password; + false -> "" + end), + {xmlelement, "field", + [{"type", "list-single"}, + {"label", translate:translate(Lang, "Maximum Number of Occupants")}, + {"var", "muc#roomconfig_maxusers"}], + [{xmlelement, "value", [], [{xmlcdata, + case get_max_users(StateData) of + N when is_integer(N) -> + erlang:integer_to_list(N); + _ -> "none" + end + }]}] ++ + if + is_integer(ServiceMaxUsers) -> []; + true -> + [{xmlelement, "option", + [{"label", translate:translate(Lang, "No limit")}], + [{xmlelement, "value", [], [{xmlcdata, "none"}]}]}] + end ++ + [{xmlelement, "option", [{"label", erlang:integer_to_list(N)}], + [{xmlelement, "value", [], + [{xmlcdata, erlang:integer_to_list(N)}]}]} || + N <- ?MAX_USERS_DEFAULT_LIST, N =< ServiceMaxUsers] + }, + {xmlelement, "field", + [{"type", "list-single"}, + {"label", translate:translate(Lang, "Present real JIDs to")}, + {"var", "muc#roomconfig_whois"}], + [{xmlelement, "value", [], [{xmlcdata, + if Config#config.anonymous -> + "moderators"; + true -> + "anyone" + end}]}, + {xmlelement, "option", [{"label", translate:translate(Lang, "moderators only")}], + [{xmlelement, "value", [], [{xmlcdata, "moderators"}]}]}, + {xmlelement, "option", [{"label", translate:translate(Lang, "anyone")}], + [{xmlelement, "value", [], [{xmlcdata, "anyone"}]}]}]}, + ?BOOLXFIELD("Make room members-only", + "muc#roomconfig_membersonly", + Config#config.members_only), + ?BOOLXFIELD("Make room moderated", + "muc#roomconfig_moderatedroom", + Config#config.moderated), + ?BOOLXFIELD("Default users as participants", + "members_by_default", + Config#config.members_by_default), + ?BOOLXFIELD("Allow users to change subject", + "muc#roomconfig_changesubject", + Config#config.allow_change_subj), + ?BOOLXFIELD("Allow users to send private messages", + "allow_private_messages", + Config#config.allow_private_messages), + ?BOOLXFIELD("Allow users to query other users", + "allow_query_users", + Config#config.allow_query_users), + ?BOOLXFIELD("Allow users to send invites", + "muc#roomconfig_allowinvites", + Config#config.allow_user_invites) + ] ++ + case mod_muc_log:check_access_log( + StateData#state.server_host, From) of + allow -> + [?BOOLXFIELD( + "Enable logging", + "muc#roomconfig_enablelogging", + Config#config.logging)]; + _ -> [] + end, + {result, [{xmlelement, "instructions", [], + [{xmlcdata, + translate:translate( + Lang, "You need an x:data capable client to configure room")}]}, + {xmlelement, "x", [{"xmlns", ?NS_XDATA}, + {"type", "form"}], + Res}], + StateData}. + + + +set_config(XEl, StateData) -> + XData = jlib:parse_xdata_submit(XEl), + case XData of + invalid -> + {error, ?ERR_BAD_REQUEST}; + _ -> + case set_xoption(XData, StateData#state.config) of + #config{} = Config -> + Res = change_config(Config, StateData), + {result, _, NSD} = Res, + add_to_log(roomconfig_change, [], NSD), + Res; + Err -> + Err + end + end. + +-define(SET_BOOL_XOPT(Opt, Val), + case Val of + "0" -> set_xoption(Opts, Config#config{Opt = false}); + "false" -> set_xoption(Opts, Config#config{Opt = false}); + "1" -> set_xoption(Opts, Config#config{Opt = true}); + "true" -> set_xoption(Opts, Config#config{Opt = true}); + _ -> {error, ?ERR_BAD_REQUEST} + end). + +-define(SET_NAT_XOPT(Opt, Val), + case catch list_to_integer(Val) of + I when is_integer(I), + I > 0 -> + set_xoption(Opts, Config#config{Opt = I}); + _ -> + {error, ?ERR_BAD_REQUEST} + end). + +-define(SET_STRING_XOPT(Opt, Val), + set_xoption(Opts, Config#config{Opt = Val})). + + +set_xoption([], Config) -> + Config; +set_xoption([{"muc#roomconfig_roomname", [Val]} | Opts], Config) -> + ?SET_STRING_XOPT(title, Val); +set_xoption([{"muc#roomconfig_changesubject", [Val]} | Opts], Config) -> + ?SET_BOOL_XOPT(allow_change_subj, Val); +set_xoption([{"allow_query_users", [Val]} | Opts], Config) -> + ?SET_BOOL_XOPT(allow_query_users, Val); +set_xoption([{"allow_private_messages", [Val]} | Opts], Config) -> + ?SET_BOOL_XOPT(allow_private_messages, Val); +set_xoption([{"muc#roomconfig_publicroom", [Val]} | Opts], Config) -> + ?SET_BOOL_XOPT(public, Val); +set_xoption([{"public_list", [Val]} | Opts], Config) -> + ?SET_BOOL_XOPT(public_list, Val); +set_xoption([{"muc#roomconfig_persistentroom", [Val]} | Opts], Config) -> + ?SET_BOOL_XOPT(persistent, Val); +set_xoption([{"muc#roomconfig_moderatedroom", [Val]} | Opts], Config) -> + ?SET_BOOL_XOPT(moderated, Val); +set_xoption([{"members_by_default", [Val]} | Opts], Config) -> + ?SET_BOOL_XOPT(members_by_default, Val); +set_xoption([{"muc#roomconfig_membersonly", [Val]} | Opts], Config) -> + ?SET_BOOL_XOPT(members_only, Val); +set_xoption([{"muc#roomconfig_allowinvites", [Val]} | Opts], Config) -> + ?SET_BOOL_XOPT(allow_user_invites, Val); +set_xoption([{"muc#roomconfig_passwordprotectedroom", [Val]} | Opts], Config) -> + ?SET_BOOL_XOPT(password_protected, Val); +set_xoption([{"muc#roomconfig_roomsecret", [Val]} | Opts], Config) -> + ?SET_STRING_XOPT(password, Val); +set_xoption([{"anonymous", [Val]} | Opts], Config) -> + ?SET_BOOL_XOPT(anonymous, Val); +set_xoption([{"muc#roomconfig_whois", [Val]} | Opts], Config) -> + case Val of + "moderators" -> + ?SET_BOOL_XOPT(anonymous, "1"); + "anyone" -> + ?SET_BOOL_XOPT(anonymous, "0"); + _ -> + {error, ?ERR_BAD_REQUEST} + end; +set_xoption([{"muc#roomconfig_maxusers", [Val]} | Opts], Config) -> + case Val of + "none" -> + ?SET_STRING_XOPT(max_users, none); + _ -> + ?SET_NAT_XOPT(max_users, Val) + end; +set_xoption([{"muc#roomconfig_enablelogging", [Val]} | Opts], Config) -> + ?SET_BOOL_XOPT(logging, Val); +set_xoption([{"FORM_TYPE", _} | Opts], Config) -> + %% Ignore our FORM_TYPE + set_xoption(Opts, Config); +set_xoption([_ | _Opts], _Config) -> + {error, ?ERR_BAD_REQUEST}. + + +change_config(Config, StateData) -> + NSD = StateData#state{config = Config}, + case {(StateData#state.config)#config.persistent, + Config#config.persistent} of + {_, true} -> + mod_muc:store_room(NSD#state.host, NSD#state.room, make_opts(NSD)); + {true, false} -> + mod_muc:forget_room(NSD#state.host, NSD#state.room); + {false, false} -> + ok + end, + case {(StateData#state.config)#config.members_only, + Config#config.members_only} of + {false, true} -> + NSD1 = remove_nonmembers(NSD), + {result, [], NSD1}; + _ -> + {result, [], NSD} + end. + +remove_nonmembers(StateData) -> + lists:foldl( + fun({_LJID, #user{jid = JID}}, SD) -> + Affiliation = get_affiliation(JID, SD), + case Affiliation of + none -> + catch send_kickban_presence( + JID, "", "322", SD), + set_role(JID, none, SD); + _ -> + SD + end + end, StateData, ?DICT:to_list(StateData#state.users)). + + +-define(CASE_CONFIG_OPT(Opt), + Opt -> StateData#state{ + config = (StateData#state.config)#config{Opt = Val}}). + +set_opts([], StateData) -> + StateData; +set_opts([{Opt, Val} | Opts], StateData) -> + NSD = case Opt of + ?CASE_CONFIG_OPT(title); + ?CASE_CONFIG_OPT(allow_change_subj); + ?CASE_CONFIG_OPT(allow_query_users); + ?CASE_CONFIG_OPT(allow_private_messages); + ?CASE_CONFIG_OPT(public); + ?CASE_CONFIG_OPT(public_list); + ?CASE_CONFIG_OPT(persistent); + ?CASE_CONFIG_OPT(moderated); + ?CASE_CONFIG_OPT(members_by_default); + ?CASE_CONFIG_OPT(members_only); + ?CASE_CONFIG_OPT(allow_user_invites); + ?CASE_CONFIG_OPT(password_protected); + ?CASE_CONFIG_OPT(password); + ?CASE_CONFIG_OPT(anonymous); + ?CASE_CONFIG_OPT(logging); + max_users -> + ServiceMaxUsers = get_service_max_users(StateData), + MaxUsers = if + Val =< ServiceMaxUsers -> Val; + true -> ServiceMaxUsers + end, + StateData#state{ + config = (StateData#state.config)#config{ + max_users = MaxUsers}}; + affiliations -> + StateData#state{affiliations = ?DICT:from_list(Val)}; + subject -> + StateData#state{subject = Val}; + subject_author -> + StateData#state{subject_author = Val}; + _ -> StateData + end, + set_opts(Opts, NSD). + +-define(MAKE_CONFIG_OPT(Opt), {Opt, Config#config.Opt}). + +make_opts(StateData) -> + Config = StateData#state.config, + [ + ?MAKE_CONFIG_OPT(title), + ?MAKE_CONFIG_OPT(allow_change_subj), + ?MAKE_CONFIG_OPT(allow_query_users), + ?MAKE_CONFIG_OPT(allow_private_messages), + ?MAKE_CONFIG_OPT(public), + ?MAKE_CONFIG_OPT(public_list), + ?MAKE_CONFIG_OPT(persistent), + ?MAKE_CONFIG_OPT(moderated), + ?MAKE_CONFIG_OPT(members_by_default), + ?MAKE_CONFIG_OPT(members_only), + ?MAKE_CONFIG_OPT(allow_user_invites), + ?MAKE_CONFIG_OPT(password_protected), + ?MAKE_CONFIG_OPT(password), + ?MAKE_CONFIG_OPT(anonymous), + ?MAKE_CONFIG_OPT(logging), + ?MAKE_CONFIG_OPT(max_users), + {affiliations, ?DICT:to_list(StateData#state.affiliations)}, + {subject, StateData#state.subject}, + {subject_author, StateData#state.subject_author} + ]. + + + +destroy_room(DEl, StateData) -> + lists:foreach( + fun({_LJID, Info}) -> + Nick = Info#user.nick, + ItemAttrs = [{"affiliation", "none"}, + {"role", "none"}], + Packet = {xmlelement, "presence", [{"type", "unavailable"}], + [{xmlelement, "x", [{"xmlns", ?NS_MUC_USER}], + [{xmlelement, "item", ItemAttrs, []}, DEl]}]}, + ejabberd_router:route( + jlib:jid_replace_resource(StateData#state.jid, Nick), + Info#user.jid, + Packet) + end, ?DICT:to_list(StateData#state.users)), + case (StateData#state.config)#config.persistent of + true -> + mod_muc:forget_room(StateData#state.host, StateData#state.room); + false -> + ok + end, + {result, [], stop}. + + + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% Disco + +-define(FEATURE(Var), {xmlelement, "feature", [{"var", Var}], []}). + +-define(CONFIG_OPT_TO_FEATURE(Opt, Fiftrue, Fiffalse), + case Opt of + true -> + ?FEATURE(Fiftrue); + false -> + ?FEATURE(Fiffalse) + end). + +process_iq_disco_info(_From, set, _Lang, _StateData) -> + {error, ?ERR_NOT_ALLOWED}; + +process_iq_disco_info(_From, get, Lang, StateData) -> + Config = StateData#state.config, + {result, [{xmlelement, "identity", + [{"category", "conference"}, + {"type", "text"}, + {"name", get_title(StateData)}], []}, + {xmlelement, "feature", + [{"var", ?NS_MUC}], []}, + ?CONFIG_OPT_TO_FEATURE(Config#config.public, + "muc_public", "muc_hidden"), + ?CONFIG_OPT_TO_FEATURE(Config#config.persistent, + "muc_persistent", "muc_temporary"), + ?CONFIG_OPT_TO_FEATURE(Config#config.members_only, + "muc_membersonly", "muc_open"), + ?CONFIG_OPT_TO_FEATURE(Config#config.anonymous, + "muc_semianonymous", "muc_nonanonymous"), + ?CONFIG_OPT_TO_FEATURE(Config#config.moderated, + "muc_moderated", "muc_unmoderated"), + ?CONFIG_OPT_TO_FEATURE(Config#config.password_protected, + "muc_passwordprotected", "muc_unsecured") + ] ++ iq_disco_info_extras(Lang, StateData), StateData}. + +-define(RFIELDT(Type, Var, Val), + {xmlelement, "field", [{"type", Type}, {"var", Var}], + [{xmlelement, "value", [], [{xmlcdata, Val}]}]}). + +-define(RFIELD(Label, Var, Val), + {xmlelement, "field", [{"label", translate:translate(Lang, Label)}, + {"var", Var}], + [{xmlelement, "value", [], [{xmlcdata, Val}]}]}). + +iq_disco_info_extras(Lang, StateData) -> + Len = length(?DICT:to_list(StateData#state.users)), + [{xmlelement, "x", [{"xmlns", ?NS_XDATA}, {"type", "result"}], + [?RFIELDT("hidden", "FORM_TYPE", + "http://jabber.org/protocol/muc#roominfo"), + ?RFIELD("Description", "muc#roominfo_description", + (StateData#state.config)#config.title), + %?RFIELD("Subject", "muc#roominfo_subject", StateData#state.subject), + ?RFIELD("Number of occupants", "muc#roominfo_occupants", + integer_to_list(Len)) + ]}]. + +process_iq_disco_items(_From, set, _Lang, _StateData) -> + {error, ?ERR_NOT_ALLOWED}; + +process_iq_disco_items(From, get, _Lang, StateData) -> + FAffiliation = get_affiliation(From, StateData), + FRole = get_role(From, StateData), + case ((StateData#state.config)#config.public_list == true) orelse + (FRole /= none) orelse + (FAffiliation == admin) orelse + (FAffiliation == owner) of + true -> + UList = + lists:map( + fun({_LJID, Info}) -> + Nick = Info#user.nick, + {xmlelement, "item", + [{"jid", jlib:jid_to_string( + {StateData#state.room, + StateData#state.host, + Nick})}, + {"name", Nick}], []} + end, + ?DICT:to_list(StateData#state.users)), + {result, UList, StateData}; + _ -> + {error, ?ERR_FORBIDDEN} + end. + +get_title(StateData) -> + case (StateData#state.config)#config.title of + "" -> + StateData#state.room; + Name -> + Name + end. + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% Invitation support + +check_invitation(From, Els, Lang, StateData) -> + FAffiliation = get_affiliation(From, StateData), + CanInvite = (StateData#state.config)#config.allow_user_invites + orelse (FAffiliation == admin) orelse (FAffiliation == owner), + InviteEl = case xml:remove_cdata(Els) of + [{xmlelement, "x", _Attrs1, Els1} = XEl] -> + case xml:get_tag_attr_s("xmlns", XEl) of + ?NS_MUC_USER -> + ok; + _ -> + throw({error, ?ERR_BAD_REQUEST}) + end, + case xml:remove_cdata(Els1) of + [{xmlelement, "invite", _Attrs2, _Els2} = InviteEl1] -> + InviteEl1; + _ -> + throw({error, ?ERR_BAD_REQUEST}) + end; + _ -> + throw({error, ?ERR_BAD_REQUEST}) + end, + JID = case jlib:string_to_jid( + xml:get_tag_attr_s("to", InviteEl)) of + error -> + throw({error, ?ERR_JID_MALFORMED}); + JID1 -> + JID1 + end, + case CanInvite of + false -> + throw({error, ?ERR_NOT_ALLOWED}); + true -> + Reason = + xml:get_path_s( + InviteEl, + [{elem, "reason"}, cdata]), + IEl = + [{xmlelement, "invite", + [{"from", + jlib:jid_to_string(From)}], + [{xmlelement, "reason", [], + [{xmlcdata, Reason}]}]}], + PasswdEl = + case (StateData#state.config)#config.password_protected of + true -> + [{xmlelement, "password", [], + [{xmlcdata, (StateData#state.config)#config.password}]}]; + _ -> + [] + end, + Body = + {xmlelement, "body", [], + [{xmlcdata, + lists:flatten( + io_lib:format( + translate:translate( + Lang, + "You have been invited to ~s by ~s"), + [jlib:jid_to_string({StateData#state.room, + StateData#state.host, + ""}), + jlib:jid_to_string(From)])) ++ + case (StateData#state.config)#config.password_protected of + true -> + ", " ++ + translate:translate(Lang, "the password is") ++ + " '" ++ + (StateData#state.config)#config.password ++ "'"; + _ -> + "" + end ++ + case Reason of + "" -> ""; + _ -> " (" ++ Reason ++ ") " + end + }]}, + Msg = + {xmlelement, "message", + [{"type", "normal"}], + [{xmlelement, "x", [{"xmlns", ?NS_MUC_USER}], IEl ++ PasswdEl}, + {xmlelement, "x", + [{"xmlns", ?NS_XCONFERENCE}, + {"jid", jlib:jid_to_string( + {StateData#state.room, + StateData#state.host, + ""})}], + [{xmlcdata, Reason}]}, + Body]}, + ejabberd_router:route(StateData#state.jid, JID, Msg), + JID + end. + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% Logging + +add_to_log(Type, Data, StateData) -> + case (StateData#state.config)#config.logging of + true -> + mod_muc_log:add_to_log( + StateData#state.server_host, Type, Data, + StateData#state.jid, make_opts(StateData)); + false -> + ok + end. + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% 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). diff --git a/mod_multicast/src/mod_multicast.erl b/mod_multicast/src/mod_multicast.erl new file mode 100644 index 0000000..f51f8d7 --- /dev/null +++ b/mod_multicast/src/mod_multicast.erl @@ -0,0 +1,1302 @@ +%%%---------------------------------------------------------------------- +%%% File : mod_multicast.erl +%%% Author : Badlop +%%% Purpose : Extended Stanza Addressing (XEP-0033) support +%%% Created : 29 May 2007 by Badlop +%%% Id : $Id: mod_multicast.erl 440 2007-12-06 22:36:21Z badlop $ +%%%---------------------------------------------------------------------- + +-module(mod_multicast). +-author('badlop@ono.com'). + +-behaviour(gen_server). +-behaviour(gen_mod). + +%% API +-export([start_link/2, start/2, stop/1]). + +%% gen_server callbacks +-export([init/1, + handle_info/2, + handle_call/3, + handle_cast/2, + terminate/2, + code_change/3 + ]). + +-export([ + purge_loop/1 + ]). + +-include("ejabberd.hrl"). +-include("jlib.hrl"). + +-record(state, {lserver, lservice, access, service_limits}). + +-record(multicastc, {rserver, response, ts}). +%% ts: timestamp (in seconds) when the cache item was last updated + +-record(dest, {jid_string, jid_jid, type, full_xml}). +%% jid_string = string() +%% jid_jid = jid() +%% full_xml = xml() + +-record(group, {server, dests, multicast, others, addresses}). +%% server = string() +%% dests = [string()] +%% multicast = {cached, local_server} | {cached, string()} | {cached, not_supported} | {obsolete, not_supported} | {obsolete, string()} | not_cached +%% after being updated, possible values are: local | multicast_not_supported | {multicast_supported, string(), limits()} +%% others = [xml()] +%% packet = xml() + +-record(waiter, {awaiting, group, renewal=false, sender, packet, aattrs, addresses}). +%% awaiting = {[Remote_service], Local_service, Type_awaiting} +%% Remote_service = Local_service = string() +%% Type_awaiting = info | items +%% group = #group +%% renewal = true | false +%% sender = From +%% packet = xml() +%% aattrs = [xml()] + +-record(limits, {message, presence}). +%% message = presence = integer() | infinite + +-record(service_limits, {local, remote}). +%% local = remote = limits() + +%% All the elements are of type value() + +-define(VERSION_MULTICAST, "$Revision: 440 $ "). +-define(PROCNAME, ejabberd_mod_multicast). + +%% TODO: move this line to jlib.hrl +-define(NS_ADDRESS, "http://jabber.org/protocol/address"). + +-define(PURGE_PROCNAME, ejabberd_mod_multicast_purgeloop). + +%% TODO: allow configuration instead of hard-coding +%% Time in seconds +-define(MAXTIME_CACHE_POSITIVE, 86400). +-define(MAXTIME_CACHE_NEGATIVE, 86400). + +%% Time in miliseconds +-define(CACHE_PURGE_TIMER, 86400000). % Purge the cache every 24 hours +-define(DISCO_QUERY_TIMEOUT, 10000). % After 10 seconds of delay the server is declared dead + +%% TODO: Put the correct values once XEP33 is updated +-define(DEFAULT_LIMIT_LOCAL_MESSAGE, 100). +-define(DEFAULT_LIMIT_LOCAL_PRESENCE, 100). +-define(DEFAULT_LIMIT_REMOTE_MESSAGE, 20). +-define(DEFAULT_LIMIT_REMOTE_PRESENCE,20). + + +%%==================================================================== +%% API +%%==================================================================== +%%-------------------------------------------------------------------- +%% Function: start_link() -> {ok,Pid} | ignore | {error,Error} +%% Description: Starts the server +%%-------------------------------------------------------------------- +start_link(LServerS, Opts) -> + Proc = gen_mod:get_module_proc(LServerS, ?PROCNAME), + gen_server:start_link({local, Proc}, ?MODULE, [LServerS, Opts], []). + +start(LServerS, Opts) -> + Proc = gen_mod:get_module_proc(LServerS, ?PROCNAME), + ChildSpec = { + Proc, + {?MODULE, start_link, [LServerS, Opts]}, + temporary, + 1000, + worker, + [?MODULE]}, + supervisor:start_child(ejabberd_sup, ChildSpec). + +stop(LServerS) -> + Proc = gen_mod:get_module_proc(LServerS, ?PROCNAME), + gen_server:call(Proc, stop), + supervisor:terminate_child(ejabberd_sup, Proc), + supervisor:delete_child(ejabberd_sup, Proc). + + +%%==================================================================== +%% gen_server callbacks +%%==================================================================== + +%%-------------------------------------------------------------------- +%% Function: init(Args) -> {ok, State} | +%% {ok, State, Timeout} | +%% ignore | +%% {stop, Reason} +%% Description: Initiates the server +%%-------------------------------------------------------------------- +init([LServerS, Opts]) -> + LServiceS = gen_mod:get_opt(host, Opts, "multicast." ++ LServerS), + Access = gen_mod:get_opt(access, Opts, all), + SLimits = build_service_limit_record(gen_mod:get_opt(limits, Opts, [])), + create_cache(), + try_start_loop(), + create_pool(), + ejabberd_router_multicast:register_route(LServerS), + ejabberd_router:register_route(LServiceS), + {ok, #state{lservice = LServiceS, + lserver = LServerS, + access = Access, + service_limits = SLimits}}. + +%%-------------------------------------------------------------------- +%% Function: %% handle_call(Request, From, State) -> {reply, Reply, State} | +%% {reply, Reply, State, Timeout} | +%% {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, Reply, State} | +%% {stop, Reason, State} +%% Description: Handling call messages +%%-------------------------------------------------------------------- +handle_call(stop, _From, State) -> + try_stop_loop(), + {stop, normal, ok, State}. + +%%-------------------------------------------------------------------- +%% Function: handle_cast(Msg, State) -> {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, State} +%% Description: Handling cast messages +%%-------------------------------------------------------------------- +handle_cast(_Msg, State) -> + {noreply, State}. + +%%-------------------------------------------------------------------- +%% Function: handle_info(Info, State) -> {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, State} +%% Description: Handling all non call/cast messages +%%-------------------------------------------------------------------- + +handle_info({route, From, To, {xmlelement, "iq", Attrs, _Els} = Packet}, State) -> + IQ = jlib:iq_query_info(Packet), + case catch process_iq(From, IQ, State) of + Result when is_record(Result, iq) -> + ejabberd_router:route(To, From, jlib:iq_to_xml(Result)); + {'EXIT', Reason} -> + ?ERROR_MSG("Error when processing IQ stanza: ~p", [Reason]), + Err = jlib:make_error_reply(Packet, ?ERR_INTERNAL_SERVER_ERROR), + ejabberd_router:route(To, From, Err); + reply -> + LServiceS = jts(To), + case xml:get_attr_s("type", Attrs) of + "result" -> process_iqreply_result(From, LServiceS, Packet, State); + "error" -> process_iqreply_error(From, LServiceS, Packet) + end + end, + {noreply, State}; + +%% XEP33 allows only 'message' and 'presence' stanza type +handle_info({route, From, To, {xmlelement, Stanza_type, _, _} = Packet}, + #state{lservice = LServiceS, + lserver = LServerS, + access = Access, + service_limits = SLimits} = State) + when (Stanza_type == "message") or (Stanza_type == "presence") -> + %%io:format("Multicast packet: ~nFrom: ~p~nTo: ~p~nPacket: ~p~n", [From, To, Packet]), + route_untrusted(LServiceS, LServerS, Access, SLimits, From, To, Packet), + {noreply, State}; + +%% Handle multicast packets sent by trusted local services +handle_info({route_trusted, From, Destinations, Packet}, + #state{lservice = LServiceS, + lserver = LServerS} = State) -> + %%io:format("Multicast packet2: ~nFrom: ~p~nDestinations: ~p~nPacket: ~p~n", [From, Destinations, Packet]), + route_trusted(LServiceS, LServerS, From, Destinations, Packet), + {noreply, State}; + +handle_info({get_host, Pid}, State) -> + Pid ! {my_host, State#state.lservice}, + {noreply, State}; + +handle_info(_Info, State) -> + {noreply, State}. + + +%%-------------------------------------------------------------------- +%% Function: terminate(Reason, State) -> void() +%% Description: This function is called by a gen_server when it is about to +%% terminate. It should be the opposite of Module:init/1 and do any necessary +%% cleaning up. When it returns, the gen_server terminates with Reason. +%% The return value is ignored. +%%-------------------------------------------------------------------- +terminate(_Reason, State) -> + ejabberd_router_multicast:unregister_route(State#state.lserver), + ejabberd_router:unregister_route(State#state.lservice), + ok. + +%%-------------------------------------------------------------------- +%% Func: code_change(OldVsn, State, Extra) -> {ok, NewState} +%% Description: Convert process state when code is changed +%%-------------------------------------------------------------------- +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + + +%%==================================================================== +%%% Internal functions +%%==================================================================== + + +%%%------------------------ +%%% IQ Request Processing +%%%------------------------ + +%% disco#info request +process_iq(From, #iq{type = get, xmlns = ?NS_DISCO_INFO, lang = Lang} = IQ, State) -> + IQ#iq{type = result, sub_el = + [{xmlelement, "query", [{"xmlns", ?NS_DISCO_INFO}], iq_disco_info(From, Lang, State)}]}; + +%% disco#items request +process_iq(_, #iq{type = get, xmlns = ?NS_DISCO_ITEMS} = IQ, _) -> + IQ#iq{type = result, sub_el = + [{xmlelement, "query", [{"xmlns", ?NS_DISCO_ITEMS}], []}]}; + +%% vCard request +process_iq(_, #iq{type = get, xmlns = ?NS_VCARD, lang = Lang} = IQ, _) -> + IQ#iq{type = result, sub_el = + [{xmlelement, "vCard", [{"xmlns", ?NS_VCARD}], iq_vcard(Lang)}]}; + +%% version request +process_iq(_, #iq{type = get, xmlns = ?NS_VERSION} = IQ, _) -> + IQ#iq{type = result, sub_el = + [{xmlelement, "query", [{"xmlns", ?NS_VERSION}], iq_version()}]}; + +%% Unknown "set" or "get" request +process_iq(_, #iq{type=Type, sub_el=SubEl} = IQ, _) when Type==get; Type==set -> + IQ#iq{type = error, sub_el = [SubEl, ?ERR_SERVICE_UNAVAILABLE]}; + +%% IQ "result" or "error". +process_iq(_, reply, _) -> + reply; + +%% IQ "result" or "error". +process_iq(_, _, _) -> + ok. + +-define(FEATURE(Feat), {xmlelement,"feature",[{"var", Feat}],[]}). + +iq_disco_info(From, Lang, State) -> + [{xmlelement, "identity", + [{"category", "service"}, + {"type", "multicast"}, + {"name", translate:translate(Lang, "Multicast")}], []}, + ?FEATURE(?NS_DISCO_INFO), + ?FEATURE(?NS_DISCO_ITEMS), + ?FEATURE(?NS_VCARD), + ?FEATURE(?NS_ADDRESS)] ++ + iq_disco_info_extras(From, State). + +iq_vcard(Lang) -> + [{xmlelement, "FN", [], + [{xmlcdata, "ejabberd/mod_multicast"}]}, + {xmlelement, "URL", [], + [{xmlcdata, ?EJABBERD_URI}]}, + {xmlelement, "DESC", [], + [{xmlcdata, translate:translate(Lang, "ejabberd Multicast service\n" + "Copyright (c) 2007 Alexey Shchepin")}]}]. + +iq_version() -> + [{xmlelement, "name", [], + [{xmlcdata, "mod_multicast"}]}, + {xmlelement, "version", [], + [{xmlcdata, ?VERSION_MULTICAST}]}]. + + +%%%------------------------- +%%% Route +%%%------------------------- + +%% Destinations = [string()] +route_trusted(LServiceS, LServerS, FromJID, Destinations, Packet) -> + + %% Strip 'addresses' from packet + Packet_stripped = Packet, + AAttrs = [{"xmlns", ?NS_ADDRESS}], + Delivereds = [], + + Dests2 = lists:map( + fun(D) -> + DS = jts(D), + XML = {xmlelement, "address", + [{"type", "bcc"}, {"jid", DS}], + []}, + #dest{jid_string = DS, + jid_jid = D, + type = "bcc", + full_xml = XML} + end, + Destinations), + + %% Group Not_delivered by server + Groups = group_dests(Dests2), + + route_common(LServerS, LServiceS, FromJID, Groups, Delivereds, Packet_stripped, AAttrs). + +route_untrusted(LServiceS, LServerS, Access, SLimits, From, To, Packet) -> + try route_untrusted2(LServiceS, LServerS, Access, SLimits, From, Packet) + catch + throw:adenied -> route_error(To, From, Packet, forbidden, "Access denied by service policy"); + throw:eadsele -> route_error(To, From, Packet, bad_request, "No addresses element found"); + throw:eadeles -> route_error(To, From, Packet, bad_request, "No address elements found"); + throw:ewxmlns -> route_error(To, From, Packet, bad_request, "Wrong xmlns"); + throw:etoorec -> route_error(To, From, Packet, not_acceptable, "Too many receiver fields were specified"); + throw:edrelay -> route_error(To, From, Packet, forbidden, "Packet relay is denied by service policy"); + EType:EReason -> + ?ERROR_MSG("Multicast unknown error: Type: ~p~nReason: ~p", [EType, EReason]), + route_error(To, From, Packet, internal_server_error, "Unknown problem") + end. + +route_untrusted2(LServiceS, LServerS, Access, SLimits, FromJID, Packet) -> + ok = check_access(LServerS, Access, FromJID), + + %% Strip 'addresses' from packet + {ok, Packet_stripped, AAttrs, Addresses} = strip_addresses_element(Packet), + + %% Split Addresses in To_deliver and Delivered + {To_deliver, Delivereds} = split_addresses_todeliver(Addresses), + + %% Convert XML to record + Dests = convert_dest_record(To_deliver), + + %% Split the destinations by JID + {Dests2, Not_jids} = split_dests_jid(Dests), + report_not_jid(FromJID, Packet, Not_jids), + + %% Check limit + ok = check_limit_dests(SLimits, FromJID, Packet, Dests2), + + %% Group Not_delivered by server + Groups = group_dests(Dests2), + + %% Check relay for each Group + ok = check_relay(FromJID#jid.server, LServerS, Groups), + + route_common(LServerS, LServiceS, FromJID, Groups, Delivereds, Packet_stripped, AAttrs). + +route_common(LServerS, LServiceS, FromJID, Groups, Delivereds, Packet_stripped, AAttrs) -> + %% Gather multicast service for each Group + Groups2 = look_cached_servers(LServerS, Groups), + + %% Create Delivered XML element for each Group + Groups3 = build_others_xml(Groups2), + + %% Add preliminary packet for each group + Groups4 = add_addresses(Delivereds, Groups3), + + %% Decide action for each Group + AGroups = decide_action_groups(Groups4), + + act_groups(FromJID, Packet_stripped, AAttrs, LServiceS, AGroups). + +act_groups(FromJID, Packet_stripped, AAttrs, LServiceS, AGroups) -> + [perform(FromJID, Packet_stripped, AAttrs, LServiceS, AGroup) || AGroup <- AGroups]. + +perform(From, Packet, AAttrs, _, {route_single, Group}) -> + [route_packet(From, ToUser, Packet, AAttrs, Group#group.addresses) || ToUser <- Group#group.dests]; + +perform(From, Packet, AAttrs, _, {{route_multicast, JID, RLimits}, Group}) -> + route_packet_multicast(From, JID, Packet, AAttrs, Group#group.dests, Group#group.addresses, RLimits); + +perform(From, Packet, AAttrs, LServiceS, {{ask, Old_service, renewal}, Group}) -> + send_query_info(Old_service, LServiceS), + add_waiter(#waiter{awaiting = {[Old_service], LServiceS, info}, + group = Group, + renewal = true, + sender = From, + packet = Packet, + aattrs = AAttrs, + addresses = Group#group.addresses + }); + +perform(From, Packet, AAttrs, LServiceS, {{ask, Server, not_renewal}, Group}) -> + send_query_info(Server, LServiceS), + add_waiter(#waiter{awaiting = {[Server], LServiceS, info}, + group = Group, + renewal = false, + sender = From, + packet = Packet, + aattrs = AAttrs, + addresses = Group#group.addresses + }). + + +%%%------------------------- +%%% Check access permission +%%%------------------------- + +check_access(LServerS, Access, From) -> + case acl:match_rule(LServerS, Access, From) of + allow -> + ok; + _ -> + throw(adenied) + end. + + +%%%------------------------- +%%% Strip 'addresses' XML element +%%%------------------------- + +strip_addresses_element(Packet) -> + case xml:get_subtag(Packet, "addresses") of + {xmlelement, "addresses", AAttrs, Addresses} -> + case xml:get_attr_s("xmlns", AAttrs) of + ?NS_ADDRESS -> + {xmlelement, Name, Attrs, Els} = Packet, + Els_stripped = lists:keydelete("addresses", 2, Els), + Packet_stripped = {xmlelement, Name, Attrs, Els_stripped}, + {ok, Packet_stripped, AAttrs, Addresses}; + _ -> throw(ewxmlns) + end; + _ -> throw(eadsele) + end. + + +%%%------------------------- +%%% Split Addresses +%%%------------------------- + +split_addresses_todeliver(Addresses) -> + lists:partition( + fun(XML) -> + case XML of + {xmlelement, "address", Attrs, _El} -> + case xml:get_attr_s("delivered", Attrs) of + "true" -> false; + _ -> + Type = xml:get_attr_s("type", Attrs), + case Type of + "to" -> true; + "cc" -> true; + "bcc" -> true; + _ -> false + end + end; + _ -> false + end + end, + Addresses). + + +%%%------------------------- +%%% Check does not exceed limit of destinations +%%%------------------------- + +check_limit_dests(SLimits, FromJID, Packet, Addresses) -> + SenderT = sender_type(FromJID), + Limits = get_slimit_group(SenderT, SLimits), + Type_of_stanza = type_of_stanza(Packet), + {_Type, Limit_number} = get_limit_number(Type_of_stanza, Limits), + case length(Addresses) > Limit_number of + false -> + ok; + true -> + throw(etoorec) + end. + + +%%%------------------------- +%%% Convert Destination XML to record +%%%------------------------- + +convert_dest_record(XMLs) -> + lists:map( + fun(XML) -> + case xml:get_tag_attr_s("jid", XML) of + [] -> + #dest{jid_string = none, full_xml = XML}; + JIDS -> + Type = xml:get_tag_attr_s("type", XML), + JIDJ = stj(JIDS), + #dest{jid_string = JIDS, + jid_jid = JIDJ, + type = Type, + full_xml = XML} + end + end, + XMLs). + + +%%%------------------------- +%%% Split destinations by existence of JID +%%% and send error messages for other dests +%%%------------------------- + +split_dests_jid(Dests) -> + lists:partition( + fun(Dest) -> + case Dest#dest.jid_string of + none -> false; + _ -> true + end + end, + Dests). + +%% Sends an error message for each unknown address +%% Currently only 'jid' addresses are acceptable on ejabberd +report_not_jid(From, Packet, Dests) -> + Dests2 = [xml:element_to_string(Dest#dest.full_xml) || Dest <- Dests], + [route_error(From, From, Packet, jid_malformed, + "This service can not process the address: " ++ D) + || D <- Dests2]. + + +%%%------------------------- +%%% Group destinations by their servers +%%%------------------------- + +group_dests(Dests) -> + D = lists:foldl( + fun(Dest, Dict) -> + ServerS = (Dest#dest.jid_jid)#jid.server, + dict:append(ServerS, Dest, Dict) + end, + dict:new(), + Dests), + Keys = dict:fetch_keys(D), + [ #group{server = Key, dests = dict:fetch(Key, D)} || Key <- Keys ]. + + +%%%------------------------- +%%% Look for cached responses +%%%------------------------- + +look_cached_servers(LServerS, Groups) -> + [look_cached(LServerS, Group) || Group <- Groups]. + +look_cached(LServerS, G) -> + Maxtime_positive = ?MAXTIME_CACHE_POSITIVE, + Maxtime_negative = ?MAXTIME_CACHE_NEGATIVE, + + Cached_response = + search_server_on_cache(G#group.server, LServerS, + {Maxtime_positive, Maxtime_negative}), + G#group{multicast = Cached_response}. + + +%%%------------------------- +%%% Build delivered XML element +%%%------------------------- + +build_others_xml(Groups) -> + [Group#group{others = build_other_xml(Group#group.dests)} || Group <- Groups]. + +%% Add delivered=true +%% and remove addresses which type == bcc +build_other_xml(Dests) -> + lists:foldl( + fun(Dest, R) -> + XML = Dest#dest.full_xml, + case Dest#dest.type of + "to" -> [add_delivered(XML) | R]; + "cc" -> [add_delivered(XML) | R]; + "bcc" -> R; + _ -> [XML | R] + end + end, + [], + Dests). + +add_delivered({xmlelement, Name, Attrs, Els}) -> + Attrs2 = [{"delivered", "true"} | Attrs], + {xmlelement, Name, Attrs2, Els}. + + +%%%------------------------- +%%% Add preliminary packets +%%%------------------------- + +add_addresses(Delivereds, Groups) -> + Ps = [Group#group.others || Group <- Groups], + add_addresses2(Delivereds, Groups, [], [], Ps). + +add_addresses2(_, [], Res, _, []) -> + Res; + +add_addresses2(Delivereds, [Group | Groups], Res, Pa, [Pi | Pz]) -> + Addresses = lists:append([Delivereds] ++ Pa ++ Pz), + Group2 = Group#group{addresses = Addresses}, + add_addresses2(Delivereds, Groups, [Group2 | Res], [Pi | Pa], Pz). + + +%%%------------------------- +%%% Decide action groups +%%%------------------------- + +decide_action_groups(Groups) -> + [{decide_action_group(Group), Group} || Group <- Groups]. + +decide_action_group(Group) -> + Server = Group#group.server, + case Group#group.multicast of + + {cached, local_server} -> + %% Send a copy of the packet to each local user on Dests + route_single; + + {cached, not_supported} -> + %% Send a copy of the packet to each remote user on Dests + route_single; + + {cached, {multicast_supported, JID, RLimits}} -> + %% XEP33 is supported by the server, thanks to this service + {route_multicast, JID, RLimits}; + + {obsolete, {multicast_supported, Old_service, _RLimits}} -> + {ask, Old_service, renewal}; + + {obsolete, not_supported} -> + {ask, Server, not_renewal}; + + not_cached -> + {ask, Server, not_renewal} + + end. + + +%%%------------------------- +%%% Route packet +%%%------------------------- + +%% Build and send packet to this group of destinations +%% From = jid() +%% ToS = string() +route_packet(From, ToDest, Packet, AAttrs, Addresses) -> + Dests = case ToDest#dest.type of + "bcc" -> []; + _ -> [ToDest] + end, + route_packet2(From, ToDest#dest.jid_string, Dests, Packet, AAttrs, Addresses). + +route_packet_multicast(From, ToS, Packet, AAttrs, Dests, Addresses, Limits) -> + Type_of_stanza = type_of_stanza(Packet), + {_Type, Limit_number} = get_limit_number(Type_of_stanza, Limits), + Fragmented_dests = fragment_dests(Dests, Limit_number), + [route_packet2(From, ToS, DFragment, Packet, AAttrs, Addresses) || DFragment <- Fragmented_dests]. + +route_packet2(From, ToS, Dests, Packet, AAttrs, Addresses) -> + {xmlelement, T, A, C} = Packet, + C2 = case append_dests(Dests, Addresses) of + [] -> + C; + ACs -> + [{xmlelement, "addresses", AAttrs, ACs} | C] + end, + + Packet2 = {xmlelement, T, A, C2}, + ToJID = stj(ToS), + ejabberd_router:route(From, ToJID, Packet2). + +append_dests([], Addresses) -> + Addresses; +append_dests([Dest | Dests], Addresses) -> + append_dests(Dests, [Dest#dest.full_xml | Addresses]). + + +%%%------------------------- +%%% Check relay +%%%------------------------- + +check_relay(RS, LS, Gs) -> + case check_relay_required(RS, LS, Gs) of + false -> ok; + true -> throw(edrelay) + end. + +%% If the sender is external, and at least one destination is external, +%% then this package requires relaying +check_relay_required(RServer, LServerS, Groups) -> + case string:str(RServer, LServerS) > 0 of + true -> false; + false -> check_relay_required(LServerS, Groups) + end. + +check_relay_required(LServerS, Groups) -> + lists:any( + fun(Group) -> + Group#group.server /= LServerS + end, + Groups). + + +%%%------------------------- +%%% Check protocol support: Send request +%%%------------------------- + +%% Ask the server if it supports XEP33 +send_query_info(RServerS, LServiceS) -> + %% Don't ask a service which JID is "echo.*", + case string:str(RServerS, "echo.") of + 1 -> false; + _ -> send_query(RServerS, LServiceS, ?NS_DISCO_INFO) + end. + +send_query_items(RServerS, LServiceS) -> + send_query(RServerS, LServiceS, ?NS_DISCO_ITEMS). + +send_query(RServerS, LServiceS, XMLNS) -> + Packet = {xmlelement, "iq", + [{"to", RServerS}, {"type", "get"}], + [{xmlelement, "query", [{"xmlns", XMLNS}], []}]}, + + ejabberd_router:route(stj(LServiceS), stj(RServerS), Packet). + + +%%%------------------------- +%%% Check protocol support: Receive response: Error +%%%------------------------- + +process_iqreply_error(From, LServiceS, _Packet) -> + %% We don't need to change the TO attribute in the outgoing XMPP packet, + %% since ejabberd will do it + + %% We do not change the FROM attribute in the outgoing XMPP packet, + %% this way the user will know what server reported the error + + FromS = jts(From), + case search_waiter(FromS, LServiceS, info) of + {found_waiter, Waiter} -> + received_awaiter(FromS, Waiter, LServiceS); + _ -> ok + end. + + +%%%------------------------- +%%% Check protocol support: Receive response: Disco +%%%------------------------- + +process_iqreply_result(From, LServiceS, Packet, State) -> + {xmlelement, "query", Attrs2, Els2} = xml:get_subtag(Packet, "query"), + case xml:get_attr_s("xmlns", Attrs2) of + ?NS_DISCO_INFO -> + process_discoinfo_result(From, LServiceS, Els2, State); + ?NS_DISCO_ITEMS -> + process_discoitems_result(From, LServiceS, Els2) + end. + + +%%%------------------------- +%%% Check protocol support: Receive response: Disco Info +%%%------------------------- + +process_discoinfo_result(From, LServiceS, Els, _State) -> + FromS = jts(From), + case search_waiter(FromS, LServiceS, info) of + {found_waiter, Waiter} -> + process_discoinfo_result2(From, FromS, LServiceS, Els, Waiter); + _ -> + ok + end. + +process_discoinfo_result2(From, FromS, LServiceS, Els, Waiter) -> + %% Check the response, to see if it includes the XEP33 feature. If support == + Multicast_support = + lists:any( + fun(XML) -> + case XML of + {xmlelement, "feature", Attrs, _} -> + ?NS_ADDRESS == xml:get_attr_s("var", Attrs); + _ -> false + end + end, + Els), + + Group = Waiter#waiter.group, + RServer = Group#group.server, + + case Multicast_support of + true -> + %% Inspect the XML of the disco#info response to get the limits of the remote service + SenderT = sender_type(From), + RLimits = get_limits_xml(Els, SenderT), + + %% Store this response on cache + add_response(RServer, {multicast_supported, FromS, RLimits}), + + %% Send XEP33 packet to JID + FromM = Waiter#waiter.sender, + DestsM = Group#group.dests, + PacketM = Waiter#waiter.packet, + AAttrsM = Waiter#waiter.aattrs, + AddressesM = Waiter#waiter.addresses, + RServiceM = FromS, + route_packet_multicast(FromM, RServiceM, PacketM, AAttrsM, DestsM, AddressesM, RLimits), + + %% Remove from Pool + delo_waiter(Waiter); + + false -> + %% So we now know that JID does not support XEP33 + case FromS of + + RServer -> + %% We asked the server, now let's see if any component supports it: + + %% Send disco#items query to JID + send_query_items(FromS, LServiceS), + + %% Store on Pool + delo_waiter(Waiter), + add_waiter(Waiter#waiter{ + awaiting = {[FromS], LServiceS, items}, + renewal = false + }); + + %% We asked a component, and it does not support XEP33 + _ -> + received_awaiter(FromS, Waiter, LServiceS) + + end + end. + +get_limits_xml(Els, SenderT) -> + %% Get limits reported by the remote service + LimitOpts = get_limits_els(Els), + + %% Build the final list of limits + %% For the ones not reported, put default numbers + build_remote_limit_record(LimitOpts, SenderT). + +%% Look for disco#info extras which may report limits +%% TODO: Check if there are useful functions in xml.erl to clean this code +get_limits_els(Els) -> + lists:foldl( + fun(XML, R) -> + case XML of + {xmlelement, "x", Attrs, SubEls} -> + case (?NS_XDATA == xml:get_attr_s("xmlns", Attrs)) and + ("result" == xml:get_attr_s("type", Attrs)) of + true -> get_limits_fields(SubEls) ++ R; + false -> R + end; + _ -> R + end + end, + [], + Els + ). + +get_limits_fields(Fields) -> + {Head, Tail} = lists:partition( + fun(Field) -> + case Field of + {xmlelement, "field", Attrs, _SubEls} -> + ("FORM_TYPE" == xml:get_attr_s("var", Attrs)) + and ("hidden" == xml:get_attr_s("type", Attrs)); + _ -> false + end + end, + Fields + ), + case Head of + [] -> []; + _ -> get_limits_values(Tail) + end. + +get_limits_values(Values) -> + lists:foldl( + fun(Value, R) -> + case Value of + {xmlelement, "field", Attrs, SubEls} -> + %% TODO: Only one subel is expected here, but there may be several + [{xmlelement, "value", _AttrsV, SubElsV}] = SubEls, + Number = xml:get_cdata(SubElsV), + Name = xml:get_attr_s("var", Attrs), + [{list_to_atom(Name), list_to_integer(Number)} | R]; + _ -> R + end + end, + [], + Values + ). + + +%%%------------------------- +%%% Check protocol support: Receive response: Disco Items +%%%------------------------- + +process_discoitems_result(From, LServiceS, Els) -> + %% Convert list of xmlelement into list of strings + List = lists:foldl( + fun(XML, Res) -> + %% For each one, if it's "item", look for jid + case XML of + {xmlelement, "item", Attrs, _} -> + Res ++ [xml:get_attr_s("jid", Attrs)]; + _ -> Res + end + end, + [], + Els), + + %% Send disco#info queries to each item + [send_query_info(Item, LServiceS) || Item <- List], + + %% Search who was awaiting a disco#items response from this JID + FromS = jts(From), + {found_waiter, Waiter} = search_waiter(FromS, LServiceS, items), + + delo_waiter(Waiter), + add_waiter(Waiter#waiter{ + awaiting = {List, LServiceS, info}, + renewal = false + }). + + +%%%------------------------- +%%% Check protocol support: Receive response: Received awaiter +%%%------------------------- + +received_awaiter(JID, Waiter, LServiceS) -> + {JIDs, LServiceS, info} = Waiter#waiter.awaiting, + delo_waiter(Waiter), + Group = Waiter#waiter.group, + RServer = Group#group.server, + + %% Remove this awaiter from the list of awaiting JIDs. + case lists:delete(JID, JIDs) of + + [] -> + %% We couldn't find any service in this server that supports XEP33 + case Waiter#waiter.renewal of + + false -> + %% Store on cache the response + add_response(RServer, not_supported), + + %% Send a copy of the packet to each remote user on Dests + From = Waiter#waiter.sender, + Packet = Waiter#waiter.packet, + AAttrs = Waiter#waiter.aattrs, + Addresses = Waiter#waiter.addresses, + [route_packet(From, ToUser, Packet, AAttrs, Addresses) + || ToUser <- Group#group.dests]; + + true -> + %% We asked this component because the cache + %% said it would support XEP33, but it doesn't! + send_query_info(RServer, LServiceS), + add_waiter(Waiter#waiter{ + awaiting = {[RServer], LServiceS, info}, + renewal = false + }) + end; + + JIDs2 -> + %% Maybe other component on the server supports XEP33 + add_waiter(Waiter#waiter{ + awaiting = {JIDs2, LServiceS, info}, + renewal = false + }) + end. + + +%%%------------------------- +%%% Cache +%%%------------------------- + +create_cache() -> + mnesia:create_table(multicastc, [{ram_copies, [node()]}, + {attributes, record_info(fields, multicastc)}]). + +%% Add this response to the cache. +%% If a previous response still exists, it's overwritten +add_response(RServer, Response) -> + Secs = calendar:datetime_to_gregorian_seconds(calendar:now_to_datetime(now())), + mnesia:dirty_write(#multicastc{rserver = RServer, + response = Response, + ts = Secs}). + +%% Search on the cache if there is a response for the server +%% If there is a response but is obsolete, +%% don't bother removing since it will later be overwritten anyway +search_server_on_cache(RServer, LServerS, _Maxmins) + when RServer == LServerS -> + {cached, local_server}; + +search_server_on_cache(RServer, _LServerS, Maxmins) -> + case look_server(RServer) of + not_cached -> + not_cached; + {cached, Response, Ts} -> + Now = calendar:datetime_to_gregorian_seconds(calendar:now_to_datetime(now())), + case is_obsolete(Response, Ts, Now, Maxmins) of + false -> {cached, Response}; + true -> {obsolete, Response} + end + end. + +look_server(RServer) -> + case mnesia:dirty_read(multicastc, RServer) of + [] -> not_cached; + [M] -> {cached, M#multicastc.response, M#multicastc.ts} + end. + +is_obsolete(Response, Ts, Now, {Max_pos, Max_neg}) -> + Max = case Response of + multicast_not_supported -> Max_neg; + _ -> Max_pos + end, + (Now - Ts) > Max. + + +%%%------------------------- +%%% Purge cache +%%%------------------------- + +purge() -> + Maxmins_positive = ?MAXTIME_CACHE_POSITIVE, + Maxmins_negative = ?MAXTIME_CACHE_NEGATIVE, + Now = calendar:datetime_to_gregorian_seconds(calendar:now_to_datetime(now())), + purge(Now, {Maxmins_positive, Maxmins_negative}). + +purge(Now, Maxmins) -> + F = fun() -> + mnesia:foldl( + fun(R, _) -> + #multicastc{response = Response, ts = Ts} = R, + %% If this record is obsolete, delete it + case is_obsolete(Response, Ts, Now, Maxmins) of + true -> mnesia:delete_object(R); + false -> ok + end + end, + none, + multicastc) + end, + mnesia:transaction(F). + + +%%%------------------------- +%%% Purge cache loop +%%%------------------------- + +try_start_loop() -> + case lists:member(?PURGE_PROCNAME, registered()) of + true -> ok; + false -> start_loop() + end, + ?PURGE_PROCNAME ! new_module. + +start_loop() -> + register(?PURGE_PROCNAME, spawn(?MODULE, purge_loop, [0])), + ?PURGE_PROCNAME ! purge_now. + +try_stop_loop() -> + ?PURGE_PROCNAME ! try_stop. + +%% NM = number of modules are running on this node +purge_loop(NM) -> + receive + purge_now -> + purge(), + timer:send_after(?CACHE_PURGE_TIMER, ?PURGE_PROCNAME, purge_now), + purge_loop(NM); + new_module -> + purge_loop(NM + 1); + try_stop when NM > 1 -> + purge_loop(NM - 1); + try_stop -> + purge_loop_finished + end. + + +%%%------------------------- +%%% Pool +%%%------------------------- + +create_pool() -> + catch ets:new(multicastp, [duplicate_bag, public, named_table, {keypos, 2}]). + +%% If a Waiter with the same key exists, it overwrites it +add_waiter(Waiter) -> + true = ets:insert(multicastp, Waiter). + +delo_waiter(Waiter) -> + true = ets:delete_object(multicastp, Waiter). + +%% Search on the Pool who is waiting for this result +%% If there are several matches, pick the first one only +search_waiter(JID, LServiceS, Type) -> + Rs = ets:foldl( + fun(W, Res) -> + {JIDs, LServiceS1, Type1} = W#waiter.awaiting, + case lists:member(JID, JIDs) + and (LServiceS == LServiceS1) + and (Type1 == Type) of + true -> Res ++ [W]; + false -> Res + end + end, + [], + multicastp + ), + case Rs of + [R | _] -> {found_waiter, R}; + [] -> waiter_not_found + end. + + +%%%------------------------- +%%% Limits: utils +%%%------------------------- + +%% Type definitions for data structures related with XEP33 limits +%% limit() = {Name, Value} +%% Name = atom() +%% Value = {Type, Number} +%% Type = default | custom +%% Number = integer() | infinite + +list_of_limits(local) -> + [{message, ?DEFAULT_LIMIT_LOCAL_MESSAGE}, + {presence, ?DEFAULT_LIMIT_LOCAL_PRESENCE}]; + +list_of_limits(remote) -> + [{message, ?DEFAULT_LIMIT_REMOTE_MESSAGE}, + {presence, ?DEFAULT_LIMIT_REMOTE_PRESENCE}]. + +build_service_limit_record(LimitOpts) -> + LimitOptsL = get_from_limitopts(LimitOpts, local), + LimitOptsR = get_from_limitopts(LimitOpts, remote), + {service_limits, + build_limit_record(LimitOptsL, local), + build_limit_record(LimitOptsR, remote) + }. + +get_from_limitopts(LimitOpts, SenderT) -> + [{StanzaT, Number} + || {SenderT2, StanzaT, Number} <- LimitOpts, + SenderT =:= SenderT2]. + +%% Build a record of type #limits{} +%% In fact, it builds a list and then converts to tuple +%% It is important to put the elements in the list in +%% the same order than the elements in record #limits +build_remote_limit_record(LimitOpts, SenderT) -> + build_limit_record(LimitOpts, SenderT). + +build_limit_record(LimitOpts, SenderT) -> + Limits = [ + get_limit_value(Name, Default, LimitOpts) + || {Name, Default} <- list_of_limits(SenderT)], + list_to_tuple([limits | Limits]). + +get_limit_value(Name, Default, LimitOpts) -> + case lists:keysearch(Name, 1, LimitOpts) of + {value, {Name, Number}} -> + {custom, Number}; + false -> + {default, Default} + end. + +type_of_stanza({xmlelement, "message", _, _}) -> message; +type_of_stanza({xmlelement, "presence", _, _}) -> presence. + +get_limit_number(message, Limits) -> Limits#limits.message; +get_limit_number(presence, Limits) -> Limits#limits.presence. + +get_slimit_group(local, SLimits) -> SLimits#service_limits.local; +get_slimit_group(remote, SLimits) -> SLimits#service_limits.remote. + +fragment_dests(Dests, Limit_number) -> + {R, _} = lists:foldl( + fun(Dest, {Res, Count}) -> + case Count of + Limit_number -> + Head2 = [Dest], + {[Head2 | Res], 0}; + _ -> + [Head | Tail] = Res, + Head2 = [Dest | Head], + {[Head2 | Tail], Count+1} + end + end, + {[[]], 0}, + Dests), + R. + + +%%%------------------------- +%%% Limits: XEP-0128 Service Discovery Extensions +%%%------------------------- + +%% Some parts of code are borrowed from mod_muc_room.erl + +-define(RFIELDT(Type, Var, Val), + {xmlelement, "field", [{"var", Var}, {"type", Type}], + [{xmlelement, "value", [], [{xmlcdata, Val}]}]}). + +-define(RFIELDV(Var, Val), + {xmlelement, "field", [{"var", Var}], + [{xmlelement, "value", [], [{xmlcdata, Val}]}]}). + +iq_disco_info_extras(From, State) -> + SenderT = sender_type(From), + Service_limits = State#state.service_limits, + case iq_disco_info_extras2(SenderT, Service_limits) of + [] -> []; + List_limits_xmpp -> + [{xmlelement, "x", [{"xmlns", ?NS_XDATA}, {"type", "result"}], + [?RFIELDT("hidden", "FORM_TYPE", ?NS_ADDRESS)] ++ List_limits_xmpp + }] + end. + +sender_type(From) -> + Local_hosts = ?MYHOSTS, + case lists:member(From#jid.lserver, Local_hosts) of + true -> local; + false -> remote + end. + +iq_disco_info_extras2(SenderT, SLimits) -> + %% And report only the limits that are interesting for this sender + Limits = get_slimit_group(SenderT, SLimits), + Stanza_types = [message, presence], + lists:foldl( + fun(Type_of_stanza, R) -> + %% Report only custom limits + case get_limit_number(Type_of_stanza, Limits) of + {custom, Number} -> + [?RFIELDV(to_string(Type_of_stanza), to_string(Number)) | R]; + {default, _} -> R + end + end, + [], + Stanza_types). + +to_string(A) -> + hd(io_lib:format("~p",[A])). + + +%%%------------------------- +%%% Error report +%%%------------------------- + +route_error(From, To, Packet, ErrType, ErrText) -> + {xmlelement, _Name, Attrs, _Els} = Packet, + Lang = xml:get_attr_s("xml:lang", Attrs), + Reply = make_reply(ErrType, Lang, ErrText), + Err = jlib:make_error_reply(Packet, Reply), + ejabberd_router:route(From, To, Err). + +make_reply(bad_request, Lang, ErrText) -> + ?ERRT_BAD_REQUEST(Lang, ErrText); +make_reply(jid_malformed, Lang, ErrText) -> + ?ERRT_JID_MALFORMED(Lang, ErrText); +make_reply(not_acceptable, Lang, ErrText) -> + ?ERRT_NOT_ACCEPTABLE(Lang, ErrText); +make_reply(internal_server_error, Lang, ErrText) -> + ?ERRT_INTERNAL_SERVER_ERROR(Lang, ErrText); +make_reply(forbidden, Lang, ErrText) -> + ?ERRT_FORBIDDEN(Lang, ErrText). + +stj(String) -> jlib:string_to_jid(String). +jts(String) -> jlib:jid_to_string(String). diff --git a/mod_openid/Emakefile b/mod_openid/Emakefile new file mode 100644 index 0000000..076b5f2 --- /dev/null +++ b/mod_openid/Emakefile @@ -0,0 +1 @@ +{'src/mod_openid', [{outdir, "ebin"},{i,"../ejabberd-dev/include"}]}. diff --git a/mod_openid/README b/mod_openid/README new file mode 100644 index 0000000..bc8509c --- /dev/null +++ b/mod_openid/README @@ -0,0 +1,34 @@ +mod_openid +Transform the Jabber Server in an openid provider. +(http://openid.net/) + +Author: Olivier Goffart + +Motivation: +There are already severals existing openid provider that uses the JabberId as id. +( http://openid.xmpp.za.net/ http://xmppid.net/ ) +But none of them are open source. +The idea is that having the openid server in the same place as the jabber server reduce +the size of the security chain we have to trust. +Instead of trusting both the jabber server and the openid provider, we can trust only +the Jabber server. + + +Status: +Currently, the implementation just ask for the jabber password. +Some security function are also lacking. +The plan was to use something similair to XEP-0070 + +How it works: +Add in your ejabberd.cfg +{listen, [ ... + {5280, ejabberd_http, [http_poll, web_admin, {request_handlers , [{["openid"],mod_openid }]}]} , + +Then your open id is http://server.org:5280/openid/user@server.org +Hopelifully it should be possible to have more nice-looking urls. + + +Future: +I have no plan to continue working on it. Feel free to take over. +I'd be happy to reply to questions. + diff --git a/mod_openid/build.bat b/mod_openid/build.bat new file mode 100644 index 0000000..0188083 --- /dev/null +++ b/mod_openid/build.bat @@ -0,0 +1 @@ +erl -pa ../../ejabberd-dev/trunk/ebin -pa ebin -make diff --git a/mod_openid/build.sh b/mod_openid/build.sh new file mode 100755 index 0000000..f0a0cfa --- /dev/null +++ b/mod_openid/build.sh @@ -0,0 +1,2 @@ +#!/bin/sh +erl -pa ../ejabberd-dev/ebin -pz ebin -make diff --git a/mod_openid/src/mod_openid.erl b/mod_openid/src/mod_openid.erl new file mode 100644 index 0000000..5cb7cd9 --- /dev/null +++ b/mod_openid/src/mod_openid.erl @@ -0,0 +1,251 @@ + +%%%---------------------------------------------------------------------- +%%% File : mod_openid.erl +%%% Author : Olivier Goffart +%%% Purpose : Open id provider using XEP-0070 +%%% Created : 24 Dec 2007 Olivier Goffart +%%%---------------------------------------------------------------------- +%%% Copyright (c) 2007-2008 Olivier Goffart +%%%---------------------------------------------------------------------- + + +%% WARNING: secret/1 and new_assoc/2 MUST be implemented correctly for security issue + +-module(mod_openid). +-author('ogoffart@kde.org'). + +%% External exports +-export([process/2]). + + +-include("ejabberd.hrl"). +-include("jlib.hrl"). +-include("web/ejabberd_http.hrl"). +-include("web/ejabberd_web_admin.hrl"). + +-record(profile, {identity, server, lang, jid}). + +-define(MYDEBUG(Format,Args),io:format("D(~p:~p:~p) : "++Format++"~n", + [calendar:local_time(),?MODULE,?LINE]++Args)). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% PART 1 : OpenID + +process([Jid], #request{ q = Query, lang = Lang} = Request) -> + %%?MYDEBUG("Auth Failed ~p ~n", [C]), + %%[{server, Server}] = ets:lookup(mod_openid, server) + JJid = jlib:string_to_jid(Jid), + Server = "http://" ++ JJid#jid.server ++ ":5280/openid", + Profile = #profile{identity = Server ++"/"++ Jid, + server = Server ++ "/" ++ Jid, + lang = Lang, jid= JJid}, + case lists:keysearch("openid.mode", 1, Query) of + {value, {_, "associate"}} -> associate(Query,Profile); + {value, {_, "checkid_immediate"}} -> checkid_immediate(Query,Profile); + {value, {_, "checkid_setup"}} -> checkid_setup(Request,Profile); + {value, {_, "check_authentication"}} -> check_authentication(Query,Profile); + _ -> default_page(Profile) + end; + +process(_, _) ->error_400("Invalid identity"). + +default_page(Profile) -> + make_xhtml([{xmlelement,"h1",[],[{xmlcdata, "400 Bad Request"}]}],Profile). + +associate(_,Profile) -> + not_implemented(Profile). + +checkid_immediate(_,Profile) -> + not_implemented(Profile). + +checkid_setup(#request{ q = Query} = Request, Profile) -> + case lists:keysearch("openid.return_to", 1, Query) of + {value, {_, ReturnTo}} -> + case catch verify_id(Request,Profile) of + verified -> checkid2(Request,Profile,ReturnTo); + {return, Value} -> Value; + _ -> redirect_reply([{"openid_mode","error"}, + {"openid_error","InternalError"}], ReturnTo) + end; + _ -> error_400("Missing 'openid.return_to'") + end. + +%% Helper for checkid_setup +checkid2(#request{ q = Query} = _Request, Profile,ReturnTo) -> + case lists:keysearch("openid.identity", 1, Query) of + {value, {_, Ident}} when Ident == Profile#profile.identity -> + {AssocHandle,Secret} = + case lists:keysearch("openid.assoc_handle", 1, Query) of + {value, {_, V}} -> case secret(V) of + {ok, S} -> {V,S}; + false -> new_assoc() + end; + false -> new_assoc() + end, + _TrustRoot = case lists:keysearch("openid.trust_root", 1, Query) of + {value, {_, Vs}} -> Vs; + false -> ReturnTo + end, + Params = [{"identity",Ident} , + {"return_to",ReturnTo}, {"assoc_handle", AssocHandle} ], + {Signed,Sig} = make_signature(Params,Secret), + redirect_reply( + lists:map(fun({K,V}) -> {"openid_" ++ K, V} end, Params) + ++ [{"openid_mode","id_res"}, + {"openid_signed", Signed}, + {"openid_sig", Sig}], + ReturnTo); + _ -> redirect_reply([{"openid_mode","error"}, + {"openid_error","WrongIdentity"}], + ReturnTo) + end. + +check_authentication(Query, _Profile) -> + case lists:keysearch("openid.assoc_handle", 1, Query) of + {value, {_, AssocHandle} } -> + case lists:keysearch("openid.sig", 1, Query) of + {value, {_, Sig} } -> + case lists:keysearch("openid.signed", 1, Query) of + {value, {_, Signed} } -> + direct_reply([{"openid.mode","id_res"}, + {"is_valid", + check_authentication2(AssocHandle, Sig, + Signed, Query)}]); + false -> error_reply("missing sig") + end; + false -> error_reply("missing sig") + end; + false -> error_reply("missing handle") + end. + +%% Helper for check_authentication +%% return "true" if the authentication is valid, "false" otherwhise +check_authentication2(AssocHandle, Sig, Signed, Query) -> + case secret(AssocHandle) of + {ok, Secret} -> + case catch make_signature(retrieve_params(Signed,Query),Secret) of + {Signed,Sig} -> "true"; + _ -> "false" + end; + _ -> "false" + end. + +%% Fields is a list of fields which should be in the query as openid.Field +%% return the list of argument [{Key,Value}] as they appears in the query +retrieve_params(Fields,Query) -> + {ok, FList} = regexp:split(Fields, ","), + retrieve_params_recurse(FList,Query). +retrieve_params_recurse([],_) -> []; +retrieve_params_recurse([Key | Tail ], Query) -> + {value, {_, Value} } = lists:keysearch("openid." ++ Key, 1, Query), + [ {Key, Value} | retrieve_params_recurse(Tail,Query) ]. + +not_implemented(Profile) -> + make_xhtml([{xmlelement,"h1",[],[{xmlcdata, "NOT IMPLEMENTED"}]}],Profile). + +make_xhtml(Els, #profile{lang=Lang} = Profile) -> + {200, [html], + {xmlelement, "html", [{"xmlns", "http://www.w3.org/1999/xhtml"}, + {"xml:lang", Profile#profile.lang}, + {"lang", Profile#profile.lang}], + [{xmlelement, "head", [], + [?XCT("title", "ejabberd OpenId Provider"), + {xmlelement, "meta", [{"http-equiv", "Content-Type"}, + {"content", "text/html; charset=utf-8"}], []}, + {xmlelement, "link", [{"rel", "openid.server"}, + {"href", Profile#profile.server}], []}, + {xmlelement, "link", [{"rel", "openid.delegate"}, + {"href", Profile#profile.identity}], []}]}, + ?XE("body", Els) ]}}. + +error_400(Message) -> + {400, [], ejabberd_web:make_xhtml([{xmlelement,"h1",[], + [{xmlcdata, "400 Bad Request"}]}, + {xmlelement,"p",[],[{xmlcdata, Message}]}])}. + +%% Ask the user agent to go to the ReturnTo URL with the specified +%% Params in the query +redirect_reply(Params, ReturnTo) -> + Delim = case lists:member($?, ReturnTo) of + true -> $&; + false -> $? + end, + {303, [{"Location", ReturnTo ++ build_query(Params, Delim)}], []}. + +%% Given a list of {Key,Value}, construct the query string, starting +%% with Delim (either '?' or '&') +build_query([ {Key,Value} | Tail ], Delim) -> + [Delim] ++ Key ++ "=" ++ Value ++ build_query(Tail, $&); +build_query([], _) -> + []. + +%% return the parameters directly in the HTTP reply +direct_reply(Params) -> + {200, [], build_reply(Params)}. + +%% Given a list of {Key,Value}, return the string which should +%% appears in the dirrect reply. +build_reply([ {Key,Value} | Tail ]) -> + Key ++ ":" ++ Value ++ "\n" ++ build_reply(Tail); +build_reply([]) -> []. + +%% Direct reply of an error +error_reply(Message) -> + {400, [], "error:" ++ Message ++ "\n"}. + +%% Given a list of parameters, sign them. +make_signature(Param, Secret) -> + {field_list(Param), + jlib:encode_base64(binary_to_list(crypto:sha_mac(Secret, build_reply(Param))))}. + +%% Given a list of parameters, return a string containing the list of +%% key separated by comas. +field_list([]) -> []; +field_list([ {Key,_Value} ]) -> Key; +field_list([ {Key,_Value} | Tail ]) -> + Key ++ "," ++ field_list(Tail). + +%% TODO: thoses function need to be implemented for security +%% given an association key, return {ok, Secret} or false +secret(_) -> {ok, "Secret"}. +%% create a new association and return {Key, Secret} +new_assoc() -> {"assoc", "Secret"}. + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% PART 2 : Glue + +%% return "verified" if the user agent is authorized, otherwhise, return an HTTP reply. +verify_id(#request{auth = Auth} = _Request, #profile{ jid = Jid} = _Profile) -> + %% TODO verify that the Jid in the identity is the same as the Jid in the profile + Exp = {Jid#jid.luser, Jid#jid.lserver}, + case get_auth(Auth) of + Exp -> + verified; + C -> + ?MYDEBUG("Auth Failed ~p ~n", [C]), + {return, {401, + [{"WWW-Authenticate", "basic realm=\"ejabberd-mod_openid\""}], + ejabberd_web:make_xhtml([{xmlelement, "h1", [], + [{xmlcdata, "401 Unauthorized"}]}])}} + end. + +%% return the {User,Server} jid if the authentification is correct, or unauthorized +get_auth(Auth) -> + case Auth of + {SJID, P} -> + case jlib:string_to_jid(SJID) of + error -> + unauthorized; + #jid{user = U, server = S} -> + case ejabberd_auth:check_password(U, S, P) of + true -> + {U, S}; + false -> + unauthorized + end + end; + _ -> + unauthorized + end. + diff --git a/mod_profile/Emakefile b/mod_profile/Emakefile new file mode 100644 index 0000000..044d805 --- /dev/null +++ b/mod_profile/Emakefile @@ -0,0 +1,2 @@ +{'../ejabberd-dev/src/gen_mod', [{outdir, "../ejabberd-dev/ebin"},{i,"../ejabberd-dev/include"}]}. +{'src/mod_profile', [{outdir, "ebin"},{i,"../ejabberd-dev/include"}]}. diff --git a/mod_profile/README.txt b/mod_profile/README.txt new file mode 100644 index 0000000..ac0e50f --- /dev/null +++ b/mod_profile/README.txt @@ -0,0 +1,27 @@ + + mod_profile - User Profile (XEP-0154) in Mnesia table + + http://www.ejabberd.im/mod_profile + Author: Magnus Henoch + mailto:henoch@dtek.chalmers.se + xmpp:legoscia@jabber.cd.chalmers.se + Requirements: ejabberd 2.x.x + + +This module supports storing and retrieving a profile according to +XEP-0154. It does no validation of the data, but simply stores +whatever XML the user sends in a Mnesia table. The PEP parts of +XEP-0154 are out of scope for this module. + +In the beginning of the erl file it says what parts of the XEP +are implemented. + + + BASIC CONFIGURATION + =================== + +To install this module, follow the general build instructions, and add the +following to your configuration, among the other modules: + +{mod_profile, []} + diff --git a/mod_profile/build.bat b/mod_profile/build.bat new file mode 100644 index 0000000..fff9b3b --- /dev/null +++ b/mod_profile/build.bat @@ -0,0 +1 @@ +erl -pa ../../ejabberd-dev/trunk/ebin -pa ebin -make diff --git a/mod_profile/build.sh b/mod_profile/build.sh new file mode 100755 index 0000000..f0a0cfa --- /dev/null +++ b/mod_profile/build.sh @@ -0,0 +1,2 @@ +#!/bin/sh +erl -pa ../ejabberd-dev/ebin -pz ebin -make diff --git a/mod_profile/src/mod_profile.erl b/mod_profile/src/mod_profile.erl new file mode 100644 index 0000000..600b15f --- /dev/null +++ b/mod_profile/src/mod_profile.erl @@ -0,0 +1,303 @@ +%%%---------------------------------------------------------------------- +%%% File : mod_profile.erl +%%% Author : Magnus Henoch +%%% Purpose : Store profile stanzas in Mnesia table (XEP-0154 0.5 IQ semantics) +%%% Created : 22 Oct 2006 by Magnus Henoch +%%% Id : $Id: mod_profile.erl 1039 2009-11-24 22:47:40Z badlop $ +%%%---------------------------------------------------------------------- + +%%%% Documentation + +%%% Protocol implementation (as of XEP-0154 0.6) +%%% +%%% 4.1. Publishing a Full Profile +%%% + Stanza is processed, and a response is returned +%%% - But the stanza is stored in a Mnesia table, not in PubSub +%%% +%%% 4.2. Updating One or More Profile Fields +%%% - Not possible because PubSub is not used for storage +%%% +%%% 5.1. Discovering Support +%%% + Fully implemented +%%% +%%% 5.2. Requesting Full Profile +%%% + Stanza is processed, and a response is returned +%%% - The information is read from Mnesia table, not from PubSub +%%% +%%% 5.3. Receiving Profile Updates +%%% - Does not work because storage is done on Mnesia, not PubSub +%%% +%%% 7. Security Considerations +%%% - Implement Access option to restrict who is allowed to read/write profiles +%%% - any pubsub node it may create for profile data has an access model of "presence" or "roster" +%%% +%%% +%%% The server should include code to forward information of IQ queries into PubSub, +%%% so all the information provided with any semantic mentioned in XEP-0154 +%%% must be stored in PubSub. +%%% stpeter: I think the idea was (B), but it's not well-defined +%%% +%%% +%%% Custom IQ semantics for XEP-0154: +%%% +%%% 5.4. Requesting One or More Profile Fields +%%% +%%% A user can query some specific fields from his profile: +%%% +%%% +%%% +%%% +%%% urn:xmpp:tmp:profile +%%% +%%% +%%% +%%% +%%% +%%% +%%% +%%% The server will return only the fields that were requested and the user had defined previously: +%%% +%%% +%%% +%%% +%%% urn:xmpp:tmp:profile +%%% Drummy +%%% DK +%%% +%%% +%%% +%%% +%%% 4.3. Updating One or More Profile Fields using IQ Semantics +%%% +%%% - if a field is set empty value, delete field in the stored profile +%%% + +%%%======================= +%%%% Headers + +-module(mod_profile). +-author('henoch@dtek.chalmers.se'). + +-behaviour(gen_mod). + +-export([start/2, stop/1, + process_sm_iq/3, + get_sm_features/5, + remove_user/2]). + +-include("ejabberd.hrl"). +-include("jlib.hrl"). + +-record(profile, {us, fields}). + +-define(NS_PROFILE, "urn:xmpp:tmp:profile"). + +%%%======================= +%%%% gen_mod + +start(Host, Opts) -> + mnesia:create_table(profile, [{disc_only_copies, [node()]}, + {attributes, record_info(fields, profile)}]), + ejabberd_hooks:add(remove_user, Host, ?MODULE, remove_user, 50), + ejabberd_hooks:add(disco_sm_features, Host, ?MODULE, get_sm_features, 50), + IQDisc = gen_mod:get_opt(iqdisc, Opts, one_queue), + gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_PROFILE, + ?MODULE, process_sm_iq, IQDisc). + +stop(Host) -> + ejabberd_hooks:delete(remove_user, Host, ?MODULE, remove_user, 50), + ejabberd_hooks:delete(disco_sm_features, Host, ?MODULE, get_sm_features,50), + gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_PROFILE). + +%%%======================= +%%%% Hooks + +get_sm_features({error, _} = Acc, _From, _To, _Node, _Lang) -> + Acc; +get_sm_features(Acc, _From, _To, Node, _Lang) -> + %% XXX: this will make nonexistent users seem to exist. But + %% mod_adhoc and mod_vcard do that already. + case Node of + [] -> + case Acc of + {result, Features} -> + {result, [?NS_PROFILE | Features]}; + empty -> + {result, [?NS_PROFILE]} + end; + _ -> + Acc + end. + +remove_user(User, Server) -> + LUser = jlib:nodeprep(User), + LServer = jlib:nameprep(Server), + US = {LUser, LServer}, + F = fun() -> + mnesia:delete({profile, US}) + end, + mnesia:transaction(F). + +%%%======================= +%%%% IQ handler + +process_sm_iq(From, To, #iq{type = Type, sub_el = SubEl} = IQ) -> + case Type of + set -> + #jid{luser = LUser, lserver = LServer} = From, + process_sm_iq_set(LUser, LServer, SubEl, IQ); + get -> + #jid{luser = LUser, lserver = LServer} = To, + process_sm_iq_get(LUser, LServer, SubEl, IQ) + end. + +process_sm_iq_set(LUser, LServer, SubEl, IQ) -> + case lists:member(LServer, ?MYHOSTS) of + true -> + {xmlelement, _, _, SubSubEls} = SubEl, + ElsList = [El || {xmlelement, Name, _Attrs, _Els} = El + <- xml:remove_cdata(SubSubEls), + Name == "x"], + case ElsList of + [XData] -> + case set_profile(LUser, LServer, XData) of + ok -> + IQ#iq{type = result, sub_el = []}; + {error, Error} -> + IQ#iq{type = error, sub_el = [SubEl, Error]} + end; + _ -> + IQ#iq{type = error, + sub_el = [SubEl, ?ERR_BAD_REQUEST]} + end; + false -> + IQ#iq{type = error, + sub_el = [SubEl, ?ERR_NOT_ALLOWED]} + end. + +process_sm_iq_get(LUser, LServer, SubEl, IQ) -> + ReqFields = get_requested_fields(SubEl), + case get_profile(LUser, LServer, ReqFields) of + {ok, Fields} -> + XEl = {xmlelement, "x", [{"xmlns", "jabber:x:data"}, + {"type", "result"}], Fields}, + ProfileEl = {xmlelement, "profile", + [{"xmlns", ?NS_PROFILE}], [XEl]}, + IQ#iq{type = result, + sub_el = + [ProfileEl] + }; + {error, user_not_found} -> + IQ#iq{type = error, + sub_el = [SubEl, ?ERR_SERVICE_UNAVAILABLE]}; + {error, OtherError} -> + ?ERROR_MSG("Problem found when getting profile of ~p@~p:~n~p", + [LUser, LServer, OtherError]), + IQ#iq{type = error, + sub_el = [SubEl, ?ERR_INTERNAL_SERVER_ERROR]} + end. + +%%%======================= +%%%% Set profile + +set_profile(LUser, LServer, {xmlelement, "x", _Attrs, Els}) -> + US = {LUser, LServer}, + F = fun() -> + mnesia:write(#profile{us = US, fields = Els}) + end, + case mnesia:transaction(F) of + {atomic, _} -> + ok; + _ -> + {error, ?ERR_INTERNAL_SERVER_ERROR} + end. + +%%%======================= +%%%% Get profile + +get_profile(LUser, LServer, []) -> + US = {LUser, LServer}, + F = fun() -> + mnesia:read({profile, US}) + end, + case mnesia:transaction(F) of + {atomic, [#profile{fields = Fields}]} -> + {ok, Fields}; + {atomic, []} -> + {error, user_not_found}; + OtherResult -> + {error, OtherResult} + end; + +get_profile(LUser, LServer, ReqFields) -> + case get_profile(LUser, LServer, []) of + {ok, Fields} -> + filter_profile_fields(Fields, ReqFields); + Other -> Other + end. + +filter_profile_fields(Fields, ReqFields) -> + filter_profile_fields(Fields, ReqFields, []). + +%% Probably there are many stored fields and the requested fields are few, +%% so it's optimal to traverse the requested fields instead of stored fields +filter_profile_fields(StoredFields, [{xmlelement, "field", Attrs, []} | ReqFields], ResFields) -> + case lists:keysearch(Attrs, 3, StoredFields) of + {value, StoredField} -> + filter_profile_fields(StoredFields, ReqFields, [StoredField | ResFields]); + false -> + filter_profile_fields(StoredFields, ReqFields, ResFields) + end; +filter_profile_fields(StoredFields, [_FieldForm | ReqFields], ResFields) -> + filter_profile_fields(StoredFields, ReqFields, ResFields); +filter_profile_fields(_StoredFields, [], ResFields) -> + FieldFormType = {xmlelement, "field", [{"var", "FORM_TYPE"}, {"type", "hidden"}], + [{xmlelement, "value",[], [{xmlcdata, <<"urn:xmpp:tmp:profile">>}]}]}, + %% NOTE: This list reverse is not necessary, it just ensure the returned fields are in the same order than the request + {ok, [FieldFormType | lists:reverse(ResFields)]}. + + +%% TODO: la respuesta a una iq query de fields especificos ha de incluir + +%%%======================= +%%%% Mnesia storage + + +%%%======================= +%%%% PubSub storage + + +%%%======================= +%%%% XML processing + +%% Copied from exmpp_xml.erl, then customized + +get_requested_fields(SubEl) -> + case xml:get_subtag(SubEl, "x") of + false -> []; + XEl -> get_elements(XEl, "field") + end. + +get_elements({xmlelement, "x", _, Children}, Name) -> + get_elements2(Children, Name); +get_elements(_, _Name) -> + []. + +get_elements2([], _Name) -> + []; +get_elements2(Children, Name) -> + lists:filter(filter_by_name(Name), Children). + +filter_by_name(Searched_Name) -> + fun(XML_Element) -> + element_matches(XML_Element, Searched_Name) + end. + +element_matches({xmlelement, Name, _, _}, Name) -> + true; +element_matches(_XML_Element, _Name) -> + false. + +%%%================ + +%%% vim: set foldmethod=marker foldmarker=%%%%,%%%=: diff --git a/mod_register_web/ChangeLog b/mod_register_web/ChangeLog new file mode 100644 index 0000000..77ad500 --- /dev/null +++ b/mod_register_web/ChangeLog @@ -0,0 +1,37 @@ +2009-05-02 Badlop + + * src/msgs/es.mod_register_web.po: Update Spanish translation + * src/msgs/es.mod_register_web.msg: Likewise + + * src/mod_register_web.erl: Update translatable strings + * src/msgs/mod_register_web.pot: Likewise + + * src/msgs/: New directory for module translations (EJAB-925) + +2009-03-30 Badlop + + * src/mod_register_web.erl: Use captcha in account registration + page, using ejabberd_captcha, requires recent ejabberd + version. Fix password change. Hide password text. Show host in + forms. Fix vhost support. + + * README.txt: Require ejabberd trunk SVN r2001 + +2009-03-11 Badlop + + * src/mod_register_web.erl: Fix capitalization of HTTP headers + +2009-01-27 Badlop + + * README.txt: Require ejabberd trunk SVN. There is a 2.0.x branch. + +2008-05-31 Badlop + + * README.txt: Require ejabberd 2.0.2. Add explanation about last + character / in URL + +2008-05-18 Badlop + + * mod_register_web: New module that provides web pages to register + account and other related tasks + diff --git a/mod_register_web/Emakefile b/mod_register_web/Emakefile new file mode 100644 index 0000000..a1726dd --- /dev/null +++ b/mod_register_web/Emakefile @@ -0,0 +1,2 @@ +{'../ejabberd-dev/src/gen_mod', [{outdir, "../ejabberd-dev/ebin"},{i,"../ejabberd-dev/include"}]}. +{'src/mod_register_web', [{outdir, "ebin"},{i,"../ejabberd-dev/include"}]}. diff --git a/mod_register_web/README.txt b/mod_register_web/README.txt new file mode 100644 index 0000000..8a67e8b --- /dev/null +++ b/mod_register_web/README.txt @@ -0,0 +1,77 @@ + + mod_register_web - Web to register account + + Homepage: http://www.ejabberd.im/mod_register_web + Author: Badlop + Requirements: ejabberd ejabberd 2.1.0 or higher + + + DESCRIPTION + ----------- + +This module provides a web page where users can register Jabber accounts, +change password and other related tasks. + + + CONFIGURATION + ------------- + +Add to ejabberd.cfg, 'modules' section the basic configuration: +{modules, [ + ... + {mod_register_web, []}, + ... +]}. + + +In the 'listen' section enable the web page: +{listen, [ + ... + {5281, ejabberd_http, [ + tls, + {certfile, "/etc/ejabberd/certificate.pem"}, + {request_handlers, [ + {["register"], mod_register_web} + ]} + ]}, + ... +]}. + +In this example the page is served in https://example.org:5281/register/ + +Make sure to include the last / character in the URL. +Otherwise when you enter a subpage the URL will not be correct, +for example: http://localhost:5281/new ---> 404 Not Found + +This module supports CAPTCHA image to register a new account. +To enable this feature, configure the options captcha_cmd and captcha_host. +See the ejabberd Guide for more information about those options. + +This module supports the option registration_watchers, +which works similarly to the mod_register option. + + + FEATURE REQUESTS + ---------------- + + * Allow configuration of the hardcoded "register" path in URL. + * Enforce configurable ACL+ACCESS to register + + * Improve the default CSS to provide an acceptable look. + * Option to use a custom CSS file. + + * Optionally registration request is only forwarded to admin, no account created. + + * Option to select which subpages are available + + * Store in a custom mnesia table: timestamp of account register and IP. + * Use time limiter by IP like mod_register for: register, changepass. + + * Allow private email during register, and store in custom table. + * Optionally require private email to register. + * Optionally require email confirmation to register. + + * Allow to set a private email address anytime. + * Allow to recover password using the private email to confirm (see mod_passrecover). + + * Optionally require invitation diff --git a/mod_register_web/build.bat b/mod_register_web/build.bat new file mode 100644 index 0000000..fff9b3b --- /dev/null +++ b/mod_register_web/build.bat @@ -0,0 +1 @@ +erl -pa ../../ejabberd-dev/trunk/ebin -pa ebin -make diff --git a/mod_register_web/build.sh b/mod_register_web/build.sh new file mode 100755 index 0000000..f0a0cfa --- /dev/null +++ b/mod_register_web/build.sh @@ -0,0 +1,2 @@ +#!/bin/sh +erl -pa ../ejabberd-dev/ebin -pz ebin -make diff --git a/mod_register_web/src/mod_register_web.erl b/mod_register_web/src/mod_register_web.erl new file mode 100644 index 0000000..42b65a1 --- /dev/null +++ b/mod_register_web/src/mod_register_web.erl @@ -0,0 +1,584 @@ +%%%------------------------------------------------------------------- +%%% File : mod_register_web.erl +%%% Author : Badlop +%%% Purpose : Web pages to register account and related tasks +%%% Created : 4 May 2008 by Badlop +%%%------------------------------------------------------------------- + +-module(mod_register_web). +-author('badlop@process-one.net'). + +-behaviour(gen_mod). + +-export([ + start/2, + stop/1, + process/2 + ]). + +-include("ejabberd.hrl"). +-include("jlib.hrl"). +-include("web/ejabberd_http.hrl"). +-include("web/ejabberd_web_admin.hrl"). + +%% TODO: Check that all the text is translatable. +%% TODO: Check EDoc syntax is correct. + + +%%%---------------------------------------------------------------------- +%%% gen_mod callbacks +%%%---------------------------------------------------------------------- + +%% TODO: Improve this module to allow each virtual host to have different +%% options. See http://support.process-one.net/browse/EJAB-561 +start(_Host, _Opts) -> + %% case gen_mod:get_opt(docroot, Opts, undefined) of + ok. + +stop(_Host) -> + ok. + + +%%%---------------------------------------------------------------------- +%%% HTTP handlers +%%%---------------------------------------------------------------------- + +process([], #request{method = 'GET', lang = Lang}) -> + index_page(Lang); + +process(["register.css"], #request{method = 'GET'}) -> + serve_css(); + +process(["new"], #request{method = 'GET', lang = Lang, host = Host}) -> + form_new_get(Host, Lang); + +process(["delete"], #request{method = 'GET', lang = Lang, host = Host}) -> + form_del_get(Host, Lang); + +process(["change_password"], #request{method = 'GET', lang = Lang, host = Host}) -> + form_changepass_get(Host, Lang); + +process(["new"], #request{method = 'POST', q = Q, ip = {Ip,_Port}, lang = Lang, host = Host}) -> + case form_new_post(Q, Host) of + {success, ok, {Username, Host, _Password}} -> + Jid = jlib:make_jid(Username, Host, ""), + send_registration_notifications(Jid, Ip), + Text = ?T("Your Jabber account was succesfully created."), + {200, [], Text}; + Error -> + ErrorText = ?T("There was an error creating the account: ") ++ + ?T(get_error_text(Error)), + {404, [], ErrorText} + end; + +process(["delete"], #request{method = 'POST', q = Q, lang = Lang, host = Host}) -> + case form_del_post(Q, Host) of + {atomic, ok} -> + Text = ?T("Your Jabber account was succesfully deleted."), + {200, [], Text}; + Error -> + ErrorText = ?T("There was an error deleting the account: ") ++ + ?T(get_error_text(Error)), + {404, [], ErrorText} + end; + +%% TODO: Currently only the first vhost is usable. The web request record +%% should include the host where the POST was sent. +process(["change_password"], #request{method = 'POST', q = Q, lang = Lang, host = Host}) -> + case form_changepass_post(Q, Host) of + {atomic, ok} -> + Text = ?T("The password of your Jabber account was succesfully changed."), + {200, [], Text}; + Error -> + ErrorText = ?T("There was an error changing the password: ") ++ + ?T(get_error_text(Error)), + {404, [], ErrorText} + end. + + +%%%---------------------------------------------------------------------- +%%% CSS +%%%---------------------------------------------------------------------- + +serve_css() -> + {200, [{"Content-Type", "text/css"}, + last_modified(), cache_control_public()], css()}. + +last_modified() -> + {"Last-Modified", "Mon, 25 Feb 2008 13:23:30 GMT"}. +cache_control_public() -> + {"Cache-Control", "public"}. + +css() -> + "html,body { +background: white; +margin: 0; +padding: 0; +height: 100%; +}". + + +%%%---------------------------------------------------------------------- +%%% Index page +%%%---------------------------------------------------------------------- + +index_page(Lang) -> + HeadEls = [ + ?XCT("title", "Jabber Account Registration"), + ?XA("link", + [{"href", "/register/register.css"}, + {"type", "text/css"}, + {"rel", "stylesheet"}]) + ], + Els=[ + ?XACT("h1", + [{"class", "title"}, {"style", "text-align:center;"}], + "Jabber Account Registration"), + ?XE("ul", [ + ?XE("li", [?ACT("new", "Register a Jabber account")]), + ?XE("li", [?ACT("change_password", "Change Password")]), + ?XE("li", [?ACT("delete", "Unregister a Jabber account")]) + ] + ) + ], + {200, + [{"Server", "ejabberd"}, + {"Content-Type", "text/html"}], + ejabberd_web:make_xhtml(HeadEls, Els)}. + + +%%%---------------------------------------------------------------------- +%%% Formulary new account GET +%%%---------------------------------------------------------------------- + +form_new_get(Host, Lang) -> + CaptchaEls = build_captcha_li_list(Lang), + HeadEls = [ + ?XCT("title", "Register a Jabber account"), + ?XA("link", + [{"href", "/register/register.css"}, + {"type", "text/css"}, + {"rel", "stylesheet"}]) + ], + Els=[ + ?XACT("h1", + [{"class", "title"}, {"style", "text-align:center;"}], + "Register a Jabber account"), + ?XCT("p", + "This page allows to create a Jabber account in this Jabber server. " + "Your JID (Jabber IDentifier) will be of the form: username@server. " + "Please read carefully the instructions to fill correctly the fields."), + %% + ?XAE("form", [{"action", ""}, {"method", "post"}], + [ + ?XE("ol", [ + ?XE("li", [ + ?CT("Username:"), + ?C(" "), + ?INPUTS("text", "username", "", "20"), + ?BR, + ?XE("ul", [ + ?XCT("li", "This is case insensitive: macbeth is the same that MacBeth and Macbeth."), + ?XCT("li", "Characters not allowed: @ : ' \" < > &") + ]) + ]), + ?XE("li", [ + ?CT("Server:"), + ?C(" "), + ?C(Host) + ]), + ?XE("li", [ + ?CT("Password:"), + ?C(" "), + ?INPUTS("password", "password", "", "20"), + ?BR, + ?XE("ul", [ + ?XCT("li", "Don't tell your password to anybody, " + "not even the administrators of the Jabber server."), + ?XCT("li", "You can later change your password using a Jabber client."), + ?XCT("li", "Some Jabber clients can store your password in your computer. " + "Use that feature only if you trust your computer is safe."), + ?XCT("li", "Memorize your password, or write it in a paper placed in a safe place. " + "In Jabber there isn't an automated way to recover your password if you forget it.") + ]) + ]), + ?XE("li", [ + ?CT("Password Verification:"), + ?C(" "), + ?INPUTS("password", "password2", "", "20") + ])] ++ CaptchaEls ++ [ + %% Nombre (opcional):

    --> + %% + %% Dirección de correo (opcional):

    --> + ?XE("li", [ + ?INPUTT("submit", "register", "Register") + ]) + ]) + ]) + ], + {200, + [{"Server", "ejabberd"}, + {"Content-Type", "text/html"}], + ejabberd_web:make_xhtml(HeadEls, Els)}. + +%% Copied from mod_register.erl +send_registration_notifications(UJID, Source) -> + Host = UJID#jid.lserver, + case gen_mod:get_module_opt(Host, ?MODULE, registration_watchers, []) of + [] -> ok; + JIDs when is_list(JIDs) -> + Body = lists:flatten( + io_lib:format( + "[~s] The account ~s was registered from IP address ~s " + "on node ~w using ~p.", + [get_time_string(), jlib:jid_to_string(UJID), + ip_to_string(Source), node(), ?MODULE])), + lists:foreach( + fun(S) -> + case jlib:string_to_jid(S) of + error -> ok; + JID -> + ejabberd_router:route( + jlib:make_jid("", Host, ""), + JID, + {xmlelement, "message", [{"type", "chat"}], + [{xmlelement, "body", [], + [{xmlcdata, Body}]}]}) + end + end, JIDs); + _ -> + ok + end. +ip_to_string(Source) when is_tuple(Source) -> inet_parse:ntoa(Source); +ip_to_string(undefined) -> "undefined"; +ip_to_string(_) -> "unknown". +get_time_string() -> write_time(erlang:localtime()). +%% Function copied from ejabberd_logger_h.erl and customized +write_time({{Y,Mo,D},{H,Mi,S}}) -> + io_lib:format("~w-~.2.0w-~.2.0w ~.2.0w:~.2.0w:~.2.0w", + [Y, Mo, D, H, Mi, S]). + +%%%---------------------------------------------------------------------- +%%% Formulary new POST +%%%---------------------------------------------------------------------- + +form_new_post(Q, Host) -> + case catch get_register_parameters(Q) of + [Username, Password, Password, Id, Key] -> + form_new_post(Username, Host, Password, {Id, Key}); + [_Username, _Password, _Password2, false, false] -> + {error, passwords_not_identical}; + [_Username, _Password, _Password2, Id, Key] -> + ejabberd_captcha:check_captcha(Id, Key), %% This deletes the captcha + {error, passwords_not_identical}; + _ -> + {error, wrong_parameters} + end. + +get_register_parameters(Q) -> + lists:map( + fun(Key) -> + case lists:keysearch(Key, 1, Q) of + {value, {_Key, Value}} -> Value; + false -> false + end + end, + ["username", "password", "password2", "id", "key"]). + +form_new_post(Username, Host, Password, {false, false}) -> + register_account(Username, Host, Password); +form_new_post(Username, Host, Password, {Id, Key}) -> + case ejabberd_captcha:check_captcha(Id, Key) of + captcha_valid -> + register_account(Username, Host, Password); + captcha_non_valid -> + {error, captcha_non_valid}; + captcha_not_found -> + {error, captcha_non_valid} + end. + + +%%%---------------------------------------------------------------------- +%%% Formulary Captcha support for new GET/POST +%%%---------------------------------------------------------------------- + +build_captcha_li_list(Lang) -> + case ejabberd_captcha:is_feature_available() of + true -> build_captcha_li_list2(Lang); + false -> [] + end. + +build_captcha_li_list2(Lang) -> + Id = randoms:get_string(), + SID = "", + From = #jid{user = "", server = "test", resource = ""}, + To = #jid{user = "", server = "test", resource = ""}, + Args = [], + ejabberd_captcha:create_captcha(Id, SID, From, To, Lang, Args), + {_, {CImg,CText,CId,CKey}} = ejabberd_captcha:build_captcha_html(Id, Lang), + [?XE("li", [CText, + ?C(" "), + CId, + CKey, + ?BR, + CImg] + )]. + +%%%---------------------------------------------------------------------- +%%% Formulary change password GET +%%%---------------------------------------------------------------------- + +form_changepass_get(Host, Lang) -> + HeadEls = [ + ?XCT("title", "Change Password"), + ?XA("link", + [{"href", "/register/register.css"}, + {"type", "text/css"}, + {"rel", "stylesheet"}]) + ], + Els=[ + ?XACT("h1", + [{"class", "title"}, {"style", "text-align:center;"}], + "Change Password"), + ?XAE("form", [{"action", ""}, {"method", "post"}], + [ + ?XE("ol", [ + ?XE("li", [ + ?CT("Username:"), + ?C(" "), + ?INPUTS("text", "username", "", "20") + ]), + ?XE("li", [ + ?CT("Server:"), + ?C(" "), + ?C(Host) + ]), + ?XE("li", [ + ?CT("Old Password:"), + ?C(" "), + ?INPUTS("password", "passwordold", "", "20") + ]), + ?XE("li", [ + ?CT("New Password:"), + ?C(" "), + ?INPUTS("password", "password", "", "20") + ]), + ?XE("li", [ + ?CT("Password Verification:"), + ?C(" "), + ?INPUTS("password", "password2", "", "20") + ]), + ?XE("li", [ + ?INPUTT("submit", "changepass", "Change Password") + ]) + ]) + ]) + ], + {200, + [{"Server", "ejabberd"}, + {"Content-Type", "text/html"}], + ejabberd_web:make_xhtml(HeadEls, Els)}. + + +%%%---------------------------------------------------------------------- +%%% Formulary change password POST +%%%---------------------------------------------------------------------- + +form_changepass_post(Q, Host) -> + case catch get_changepass_parameters(Q) of + [Username, PasswordOld, Password, Password] -> + try_change_password(Username, Host, PasswordOld, Password); + [_Username, _PasswordOld, _Password, _Password2] -> + {error, passwords_not_identical}; + _ -> + {error, wrong_parameters} + end. + +get_changepass_parameters(Q) -> + lists:map( + fun(Key) -> + {value, {_Key, Value}} = lists:keysearch(Key, 1, Q), + Value + end, + ["username", "passwordold", "password", "password2"]). + +%% @spec(Username,Host,PasswordOld,Password) -> {atomic, ok} | +%% {error, account_doesnt_exist} | +%% {error, password_not_changed} | +%% {error, password_incorrect} +try_change_password(Username, Host, PasswordOld, Password) -> + try change_password(Username, Host, PasswordOld, Password) of + {atomic, ok} -> + {atomic, ok} + catch + error:{badmatch, Error} -> + {error, Error} + end. + +change_password(Username, Host, PasswordOld, Password) -> + %% Check the account exists + account_exists = check_account_exists(Username, Host), + + %% Check the old password is correct + password_correct = check_password(Username, Host, PasswordOld), + + %% This function always returns: ok + %% Change the password + ok = ejabberd_auth:set_password(Username, Host, Password), + + %% Check the new password is correct + case check_password(Username, Host, Password) of + password_correct -> + {atomic, ok}; + password_incorrect -> + {error, password_not_changed} + end. + +check_account_exists(Username, Host) -> + case ejabberd_auth:is_user_exists(Username, Host) of + true -> account_exists; + false -> account_doesnt_exist + end. + +check_password(Username, Host, Password) -> + case ejabberd_auth:check_password(Username, Host, Password) of + true -> password_correct; + false -> password_incorrect + end. + + +%%%---------------------------------------------------------------------- +%%% Formulary delete account GET +%%%---------------------------------------------------------------------- + +form_del_get(Host, Lang) -> + HeadEls = [ + ?XCT("title", "Unregister a Jabber account"), + ?XA("link", + [{"href", "/register/register.css"}, + {"type", "text/css"}, + {"rel", "stylesheet"}]) + ], + Els=[ + ?XACT("h1", + [{"class", "title"}, {"style", "text-align:center;"}], + "Unregister a Jabber account"), + ?XCT("p", + "This page allows to unregister a Jabber account in this Jabber server."), + ?XAE("form", [{"action", ""}, {"method", "post"}], + [ + ?XE("ol", [ + ?XE("li", [ + ?CT("Username:"), + ?C(" "), + ?INPUTS("text", "username", "", "20") + ]), + ?XE("li", [ + ?CT("Server:"), + ?C(" "), + ?C(Host) + ]), + ?XE("li", [ + ?CT("Password:"), + ?C(" "), + ?INPUTS("password", "password", "", "20") + ]), + ?XE("li", [ + ?INPUTT("submit", "unregister", "Unregister") + ]) + ]) + ]) + ], + {200, + [{"Server", "ejabberd"}, + {"Content-Type", "text/html"}], + ejabberd_web:make_xhtml(HeadEls, Els)}. + +%% @spec(Username, Host, Password) -> {success, ok, {Username, Host, Password} | +%% {success, exists, {Username, Host, Password}} | +%% {error, not_allowed} | +%% {error, invalid_jid} +register_account(Username, Host, Password) -> + case ejabberd_auth:try_register(Username, Host, Password) of + {atomic, Res} -> + {success, Res, {Username, Host, Password}}; + Other -> + Other + end. + +%%%---------------------------------------------------------------------- +%%% Formulary delete POST +%%%---------------------------------------------------------------------- + +form_del_post(Q, Host) -> + case catch get_unregister_parameters(Q) of + [Username, Password] -> + try_unregister_account(Username, Host, Password); + _ -> + {error, wrong_parameters} + end. + +get_unregister_parameters(Q) -> + lists:map( + fun(Key) -> + {value, {_Key, Value}} = lists:keysearch(Key, 1, Q), + Value + end, + ["username", "password"]). + +%% @spec(Username, Host, Password) -> {atomic, ok} | +%% {error, account_doesnt_exist} | +%% {error, account_exists} | +%% {error, password_incorrect} +try_unregister_account(Username, Host, Password) -> + try unregister_account(Username, Host, Password) of + {atomic, ok} -> + {atomic, ok} + catch + error:{badmatch, Error} -> + {error, Error} + end. + +unregister_account(Username, Host, Password) -> + %% Check the account exists + account_exists = check_account_exists(Username, Host), + + %% Check the password is correct + password_correct = check_password(Username, Host, Password), + + %% This function always returns: ok + ok = ejabberd_auth:remove_user(Username, Host, Password), + + %% Check the account does not exist anymore + account_doesnt_exist = check_account_exists(Username, Host), + + %% If we reached this point, return success + {atomic, ok}. + + +%%%---------------------------------------------------------------------- +%%% Error texts +%%%---------------------------------------------------------------------- + +get_error_text({error, captcha_non_valid}) -> + "The captcha you entered is wrong"; +get_error_text({atomic, exists}) -> + "The account already exists"; +get_error_text({error, password_incorrect}) -> + "Incorrect password"; +get_error_text({error, invalid_jid}) -> + "The username is not valid"; +get_error_text({error, not_allowed}) -> + "Not allowed"; +get_error_text({error, account_doesnt_exist}) -> + "Account doesn't exist"; +get_error_text({error, account_exists}) -> + "The account was not deleted"; +get_error_text({error, password_not_changed}) -> + "The password was not changed"; +get_error_text({error, passwords_not_identical}) -> + "The passwords are different"; +get_error_text({error, wrong_parameters}) -> + "Wrong parameters in the web formulary". + diff --git a/mod_register_web/src/msgs/es.mod_register_web.msg b/mod_register_web/src/msgs/es.mod_register_web.msg new file mode 100644 index 0000000..83b1e58 --- /dev/null +++ b/mod_register_web/src/msgs/es.mod_register_web.msg @@ -0,0 +1,24 @@ +{"Characters not allowed: @ : ' \" < > &","Caracteres no permitidos: @ : ' \" < > &"}. +{"Don't tell your password to anybody, not even the administrators of the Jabber server.","No le digas tu contraseña a nadie, ni siquiera a los administradores del servidor Jabber."}. +{"Jabber Account Registration","Registro de Cuenta Jabber"}. +{"Memorize your password, or write it in a paper placed in a safe place. In Jabber there isn't an automated way to recover your password if you forget it.","Memoriza tu contraseña, o apúntala en un papel en un lugar seguro. En Jabber no hay un método automatizado para recuperar la contraseña si la olvidas."}. +{"New Password:","Contraseña nueva:"}. +{"Old Password:","Contraseña antigua:"}. +{"Password Verification:","Verificación de contraseña:"}. +{"Register a Jabber account","Registrar una cuenta Jabber"}. +{"Register","Registrar"}. +{"Server:","Servidor:"}. +{"Some Jabber clients can store your password in your computer. Use that feature only if you trust your computer is safe.","Algunos clientes Jabber pueden recordar tu contraseña en la máquina. Usa esa opción solo si confías en que la máquina que usas es segura."}. +{"The password of your Jabber account was succesfully changed.","La contraseña de tu cuenta Jabber se ha cambiado correctamente."}. +{"There was an error changing the password: ","Hubo un error cambiando la contraseña."}. +{"There was an error creating the account: ","Hubo uno error al crear la cuenta:"}. +{"There was an error deleting the account: ","Hubo un error borrando la cuenta."}. +{"This is case insensitive: macbeth is the same that MacBeth and Macbeth.","No importa si usas mayúsculas: macbeth es lo mismo que MacBeth y Macbeth."}. +{"This page allows to create a Jabber account in this Jabber server. Your JID (Jabber IDentifier) will be of the form: username@server. Please read carefully the instructions to fill correctly the fields.","Esta página te permite crear una cuenta Jabber este servidor Jabber. Tu JID (Jabber IDentificador) será de la forma: nombredeusuario@servidor. Por favor lee detenidamente las instrucciones para rellenar correctamente los campos."}. +{"This page allows to unregister a Jabber account in this Jabber server.","Esta página te permite borrar tu cuenta Jabber en este servidor Jabber."}. +{"Unregister a Jabber account","Borrar una cuenta Jabber"}. +{"Unregister","Borrar"}. +{"Username:","Nombre de usuario:"}. +{"You can later change your password using a Jabber client.","Puedes cambiar tu contraseña después, usando un cliente Jabber."}. +{"Your Jabber account was succesfully created.","Tu cuenta Jabber se ha creado correctamente."}. +{"Your Jabber account was succesfully deleted.","Tu cuenta Jabber se ha borrado correctamente."}. diff --git a/mod_register_web/src/msgs/es.mod_register_web.po b/mod_register_web/src/msgs/es.mod_register_web.po new file mode 100644 index 0000000..0a34909 --- /dev/null +++ b/mod_register_web/src/msgs/es.mod_register_web.po @@ -0,0 +1,126 @@ +msgid "" +msgstr "" +"Project-Id-Version: es.mod_register_web\n" +"PO-Revision-Date: 2009-05-02 15:47+0200\n" +"Last-Translator: Badlop \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Language: Spanish (castellano)\n" + +#: mod_register_web.erl:66 +msgid "Your Jabber account was succesfully created." +msgstr "Tu cuenta Jabber se ha creado correctamente." + +#: mod_register_web.erl:69 +msgid "There was an error creating the account: " +msgstr "Hubo uno error al crear la cuenta:" + +#: mod_register_web.erl:79 +msgid "Your Jabber account was succesfully deleted." +msgstr "Tu cuenta Jabber se ha borrado correctamente." + +#: mod_register_web.erl:82 +msgid "There was an error deleting the account: " +msgstr "Hubo un error borrando la cuenta." + +#: mod_register_web.erl:92 +msgid "The password of your Jabber account was succesfully changed." +msgstr "La contraseña de tu cuenta Jabber se ha cambiado correctamente." + +#: mod_register_web.erl:95 +msgid "There was an error changing the password: " +msgstr "Hubo un error cambiando la contraseña." + +#: mod_register_web.erl:129 mod_register_web.erl:138 +msgid "Jabber Account Registration" +msgstr "Registro de Cuenta Jabber" + +#: mod_register_web.erl:140 mod_register_web.erl:165 mod_register_web.erl:174 +msgid "Register a Jabber account" +msgstr "Registrar una cuenta Jabber" + +#: mod_register_web.erl:142 mod_register_web.erl:408 mod_register_web.erl:417 +msgid "Unregister a Jabber account" +msgstr "Borrar una cuenta Jabber" + +#: mod_register_web.erl:176 +msgid "" +"This page allows to create a Jabber account in this Jabber server. Your JID " +"(Jabber IDentifier) will be of the form: username@server. Please read " +"carefully the instructions to fill correctly the fields." +msgstr "" +"Esta página te permite crear una cuenta Jabber este servidor Jabber. Tu JID " +"(Jabber IDentificador) será de la forma: nombredeusuario@servidor. Por favor " +"lee detenidamente las instrucciones para rellenar correctamente los campos." + +#: mod_register_web.erl:185 mod_register_web.erl:299 mod_register_web.erl:424 +msgid "Username:" +msgstr "Nombre de usuario:" + +#: mod_register_web.erl:190 +msgid "This is case insensitive: macbeth is the same that MacBeth and Macbeth." +msgstr "" +"No importa si usas mayúsculas: macbeth es lo mismo que MacBeth y Macbeth." + +#: mod_register_web.erl:191 +msgid "Characters not allowed: @ : ' \" < > &" +msgstr "Caracteres no permitidos: @ : ' \" < > &" + +#: mod_register_web.erl:195 mod_register_web.erl:304 mod_register_web.erl:429 +msgid "Server:" +msgstr "Servidor:" + +#: mod_register_web.erl:205 +msgid "" +"Don't tell your password to anybody, not even the administrators of the " +"Jabber server." +msgstr "" +"No le digas tu contraseña a nadie, ni siquiera a los administradores del " +"servidor Jabber." + +#: mod_register_web.erl:207 +msgid "You can later change your password using a Jabber client." +msgstr "Puedes cambiar tu contraseña después, usando un cliente Jabber." + +#: mod_register_web.erl:208 +msgid "" +"Some Jabber clients can store your password in your computer. Use that " +"feature only if you trust your computer is safe." +msgstr "" +"Algunos clientes Jabber pueden recordar tu contraseña en la máquina. Usa esa " +"opción solo si confías en que la máquina que usas es segura." + +#: mod_register_web.erl:210 +msgid "" +"Memorize your password, or write it in a paper placed in a safe place. In " +"Jabber there isn't an automated way to recover your password if you forget " +"it." +msgstr "" +"Memoriza tu contraseña, o apúntala en un papel en un lugar seguro. En Jabber " +"no hay un método automatizado para recuperar la contraseña si la olvidas." + +#: mod_register_web.erl:215 mod_register_web.erl:319 +msgid "Password Verification:" +msgstr "Verificación de contraseña:" + +#: mod_register_web.erl:232 +msgid "Register" +msgstr "Registrar" + +#: mod_register_web.erl:309 +msgid "Old Password:" +msgstr "Contraseña antigua:" + +#: mod_register_web.erl:314 +msgid "New Password:" +msgstr "Contraseña nueva:" + +#: mod_register_web.erl:419 +msgid "This page allows to unregister a Jabber account in this Jabber server." +msgstr "" +"Esta página te permite borrar tu cuenta Jabber en este servidor Jabber." + +#: mod_register_web.erl:439 +msgid "Unregister" +msgstr "Borrar" diff --git a/mod_register_web/src/msgs/mod_register_web.pot b/mod_register_web/src/msgs/mod_register_web.pot new file mode 100644 index 0000000..7215a35 --- /dev/null +++ b/mod_register_web/src/msgs/mod_register_web.pot @@ -0,0 +1,114 @@ +msgid "" +msgstr "" +"Project-Id-Version: 2.1.0-alpha\n" +"X-Language: Language Name\n" +"Last-Translator: Translator name and contact method\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: mod_register_web.erl:66 +msgid "Your Jabber account was succesfully created." +msgstr "" + +#: mod_register_web.erl:69 +msgid "There was an error creating the account: " +msgstr "" + +#: mod_register_web.erl:79 +msgid "Your Jabber account was succesfully deleted." +msgstr "" + +#: mod_register_web.erl:82 +msgid "There was an error deleting the account: " +msgstr "" + +#: mod_register_web.erl:92 +msgid "The password of your Jabber account was succesfully changed." +msgstr "" + +#: mod_register_web.erl:95 +msgid "There was an error changing the password: " +msgstr "" + +#: mod_register_web.erl:129 mod_register_web.erl:138 +msgid "Jabber Account Registration" +msgstr "" + +#: mod_register_web.erl:140 mod_register_web.erl:165 mod_register_web.erl:174 +msgid "Register a Jabber account" +msgstr "" + +#: mod_register_web.erl:142 mod_register_web.erl:408 mod_register_web.erl:417 +msgid "Unregister a Jabber account" +msgstr "" + +#: mod_register_web.erl:176 +msgid "" +"This page allows to create a Jabber account in this Jabber server. Your JID " +"(Jabber IDentifier) will be of the form: username@server. Please read " +"carefully the instructions to fill correctly the fields." +msgstr "" + +#: mod_register_web.erl:185 mod_register_web.erl:299 mod_register_web.erl:424 +msgid "Username:" +msgstr "" + +#: mod_register_web.erl:190 +msgid "This is case insensitive: macbeth is the same that MacBeth and Macbeth." +msgstr "" + +#: mod_register_web.erl:191 +msgid "Characters not allowed: @ : ' \" < > &" +msgstr "" + +#: mod_register_web.erl:195 mod_register_web.erl:304 mod_register_web.erl:429 +msgid "Server:" +msgstr "" + +#: mod_register_web.erl:205 +msgid "" +"Don't tell your password to anybody, not even the administrators of the " +"Jabber server." +msgstr "" + +#: mod_register_web.erl:207 +msgid "You can later change your password using a Jabber client." +msgstr "" + +#: mod_register_web.erl:208 +msgid "" +"Some Jabber clients can store your password in your computer. Use that " +"feature only if you trust your computer is safe." +msgstr "" + +#: mod_register_web.erl:210 +msgid "" +"Memorize your password, or write it in a paper placed in a safe place. In " +"Jabber there isn't an automated way to recover your password if you forget " +"it." +msgstr "" + +#: mod_register_web.erl:215 mod_register_web.erl:319 +msgid "Password Verification:" +msgstr "" + +#: mod_register_web.erl:232 +msgid "Register" +msgstr "" + +#: mod_register_web.erl:309 +msgid "Old Password:" +msgstr "" + +#: mod_register_web.erl:314 +msgid "New Password:" +msgstr "" + +#: mod_register_web.erl:419 +msgid "This page allows to unregister a Jabber account in this Jabber server." +msgstr "" + +#: mod_register_web.erl:439 +msgid "Unregister" +msgstr "" diff --git a/mod_register_web/src/msgs/ru.mod_register_web.msg b/mod_register_web/src/msgs/ru.mod_register_web.msg new file mode 100644 index 0000000..61694c6 --- /dev/null +++ b/mod_register_web/src/msgs/ru.mod_register_web.msg @@ -0,0 +1,24 @@ +{"Your Jabber account was succesfully created.", "Ваш Jabber-аккаунт был уÑпешно Ñоздан."}. +{"There was an error creating the account: ", "Ошибка при Ñоздании аккаунта:"}. +{"Your Jabber account was succesfully deleted.", "Ваш Jabber-аккаунт был уÑпешно удален."}. +{"There was an error deleting the account: ", "Ошибка при удалении аккаунта:"}. +{"The password of your Jabber account was succesfully changed.", "Пароль Вашего Jabber-аккаунта был уÑпешно изменен."}. +{"There was an error changing the password: ", "Ошибка при Ñмене паролÑ:"}. +{"Jabber Account Registration", "РегиÑÑ‚Ñ€Ð°Ñ†Ð¸Ñ Jabber-аккаунта"}. +{"Register a Jabber account", "ЗарегиÑтрировать Jabber-аккаунт"}. +{"Unregister a Jabber account", "Удалить Jabber-аккаунт"}. +{"This page allows to create a Jabber account in this Jabber server. Your JID (Jabber IDentifier) will be of the form: username@server. Please read carefully the instructions to fill correctly the fields.", "ЗдеÑÑŒ Ð’Ñ‹ можете Ñоздать Jabber-аккаунт на Ñтом Jabber-Ñервере. Ваш JID (Jabber-идентификатор) будет в виде: \"пользователь@Ñервер\". ПожалуйÑта, внимательно читайте инÑтрукции Ð´Ð»Ñ Ð¿Ñ€Ð°Ð²Ð¸Ð»ÑŒÐ½Ð¾Ð³Ð¾ Ð·Ð°Ð¿Ð¾Ð»Ð½ÐµÐ½Ð¸Ñ Ð¿Ð¾Ð»ÐµÐ¹."}. +{"Username:", "Ð˜Ð¼Ñ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ:"}. +{"This is case insensitive: macbeth is the same that MacBeth and Macbeth.", "РегиÑÑ‚Ñ€ не имеет значениÑ: \"маша\" и \"ÐœÐШÐ\" будет ÑчитатьÑÑ Ð¾Ð´Ð½Ð¸Ð¼ и тем же именем."}. +{"Characters not allowed: @ : ' \" < > &", "ÐедопуÑтимые Ñимволы: @ : ' \" < > &"}. +{"Server:", "Сервер:"}. +{"Don't tell your password to anybody, not even the administrators of the Jabber server.", "Ðе говорите никому Ñвой пароль, даже админиÑтраторам Ñервера."}. +{"You can later change your password using a Jabber client.", "Позже Ð’Ñ‹ можете изменить пароль через Jabber-клиент."}. +{"Some Jabber clients can store your password in your computer. Use that feature only if you trust your computer is safe.", "Ðекоторые Jabber-клиенты могут ÑохранÑÑ‚ÑŒ пароль на Вашем компьютере. ИÑпользуйте Ñту функцию только в том Ñлучае, еÑли Ñчитаете Ñто безопаÑным."}. +{"Memorize your password, or write it in a paper placed in a safe place. In Jabber there isn't an automated way to recover your password if you forget it.", "Запомните пароль или запишите его на бумаге, которую Ñохраните в безопаÑном меÑте. Ð’ Jabber'е нет автоматизированного ÑредÑтва воÑÑÑ‚Ð°Ð½Ð¾Ð²Ð»ÐµÐ½Ð¸Ñ Ð¿Ð°Ñ€Ð¾Ð»Ñ Ð² том Ñлучае, еÑли Ð’Ñ‹ его забудете."}. +{"Password Verification:", "Проверка паролÑ:"}. +{"Register", "ЗарегиÑтрироватьÑÑ"}. +{"Old Password:", "Старый пароль:"}. +{"New Password:", "Ðовый пароль:"}. +{"This page allows to unregister a Jabber account in this Jabber server.", "ЗдеÑÑŒ Ð’Ñ‹ можете удалить Jabber-аккаунт Ñ Ñтого Ñервера."}. +{"Unregister", "Удалить"}. diff --git a/mod_register_web/src/msgs/ru.mod_register_web.po b/mod_register_web/src/msgs/ru.mod_register_web.po new file mode 100644 index 0000000..374d131 --- /dev/null +++ b/mod_register_web/src/msgs/ru.mod_register_web.po @@ -0,0 +1,126 @@ +# translation of ru.mod_register_web.po to Russian +# Header entry was created by KBabel! +# +# rain , 2010. +# xmpp:rain@jabberworld.info +msgid "" +msgstr "" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Mime-Version: 1.0\n" +"Last-Translator: rain \n" +"PO-Revision-Date: 2010-11-17 18:00+0200\n" +"Project-Id-Version: ru.mod_register_web\n" +"Language-Team: Russian\n" +"X-Generator: KBabel 1.11.4\n" +"MIME-Version: 1.0\n" +"Last-Translator: rain \n" + +#: mod_register_web.erl:66 +msgid "Your Jabber account was succesfully created." +msgstr "Ваш Jabber-аккаунт был уÑпешно Ñоздан." + +#: mod_register_web.erl:69 +msgid "There was an error creating the account: " +msgstr "Ошибка при Ñоздании аккаунта:" + +#: mod_register_web.erl:79 +msgid "Your Jabber account was succesfully deleted." +msgstr "Ваш Jabber-аккаунт был уÑпешно удален." + +#: mod_register_web.erl:82 +msgid "There was an error deleting the account: " +msgstr "Ошибка при удалении аккаунта:" + +#: mod_register_web.erl:92 +msgid "The password of your Jabber account was succesfully changed." +msgstr "Пароль Вашего Jabber-аккаунта был уÑпешно изменен." + +#: mod_register_web.erl:95 +msgid "There was an error changing the password: " +msgstr "Ошибка при Ñмене паролÑ:" + +#: mod_register_web.erl:129 mod_register_web.erl:138 +msgid "Jabber Account Registration" +msgstr "РегиÑÑ‚Ñ€Ð°Ñ†Ð¸Ñ Jabber-аккаунта" + +#: mod_register_web.erl:140 mod_register_web.erl:165 mod_register_web.erl:174 +msgid "Register a Jabber account" +msgstr "ЗарегиÑтрировать Jabber-аккаунт" + +#: mod_register_web.erl:142 mod_register_web.erl:408 mod_register_web.erl:417 +msgid "Unregister a Jabber account" +msgstr "Удалить Jabber-аккаунт" + +#: mod_register_web.erl:176 +msgid "" +"This page allows to create a Jabber account in this Jabber server. Your JID " +"(Jabber IDentifier) will be of the form: username@server. Please read " +"carefully the instructions to fill correctly the fields." +msgstr "" +"ЗдеÑÑŒ Ð’Ñ‹ можете Ñоздать Jabber-аккаунт на Ñтом Jabber-Ñервере. Ваш JID (Jabber-идентификатор) будет в виде: \"пользователь@Ñервер\". " +"ПожалуйÑта, внимательно читайте инÑтрукции Ð´Ð»Ñ Ð¿Ñ€Ð°Ð²Ð¸Ð»ÑŒÐ½Ð¾Ð³Ð¾ Ð·Ð°Ð¿Ð¾Ð»Ð½ÐµÐ½Ð¸Ñ Ð¿Ð¾Ð»ÐµÐ¹." + +#: mod_register_web.erl:185 mod_register_web.erl:299 mod_register_web.erl:424 +msgid "Username:" +msgstr "Ð˜Ð¼Ñ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ:" + +#: mod_register_web.erl:190 +msgid "This is case insensitive: macbeth is the same that MacBeth and Macbeth." +msgstr "РегиÑÑ‚Ñ€ не имеет значениÑ: \"маша\" и \"ÐœÐШÐ\" будет ÑчитатьÑÑ Ð¾Ð´Ð½Ð¸Ð¼ и тем же именем." + +#: mod_register_web.erl:191 +msgid "Characters not allowed: @ : ' \" < > &" +msgstr "ÐедопуÑтимые Ñимволы: @ : ' \" < > &" + +#: mod_register_web.erl:195 mod_register_web.erl:304 mod_register_web.erl:429 +msgid "Server:" +msgstr "Сервер:" + +#: mod_register_web.erl:205 +msgid "" +"Don't tell your password to anybody, not even the administrators of the " +"Jabber server." +msgstr "Ðе говорите никому Ñвой пароль, даже админиÑтраторам Ñервера." + +#: mod_register_web.erl:207 +msgid "You can later change your password using a Jabber client." +msgstr "Позже Ð’Ñ‹ можете изменить пароль через Jabber-клиент." + +#: mod_register_web.erl:208 +msgid "" +"Some Jabber clients can store your password in your computer. Use that " +"feature only if you trust your computer is safe." +msgstr "Ðекоторые Jabber-клиенты могут ÑохранÑÑ‚ÑŒ пароль на Вашем компьютере. ИÑпользуйте Ñту функцию только в том Ñлучае, еÑли Ñчитаете Ñто безопаÑным." + +#: mod_register_web.erl:210 +msgid "" +"Memorize your password, or write it in a paper placed in a safe place. In " +"Jabber there isn't an automated way to recover your password if you forget " +"it." +msgstr "Запомните пароль или запишите его на бумаге, которую Ñохраните в безопаÑном меÑте. Ð’ Jabber'е нет автоматизированного ÑредÑтва воÑÑÑ‚Ð°Ð½Ð¾Ð²Ð»ÐµÐ½Ð¸Ñ Ð¿Ð°Ñ€Ð¾Ð»Ñ Ð² том Ñлучае, еÑли Ð’Ñ‹ его забудете." + +#: mod_register_web.erl:215 mod_register_web.erl:319 +msgid "Password Verification:" +msgstr "Проверка паролÑ:" + +#: mod_register_web.erl:232 +msgid "Register" +msgstr "ЗарегиÑтрироватьÑÑ" + +#: mod_register_web.erl:309 +msgid "Old Password:" +msgstr "Старый пароль:" + +#: mod_register_web.erl:314 +msgid "New Password:" +msgstr "Ðовый пароль:" + +#: mod_register_web.erl:419 +msgid "This page allows to unregister a Jabber account in this Jabber server." +msgstr "ЗдеÑÑŒ Ð’Ñ‹ можете удалить Jabber-аккаунт Ñ Ñтого Ñервера." + +#: mod_register_web.erl:439 +msgid "Unregister" +msgstr "Удалить" + diff --git a/mod_register_web/src/msgs/uk.mod_register_web.msg b/mod_register_web/src/msgs/uk.mod_register_web.msg new file mode 100644 index 0000000..c3ea01b --- /dev/null +++ b/mod_register_web/src/msgs/uk.mod_register_web.msg @@ -0,0 +1,24 @@ +{"Your Jabber account was succesfully created.", "Ваш Jabber-акаунт було уÑпішно Ñтворено."}. +{"There was an error creating the account: ", "Помилка при Ñтворенні акаунту:"}. +{"Your Jabber account was succesfully deleted.", "Ваш Jabber-акаунт було уÑпішно видалено."}. +{"There was an error deleting the account: ", "Помилка при видаленні акаунту: "}. +{"The password of your Jabber account was succesfully changed.", "Пароль вашого Jabber-акаунту був уÑпішно змінений."}. +{"There was an error changing the password: ", "Помилка при зміні паролÑ: "}. +{"Jabber Account Registration", "РеєÑÑ‚Ñ€Ð°Ñ†Ñ–Ñ Jabber-акаунту"}. +{"Register a Jabber account", "ЗареєÑтрувати Jabber-акаунт"}. +{"Unregister a Jabber account", "Видалити Jabber-акаунт"}. +{"This page allows to create a Jabber account in this Jabber server. Your JID (Jabber IDentifier) will be of the form: username@server. Please read carefully the instructions to fill correctly the fields.", "Тут ви можете зареєÑтрувати обліковий Ð·Ð°Ð¿Ð¸Ñ Jabber на цьому Ñервері. Ваш JID (ідентифікатор Jabber) матиме виглÑд \"кориÑтувач@Ñервер\". Щоб вірно заповнити Ð¿Ð¾Ð»Ñ Ð½Ð¸Ð¶Ñ‡Ðµ, будь лаÑка, уважно читайте інÑтрукції до них."}. +{"Username:", "Ім'Ñ ÐºÐ¾Ñ€Ð¸Ñтувача:"}. +{"This is case insensitive: macbeth is the same that MacBeth and Macbeth.", "РегіÑÑ‚Ñ€ не має значеннÑ: \"ÐœÐШÐ\" та \"маша\" буде ÑприйматиÑÑ Ñк одне й те Ñаме ім'Ñ."}. +{"Characters not allowed: @ : ' \" < > &", "Заборонені Ñимволи: @ : ' \" < > &"}. +{"Server:", "Сервер:"}. +{"Don't tell your password to anybody, not even the administrators of the Jabber server.", "Ðікому не кажіть Ñвій пароль, навіть адмініÑтраторам Ñервера."}. +{"You can later change your password using a Jabber client.", "Пізніше можна змінити пароль через Jabber-клієнт."}. +{"Some Jabber clients can store your password in your computer. Use that feature only if you trust your computer is safe.", "ДеÑкі Jabber-клієнти можуть зберігати пароль на вашому комп'ютері. КориÑтуйтеÑÑŒ цією функцією тільки у тому випадку, Ñкщо вважаєте Ñ—Ñ— безпечною."}. +{"Memorize your password, or write it in a paper placed in a safe place. In Jabber there isn't an automated way to recover your password if you forget it.", "Запам'Ñтайте пароль, або запишіть його на папері, Ñкий треба зберегти у безпечному міÑці. У Jabber'Ñ– немає автоматизованих заÑобів Ð²Ñ–Ð´Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð¿Ð°Ñ€Ð¾Ð»Ñ Ð½Ð° той випадок, Ñкщо ви його забудете."}. +{"Password Verification:", "Перевірка паролÑ:"}. +{"Register", "ЗареєÑтрувати"}. +{"Old Password:", "Старий пароль:"}. +{"New Password:", "Ðовий пароль:"}. +{"This page allows to unregister a Jabber account in this Jabber server.", "Тут ви можете видалити Ñвій акаунт з цього Jabber-Ñервера."}. +{"Unregister", "Видалити"}. diff --git a/mod_register_web/src/msgs/uk.mod_register_web.po b/mod_register_web/src/msgs/uk.mod_register_web.po new file mode 100644 index 0000000..d532493 --- /dev/null +++ b/mod_register_web/src/msgs/uk.mod_register_web.po @@ -0,0 +1,119 @@ +# translation of uk.mod_register_web.po to Russian +# rain , 2010. +msgid "" +msgstr "" +"Project-Id-Version: uk.mod_register_web\n" +"Last-Translator: rain \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"PO-Revision-Date: 2010-11-18 02:31+0200\n" +"Language-Team: Ukrainian\n" +"X-Generator: KBabel 1.11.4\n" + +#: mod_register_web.erl:66 +msgid "Your Jabber account was succesfully created." +msgstr "Ваш Jabber-акаунт було уÑпішно Ñтворено." + +#: mod_register_web.erl:69 +msgid "There was an error creating the account: " +msgstr "Помилка при Ñтворенні акаунту:" + +#: mod_register_web.erl:79 +msgid "Your Jabber account was succesfully deleted." +msgstr "Ваш Jabber-акаунт було уÑпішно видалено." + +#: mod_register_web.erl:82 +msgid "There was an error deleting the account: " +msgstr "Помилка при видаленні акаунту: " + +#: mod_register_web.erl:92 +msgid "The password of your Jabber account was succesfully changed." +msgstr "Пароль вашого Jabber-акаунту був уÑпішно змінений." + +#: mod_register_web.erl:95 +msgid "There was an error changing the password: " +msgstr "Помилка при зміні паролÑ: " + +#: mod_register_web.erl:129 mod_register_web.erl:138 +msgid "Jabber Account Registration" +msgstr "РеєÑÑ‚Ñ€Ð°Ñ†Ñ–Ñ Jabber-акаунту" + +#: mod_register_web.erl:140 mod_register_web.erl:165 mod_register_web.erl:174 +msgid "Register a Jabber account" +msgstr "ЗареєÑтрувати Jabber-акаунт" + +#: mod_register_web.erl:142 mod_register_web.erl:408 mod_register_web.erl:417 +msgid "Unregister a Jabber account" +msgstr "Видалити Jabber-акаунт" + +#: mod_register_web.erl:176 +msgid "" +"This page allows to create a Jabber account in this Jabber server. Your JID " +"(Jabber IDentifier) will be of the form: username@server. Please read " +"carefully the instructions to fill correctly the fields." +msgstr "Тут ви можете зареєÑтрувати обліковий Ð·Ð°Ð¿Ð¸Ñ Jabber на цьому Ñервері. Ваш JID (ідентифікатор Jabber) матиме виглÑд \"кориÑтувач@Ñервер\". Щоб вірно заповнити Ð¿Ð¾Ð»Ñ Ð½Ð¸Ð¶Ñ‡Ðµ, будь лаÑка, уважно читайте інÑтрукції до них." + +#: mod_register_web.erl:185 mod_register_web.erl:299 mod_register_web.erl:424 +msgid "Username:" +msgstr "Ім'Ñ ÐºÐ¾Ñ€Ð¸Ñтувача:" + +#: mod_register_web.erl:190 +msgid "This is case insensitive: macbeth is the same that MacBeth and Macbeth." +msgstr "РегіÑÑ‚Ñ€ не має значеннÑ: \"ÐœÐШÐ\" та \"маша\" буде ÑприйматиÑÑ Ñк одне й те Ñаме ім'Ñ." + +#: mod_register_web.erl:191 +msgid "Characters not allowed: @ : ' \" < > &" +msgstr "Заборонені Ñимволи: @ : ' \" < > &" + +#: mod_register_web.erl:195 mod_register_web.erl:304 mod_register_web.erl:429 +msgid "Server:" +msgstr "Сервер:" + +#: mod_register_web.erl:205 +msgid "" +"Don't tell your password to anybody, not even the administrators of the " +"Jabber server." +msgstr "Ðікому не кажіть Ñвій пароль, навіть адмініÑтраторам Ñервера." + +#: mod_register_web.erl:207 +msgid "You can later change your password using a Jabber client." +msgstr "Пізніше можна змінити пароль через Jabber-клієнт." + +#: mod_register_web.erl:208 +msgid "" +"Some Jabber clients can store your password in your computer. Use that " +"feature only if you trust your computer is safe." +msgstr "ДеÑкі Jabber-клієнти можуть зберігати пароль на вашому комп'ютері. КориÑтуйтеÑÑŒ цією функцією тільки у тому випадку, Ñкщо вважаєте Ñ—Ñ— безпечною." + +#: mod_register_web.erl:210 +msgid "" +"Memorize your password, or write it in a paper placed in a safe place. In " +"Jabber there isn't an automated way to recover your password if you forget " +"it." +msgstr "Запам'Ñтайте пароль, або запишіть його на папері, Ñкий треба зберегти у безпечному міÑці. У Jabber'Ñ– немає автоматизованих заÑобів Ð²Ñ–Ð´Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð¿Ð°Ñ€Ð¾Ð»Ñ Ð½Ð° той випадок, Ñкщо ви його забудете." + +#: mod_register_web.erl:215 mod_register_web.erl:319 +msgid "Password Verification:" +msgstr "Перевірка паролÑ:" + +#: mod_register_web.erl:232 +msgid "Register" +msgstr "ЗареєÑтрувати" + +#: mod_register_web.erl:309 +msgid "Old Password:" +msgstr "Старий пароль:" + +#: mod_register_web.erl:314 +msgid "New Password:" +msgstr "Ðовий пароль:" + +#: mod_register_web.erl:419 +msgid "This page allows to unregister a Jabber account in this Jabber server." +msgstr "Тут ви можете видалити Ñвій акаунт з цього Jabber-Ñервера." + +#: mod_register_web.erl:439 +msgid "Unregister" +msgstr "Видалити" + diff --git a/mod_rest/Emakefile b/mod_rest/Emakefile new file mode 100644 index 0000000..4177eac --- /dev/null +++ b/mod_rest/Emakefile @@ -0,0 +1 @@ +{'src/mod_rest', [{outdir, "ebin"},{i,"../ejabberd-dev/include"}]}. diff --git a/mod_rest/README.txt b/mod_rest/README.txt new file mode 100644 index 0000000..1e47510 --- /dev/null +++ b/mod_rest/README.txt @@ -0,0 +1,252 @@ + + mod_rest - HTTP interface to POST stanzas into ejabberd + + Author: Nolan Eakins + Copyright (C) 2008 Nolan Eakins + + Requirements: ejabberd trunk SVN 2025 (ejabberd 2.1.0, once released) + + + +This is an ejabberd module that adds an HTTP handler that allows HTTP +clients to literally post arbitrary message stanzas to ejabberd. Those +stanzas then get shoved through ejabberd's router just like any other +stanza. + +This module can also be used as a frontend to execute ejabberd commands. + + + CONFIGURATION + ============= + +To use this module, follow the general build instructions, and configure +in ejabberd.cfg as described. + +Enable the module: +{modules, + [ + {mod_rest, [ {allowed_ips, [ {127,0,0,1} ]} ]}, + ... + ] +}. + +And enable the HTTP request handler in the listen section: +{listen, + [ + ... + {5285, ejabberd_http, [ + {request_handlers, [ + {["rest"], mod_rest} + ]} + ]} + ] +}. + +With that configuration, you can send HTTP POST requests to the URL: + http://localhost:5285/rest + +Configurable options: + + allowed_ips: IP addresses that can use the rest service. + Allowed values: 'all' or a list of Erlang tuples. + Default value: all + Notice that the IP address is checked after the connection is established. + If you want to restrict the IP address that listens connections, and + only allow a certain IP to be able to connect to the port, then the + option allowed_ips is not useful to you: you better define the + listening IP address in the ejabberd listeners (see the ejabberd Guide). + + allowed_destinations: Allowed destination Jabber ID addresses in the stanza. + Allowed values: 'all' or a list of strings. + Default value: all + + allowed_stanza_types: Allowed stanza types of the posted stanza. + Allowed values: 'all' or a list of strings. + Default value: all + + access_commands: Access restrictions to execute ejabberd commands. + This option is similar to the option ejabberdctl_access_commands that + is documented in the ejabberd Guide. + There is more information about AccessCommands in the ejabberd Guide. + Default value: [] + +Complex example configuration: +{modules, + [ + {mod_rest, [ + {allowed_ips, [ {127,0,0,1}, {192,168,1,12} ]}, + {allowed_destinations, [ "nolan@localhost", "admin@example.com" ]}, + {allowed_stanza_types, [ "message", "presence", "iq" ]}, + {access_commands, [ {configure, [registered_users], []} ]} + ] + }, + ... + ] +}. + +This module gives many power to perform tasks in ejabberd, +such power in bad hands can harm your server, so you should +restrict the IP address that can connect to the service using: +a firewall, allowed_ips option, or the listener IP option. + +In ejabberd 2.0.x versions, +it is important that the value indicated in Content-Length matches +exactly the size of the content. + + + EXAMPLE REST CALL + ================= + +When the module receives this: +------- +POST /rest HTTP/1.1 +Host: localhost +Content-Length: 85 + +World +------- + +ejabberd.log shows those messages: +------- +=INFO REPORT==== 2-Mar-2009::11:46:05 === +I(<0.484.0>:ejabberd_listener:201) : (#Port<0.3661>) Accepted connection {{127,0,0,1},55945} -> {{127,0,0,1},5280} + +=INFO REPORT==== 2-Mar-2009::11:46:05 === +I(<0.251.0>:ejabberd_http:127) : started: {gen_tcp,#Port<0.3661>} + +=INFO REPORT==== 2-Mar-2009::11:46:05 === +I(<0.841.0>:mod_rest:81) : Got request from localhost/rest +with IP {{127,0,0,1},49613} +to nolan@localhost: +{xmlelement,"message", + [{"to","nolan@localhost"},{"from","localhost/rest"}], + [{xmlelement,"body",[],[{xmlcdata,<<"World">>}]}]} +------- + +If the user nolan@localhost exists, he will receive this message: +------- + + World + +------- + +Instead of an XMPP stanza, you can provide an ejabberd command to execute: +registered_users localhost + +If you configure access_commands in mod_rest, you need to provide information +about a local Jabber account with enough privileges according to your option: +--auth robot localhost pass0011 registered_users localhost + + + EXAMPLE CALL WITH LYNX + ====================== + +This example shows how to send a POST using Lynx: + +$ lynx http://localhost:5280/rest/ -mime_header -post_data +World +--- +HTTP/1.0 200 OK +Connection: close +Content-Type: text/html; charset=utf-8 +Content-Length: 2 + +Ok + + + EXAMPLE CALL WITH WGET + ====================== + +This example shows how to send a POST using Wget: + +$ wget http://localhost:5280/rest/ --server-response --post-data 'World' + +--2009-03-02 12:01:42-- http://localhost:5280/rest/ +Resolving localhost... 127.0.0.1 +Connecting to localhost|127.0.0.1|:5280... connected. +HTTP request sent, awaiting response... + HTTP/1.0 200 OK + Connection: keep-alive + Content-Type: text/html; charset=utf-8 + Content-Length: 2 +Length: 2 [text/html] +Saving to: `index.html' + +100%[======================================>] 2 --.-K/s in 0s + +2009-03-02 12:01:42 (285 KB/s) - `index.html' saved [2/2] + + +The content of the index.html is simply: +Ok + +Please notice that mod_rest and wget don't work correctly over HTTPS. + + + EXAMPLE AUTH COMMAND WITH WGET + ============================== + +To execute an ejabberd command, simply provide its name and arguments as in ejabberdctl: +wget http://localhost:5280/rest/ --server-response --post-data 'registered_users localhost' + +If you configure access_commands option, you must provide the credentials like this: + wget http://localhost:5280/rest/ --server-response --post-data '--auth user1 localhost thepass registered_users localhost' + + + EXAMPLE CALL WITH PYTHON + ======================== + +This example Python code first calls to send a stanza, and then calls to execute a command: +------- +import urllib2 + +server_url = 'http://localhost:5280/rest/' + +call = 'World' +resp = urllib2.urlopen(server_url, call) +result = resp.read() +print result + +call = 'registered_users localhost' +resp = urllib2.urlopen(server_url, call) +result = resp.read() +print result +------- + + + EXAMPLE CALL WITH PHP + ===================== + +This example PHP code implements a call to execute a command (thanks to Qu1cksand): +------- + array ('method' => 'POST' + ,'header' => "Host: localhost:5280\nContent-Type: text/html; charset=utf-8\nContent-Length: ".strlen($request) + ,'content' => $request))); + // Use file_get_contents for PHP 5+ otherwise use fopen, fread, fclose + if (version_compare(PHP_VERSION, '5.0.0', '>=')) { + $result = file_get_contents($url, false, $context); + } else { + // This is the PHP4 workaround which is slightly less elegant + // Suppress fopen warnings, otherwise they interfere with the page headers + $fp = @fopen($url, 'r', false, $context); + $result = ''; + // Only proceed if we have a file handle, otherwise we enter an infinite loop + if ($fp) { + while(!feof($fp)) { + $result .= fread($fp, 4096); + } + fclose($fp); + } + } + return $result; +} +$url = "http://localhost:5280/rest"; +$request = "register user12 localhost somepass"; +$response = sendRESTRequest($url, $request); +echo "Response: $response\n"; +?> +------- diff --git a/mod_rest/build.bat b/mod_rest/build.bat new file mode 100644 index 0000000..0188083 --- /dev/null +++ b/mod_rest/build.bat @@ -0,0 +1 @@ +erl -pa ../../ejabberd-dev/trunk/ebin -pa ebin -make diff --git a/mod_rest/build.sh b/mod_rest/build.sh new file mode 100755 index 0000000..f0a0cfa --- /dev/null +++ b/mod_rest/build.sh @@ -0,0 +1,2 @@ +#!/bin/sh +erl -pa ../ejabberd-dev/ebin -pz ebin -make diff --git a/mod_rest/src/mod_rest.erl b/mod_rest/src/mod_rest.erl new file mode 100644 index 0000000..0230fff --- /dev/null +++ b/mod_rest/src/mod_rest.erl @@ -0,0 +1,159 @@ +%%%------------------------------------------------------------------- +%%% File : mod_rest.erl +%%% Author : Nolan Eakins +%%% Purpose : Provide an HTTP interface to POST stanzas into ejabberd +%%% +%%% Copyright (C) 2008 Nolan Eakins +%%% +%%% 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., 59 Temple Place, Suite 330, Boston, MA +%%% 02111-1307 USA +%%% +%%%------------------------------------------------------------------- + +-module(mod_rest). +-author('sneakin@semanticgap.com'). + +-behavior(gen_mod). + +-export([start/2, + stop/1, + process/2 + ]). + +-include("ejabberd.hrl"). +-include("jlib.hrl"). +-include("web/ejabberd_http.hrl"). +-include("ejabberd_ctl.hrl"). + +start(_Host, _Opts) -> + ?DEBUG("Starting: ~p ~p", [_Host, _Opts]), + ok. + +stop(_Host) -> + ok. + +process([], #request{method = 'POST', + data = Data, + host = Host, + ip = ClientIp + }) -> + try + {ClientAddress, _PortNumber} = ClientIp, + check_member_option(Host, ClientAddress, allowed_ips), + maybe_post_request(Data, Host, ClientIp) + catch + error:{badmatch, _} = Error -> + ?DEBUG("Error when processing REST request: ~nData: ~p~nError: ~p", [Data, Error]), + {406, [], "Error: REST request is rejected by service."} + end; +process(Path, Request) -> + ?DEBUG("Got request to ~p: ~p", [Path, Request]), + {200, [], "Try POSTing a stanza."}. + + +%% If the first character of Data is <, it is considered a stanza to deliver. +%% Otherwise, it is considered an ejabberd command to execute. +maybe_post_request([$< | _ ] = Data, Host, ClientIp) -> + try + Stanza = xml_stream:parse_element(Data), + From = jlib:string_to_jid(xml:get_tag_attr_s("from", Stanza)), + To = jlib:string_to_jid(xml:get_tag_attr_s("to", Stanza)), + allowed = check_stanza(Stanza, From, To, Host), + ?INFO_MSG("Got valid request from ~s~nwith IP ~p~nto ~s:~n~p", + [jlib:jid_to_string(From), + ClientIp, + jlib:jid_to_string(To), + Stanza]), + post_request(Stanza, From, To) + catch + error:{badmatch, _} = Error -> + ?DEBUG("Error when processing REST request: ~nData: ~p~nError: ~p", [Data, Error]), + {406, [], "Error: REST request is rejected by service."}; + error:{Reason, _} = Error -> + ?DEBUG("Error when processing REST request: ~nData: ~p~nError: ~p", [Data, Error]), + {500, [], "Error: " ++ atom_to_list(Reason)}; + Error -> + ?DEBUG("Error when processing REST request: ~nData: ~p~nError: ~p", [Data, Error]), + {500, [], "Error"} + end; +maybe_post_request(Data, Host, _ClientIp) -> + ?INFO_MSG("Data: ~p", [Data]), + Args = split_line(Data), + AccessCommands = get_option_access(Host), + case ejabberd_ctl:process2(Args, AccessCommands) of + {"", ?STATUS_SUCCESS} -> + {200, [], integer_to_list(?STATUS_SUCCESS)}; + {String, ?STATUS_SUCCESS} -> + {200, [], String}; + {"", Code} -> + {200, [], integer_to_list(Code)}; + {String, _Code} -> + {200, [], String} + end. + +%% This function throws an error if the module is not started in that VHost. +try_get_option(Host, OptionName, DefaultValue) -> + case gen_mod:is_loaded(Host, ?MODULE) of + true -> ok; + _ -> throw({module_must_be_started_in_vhost, ?MODULE, Host}) + end, + gen_mod:get_module_opt(Host, ?MODULE, OptionName, DefaultValue). + +get_option_access(Host) -> + try_get_option(Host, access_commands, []). + +%% This function crashes if the stanza does not satisfy configured restrictions +check_stanza(Stanza, _From, To, Host) -> + check_member_option(Host, jlib:jid_to_string(To), allowed_destinations), + {xmlelement, StanzaType, _Attrs, _Kids} = Stanza, + check_member_option(Host, StanzaType, allowed_stanza_types), + allowed. + +check_member_option(Host, Element, Option) -> + true = case try_get_option(Host, Option, all) of + all -> true; + AllowedValues -> lists:member(Element, AllowedValues) + end. + +post_request(Stanza, From, To) -> + case ejabberd_router:route(From, To, Stanza) of + ok -> {200, [], "Ok"}; + _ -> {500, [], "Error"} + end. + +%% Split a line into args. Args are splitted by blankspaces. Args can be enclosed in "". +%% +%% Example call: +%% mod_rest:split_line(" a1 b2 \"c3 d4\"e5\" c6 d7 \\\" e8\"f9 g0 \\\" h1 "). +%% ["a1","b2","c3 d4\"e5","c6","d7"," e8\"f9 g0 ","h1"] +%% +%% 32 is the integer that represents the blankspace +%% 34 is the integer that represents the double quotes: " +%% 92 is the integer that represents the backslash: \ +split_line(Line) -> split(Line, "", []). +split("", "", Args) -> lists:reverse(Args); +split("", Arg, Args) -> split("", "", [lists:reverse(Arg) | Args]); +split([32 | Line], "", Args) -> split(Line, [], Args); +split([32 | Line], Arg, Args) -> split(Line, [], [lists:reverse(Arg) | Args]); +split([34 | Line], "", Args) -> {Line2, Arg2} = splitend(Line), split([32 | Line2], Arg2, Args); +split([92, 34 | Line], "", Args) -> {Line2, Arg2} = splitend(Line), split([32 | Line2], Arg2, Args); +split([Char | Line], Arg, Args) -> split(Line, [Char | Arg], Args). +splitend(Line) -> splitend(Line, []). +splitend([], Res) -> {"", Res}; +splitend([34], Res) -> {"", Res}; +splitend([92, 34], Res) -> {"", Res}; +splitend([34, 32 | Line], Res) -> {Line, Res}; +splitend([92, 34, 32 | Line], Res) -> {Line, Res}; +splitend([Char | Line], Res) -> splitend(Line, [Char | Res]). diff --git a/mod_s2s_log/ChangeLog b/mod_s2s_log/ChangeLog new file mode 100644 index 0000000..190f899 --- /dev/null +++ b/mod_s2s_log/ChangeLog @@ -0,0 +1,11 @@ +2008-03-20 Badlop + + * README.txt: Explained requirement of ejabberd SVN 1135 trunk or + 2.0.x branch + + * ChangeLog: Added file to track changes + +2008-03-14 Mickael Remond + + * mod_s2s_log: Initial commit + diff --git a/mod_s2s_log/Emakefile b/mod_s2s_log/Emakefile new file mode 100644 index 0000000..8adff24 --- /dev/null +++ b/mod_s2s_log/Emakefile @@ -0,0 +1,2 @@ +{'../ejabberd-dev/src/gen_mod', [{outdir, "../ejabberd-dev/ebin"},{i,"../ejabberd-dev/include"}]}. +{'src/mod_s2s_log', [{outdir, "ebin"},{i,"../ejabberd-dev/include"}]}. diff --git a/mod_s2s_log/README.txt b/mod_s2s_log/README.txt new file mode 100644 index 0000000..8fab2e7 --- /dev/null +++ b/mod_s2s_log/README.txt @@ -0,0 +1,14 @@ + + mod_s2s_log + ----------- + + Requirements: ejabberd SVN r1235 trunk or 2.0.x branch + (ejabberd 2.0.1 or newer is required) + + +This module can be use to keep a track of other XMPP servers your server has +been connected with. + +You can use it by adding the following line in the modules section of +ejabberd.cfg: + {mod_s2s_log, [{filename, "/path/to/s2s.log"}]} diff --git a/mod_s2s_log/build.bat b/mod_s2s_log/build.bat new file mode 100644 index 0000000..0188083 --- /dev/null +++ b/mod_s2s_log/build.bat @@ -0,0 +1 @@ +erl -pa ../../ejabberd-dev/trunk/ebin -pa ebin -make diff --git a/mod_s2s_log/build.sh b/mod_s2s_log/build.sh new file mode 100755 index 0000000..f0a0cfa --- /dev/null +++ b/mod_s2s_log/build.sh @@ -0,0 +1,2 @@ +#!/bin/sh +erl -pa ../ejabberd-dev/ebin -pz ebin -make diff --git a/mod_s2s_log/src/mod_s2s_log.erl b/mod_s2s_log/src/mod_s2s_log.erl new file mode 100644 index 0000000..de403d2 --- /dev/null +++ b/mod_s2s_log/src/mod_s2s_log.erl @@ -0,0 +1,112 @@ +%%%---------------------------------------------------------------------- +%%% File : mod_s2s_log.erl +%%% Author : Mickael Remond +%%% Purpose : Log all s2s connections in a file +%%% Created : 14 Mar 2008 by Mickael Remond +%%% Usage : Add the following line in modules section of ejabberd.cfg: +%%% {mod_s2s_log, [{filename, "/path/to/s2s.log"}]} +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2008 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., 59 Temple Place, Suite 330, Boston, MA +%%% 02111-1307 USA +%%% +%%%---------------------------------------------------------------------- +-module(mod_s2s_log). +-author('mremond@process-one.net'). + +%% TODO: Implement multi vhosts support +%% TODO: Support reopen log event + +-behaviour(gen_mod). + +%% API: +-export([start/2, + init/1, + stop/1]). +%% Hooks: +-export([reopen_log/0, + s2s_connect/2]). + +-include("ejabberd.hrl"). + +-define(PROCNAME, ?MODULE). +-define(DEFAULT_FILENAME, "s2s.log"). +-define(FILE_OPTS, [append,raw]). + +-record(config, {filename=?DEFAULT_FILENAME, iodevice}). + +%% For now we only support one log file for all vhosts. +start(Host, Opts) -> + %% ejabberd starts modules sequentially so we assume no race + %% condition is possible here + case whereis(?PROCNAME) of + undefined -> + ?DEBUG("Starting mod_s2s_log ~p ~p~n", [Host, Opts]), + Filename = gen_mod:get_opt(filename, Opts, ?DEFAULT_FILENAME), + %% TODO: Both hooks will need Host parameter for vhost support + ejabberd_hooks:add(reopen_log_hook, ?MODULE, reopen_log, 55), + ejabberd_hooks:add(s2s_connect_hook, ?MODULE, s2s_connect, 55), + register(?PROCNAME, + spawn(?MODULE, init, [#config{filename=Filename}])); + _ -> + ok + end. + +init(Config)-> + ?DEBUG("Starting mod_s2s_log ~p with config ~p~n", [?MODULE, Config]), + {ok, IOD} = file:open(Config#config.filename, ?FILE_OPTS), + loop(Config#config{iodevice=IOD}). + +loop(Config) -> + receive + {s2s_connect, MyServer, Server} -> + log_s2s_connection(Config#config.iodevice, MyServer, Server), + loop(Config); + {reopen_log} -> + file:close(Config#config.iodevice), + {ok, IOD} = file:open(Config#config.filename, ?FILE_OPTS), + ?INFO_MSG("Reopened s2s log file", []), + loop(Config#config{iodevice = IOD}); + stop -> + file:close(Config#config.iodevice), + exit(normal) + end. + +stop(Host) -> + ejabberd_hooks:delete(s2s_connection, Host, + ?MODULE, s2s_connection, 55), + ?PROCNAME ! stop, + ok. + +s2s_connect(MyServer, Server) -> + ?PROCNAME ! {s2s_connect, MyServer, Server}. + +reopen_log() -> + ?PROCNAME ! {reopen_log}. + + +%% --- +%% Internal functions + +log_s2s_connection(IODevice, MyServer, Server) -> + {{Y, M, D}, {H, Min, S}} = calendar:local_time(), + Date = io_lib:format(template(date), [Y, M, D, H, Min, S]), + Record = [Date, "|", MyServer, "|", Server, "\n"], + ok = file:write(IODevice, Record). + +template(date) -> + "~p-~2.2.0w-~2.2.0w ~2.2.0w:~2.2.0w:~2.2.0w". diff --git a/mod_shcommands/COPYING b/mod_shcommands/COPYING new file mode 100644 index 0000000..8b325f5 --- /dev/null +++ b/mod_shcommands/COPYING @@ -0,0 +1,343 @@ +As a special exception, the authors give permission to link this program +with the OpenSSL library and distribute the resulting binary. + + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Library General +Public License instead of this License. diff --git a/mod_shcommands/ChangeLog b/mod_shcommands/ChangeLog new file mode 100644 index 0000000..c36f10e --- /dev/null +++ b/mod_shcommands/ChangeLog @@ -0,0 +1,13 @@ +2009-05-09 Badlop + + * src/mod_shcommands.erl: Fix unused variables and web calls. + +2007-12-26 Badlop + + * src/mod_shcommands.erl: Translate menu items of webadmin hooks + in each module (EJAB-485) + +2007-09-01 Badlop + + * mod_shcommands: Initial commit. + diff --git a/mod_shcommands/Emakefile b/mod_shcommands/Emakefile new file mode 100644 index 0000000..eee721b --- /dev/null +++ b/mod_shcommands/Emakefile @@ -0,0 +1,2 @@ +{'../ejabberd-dev/src/gen_mod', [{outdir, "../ejabberd-dev/ebin"},{i,"../ejabberd-dev/include"}]}. +{'src/mod_shcommands', [{outdir, "ebin"},{i,"../ejabberd-dev/include"}]}. diff --git a/mod_shcommands/README.txt b/mod_shcommands/README.txt new file mode 100644 index 0000000..b895538 --- /dev/null +++ b/mod_shcommands/README.txt @@ -0,0 +1,71 @@ + + mod_shcommands - Execute shell commands + + Author: Badlop + http://ejabberd.jabber.ru/mod_shcommands + + + + + ***************** + + W A R N I N G + + ***************** + + + USE THIS MODULE AT YOUR OWN RISK + + This module allows ejabberd administrators + to remotely execute shell commands + which could compromise both + the ejabberd server and the whole machine. + + + ***************** + + W A R N I N G + + ***************** + + + + + DESCRIPTION + =========== + +This module provides the ejabberd server administrator a method to remotely +execute shell commands in the ejabberd server. + +It provides a page in the ejabberd Web Admin. +Only the administrators of the whole server can access this page. + +Three types of commands are possible: + * ejabberd_ctl: makes a call to ejabberd_ctl + * erlang shell: executes an erlang command + * system shell: executes a command on the system shell +The result of the execution will be shown. + +In the system shell, only non-interactive commands will work correctly, +for example this will work: + ps -all +Don't use commands that start an interactive mode: + DON'T TRY THIS: top + DON'T TRY THIS: vim readme.txt + +This module does not check if the commands are dangerous or problematic, +so this module is only recommended for experienced ejabberd and Erlang/OTP +administrators. + + + + BASIC CONFIGURATION + =================== + +Add the module to your ejabberd.cfg, on the modules section: +{modules, [ + ... + {mod_shcommands, []}, + ... +]}. + diff --git a/mod_shcommands/build.bat b/mod_shcommands/build.bat new file mode 100644 index 0000000..fff9b3b --- /dev/null +++ b/mod_shcommands/build.bat @@ -0,0 +1 @@ +erl -pa ../../ejabberd-dev/trunk/ebin -pa ebin -make diff --git a/mod_shcommands/build.sh b/mod_shcommands/build.sh new file mode 100755 index 0000000..f0a0cfa --- /dev/null +++ b/mod_shcommands/build.sh @@ -0,0 +1,2 @@ +#!/bin/sh +erl -pa ../ejabberd-dev/ebin -pz ebin -make diff --git a/mod_shcommands/src/mod_shcommands.erl b/mod_shcommands/src/mod_shcommands.erl new file mode 100644 index 0000000..010b1cd --- /dev/null +++ b/mod_shcommands/src/mod_shcommands.erl @@ -0,0 +1,156 @@ +%%%---------------------------------------------------------------------- +%%% File : mod_shcommands.erl +%%% Author : Badlop +%%% Purpose : Execute shell commands +%%% Created : 1 Sep 2007 by Badlop +%%% Id : $Id: mod_shcommands.erl 1034 2009-11-17 21:44:17Z badlop $ +%%%---------------------------------------------------------------------- + +-module(mod_shcommands). +-author('badlop@ono.com'). + +-behaviour(gen_mod). + +-export([web_menu_node/3, web_page_node/5, + start/2, stop/1]). + +-include("ejabberd.hrl"). +-include("web/ejabberd_http.hrl"). +-include("web/ejabberd_web_admin.hrl"). + +%%------------------- +%% gen_mod functions +%%------------------- + +start(_Host, _Opts) -> + ejabberd_hooks:add(webadmin_menu_node, ?MODULE, web_menu_node, 50), + ejabberd_hooks:add(webadmin_page_node, ?MODULE, web_page_node, 50), + ok. + +stop(_Host) -> + ejabberd_hooks:delete(webadmin_menu_node, ?MODULE, web_menu_node, 50), + ejabberd_hooks:delete(webadmin_page_node, ?MODULE, web_page_node, 50), + ok. + +%%------------------- +%% Web Admin Menu +%%------------------- + +web_menu_node(Acc, _Node, Lang) -> + Acc ++ [{"shcommands", ?T("Shell Commands")}]. + +%%------------------- +%% Web Admin Page +%%------------------- + +web_page_node(_, Node, ["shcommands"], Query, Lang) -> + Res = [?XC("h1", "Shell Commands") | get_content(Node, Query, Lang)], + {stop, Res}; +web_page_node(Acc, _, _, _, _) -> Acc. + +%%------------------- +%% Generate content +%%------------------- + +get_content(Node, Query, Lang) -> + Instruct = ?T("Type a command in a textbox and click Execute."), + {{CommandCtl, CommandErl, CommandShell}, Res} = case catch parse_and_execute(Query, Node) of + {'EXIT', _} -> {{"", "", ""}, Instruct}; + Result_tuple -> Result_tuple + end, + TitleHTML = [ + ?XC("p", Instruct++" "++?T("Use only commands which immediately return a result.")), + ?XC("p", ?T("WARNING: Use this only if you know what you are doing.")) + ], + CommandHTML = + [?XAE("form", [{"method", "post"}], + [?XAE("table", [], + [?XE("tbody", + [?XE("tr", + [?X("td"), + ?XCT("td", "ejabberd_ctl"), + ?XE("td", [?INPUTS("text", "commandctl", CommandCtl, "70"), + ?INPUTT("submit", "executectl", "Execute")]) + ] + ), + ?XE("tr", + [?X("td"), + ?XCT("td", "erlang shell"), + ?XE("td", [?INPUTS("text", "commanderl", CommandErl, "70"), + ?INPUTT("submit", "executeerl", "Execute")]) + ] + ), + ?XE("tr", + [?X("td"), + ?XCT("td", "system shell"), + ?XE("td", [?INPUTS("text", "commandshe", CommandShell, "70"), + ?INPUTT("submit", "executeshe", "Execute")]) + ] + ) + ] + )])] + )], + ResHTML = + [?XAC("textarea", [{"wrap", "off"}, {"style", "font-family:monospace;"}, + {"name", "result"}, {"rows", "30"}, {"cols", "80"}], + Res) + ], + TitleHTML ++ CommandHTML ++ ResHTML. + +parse_and_execute(Query, Node) -> + {[Exec], _} = lists:partition( + fun(ExType) -> + lists:keymember(ExType, 1, Query) + end, + ["executectl", "executeerl", "executeshe"]), + Commands = {get_val("commandctl", Query), + get_val("commanderl", Query), + get_val("commandshe", Query)}, + R = parse1_command(Exec, Commands, Node), + {Commands, R}. + +get_val(Val, Query) -> + {value, {_, R}} = lists:keysearch(Val, 1, Query), + R. + +parse1_command("executectl", {Command, _, _}, Node) -> + Command2 = string:tokens(Command, " "), + {_E, Efile} = execute(ctl, Node, Command2), + io_lib:format("ejabberdctl ~p ~s~n~s", [Node, Command, Efile]); + +parse1_command("executeerl", {_, Command, _}, Node) -> + {ok, A2, _} = erl_scan:string(Command), + {ok, A3} = erl_parse:parse_exprs(A2), + {E, Efile} = execute(erl, Node, A3), + io_lib:format("(~p)1> ~s~s~n~p", [Node, Command, Efile, E]); + +parse1_command("executeshe", {_, _, Command}, Node) -> + E = rpc:call(Node, os, cmd, [Command]), + C1 = lists:map( + fun(C) -> string:strip(os:cmd(C), right, $\n) end, + ["whoami", "hostname -s", "pwd"]), + io_lib:format("~s@~s:~s$ ~s~n~s", C1 ++ [Command, E]). + + +execute(Type, Node, C) -> + GL = group_leader(), + Filename = "temp" ++ io_lib:format("~p", [random:uniform()*10000]), + {ok, File} = file:open(Filename, [write]), + group_leader(File, self()), + Res = case Type of + ctl -> rpc:call(Node, ejabberd_ctl, process, [C]); + erl -> rpc:call(Node, erl_eval, exprs, [C, []]) + end, + E = case Res of + {value, V, _} -> V; + O -> O + end, + group_leader(GL, self()), + file:close(File), + {ok, B} = file:read_file(Filename), + file:delete(Filename), + E2 = case binary_to_list(B) of + [] -> []; + List -> "\n"++List + end, + {E, E2}. diff --git a/mod_statsdx/COPYING b/mod_statsdx/COPYING new file mode 100644 index 0000000..8b325f5 --- /dev/null +++ b/mod_statsdx/COPYING @@ -0,0 +1,343 @@ +As a special exception, the authors give permission to link this program +with the OpenSSL library and distribute the resulting binary. + + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Library General +Public License instead of this License. diff --git a/mod_statsdx/ChangeLog b/mod_statsdx/ChangeLog new file mode 100644 index 0000000..80355a0 --- /dev/null +++ b/mod_statsdx/ChangeLog @@ -0,0 +1,60 @@ +2009-11-24 Badlop + + * src/mod_statsdx.erl: Add getstatsdx ejabberd commands. + +2009-02-04 Badlop + + * src/mod_statsdx.erl: Fix a few stats + +2009-02-03 Badlop + + * src/mod_statsdx.erl: New configuration {hooks, traffic}. In + addition to hooks for client versions, in this case also tracks + stanza traffic. Show in WebAdmin of the virtual host. + * README.txt: Document traffic hooks + +2009-02-02 Badlop + + * src/mod_statsdx.erl: New stats for erlang memory. Show new stats + in node page. Show big integers with separation for easy reading. + +2008-09-16 Badlop + + * src/mod_statsdx.erl: Fixed code indentation and + trailing-whitespace. Catch table creation. Improved hooks + feature. Remove the unused hook webadmin_user. Disable code for + traffic stats and don't show empty section 'Ratios' in + webadmin. Create an ETS table for the whole server, and a table + for each vhost. Add subpages in WebAdmin to view filtered + sessions. + +2008-09-03 Badlop + + * src/mod_statsdx.erl: If client answers with unexpected response, + forget stanza instead of crashing. + + * src/mod_statsdx.erl: The option 'hooks' in mod_statsdx is + disabled by default. + * README.txt: Likewise + + * README.txt: Several cosmetic changes. + +2007-12-26 Badlop + + * src/mod_statsdx.erl: Translate menu items of webadmin hooks in + each module (EJAB-485) + +2007-08-31 Badlop + + * src/mod_statsdx.erl: Added pages to Web Admin. Apply Emacs-mode + indentation. + * src/ejabberd_web_admin.erl: No need to patch core web admin file. + * Emakefile: Idem. + +2007-08-21 Badlop + + * src/*.erl: Removed authusers statistic. + + * src/*.erl: Fixed credits. + + * ChangeLog: New file to track changes. diff --git a/mod_statsdx/Emakefile b/mod_statsdx/Emakefile new file mode 100644 index 0000000..95e5b9d --- /dev/null +++ b/mod_statsdx/Emakefile @@ -0,0 +1,3 @@ +{'../ejabberd-dev/src/gen_mod', [{outdir, "../ejabberd-dev/ebin"},{i,"../ejabberd-dev/include"}]}. +{'src/mod_statsdx', [{outdir, "ebin"},{i,"../ejabberd-dev/include"}]}. +{'src/mod_stats2file', [{outdir, "ebin"},{i,"../ejabberd-dev/include"}]}. diff --git a/mod_statsdx/README.txt b/mod_statsdx/README.txt new file mode 100644 index 0000000..e736035 --- /dev/null +++ b/mod_statsdx/README.txt @@ -0,0 +1,95 @@ + + mod_statsdx - Calculates and gathers statistics actively + + Homepage: http://www.ejabberd.im/mod_statsdx + Author: Badlop + Requirements: ejabberd 2.1.x + + + mod_statsdx + ============== + +Measures several statistics. It provides a new section in ejabberd +Web Admin and two ejabberd commands to view the information. + + + CONFIGURE + --------- + +Enable the module in ejabberd.cfg for example with a basic configuration: +{modules, [ + ... + {mod_statsdx, []} + ]}. + +Configurable options: + hooks: Set to 'true' to enable hooks and related statistics. + This option by default 'false' because it is expected to + consume many resources in very populated servers. + If set to 'traffic', it will also keep counters of traffic stanzas. + + + EXAMPLE CONFIGURATION + --------------------- + +{modules, [ + ... + {mod_statsdx, [{hooks, true}]} + ]}. + + + FEATURE REQUESTS + ---------------- + + - fix the problem with plain/ssl/tlsusers, it crashes ejabberd + - traffic: send bytes per second, received bps + - connections to a transport + - traffic: send presence per second, received mps + - Number of SASL c2s connections + - improve to work in distributed server + + + + mod_stats2file + ============== + + Generates files with all kind of statistics + +This module writes a file with all kind of statistics every few minutes. +Available output formats are html (example), +text file with descriptions and raw text file (for MRTG, RRDTool...). + + + CONFIGURE + --------- + +This module requires mod_statsdx. + +Enable the module in ejabberd.cfg for example with a basic configuration: +{modules, [ + ... + {mod_stats2file, []} + ]}. + +Configurable options: + interval: Time between updates, in minutes (default: 5) + type: Type of output. Allowed values: html, txt, dat (default: html) + basefilename: Base filename including absolute path (default: "/tmp/ejasta") + split: If split the statistics in several files (default: false) + hosts: List of virtual hosts that will be checked. By default all + + + EXAMPLE CONFIGURATION + --------------------- + +{modules, [ + ... + {mod_stats2file, [{interval, 60}, + {type, txt}, + {split, true}, + {basefilename, "/var/www/stats"}, + {hosts, ["localhost", "server3.com"]} + ]} + ]}. + + diff --git a/mod_statsdx/build.bat b/mod_statsdx/build.bat new file mode 100644 index 0000000..fff9b3b --- /dev/null +++ b/mod_statsdx/build.bat @@ -0,0 +1 @@ +erl -pa ../../ejabberd-dev/trunk/ebin -pa ebin -make diff --git a/mod_statsdx/build.sh b/mod_statsdx/build.sh new file mode 100755 index 0000000..f0a0cfa --- /dev/null +++ b/mod_statsdx/build.sh @@ -0,0 +1,2 @@ +#!/bin/sh +erl -pa ../ejabberd-dev/ebin -pz ebin -make diff --git a/mod_statsdx/src/mod_stats2file.erl b/mod_statsdx/src/mod_stats2file.erl new file mode 100644 index 0000000..3733361 --- /dev/null +++ b/mod_statsdx/src/mod_stats2file.erl @@ -0,0 +1,409 @@ +%%%---------------------------------------------------------------------- +%%% File : mod_stats2file.erl +%%% Author : Badlop +%%% Purpose : Generates files with all kind of statistics +%%% Created : +%%% Id : $Id: mod_stats2file.erl 440 2007-12-06 22:36:21Z badlop $ +%%%---------------------------------------------------------------------- + +-module(mod_stats2file). +-author('badlop@ono.com'). + +-behaviour(gen_mod). + +-export([start/2, loop/5, stop/1]). + +-include("ejabberd.hrl"). +-include("jlib.hrl"). +-include("mod_roster.hrl"). + +-define(PROCNAME, ejabberd_mod_stats2file). +-define(T(Text), translate:translate("Lang", Text)). + +%% ------------------- +%% Module control +%% ------------------- + +start(_Host, Opts) -> + case whereis(?PROCNAME) of + undefined -> + Interval = gen_mod:get_opt(interval, Opts, 5), + I = Interval*60*1000, + %I = 9000, %+++ + + Type = gen_mod:get_opt(type, Opts, html), + + Split = gen_mod:get_opt(split, Opts, false), + + BaseFilename = gen_mod:get_opt(basefilename, Opts, "/tmp/ejasta"), + + Hosts_all = ejabberd_config:get_global_option(hosts), + Hosts = gen_mod:get_opt(hosts, Opts, Hosts_all), + + register(?PROCNAME, spawn(?MODULE, loop, [I, Hosts, BaseFilename, Type, Split])); + _ -> + ok + end, + ok. + +loop(I, Hs, O, T, Split) -> + write_statsfiles(Split, I, Hs, O, T), + timer:send_after(I, stats), + receive + stats -> ?MODULE:loop(I, Hs, O, T, Split); + stop -> ok + end. + +stop(_Host) -> + case whereis(?PROCNAME) of + undefined -> ok; + _ -> + ?PROCNAME ! stop + end. + + +%% ------------------- +%% write_stat* +%% ------------------- + +write_statsfiles(false, I, Hs, O, T) -> + Fn = filename:flatten([O, ".", T]), + {ok, F} = file:open(Fn, [write]), + fwini(F, T), + write_stats(I, server, "", F, T), + fwbr(F, T), + fwbr(F, T), + Node = node(), + write_stats(I, node, Node, F, T), + lists:foreach( + fun(H) -> + fwbr(F, T), + fwbr(F, T), + write_stats(I, vhost, H, F, T) + end, + Hs), + fwend(F, T), + file:close(F); + +write_statsfiles(true, I, Hs, O, T) -> + write_statsfile(I, server, "", O, T), + Node = node(), + write_statsfile(I, node, Node, O, T), + lists:foreach( + fun(H) -> + write_statsfile(I, vhost, H, O, T) + end, + Hs). + +write_statsfile(I, Class, Name, O, T) -> + Fn = filename:flatten([O, "-", Class, "-", Name, ".", T]), + {ok, F} = file:open(Fn, [write]), + fwini(F, T), + write_stats(I, Class, Name, F, T), + fwend(F, T), + file:close(F). + +write_stats(I, server, _Name, F, T) -> + fwh(F, "Server statistics", 1, T), + fwbl1(F, T), + + fwtstl(F, "localtime", T), + + fwh(F, "Accounts", 2, T), + fwbl1(F, T), + fwttl(F, "registeredusers", T), + fwbl2(F, T), + + fwh(F, "Roster", 2, T), + fwbl1(F, T), + fwttl(F, "totalrosteritems", T), + fwttl(F, "meanitemsinroster", T), + fwbl2(F, T), + + fwh(F, "Users", 2, T), + fwbl1(F, T), + fwttl(F, "onlineusers", T), + fwttl(F, "offlinemsg", T), + fwttl(F, "vcards", T), + fwbl2(F, T), + + fwh(F, "MUC", 2, T), + fwbl1(F, T), + fwttl(F, "totalmucrooms", T), + fwttl(F, "permmucrooms", T), + fwttl(F, "regmucrooms", T), + fwbl2(F, T), + + fwh(F, "Pub/Sub", 2, T), + fwbl1(F, T), + fwttl(F, "regpubsubnodes", T), + fwbl2(F, T), + + %fwh(F, "IRC", 2, T), + %fwbl1(F, T), + %fwttl(F, "ircconns", T), + %fwbl2(F, T), + + fwh(F, "Ratios", 2, T), + fwbl1(F, T), + fwttl(F, {"user_login", I}, server, T), + fwttl(F, {"user_logout", I}, server, T), + fwttl(F, {"remove_user", I}, server, T), + fwbl2(F, T), + + fwh(F, "Sessions", 2, T), + fwbl1(F, T), + fwh(F, "Clients", 3, T), + fwbl1(F, T), + do_stat_table(F, "client", server, T), + + fwbl2(F, T), + fwh(F, "OS", 3, T), + fwbl1(F, T), + do_stat_table(F, "os", server, T), + fwbl2(F, T), + + case T of + html -> + fwh(F, "Client/OS", 3, T), + fwbl1(F, T), + do_stat_table(F, "client_os", server, T), + fwbl2(F, T), + + fwh(F, "Languages", 3, T), + fwbl1(F, T), + do_stat_table(F, "languages", server, T), + fwbl2(F, T), + fwbl2(F, T); + _ -> + ok + end, + + fwbl2(F, T); + +write_stats(I, node, Node, F, T) -> + fwh(F, "Node statistics", 1, T), + fwbl1(F, T), + + fwt(F, "Node", Node, T), + + fwh(F, "Connections", 2, T), + fwbl1(F, T), + %fwttl(F, "plainusers", T), + %fwttl(F, "sslusers", T), + %fwttl(F, "tlsusers", T), + fwttl(F, "httppollusers", T), + fwttl(F, "httpbindusers", T), + fwttl(F, "s2sconnections", T), + fwttl(F, "s2sservers", T), + fwbl2(F, T), + + fwh(F, "Erlang", 2, T), + fwbl1(F, T), + fwttl(F, "operatingsystem", T), + fwttl(F, "erlangmachine", T), + fwttl(F, "erlangmachinetarget", T), + fwttl(F, "maxprocallowed", T), + fwttl(F, "totalerlproc", T), + fwttl(F, "procqueue", T), + fwbl2(F, T), + + fwh(F, "Times", 2, T), + fwbl1(F, T), + %fwttl(F, "uptimehuman", T), + %fwttl(F, "lastrestart", T), + + fwbr(F, T), + CPUTime = element(1, rpc:call(Node, erlang, statistics, [runtime])), + fw(F, "~s (ms): ~w",[?T("CPUtime"), CPUTime]), + + fwbr(F, T), + MT = trunc(erlang:memory(total)/1024), + fwt(F, "Memory VM (KB)", MT, T), + + fwbl2(F, T), + + fwh(F, "CPU", 2, T), + fwbl1(F, T), + fwttl(F, "cpu_avg1", T), + fwttl(F, "cpu_avg5", T), + fwttl(F, "cpu_avg15", T), + fwttl(F, "cpu_nprocs", T), + U = cpu_sup:util([detailed]), + fwttl(F, {"cpu_util_user", U}, T), + fwttl(F, {"cpu_util_nice_user", U}, T), + fwttl(F, {"cpu_util_kernel", U}, T), + fwttl(F, {"cpu_util_idle", U}, T), + fwttl(F, {"cpu_util_wait", U}, T), + fwbl2(F, T), + + fwh(F, "RAM", 2, T), + fwbl1(F, T), + M = memsup:get_system_memory_data(), + fwttl(F, {"memsup_system", M}, T), + fwttl(F, {"memsup_free", M}, T), + fwttl(F, {"reductions", I}, T), + fwbl2(F, T), + + fwbl2(F, T); + +write_stats(I, vhost, Host, F, T) -> + fwh(F, "Host statistics", 1, T), + fwbl1(F, T), + + fwtstl(F, "vhost", Host, T), + + fwh(F, "Accounts", 2, T), + fwbl1(F, T), + fwttl(F, "registeredusers", Host, T), + fwbl2(F, T), + + fwh(F, "Roster", 2, T), + fwbl1(F, T), + fwttl(F, "totalrosteritems", Host, T), + fwttl(F, "meanitemsinroster", Host, T), + fwbl2(F, T), + + fwh(F, "Users", 2, T), + fwbl1(F, T), + fwttl(F, "onlineusers", Host, T), + fwttl(F, "offlinemsg", Host, T), + fwttl(F, "vcards", Host, T), + fwbl2(F, T), + + fwh(F, "Connections", 2, T), + fwbl1(F, T), + fwttl(F, "s2sconnections", Host, T), + fwbl2(F, T), + + fwh(F, "MUC", 2, T), + fwbl1(F, T), + fwttl(F, "totalmucrooms", Host, T), + fwttl(F, "permmucrooms", Host, T), + fwttl(F, "regmucrooms", Host, T), + fwbl2(F, T), + + %fwh(F, "IRC", 2, T), + %fwbl1(F, T), + %fwttl(F, "ircconns", Host, T), + %fwbl2(F, T), + + fwh(F, "Sessions", 2, T), + fwbl1(F, T), + fwh(F, "Clients", 3, T), + fwbl1(F, T), + do_stat_table(F, "client", Host, T), + + fwbl2(F, T), + fwh(F, "OS", 3, T), + fwbl1(F, T), + do_stat_table(F, "os", Host, T), + fwbl2(F, T), + + case T of + html -> + fwh(F, "Client/OS", 3, T), + fwbl1(F, T), + do_stat_table(F, "client_os", Host, T), + fwbl2(F, T), + + fwh(F, "Languages", 3, T), + fwbl1(F, T), + do_stat_table(F, "languages", Host, T), + fwbl2(F, T), + fwbl2(F, T); + _ -> + ok + end, + + fwh(F, "Ratios", 2, T), + fwbl1(F, T), + fwttl(F, {"user_login", I}, Host, T), + fwttl(F, {"user_logout", I}, Host, T), + fwttl(F, {"remove_user", I}, Host, T), + fwttl(F, {send, iq, in, I}, Host, T), + fwttl(F, {send, iq, out, I}, Host, T), + fwttl(F, {send, message, in, I}, Host, T), + fwttl(F, {send, message, out, I}, Host, T), + fwttl(F, {send, presence, in, I}, Host, T), + fwttl(F, {send, presence, out, I}, Host, T), + fwttl(F, {recv, iq, in, I}, Host, T), + fwttl(F, {recv, iq, out, I}, Host, T), + fwttl(F, {recv, message, in, I}, Host, T), + fwttl(F, {recv, message, out, I}, Host, T), + fwttl(F, {recv, presence, in, I}, Host, T), + fwttl(F, {recv, presence, out, I}, Host, T), + fwbl2(F, T), + + fwbl2(F, T). + +%% ------------------- +%% get(* +%% ------------------- + +fwttl(F, Arg, T) -> fwt(F, gett(Arg), getl(Arg), T). +fwttl(F, Arg, Host, T) -> fwt(F, gett(Arg), getl(Arg, Host), T). + +fwtstl(F, Arg, T) -> fwts(F, gett(Arg), getl(Arg), T). +fwtstl(F, Arg, Host, T) -> fwts(F, gett(Arg), getl(Arg, Host), T). + +gett(Arg) -> get(node(), [Arg, title]). +getl(Args) -> get(node(), [Args]). +getl(Args, Host) -> get(node(), [Args, Host]). + +get(Node, A) -> + mod_statsdx:get_statistic(Node, A). + + +%% ------------------- +%% utilities +%% ------------------- + +fw(F, S) -> file:write(F, S). +fw(F, S, O) -> file:write(F, io_lib:format(S, O)). + +fwt(F, S, O, html) -> fw(F, "~s: ~p
    ~n",[?T(S), O]); +fwt(F, S, O, txt) -> fw(F, "~s: ~p~n",[?T(S), O]); +fwt(F, _, O, dat) -> fw(F, "~p~n",[O]). + +fwts(F, S, O, html) -> fw(F, "~s: ~s
    ~n",[?T(S), O]); +fwts(F, S, O, txt) -> fw(F, "~s: ~s~n",[?T(S), O]); +fwts(F, _, O, dat) -> fw(F, "~s~n",[O]). + +%fwtsn(F, S, O, html) -> fw(F, "~s: ~s
    ",[?T(S), O]); +%fwtsn(F, S, O, txt) -> fw(F, "~s: ~s",[?T(S), O]); +%fwtsn(F, _, O, dat) -> fw(F, "~s",[O]). + +fwh(F, S, N, html) -> fw(F, "~s~n",[N, S, N]); +fwh(F, S, 1, txt) -> fw(F, " ===== ~s =====~n",[S]); +fwh(F, S, 2, txt) -> fw(F, "~n --- ~s ---~n",[S]); +fwh(F, S, 3, txt) -> fw(F, "~n + ~s +~n",[S]); +fwh(_, _, _, dat) -> ok. + +fwbr(F, html) -> fw(F, "
    \n"); +fwbr(F, txt) -> fw(F, "\n"); +fwbr(_, dat) -> ok. + +fwini(F, html) -> fw(F, "\n"); +fwini(_, txt) -> ok; +fwini(_, dat) -> ok. +fwend(F, html) -> fw(F, "\n"); +fwend(_, txt) -> ok; +fwend(_, dat) -> ok. + +fwbl1(F, html) -> fw(F, "
    \n"); +fwbl1(_, txt) -> ok; +fwbl1(_, dat) -> ok. +fwbl2(F, html) -> fw(F, "
    \n"); +fwbl2(_, txt) -> ok; +fwbl2(_, dat) -> ok. + +do_stat_table(F, Stat, Host, T) -> + do_stat_table(F, Stat, Host, T, unknown). +do_stat_table(F, Stat, Host, T, _Lang) -> + lists:map( + fun({Name, Value}) -> + fwts(F, Name, io_lib:format("~p", [Value]), T) + end, + mod_statsdx:get_statistic(global, [Stat, Host]) + ). diff --git a/mod_statsdx/src/mod_statsdx.erl b/mod_statsdx/src/mod_statsdx.erl new file mode 100644 index 0000000..196c1a6 --- /dev/null +++ b/mod_statsdx/src/mod_statsdx.erl @@ -0,0 +1,1699 @@ +%%%---------------------------------------------------------------------- +%%% File : mod_statsdx.erl +%%% Author : Badlop +%%% Purpose : Calculates and gathers statistics actively +%%% Created : +%%% Id : $Id: mod_statsdx.erl 1118 2011-07-11 17:16:30Z badlop $ +%%%---------------------------------------------------------------------- + +%%%% Definitions + +-module(mod_statsdx). +-author('badlop@ono.com'). + +-behaviour(gen_mod). + +-export([start/2, loop/1, stop/1, get_statistic/2, + %% Commands + getstatsdx/1, getstatsdx/2, + get_top_users/2, + %% WebAdmin + web_menu_main/2, web_page_main/2, + web_menu_node/3, web_page_node/5, + web_menu_host/3, web_page_host/3, + %% Hooks + register_user/2, remove_user/2, user_send_packet/3, + user_send_packet_traffic/3, user_receive_packet_traffic/4, + user_login/1, user_logout/4, user_logout_sm/3]). + +-include("ejabberd.hrl"). +-include("ejabberd_commands.hrl"). +-include("jlib.hrl"). +-include("mod_roster.hrl"). +-include("web/ejabberd_http.hrl"). +-include("web/ejabberd_web_admin.hrl"). + +-define(PROCNAME, ejabberd_mod_statsdx). + +%% Copied from ejabberd_s2s.erl Used in function get_s2sconnections/1 +-record(s2s, {fromto, pid, key}). + +%%%================================== +%%%% Module control + +start(Host, Opts) -> + Hooks = gen_mod:get_opt(hooks, Opts, false), + %% Default value for the counters + CD = case Hooks of + true -> 0; + traffic -> 0; + false -> "disabled" + end, + + ejabberd_commands:register_commands(commands()), + + %% If the process that handles statistics for the server is not started yet, + %% start it now + case whereis(?PROCNAME) of + undefined -> + application:start(os_mon), + initialize_stats_server(); + _ -> + ok + end, + ?PROCNAME ! {initialize_stats, Host, Hooks, CD}. + +stop(Host) -> + finish_stats(Host), + ejabberd_commands:unregister_commands(commands()), + case whereis(?PROCNAME) of + undefined -> ok; + _ -> ?PROCNAME ! {stop, Host} + end. + + +%%%================================== +%%%% Stats Server + +table_name(server) -> gen_mod:get_module_proc("server", mod_statsdx); +table_name(Host) -> gen_mod:get_module_proc(Host, mod_statsdx). + +initialize_stats_server() -> + register(?PROCNAME, spawn(?MODULE, loop, [[]])). + +loop(Hosts) -> + receive + {initialize_stats, Host, Hooks, CD} -> + case Hosts of + [] -> prepare_stats_server(CD); + _ -> ok + end, + prepare_stats_host(Host, Hooks, CD), + loop([Host | Hosts]); + {stop, Host} -> + case Hosts -- [Host] of + [] -> + finish_stats(); + RemainingHosts -> + loop(RemainingHosts) + end + end. + +%% Si no existe una tabla de stats del server, crearla. +%% Deberia ser creada por un proceso que solo muera cuando se detenga el ultimo mod_statsdx del servidor +prepare_stats_server(CD) -> + Table = table_name("server"), + ets:new(Table, [named_table, public]), + ets:insert(Table, {{user_login, server}, CD}), + ets:insert(Table, {{user_logout, server}, CD}), + ets:insert(Table, {{register_user, server}, CD}), + ets:insert(Table, {{remove_user, server}, CD}), + lists:foreach( + fun(E) -> ets:insert(Table, {{client, server, E}, CD}) end, + list_elem(clients, id) + ), + lists:foreach( + fun(E) -> ets:insert(Table, {{conntype, server, E}, CD}) end, + list_elem(conntypes, id) + ), + lists:foreach( + fun(E) -> ets:insert(Table, {{os, server, E}, CD}) end, + list_elem(oss, id) + ), + ejabberd_hooks:add(webadmin_menu_main, ?MODULE, web_menu_main, 50), + ejabberd_hooks:add(webadmin_menu_node, ?MODULE, web_menu_node, 50), + ejabberd_hooks:add(webadmin_page_main, ?MODULE, web_page_main, 50), + ejabberd_hooks:add(webadmin_page_node, ?MODULE, web_page_node, 50). + +prepare_stats_host(Host, Hooks, CD) -> + Table = table_name(Host), + ets:new(Table, [named_table, public]), + ets:insert(Table, {{user_login, Host}, CD}), + ets:insert(Table, {{user_logout, Host}, CD}), + ets:insert(Table, {{register_user, Host}, CD}), + ets:insert(Table, {{remove_user, Host}, CD}), + ets:insert(Table, {{send, Host, iq, in}, CD}), + ets:insert(Table, {{send, Host, iq, out}, CD}), + ets:insert(Table, {{send, Host, message, in}, CD}), + ets:insert(Table, {{send, Host, message, out}, CD}), + ets:insert(Table, {{send, Host, presence, in}, CD}), + ets:insert(Table, {{send, Host, presence, out}, CD}), + ets:insert(Table, {{recv, Host, iq, in}, CD}), + ets:insert(Table, {{recv, Host, iq, out}, CD}), + ets:insert(Table, {{recv, Host, message, in}, CD}), + ets:insert(Table, {{recv, Host, message, out}, CD}), + ets:insert(Table, {{recv, Host, presence, in}, CD}), + ets:insert(Table, {{recv, Host, presence, out}, CD}), + lists:foreach( + fun(E) -> ets:insert(Table, {{client, Host, E}, CD}) end, + list_elem(clients, id) + ), + lists:foreach( + fun(E) -> ets:insert(Table, {{conntype, Host, E}, CD}) end, + list_elem(conntypes, id) + ), + lists:foreach( + fun(E) -> ets:insert(Table, {{os, Host, E}, CD}) end, + list_elem(oss, id) + ), + case Hooks of + true -> + ejabberd_hooks:add(register_user, Host, ?MODULE, register_user, 90), + ejabberd_hooks:add(remove_user, Host, ?MODULE, remove_user, 90), + ejabberd_hooks:add(user_available_hook, Host, ?MODULE, user_login, 90), + %%ejabberd_hooks:add(unset_presence_hook, Host, ?MODULE, user_logout, 90), + ejabberd_hooks:add(sm_remove_connection_hook, Host, ?MODULE, user_logout_sm, 90), + ejabberd_hooks:add(user_send_packet, Host, ?MODULE, user_send_packet, 90); + traffic -> + ejabberd_hooks:add(user_receive_packet, Host, ?MODULE, user_receive_packet_traffic, 92), + ejabberd_hooks:add(user_send_packet, Host, ?MODULE, user_send_packet_traffic, 92), + ejabberd_hooks:add(register_user, Host, ?MODULE, register_user, 90), + ejabberd_hooks:add(remove_user, Host, ?MODULE, remove_user, 90), + ejabberd_hooks:add(user_available_hook, Host, ?MODULE, user_login, 90), + ejabberd_hooks:add(sm_remove_connection_hook, Host, ?MODULE, user_logout_sm, 90), + ejabberd_hooks:add(user_send_packet, Host, ?MODULE, user_send_packet, 90); + false -> + ok + end, + ejabberd_hooks:add(webadmin_menu_host, Host, ?MODULE, web_menu_host, 50), + ejabberd_hooks:add(webadmin_page_host, Host, ?MODULE, web_page_host, 50). + +finish_stats() -> + ejabberd_hooks:delete(webadmin_menu_main, ?MODULE, web_menu_main, 50), + ejabberd_hooks:delete(webadmin_menu_node, ?MODULE, web_menu_node, 50), + ejabberd_hooks:delete(webadmin_page_main, ?MODULE, web_page_main, 50), + ejabberd_hooks:delete(webadmin_page_node, ?MODULE, web_page_node, 50), + Table = table_name("server"), + catch ets:delete(Table). + +finish_stats(Host) -> + ejabberd_hooks:delete(user_available_hook, Host, ?MODULE, user_login, 90), + %%ejabberd_hooks:delete(unset_presence_hook, Host, ?MODULE, user_logout, 90), + ejabberd_hooks:delete(sm_remove_connection_hook, Host, ?MODULE, user_logout_sm, 90), + ejabberd_hooks:delete(user_send_packet, Host, ?MODULE, user_send_packet, 90), + ejabberd_hooks:delete(user_send_packet, Host, ?MODULE, user_send_packet_traffic, 92), + ejabberd_hooks:delete(user_receive_packet, Host, ?MODULE, user_receive_packet_traffic, 92), + ejabberd_hooks:delete(register_user, Host, ?MODULE, register_user, 90), + ejabberd_hooks:delete(remove_user, Host, ?MODULE, remove_user, 90), + ejabberd_hooks:delete(webadmin_menu_host, Host, ?MODULE, web_menu_host, 50), + ejabberd_hooks:delete(webadmin_page_host, Host, ?MODULE, web_page_host, 50), + Table = table_name(Host), + catch ets:delete(Table). + + +%%%================================== +%%%% Hooks Handlers + +register_user(_User, Server) -> + Table = table_name(Server), + ets:update_counter(Table, {register_user, Server}, 1), + ets:update_counter(Table, {register_user, server}, 1). + +remove_user(_User, Server) -> + Table = table_name(Server), + ets:update_counter(Table, {remove_user, Server}, 1), + ets:update_counter(Table, {remove_user, server}, 1). + +user_send_packet(FromJID, ToJID, NewEl) -> + %% Registrarse para tramitar Host/mod_stats2file + case list_to_atom(ToJID#jid.lresource) of + ?MODULE -> received_response(FromJID, ToJID, NewEl); + _ -> ok + end. + +user_send_packet_traffic(FromJID, ToJID, NewEl) -> + %% Only required for traffic stats + Host = FromJID#jid.lserver, + HostTo = ToJID#jid.lserver, + {xmlelement, Type, _, _} = NewEl, + Type2 = case Type of + "iq" -> iq; + "message" -> message; + "presence" -> presence + end, + Dest = case is_host(HostTo, Host) of + true -> in; + false -> out + end, + Table = table_name(Host), + ets:update_counter(Table, {send, Host, Type2, Dest}, 1). + +%% Only required for traffic stats +user_receive_packet_traffic(_JID, From, To, FixedPacket) -> + HostFrom = From#jid.lserver, + Host = To#jid.lserver, + {xmlelement, Type, _, _} = FixedPacket, + Type2 = case Type of + "iq" -> iq; + "message" -> message; + "presence" -> presence + end, + Dest = case is_host(HostFrom, Host) of + true -> in; + false -> out + end, + Table = table_name(Host), + ets:update_counter(Table, {recv, Host, Type2, Dest}, 1). + + +%%%================================== +%%%% get(* + +%%gett(Arg) -> get(node(), [Arg, title]). +getl(Args) -> get(node(), [Args]). +getl(Args, Host) -> get(node(), [Args, Host]). + +%%get(_Node, ["", title]) -> ""; + +get_statistic(N, A) -> + case catch get(N, A) of + {'EXIT', R} -> + ?ERROR_MSG("get_statistic error for N: ~p, A: ~p~n~p", [N, A, R]), + unknown; + Res -> Res + end. + +get(global, A) -> get(node(), A); + +get(_, [{"reductions", _}, title]) -> "Reductions (per minute)"; +get(_, [{"reductions", I}]) -> calc_avg(element(2, statistics(reductions)), I); %+++ + +get(_, ["cpu_avg1", title]) -> "Average system load (1 min)"; +get(N, ["cpu_avg1"]) -> rpc:call(N, cpu_sup, avg1, [])/256; +get(_, ["cpu_avg5", title]) -> "Average system load (5 min)"; +get(N, ["cpu_avg5"]) -> rpc:call(N, cpu_sup, avg1, [])/256; +get(_, ["cpu_avg15", title]) -> "Average system load (15 min)"; +get(N, ["cpu_avg15"]) -> rpc:call(N, cpu_sup, avg15, [])/256; +get(_, ["cpu_nprocs", title]) -> "Number of UNIX processes running on this machine"; +get(N, ["cpu_nprocs"]) -> rpc:call(N, cpu_sup, nprocs, []); +get(_, ["cpu_util", title]) -> "CPU utilization"; +get(N, ["cpu_util"]) -> rpc:call(N, cpu_sup, util, []); + +get(_, [{"cpu_util_user", _}, title]) -> "CPU utilization - user"; +get(_, [{"cpu_util_nice_user", _}, title]) -> "CPU utilization - nice_user"; +get(_, [{"cpu_util_kernel", _}, title]) -> "CPU utilization - kernel"; +get(_, [{"cpu_util_wait", _}, title]) -> "CPU utilization - wait"; +get(_, [{"cpu_util_idle", _}, title]) -> "CPU utilization - idle"; +get(_, [{"cpu_util_user", U}]) -> proplists:get_value(user, element(2, U), -1); +get(_, [{"cpu_util_nice_user", U}]) -> proplists:get_value(nice_user, element(2, U), -1); +get(_, [{"cpu_util_kernel", U}]) -> proplists:get_value(kernel, element(2, U), -1); +get(_, [{"cpu_util_wait", U}]) -> proplists:get_value(wait, element(3, U), -1); +get(_, [{"cpu_util_idle", U}]) -> proplists:get_value(idle, element(3, U), -1); + +get(_, [{"client", Id}, title]) -> atom_to_list(Id); +get(_, [{"client", Id}, Host]) -> + Table = table_name(Host), + case ets:lookup(Table, {client, Host, Id}) of + [{_, C}] -> C; + [] -> 0 + end; +get(_, ["client", title]) -> "Client"; +get(N, ["client", Host]) -> + lists:map( + fun(Id) -> + [Id_string] = io_lib:format("~p", [Id]), + {Id_string, get(N, [{"client", Id}, Host])} + end, + lists:usort(list_elem(clients, id)) + ); + +get(_, [{"os", Id}, title]) -> atom_to_list(Id); +get(_, [{"os", _Id}, list]) -> lists:usort(list_elem(oss, id)); +get(_, [{"os", Id}, Host]) -> [{_, C}] = ets:lookup(table_name(Host), {os, Host, Id}), C; +get(_, ["os", title]) -> "Operating System"; +get(N, ["os", Host]) -> + lists:map( + fun(Id) -> + [Id_string] = io_lib:format("~p", [Id]), + {Id_string, get(N, [{"os", Id}, Host])} + end, + lists:usort(list_elem(oss, id)) + ); + +get(_, [{"conntype", Id}, title]) -> atom_to_list(Id); +get(_, [{"conntype", _Id}, list]) -> lists:usort(list_elem(conntypes, id)); +get(_, [{"conntype", Id}, Host]) -> [{_, C}] = ets:lookup(table_name(Host), {conntype, Host, Id}), C; +get(_, ["conntype", title]) -> "Connection Type"; +get(N, ["conntype", Host]) -> + lists:map( + fun(Id) -> + [Id_string] = io_lib:format("~p", [Id]), + {Id_string, get(N, [{"conntype", Id}, Host])} + end, + lists:usort(list_elem(conntypes, id)) + ); + +get(_, [{"memsup_system", _}, title]) -> "Memory physical (bytes)"; +get(_, [{"memsup_system", M}]) -> proplists:get_value(system_total_memory, M, -1); +get(_, [{"memsup_free", _}, title]) -> "Memory free (bytes)"; +get(_, [{"memsup_free", M}]) -> proplists:get_value(free_memory, M, -1); + +get(_, [{"user_login", _}, title]) -> "Logins (per minute)"; +get(_, [{"user_login", I}, Host]) -> get_stat({user_login, Host}, I); +get(_, [{"user_logout", _}, title]) -> "Logouts (per minute)"; +get(_, [{"user_logout", I}, Host]) -> get_stat({user_logout, Host}, I); +get(_, [{"register_user", _}, title]) -> "Accounts registered (per minute)"; +get(_, [{"register_user", I}, Host]) -> get_stat({register_user, Host}, I); +get(_, [{"remove_user", _}, title]) -> "Accounts deleted (per minute)"; +get(_, [{"remove_user", I}, Host]) -> get_stat({remove_user, Host}, I); +get(_, [{Table, Type, Dest, _}, title]) -> filename:flatten([Table, Type, Dest]); +get(_, [{Table, Type, Dest, I}, Host]) -> get_stat({Table, Host, Type, Dest}, I); + +get(_, ["user_login", title]) -> "Logins"; +get(_, ["user_login", Host]) -> get_stat({user_login, Host}); +get(_, ["user_logout", title]) -> "Logouts"; +get(_, ["user_logout", Host]) -> get_stat({user_logout, Host}); +get(_, ["register_user", title]) -> "Accounts registered"; +get(_, ["register_user", Host]) -> get_stat({register_user, Host}); +get(_, ["remove_user", title]) -> "Accounts deleted"; +get(_, ["remove_user", Host]) -> get_stat({remove_user, Host}); +get(_, [{Table, Type, Dest}, title]) -> filename:flatten([Table, Type, Dest]); +get(_, [{Table, Type, Dest}, Host]) -> get_stat({Table, Host, Type, Dest}); + +get(_, ["localtime", title]) -> "Local time"; +get(N, ["localtime"]) -> + localtime_to_string(rpc:call(N, erlang, localtime, [])); + +get(_, ["memory_total", title]) -> "Memory total allocated: processes and system"; +get(N, ["memory_total"]) -> rpc:call(N, erlang, memory, [total]); +get(_, ["memory_processes", title]) -> "Memory allocated by Erlang processes"; +get(N, ["memory_processes"]) -> rpc:call(N, erlang, memory, [processes]); +get(_, ["memory_processes_used", title]) -> "Memory used by Erlang processes"; +get(N, ["memory_processes_used"]) -> rpc:call(N, erlang, memory, [processes_used]); +get(_, ["memory_system", title]) -> "Memory allocated by Erlang emulator but not associated to processes"; +get(N, ["memory_system"]) -> rpc:call(N, erlang, memory, [system]); +get(_, ["memory_atom", title]) -> "Memory allocated for atoms"; +get(N, ["memory_atom"]) -> rpc:call(N, erlang, memory, [atom]); +get(_, ["memory_atom_used", title]) -> "Memory used for atoms"; +get(N, ["memory_atom_used"]) -> rpc:call(N, erlang, memory, [atom_used]); +get(_, ["memory_binary", title]) -> "Memory allocated for binaries"; +get(N, ["memory_binary"]) -> rpc:call(N, erlang, memory, [binary]); +get(_, ["memory_code", title]) -> "Memory allocated for Erlang code"; +get(N, ["memory_code"]) -> rpc:call(N, erlang, memory, [code]); +get(_, ["memory_ets", title]) -> "Memory allocated for ETS tables"; +get(N, ["memory_ets"]) -> rpc:call(N, erlang, memory, [ets]); + +get(_, ["vhost", title]) -> "Virtual host"; +get(_, ["vhost", Host]) -> Host; + +get(_, ["ejabberdversion", title]) -> "ejabberd version"; +get(N, ["ejabberdversion"]) -> element(2, rpc:call(N, application, get_key, [ejabberd, vsn])); + +get(_, ["totalerlproc", title]) -> "Total Erlang processes running"; +get(N, ["totalerlproc"]) -> rpc:call(N, erlang, system_info, [process_count]); +get(_, ["operatingsystem", title]) -> "Operating System"; +get(N, ["operatingsystem"]) -> {rpc:call(N, os, type, []), rpc:call(N, os, version, [])}; +get(_, ["erlangmachine", title]) -> "Erlang machine"; +get(N, ["erlangmachine"]) -> rpc:call(N, erlang, system_info, [system_version]); +get(_, ["erlangmachinetarget", title]) -> "Erlang machine target"; +get(N, ["erlangmachinetarget"]) -> rpc:call(N, erlang, system_info, [system_architecture]); +get(_, ["maxprocallowed", title]) -> "Maximum processes allowed"; +get(N, ["maxprocallowed"]) -> rpc:call(N, erlang, system_info, [process_limit]); +get(_, ["procqueue", title]) -> "Number of processes on the running queue"; +get(N, ["procqueue"]) -> rpc:call(N, erlang, statistics, [run_queue]); +get(_, ["uptimehuman", title]) -> "Uptime"; +get(N, ["uptimehuman"]) -> + io_lib:format("~w days ~w hours ~w minutes ~p seconds", ms_to_time(get(N, ["uptime"]))); +get(_, ["lastrestart", title]) -> "Last restart"; +get(N, ["lastrestart"]) -> + Now = calendar:datetime_to_gregorian_seconds(rpc:call(N, erlang, localtime, [])), + UptimeMS = get(N, ["uptime"]), + Last_restartS = trunc(Now - (UptimeMS/1000)), + Last_restart = calendar:gregorian_seconds_to_datetime(Last_restartS), + localtime_to_string(Last_restart); + +get(_, ["plainusers", title]) -> "Plain users"; +get(_, ["plainusers"]) -> {R, _, _} = get_connectiontype(), R; +get(_, ["tlsusers", title]) -> "TLS users"; +get(_, ["tlsusers"]) -> {_, R, _} = get_connectiontype(), R; +get(_, ["sslusers", title]) -> "SSL users"; +get(_, ["sslusers"]) -> {_, _, R} = get_connectiontype(), R; +get(_, ["registeredusers", title]) -> "Registered users"; +get(N, ["registeredusers"]) -> rpc:call(N, mnesia, table_info, [passwd, size]); +get(_, ["registeredusers", Host]) -> length(ejabberd_auth:get_vh_registered_users(Host)); +get(_, ["onlineusers", title]) -> "Online users"; +get(N, ["onlineusers"]) -> rpc:call(N, mnesia, table_info, [session, size]); +get(_, ["onlineusers", Host]) -> length(ejabberd_sm:get_vh_session_list(Host)); +get(_, ["httppollusers", title]) -> "HTTP-Poll users (aprox)"; +get(N, ["httppollusers"]) -> rpc:call(N, mnesia, table_info, [http_poll, size]); +get(_, ["httpbindusers", title]) -> "HTTP-Bind users (aprox)"; +get(N, ["httpbindusers"]) -> rpc:call(N, mnesia, table_info, [http_bind, size]); + +get(_, ["s2sconnections", title]) -> "Outgoing S2S connections"; +get(_, ["s2sconnections"]) -> length(get_S2SConns()); +get(_, ["s2sconnections", Host]) -> get_s2sconnections(Host); +get(_, ["s2sservers", title]) -> "Outgoing S2S servers"; +get(_, ["s2sservers"]) -> length(lists:usort([element(2, C) || C <- get_S2SConns()])); + +get(_, ["offlinemsg", title]) -> "Offline messages"; +get(N, ["offlinemsg"]) -> rpc:call(N, mnesia, table_info, [offline_msg, size]); +get(_, ["offlinemsg", Host]) -> get_offlinemsg(Host); +get(_, ["totalrosteritems", title]) -> "Total roster items"; +get(N, ["totalrosteritems"]) -> rpc:call(N, mnesia, table_info, [roster, size]); +get(_, ["totalrosteritems", Host]) -> get_totalrosteritems(Host); + +get(_, ["meanitemsinroster", title]) -> "Mean items in roster"; +get(_, ["meanitemsinroster"]) -> get_meanitemsinroster(); +get(_, ["meanitemsinroster", Host]) -> get_meanitemsinroster(Host); + +get(_, ["totalmucrooms", title]) -> "Total MUC rooms"; +get(_, ["totalmucrooms"]) -> ets:info(muc_online_room, size); +get(_, ["totalmucrooms", Host]) -> get_totalmucrooms(Host); +get(_, ["permmucrooms", title]) -> "Permanent MUC rooms"; +get(N, ["permmucrooms"]) -> rpc:call(N, mnesia, table_info, [muc_room, size]); +get(_, ["permmucrooms", Host]) -> get_permmucrooms(Host); +get(_, ["regmucrooms", title]) -> "Users registered at MUC service"; +get(N, ["regmucrooms"]) -> rpc:call(N, mnesia, table_info, [muc_registered, size]); +get(_, ["regmucrooms", Host]) -> get_regmucrooms(Host); +get(_, ["regpubsubnodes", title]) -> "Registered nodes at Pub/Sub"; +get(N, ["regpubsubnodes"]) -> rpc:call(N, mnesia, table_info, [pubsub_node, size]); +get(_, ["vcards", title]) -> "Total vCards published"; +get(N, ["vcards"]) -> rpc:call(N, mnesia, table_info, [vcard, size]); +get(_, ["vcards", Host]) -> get_vcards(Host); + +%%get(_, ["ircconns", title]) -> "IRC connections"; +%%get(_, ["ircconns"]) -> ets:info(irc_connection, size); +%%get(_, ["ircconns", Host]) -> get_irccons(Host); % This seems to crash for some people +get(_, ["uptime", title]) -> "Uptime"; +get(N, ["uptime"]) -> element(1, rpc:call(N, erlang, statistics, [wall_clock])); +get(_, ["cputime", title]) -> "CPU Time"; +get(N, ["cputime"]) -> element(1, rpc:call(N, erlang, statistics, [runtime])); + +get(_, ["languages", title]) -> "Languages"; +get(_, ["languages", Server]) -> get_languages(Server); + +get(_, ["client_os", title]) -> "Client/OS"; +get(_, ["client_os", Server]) -> get_client_os(Server); + +get(_, ["client_conntype", title]) -> "Client/Connection Type"; +get(_, ["client_conntype", Server]) -> get_client_conntype(Server); + +get(N, A) -> + io:format(" ----- node: '~p', A: '~p'~n", [N, A]), + "666". + +%%%================================== +%%%% get_* + +get_S2SConns() -> ejabberd_s2s:dirty_get_connections(). + +get_tls_drv() -> + R = lists:filter( + fun(P) -> + case erlang:port_info(P, name) of + {name, "tls_drv"} -> true; + _ -> false + end + end, erlang:ports()), + length(R). + +get_connections(Port) -> + R = lists:filter( + fun(P) -> + case inet:port(P) of + {ok, Port} -> true; + _ -> false + end + end, erlang:ports()), + length(R). + +get_totalrosteritems(Host) -> + F = fun() -> + F2 = fun(R, {H, A}) -> + {_LUser, LServer, _LJID} = R#roster.usj, + A2 = case LServer of + H -> A+1; + _ -> A + end, + {H, A2} + end, + mnesia:foldl(F2, {Host, 0}, roster) + end, + {atomic, {Host, Res}} = mnesia:transaction(F), + Res. + +%% Copied from ejabberd_sm.erl +%%-record(session, {sid, usr, us, priority}). + +%%get_authusers(Host) -> +%% F = fun() -> +%% F2 = fun(R, {H, A}) -> +%% {_LUser, LServer, _LResource} = R#session.usr, +%% A2 = case LServer of +%% H -> A+1; +%% _ -> A +%% end, +%% {H, A2} +%% end, +%% mnesia:foldl(F2, {Host, 0}, session) +%% end, +%% {atomic, {Host, Res}} = mnesia:transaction(F), +%% Res. + +-record(offline_msg, {us, timestamp, expire, from, to, packet}). + +get_offlinemsg(Host) -> + F = fun() -> + F2 = fun(R, {H, A}) -> + {_LUser, LServer} = R#offline_msg.us, + A2 = case LServer of + H -> A+1; + _ -> A + end, + {H, A2} + end, + mnesia:foldl(F2, {Host, 0}, offline_msg) + end, + {atomic, {Host, Res}} = mnesia:transaction(F), + Res. + +-record(vcard, {us, vcard}). + +get_vcards(Host) -> + F = fun() -> + F2 = fun(R, {H, A}) -> + {_LUser, LServer} = R#vcard.us, + A2 = case LServer of + H -> A+1; + _ -> A + end, + {H, A2} + end, + mnesia:foldl(F2, {Host, 0}, vcard) + end, + {atomic, {Host, Res}} = mnesia:transaction(F), + Res. + +get_s2sconnections(Host) -> + F = fun() -> + F2 = fun(R, {H, A}) -> + {MyServer, _Server} = R#s2s.fromto, + A2 = case MyServer of + H -> A+1; + _ -> A + end, + {H, A2} + end, + mnesia:foldl(F2, {Host, 0}, s2s) + end, + {atomic, {Host, Res}} = mnesia:transaction(F), + Res. + +%%-record(irc_connection, {jid_server_host, pid}). + +%%get_irccons(Host) -> +%% F2 = fun(R, {H, A}) -> +%% {From, _Server, _Host} = R#irc_connection.jid_server_host, +%% A2 = case From#jid.lserver of +%% H -> A+1; +%% _ -> A +%% end, +%% {H, A2} +%% end, +%% {Host, Res} = ets:foldl(F2, {Host, 0}, irc_connection), +%% Res. + +is_host(Host, Subhost) -> + Pos = string:len(Host)-string:len(Subhost)+1, + case string:rstr(Host, Subhost) of + Pos -> true; + _ -> false + end. + +-record(muc_online_room, {name_host, pid}). + +get_totalmucrooms(Host) -> + F2 = fun(R, {H, A}) -> + {_Name, MUCHost} = R#muc_online_room.name_host, + A2 = case is_host(MUCHost, H) of + true -> A+1; + false -> A + end, + {H, A2} + end, + {Host, Res} = ets:foldl(F2, {Host, 0}, muc_online_room), + Res. + +-record(muc_room, {name_host, opts}). + +get_permmucrooms(Host) -> + F = fun() -> + F2 = fun(R, {H, A}) -> + {_Name, MUCHost} = R#muc_room.name_host, + A2 = case is_host(MUCHost, H) of + true -> A+1; + false -> A + end, + {H, A2} + end, + mnesia:foldl(F2, {Host, 0}, muc_room) + end, + {atomic, {Host, Res}} = mnesia:transaction(F), + Res. + +-record(muc_registered, {us_host, nick}). + +get_regmucrooms(Host) -> + F = fun() -> + F2 = fun(R, {H, A}) -> + {_User, MUCHost} = R#muc_registered.us_host, + A2 = case is_host(MUCHost, H) of + true -> A+1; + false -> A + end, + {H, A2} + end, + mnesia:foldl(F2, {Host, 0}, muc_registered) + end, + {atomic, {Host, Res}} = mnesia:transaction(F), + Res. + +get_stat(Stat) -> + Host = case Stat of + {_, H} -> H; + {_, H, _, _} -> H + end, + Table = table_name(Host), + Res = ets:lookup(Table, Stat), + [{_, C}] = Res, + C. + +get_stat(Stat, Ims) -> + Host = case Stat of + {_, H} -> H; + {_, H, _, _} -> H + end, + Table = table_name(Host), + Res = ets:lookup(Table, Stat), + ets:update_counter(Table, Stat, {2,1,0,0}), + [{_, C}] = Res, + calc_avg(C, Ims). +%%C. + +calc_avg(Count, TimeMS) -> + TimeMIN = TimeMS/(1000*60), + Count/TimeMIN. + +%%%================================== +%%%% utilities + +get_connectiontype() -> + C2 = get_connections(5222) -1, + C3 = get_connections(5223) -1, + NUplain = C2 + C3 - get_tls_drv(), + NUtls = C2 - NUplain, + NUssl = C3, + {NUplain, NUtls, NUssl}. + +ms_to_time(T) -> + DMS = 24*60*60*1000, + HMS = 60*60*1000, + MMS = 60*1000, + SMS = 1000, + D = trunc(T/DMS), + H = trunc((T - (D*DMS)) / HMS), + M = trunc((T - (D*DMS) - (H*HMS)) / MMS), + S = trunc((T - (D*DMS) - (H*HMS) - (M*MMS)) / SMS), + [D, H, M, S]. + + +%% Cuando un usuario conecta, pedirle iq:version a nombre de Host/mod_stats2file +user_login(U) -> + User = U#jid.luser, + Host = U#jid.lserver, + Resource = U#jid.lresource, + ets:update_counter(table_name("server"), {user_login, server}, 1), + ets:update_counter(table_name(Host), {user_login, Host}, 1), + request_iqversion(User, Host, Resource). + + +user_logout_sm(_, JID, _Data) -> + user_logout(JID#jid.luser, JID#jid.lserver, JID#jid.lresource, no_status). + +%% cuando un usuario desconecta, buscar en la tabla su JID/USR y quitarlo +user_logout(User, Host, Resource, _Status) -> + TableHost = table_name(Host), + TableServer = table_name("server"), + ets:update_counter(TableServer, {user_logout, server}, 1), + ets:update_counter(TableHost, {user_logout, Host}, 1), + + JID = jlib:make_jid(User, Host, Resource), + case ets:lookup(TableHost, {session, JID}) of + [{_, Client_id, OS_id, Lang, ConnType, _Client, _Version, _OS}] -> + ets:delete(TableHost, {session, JID}), + ets:update_counter(TableHost, {client, Host, Client_id}, -1), + ets:update_counter(TableServer, {client, server, Client_id}, -1), + ets:update_counter(TableHost, {os, Host, OS_id}, -1), + ets:update_counter(TableServer, {os, server, OS_id}, -1), + ets:update_counter(TableHost, {conntype, Host, ConnType}, -1), + ets:update_counter(TableServer, {conntype, server, ConnType}, -1), + update_counter_create(TableHost, {client_os, Host, Client_id, OS_id}, -1), + update_counter_create(TableServer, {client_os, server, Client_id, OS_id}, -1), + update_counter_create(TableHost, {client_conntype, Host, Client_id, ConnType}, -1), + update_counter_create(TableServer, {client_conntype, server, Client_id, ConnType}, -1), + update_counter_create(TableHost, {lang, Host, Lang}, -1), + update_counter_create(TableServer, {lang, server, Lang}, -1); + [] -> + ok + end. + +request_iqversion(User, Host, Resource) -> + From = jlib:make_jid("", Host, atom_to_list(?MODULE)), + FromStr = jlib:jid_to_string(From), + To = jlib:make_jid(User, Host, Resource), + ToStr = jlib:jid_to_string(To), + Packet = {xmlelement,"iq", + [{"from",FromStr}, {"to",ToStr}, {"type","get"}, + {"id", "statsdx" ++ randoms:get_string()}], + [{xmlelement, "query", + [{"xmlns","jabber:iq:version"}], []}]}, + ejabberd_local:route(From, To, Packet). + +%% cuando el virtualJID recibe una respuesta iqversion, +%% almacenar su JID/USR + client + OS en una tabla +received_response(From, _To, El) -> + try received_response(From, El) + catch + _:_ -> ok + end. +received_response(From, {xmlelement, "iq", Attrs, Elc}) -> + User = From#jid.luser, + Host = From#jid.lserver, + Resource = From#jid.lresource, + + "result" = xml:get_attr_s("type", Attrs), + Lang = case xml:get_attr_s("xml:lang", Attrs) of + [] -> "unknown"; + L -> L + end, + TableHost = table_name(Host), + TableServer = table_name("server"), + update_counter_create(TableHost, {lang, Host, Lang}, 1), + update_counter_create(TableServer, {lang, server, Lang}, 1), + + [El] = xml:remove_cdata(Elc), + {xmlelement, _, Attrs2, _Els2} = El, + ?NS_VERSION = xml:get_attr_s("xmlns", Attrs2), + + Client = get_tag_cdata_subtag(El, "name"), + Version = get_tag_cdata_subtag(El, "version"), + OS = get_tag_cdata_subtag(El, "os"), + {Client_id, OS_id} = identify(Client, OS), + + ConnType = get_connection_type(User, Host, Resource), + + TableHost = table_name(Host), + TableServer = table_name("server"), + ets:update_counter(TableHost, {client, Host, Client_id}, 1), + ets:update_counter(TableServer, {client, server, Client_id}, 1), + ets:update_counter(TableHost, {os, Host, OS_id}, 1), + ets:update_counter(TableServer, {os, server, OS_id}, 1), + ets:update_counter(TableHost, {conntype, Host, ConnType}, 1), + ets:update_counter(TableServer, {conntype, server, ConnType}, 1), + update_counter_create(TableHost, {client_os, Host, Client_id, OS_id}, 1), + update_counter_create(TableServer, {client_os, server, Client_id, OS_id}, 1), + update_counter_create(TableHost, {client_conntype, Host, Client_id, ConnType}, 1), + update_counter_create(TableServer, {client_conntype, server, Client_id, ConnType}, 1), + + JID = jlib:make_jid(User, Host, Resource), + ets:insert(TableHost, {{session, JID}, Client_id, OS_id, Lang, ConnType, Client, Version, OS}). + +get_connection_type(User, Host, Resource) -> + [_Node, {conn, Conn}, _IP] = ejabberd_sm:get_user_info(User, Host, Resource), + Conn. + +update_counter_create(Table, Element, C) -> + case ets:lookup(Table, Element) of + [] -> ets:insert(Table, {Element, 1}); + _ -> ets:update_counter(Table, Element, C) + end. + +get_tag_cdata_subtag(E, T) -> + E2 = xml:get_subtag(E, T), + case E2 of + false -> "unknown"; + _ -> xml:get_tag_cdata(E2) + end. + +list_elem(Type, id) -> + {_, Ids} = lists:unzip(list_elem(Type, full)), + Ids; +list_elem(clients, full) -> + [ + {"Pidgin", pidgin}, + {"pidgin", pidgin}, + {"gaim", gaim}, + {"Gajim", gajim}, + {"Tkabber", tkabber}, + {"Psi", psi}, + {"Adium", adium}, + {"Pandion", pandion}, + {"Telepathy Gabble", 'telepathy-gabble'}, + {"Swift", swift}, + {"Kopete", kopete}, + {"Exodus", exodus}, + {"libgaim", libgaim}, + {"JBother", jbother}, + {"iChat", ichat}, + {"Miranda", miranda}, + {"Trillian", trillian}, + {"QIP Infium", qipinfium}, + {"JAJC", jajc}, + {"Coccinella", coccinella}, + {"Gabber", gabber}, + {"BitlBee", bitlbee}, + {"jabber.el", jabberel}, + {"centerim", centerim}, + {"unknown", unknown} + ]; +list_elem(conntypes, full) -> + [ + {"c2s", c2s}, + {"c2s_tls", c2s_tls}, + {"c2s_compressed", c2s_compressed}, + {"http_poll", http_poll}, + {"http_bind", http_bind}, + {"unknown", unknown} + ]; +list_elem(oss, full) -> + [ + {"Linux", linux}, + {"Win", windows}, + {"Gentoo", linux}, + {"Mac", mac}, + {"BSD", bsd}, + {"SunOS", linux}, + {"Debian", linux}, + {"Ubuntu", linux}, + {"unknown", unknown} + ]. + +identify(Client, OS) -> + Res = {try_match(Client, list_elem(clients, full)), try_match(OS, list_elem(oss, full))}, + case Res of + {libgaim, mac} -> {adium, mac}; + {adium, unknown} -> {adium, mac}; + {qipinfium, unknown} -> {qipinfium, windows}; + _ -> Res + end. + +try_match(_E, []) -> unknown; +try_match(E, [{Str, Id} | L]) -> + case string:str(E, Str) of + 0 -> try_match(E, L); + _ -> Id + end. + +get_client_os(Server) -> + CO1 = ets:match(table_name(Server), {{client_os, Server, '$1', '$2'}, '$3'}), + CO2 = lists:map( + fun([Cl, Os, A3]) -> + {lists:flatten([atom_to_list(Cl), "/", atom_to_list(Os)]), A3} + end, + CO1 + ), + lists:keysort(1, CO2). + +get_client_conntype(Server) -> + CO1 = ets:match(table_name(Server), {{client_conntype, Server, '$1', '$2'}, '$3'}), + CO2 = lists:map( + fun([Cl, Os, A3]) -> + {lists:flatten([atom_to_list(Cl), "/", atom_to_list(Os)]), A3} + end, + CO1 + ), + lists:keysort(1, CO2). + +get_languages(Server) -> + L1 = ets:match(table_name(Server), {{lang, Server, '$1'}, '$2'}), + L2 = lists:map( + fun([La, C]) -> + {La, C} + end, + L1 + ), + lists:keysort(1, L2). + +get_meanitemsinroster() -> + get_meanitemsinroster2(getl("totalrosteritems"), getl("registeredusers")). +get_meanitemsinroster(Host) -> + get_meanitemsinroster2(getl("totalrosteritems", Host), getl("registeredusers", Host)). +get_meanitemsinroster2(Items, Users) -> + case Users of + 0 -> 0; + _ -> Items/Users + end. + +localtime_to_string({{Y, Mo, D},{H, Mi, S}}) -> + lists:concat([H, ":", Mi, ":", S, " ", D, "/", Mo, "/", Y]). + +%% cuando toque mostrar estadisticas +%%get_iqversion() -> +%% contar en la tabla cuantos tienen cliente: *psi* +%%buscar en la tabla iqversion +%%ok. + + +%%%================================== +%%%% Web Admin Menu + +web_menu_main(Acc, Lang) -> + Acc ++ [{"statsdx", ?T("Statistics Dx")}]. + +web_menu_node(Acc, _Node, Lang) -> + Acc ++ [{"statsdx", ?T("Statistics Dx")}]. + +web_menu_host(Acc, _Host, Lang) -> + Acc ++ [{"statsdx", ?T("Statistics Dx")}]. + +%%%================================== +%%%% Web Admin Page + +web_page_main(_, #request{path=["statsdx"], lang = Lang} = _Request) -> + Res = [?XC("h1", ?T("Statistics")++" Dx"), + ?XC("h3", "Accounts"), + ?XAE("table", [], + [?XE("tbody", [ + do_stat(global, Lang, "registeredusers") + ]) + ]), + ?XC("h3", "Roster"), + ?XAE("table", [], + [?XE("tbody", [ + do_stat(global, Lang, "totalrosteritems"), + do_stat(global, Lang, "meanitemsinroster"), + ?XE("tr", + [?XE("td", [?CT("Top rosters")]), + ?XE("td", [ + ?ACT("top/roster/30", "30"), ?C(", "), + ?ACT("top/roster/100", "100"), ?C(", "), + ?ACT("top/roster/500", "500") ])] + ) + ]) + ]), + ?XC("h3", "Users"), + ?XAE("table", [], + [?XE("tbody", [ + do_stat(global, Lang, "onlineusers"), + do_stat(global, Lang, "offlinemsg"), + ?XE("tr", + [?XE("td", [?CT("Top offline message queues") ]), + ?XE("td", [ + ?ACT("top/offlinemsg/30", "30"), ?C(", "), + ?ACT("top/offlinemsg/100", "100"), ?C(", "), + ?ACT("top/offlinemsg/500", "500") ])] + ), + do_stat(global, Lang, "vcards"), + ?XE("tr", + [?XE("td", [?CT("Top vCard sizes") ]), + ?XE("td", [ + ?ACT("top/vcard/5", "5"), ?C(", "), + ?ACT("top/vcard/30", "30"), ?C(", "), + ?ACT("top/vcard/100", "100"), ?C(", "), + ?ACT("top/vcard/500", "500") ])] + ) + ]) + ]), + ?XC("h3", "MUC"), + ?XAE("table", [], + [?XE("tbody", [ + do_stat(global, Lang, "totalmucrooms"), + do_stat(global, Lang, "permmucrooms"), + do_stat(global, Lang, "regmucrooms") + ]) + ]), + ?XC("h3", "Pub/Sub"), + ?XAE("table", [], + [?XE("tbody", [ + do_stat(global, Lang, "regpubsubnodes") + ]) + ]), + %%?XC("h3", "IRC"), + %%?XAE("table", [], + %% [?XE("tbody", [ + %% do_stat(global, Lang, "ircconns") + %% ]) + %%]), + %%?XC("h3", "Ratios"), + %%?XAE("table", [], + %% [?XE("tbody", [ + %% ]) + %% ]), + ?XC("h3", "Sessions: " ++ get_stat_n("client")), + ?XAE("table", [], + [?XE("tbody", + do_stat_table(global, Lang, "client", server) + ) + ]), + ?XC("h3", "Sessions: " ++ get_stat_n("os")), + ?XAE("table", [], + [?XE("tbody", + do_stat_table(global, Lang, "os", server) + ) + ]), + ?XC("h3", "Sessions: " ++ get_stat_n("client") ++ "/" ++ get_stat_n("os")), + ?XAE("table", [], + [?XE("tbody", + do_stat_table(global, Lang, "client_os", server) + ) + ]), + ?XC("h3", "Sessions: " ++ get_stat_n("conntype")), + ?XAE("table", [], + [?XE("tbody", + do_stat_table(global, Lang, "conntype", server) + ) + ]), + ?XC("h3", "Sessions: " ++ get_stat_n("client") ++ "/" ++ get_stat_n("conntype")), + ?XAE("table", [], + [?XE("tbody", + do_stat_table(global, Lang, "client_conntype", server) + ) + ]), + ?XC("h3", "Sessions: " ++ get_stat_n("languages")), + ?XAE("table", [], + [?XE("tbody", + do_stat_table(global, Lang, "languages", server) + ) + ]) + ], + {stop, Res}; +web_page_main(_, #request{path=["statsdx", "top", Topic, Topnumber], q = _Q, lang = Lang} = _Request) -> + Res = [?XC("h1", ?T("Statistics")++" Dx"), + case Topic of + "offlinemsg" -> ?XCT("h2", "Top offline message queues"); + "vcard" -> ?XCT("h2", "Top vCard sizes"); + "roster" -> ?XCT("h2", "Top rosters") + end, + ?XE("table", + [?XE("thead", [?XE("tr", + [?XE("td", [?CT("#")]), + ?XE("td", [?CT("Jabber ID")]), + ?XE("td", [?CT("Value")])] + )]), + ?XE("tbody", do_top_table(global, Lang, Topic, Topnumber, server)) + ]) + ], + {stop, Res}; +web_page_main(_, #request{path=["statsdx" | FilterURL], q = Q, lang = Lang} = _Request) -> + Filter = parse_url_filter(FilterURL), + Sort_query = get_sort_query(Q), + Res = [?XC("h1", ?T("Statistics")++" Dx"), + ?XC("h2", "Sessions with: "++ io_lib:format("~p", [Filter])), + ?XE("table", + [ + ?XE("thead", [?XE("tr", make_sessions_table_tr(Lang) )]), + ?XE("tbody", do_sessions_table(global, Lang, Filter, Sort_query, server)) + ]) + ], + {stop, Res}; +web_page_main(Acc, _) -> Acc. + +do_top_table(_Node, Lang, Topic, TopnumberString, Host) -> + List = get_top_users(Host, list_to_integer(TopnumberString), Topic), + %% get_top_users(Topnumber, "roster") + {List2, _} = lists:mapfoldl( + fun({Value, User, Server}, Counter) -> + UserJID = User++"@"++Server, + UserJIDUrl = "/admin/server/" ++ Server ++ "/user/" ++ User ++ "/", + ValueString = integer_to_list(Value), + ValueEl = case Topic of + "offlinemsg" -> {url, UserJIDUrl++"queue/", ValueString}; + "vcard" -> {url, UserJIDUrl++"vcard/", ValueString}; + "roster" -> {url, UserJIDUrl++"roster/", ValueString}; + _ -> ValueString + end, + {do_table_element(Counter, Lang, UserJID, {fixed_url, UserJIDUrl}, ValueEl), + Counter+1} + end, + 1, + List + ), + List2. + +%% Code copied from mod_muc_admin.erl +%% Returns: {normal | reverse, Integer} +get_sort_query(Q) -> + case catch get_sort_query2(Q) of + {ok, Res} -> Res; + _ -> {normal, 1} + end. +get_sort_query2(Q) -> + {value, {_, String}} = lists:keysearch("sort", 1, Q), + Integer = list_to_integer(String), + case Integer >= 0 of + true -> {ok, {normal, Integer}}; + false -> {ok, {reverse, abs(Integer)}} + end. +make_sessions_table_tr(Lang) -> + Titles = ["Jabber ID", + "Client ID", + "OS ID", + "Lang", + "Connection", + "Client", + "Version", + "OS"], + {Titles_TR, _} = + lists:mapfoldl( + fun(Title, Num_column) -> + NCS = integer_to_list(Num_column), + TD = ?XE("td", [?CT(Title), + ?BR, + ?ACT("?sort="++NCS, "<"), + ?C(" "), + ?ACT("?sort=-"++NCS, ">")]), + {TD, Num_column+1} + end, + 1, + Titles), + Titles_TR. + +%% @spec (Filter::string()) -> [{Class::string(), Type::string()}] +parse_url_filter(FilterURL) -> + [List] = string:tokens(FilterURL, "/"), + parse_url_filter(List, []). +parse_url_filter([Class, Type | List], Res) -> + parse_url_filter(List, Res ++ [{Class, Type}]); +parse_url_filter(_, Res) -> + Res. + + +web_page_node(_, Node, ["statsdx"], _Query, Lang) -> + TransactionsCommited = + rpc:call(Node, mnesia, system_info, [transaction_commits]), + TransactionsAborted = + rpc:call(Node, mnesia, system_info, [transaction_failures]), + TransactionsRestarted = + rpc:call(Node, mnesia, system_info, [transaction_restarts]), + TransactionsLogged = + rpc:call(Node, mnesia, system_info, [transaction_log_writes]), + + Res = + [?XC("h1", io_lib:format(?T("~p statistics"), [Node])), + ?XC("h3", "Connections"), + ?XAE("table", [], + [?XE("tbody", [ + do_stat(global, Lang, "onlineusers"), + do_stat(Node, Lang, "httppollusers"), + do_stat(Node, Lang, "httpbindusers"), + do_stat(Node, Lang, "s2sconnections"), + do_stat(Node, Lang, "s2sservers") + ]) + ]), + ?XC("h3", "ejabberd"), + ?XAE("table", [], + [?XE("tbody", [ + do_stat(Node, Lang, "ejabberdversion") + ]) + ]), + ?XC("h3", "Erlang"), + ?XAE("table", [], + [?XE("tbody", [ + do_stat(Node, Lang, "operatingsystem"), + do_stat(Node, Lang, "erlangmachine"), + do_stat(Node, Lang, "erlangmachinetarget"), + do_stat(Node, Lang, "maxprocallowed"), + do_stat(Node, Lang, "procqueue"), + do_stat(Node, Lang, "totalerlproc") + ]) + ]), + ?XC("h3", "Times"), + ?XAE("table", [], + [?XE("tbody", [ + do_stat(Node, Lang, "uptime"), + do_stat(Node, Lang, "uptimehuman"), + do_stat(Node, Lang, "lastrestart"), + do_stat(Node, Lang, "cputime") + ]) + ]), + ?XC("h3", "CPU"), + ?XAE("table", [], + [?XE("tbody", [ + do_stat(Node, Lang, "cpu_avg1"), + do_stat(Node, Lang, "cpu_avg5"), + do_stat(Node, Lang, "cpu_avg15"), + do_stat(Node, Lang, "cpu_nprocs")%, + %%do_stat(Node, Lang, "cpu_util_user"), + %%do_stat(Node, Lang, "cpu_nice_user"), + %%do_stat(Node, Lang, "cpu_kernel"), + %%do_stat(Node, Lang, "cpu_idle"), + %%do_stat(Node, Lang, "cpu_wait") + ]) + ]), + %%?XC("h3", "RAM"), + %%?XAE("table", [], + %% [?XE("tbody", [ + %% do_stat(Node, Lang, "memsup_system"), + %% do_stat(Node, Lang, "memsup_free"), + %% do_stat(Node, Lang, "reductions") + %%]) + %%]), + ?XC("h3", "Memory (bytes)"), + ?XAE("table", [], + [?XE("tbody", [ + do_stat(Node, Lang, "memory_total"), + do_stat(Node, Lang, "memory_processes"), + do_stat(Node, Lang, "memory_processes_used"), + do_stat(Node, Lang, "memory_system"), + do_stat(Node, Lang, "memory_atom"), + do_stat(Node, Lang, "memory_atom_used"), + do_stat(Node, Lang, "memory_binary"), + do_stat(Node, Lang, "memory_code"), + do_stat(Node, Lang, "memory_ets") + ]) + ]), + ?XC("h3", "Database"), + ?XAE("table", [], + [?XE("tbody", + [ + ?XE("tr", [?XCT("td", "Transactions commited"), + ?XAC("td", [{"class", "alignright"}], + integer_to_list(TransactionsCommited))]), + ?XE("tr", [?XCT("td", "Transactions aborted"), + ?XAC("td", [{"class", "alignright"}], + integer_to_list(TransactionsAborted))]), + ?XE("tr", [?XCT("td", "Transactions restarted"), + ?XAC("td", [{"class", "alignright"}], + integer_to_list(TransactionsRestarted))]), + ?XE("tr", [?XCT("td", "Transactions logged"), + ?XAC("td", [{"class", "alignright"}], + integer_to_list(TransactionsLogged))]) + ]) + ])], + {stop, Res}; +web_page_node(Acc, _, _, _, _) -> Acc. + +web_page_host(_, Host, + #request{path = ["statsdx"], + lang = Lang} = _Request) -> + Res = [?XC("h1", ?T("Statistics")++" Dx"), + ?XC("h2", Host), + ?XC("h3", "Accounts"), + ?XAE("table", [], + [?XE("tbody", [ + do_stat(global, Lang, "registeredusers", Host) + ]) + ]), + ?XC("h3", "Roster"), + ?XAE("table", [], + [?XE("tbody", [ + do_stat(global, Lang, "totalrosteritems", Host), + %%get_meanitemsinroster2(TotalRosterItems, RegisteredUsers) + ?XE("tr", + [?XE("td", [?C("Top rosters") ]), + ?XE("td", [ + ?ACT("top/roster/30", "30"), ?C(", "), + ?ACT("top/roster/100", "100"), ?C(", "), + ?ACT("top/roster/500", "500") ])] + ) + ]) + ]), + ?XC("h3", "Users"), + ?XAE("table", [], + [?XE("tbody", [ + do_stat(global, Lang, "onlineusers", Host), + %%do_stat(global, Lang, "offlinemsg", Host), %% This make take a lot of time + %%do_stat(global, Lang, "vcards", Host) %% This make take a lot of time + ?XE("tr", + [?XE("td", [?C("Top offline message queues")]), + ?XE("td", [ + ?ACT("top/offlinemsg/30", "30"), ?C(", "), + ?ACT("top/offlinemsg/100", "100"), ?C(", "), + ?ACT("top/offlinemsg/500", "500") ])] + ), + ?XE("tr", + [?XE("td", [?C("Top vCard sizes") ]), + ?XE("td", [ + ?ACT("top/vcard/5", "5"), ?C(", "), + ?ACT("top/vcard/30", "30"), ?C(", "), + ?ACT("top/vcard/100", "100"), ?C(", "), + ?ACT("top/vcard/500", "500") ])] + ) + ]) + ]), + ?XC("h3", "Connections"), + ?XAE("table", [], + [?XE("tbody", [ + do_stat(global, Lang, "s2sconnections", Host) + ]) + ]), + ?XC("h3", "MUC"), + ?XAE("table", [], + [?XE("tbody", [ + do_stat(global, Lang, "totalmucrooms", Host), + do_stat(global, Lang, "permmucrooms", Host), + do_stat(global, Lang, "regmucrooms", Host) + ]) + ]), + %%?XC("h3", "IRC"), + %%?XAE("table", [], + %% [?XE("tbody", [ + %% do_stat(global, Lang, "ircconns", Host) + %% ]) + %%]), + %%?XC("h3", "Pub/Sub"), + %%?XAE("table", [], + %% [?XE("tbody", [ + %% do_stat(global, Lang, "regpubsubnodes", Host) + %% ]) + %%]), + ?XC("h3", "Sessions: " ++ get_stat_n("client")), + ?XAE("table", [], + [?XE("tbody", + do_stat_table(global, Lang, "client", Host) + ) + ]), + ?XC("h3", "Sessions: " ++ get_stat_n("os")), + ?XAE("table", [], + [?XE("tbody", + do_stat_table(global, Lang, "os", Host) + ) + ]), + ?XC("h3", "Sessions: " ++ get_stat_n("client") ++ "/" ++ get_stat_n("os")), + ?XAE("table", [], + [?XE("tbody", + do_stat_table(global, Lang, "client_os", Host) + ) + ]), + ?XC("h3", "Sessions: " ++ get_stat_n("conntype")), + ?XAE("table", [], + [?XE("tbody", + do_stat_table(global, Lang, "conntype", Host) + ) + ]), + ?XC("h3", "Sessions: " ++ get_stat_n("client") ++ "/" ++ get_stat_n("conntype")), + ?XAE("table", [], + [?XE("tbody", + do_stat_table(global, Lang, "client_conntype", Host) + ) + ]), + ?XC("h3", "Sessions: " ++ get_stat_n("languages")), + ?XAE("table", [], + [?XE("tbody", + do_stat_table(global, Lang, "languages", Host) + ) + ]), + ?XC("h3", "Ratios"), + ?XAE("table", [], + [?XE("tbody", [ + do_stat(global, Lang, "user_login", Host), + do_stat(global, Lang, "user_logout", Host), + do_stat(global, Lang, "register_user", Host), + do_stat(global, Lang, "remove_user", Host), + do_stat(global, Lang, {send, iq, in}, Host), + do_stat(global, Lang, {send, iq, out}, Host), + do_stat(global, Lang, {send, message, in}, Host), + do_stat(global, Lang, {send, message, out}, Host), + do_stat(global, Lang, {send, presence, in}, Host), + do_stat(global, Lang, {send, presence, out}, Host), + do_stat(global, Lang, {recv, iq, in}, Host), + do_stat(global, Lang, {recv, iq, out}, Host), + do_stat(global, Lang, {recv, message, in}, Host), + do_stat(global, Lang, {recv, message, out}, Host), + do_stat(global, Lang, {recv, presence, in}, Host), + do_stat(global, Lang, {recv, presence, out}, Host) + ]) + ]) + ], + {stop, Res}; +web_page_host(_, Host, #request{path=["statsdx", "top", Topic, Topnumber], q = _Q, lang = Lang} = _Request) -> + Res = [?XC("h1", ?T("Statistics")++" Dx"), + case Topic of + "offlinemsg" -> ?XCT("h2", "Top offline message queues"); + "vcard" -> ?XCT("h2", "Top vCard sizes"); + "roster" -> ?XCT("h2", "Top rosters") + end, + ?XE("table", + [?XE("thead", [?XE("tr", + [?XE("td", [?CT("#")]), + ?XE("td", [?CT("Jabber ID")]), + ?XE("td", [?CT("Value")])] + )]), + ?XE("tbody", do_top_table(global, Lang, Topic, Topnumber, Host)) + ]) + ], + {stop, Res}; +web_page_host(_, Host, #request{path=["statsdx" | FilterURL], q = Q, + lang = Lang} = _Request) -> + Filter = parse_url_filter(FilterURL), + Sort_query = get_sort_query(Q), + Res = [?XC("h1", ?T("Statistics")++" Dx"), + ?XC("h2", "Sessions with: "++ io_lib:format("~p", [Filter])), + ?XAE("table", [], + [?XE("tbody", + do_sessions_table(global, Lang, Filter, Sort_query, Host) + ) + ]) + ], + {stop, Res}; +web_page_host(Acc, _, _) -> Acc. + + +%%%================================== +%%%% Web Admin Utils + +do_table_element(Lang, L, StatLink, N) -> + do_table_element(no_counter, Lang, L, StatLink, N). +do_table_element(Counter, Lang, L, StatLink, N) -> + ?XE("tr", [ + case Counter of + no_counter -> ?C(""); + _ -> ?XE("td", [?C(integer_to_list(Counter))]) + end, + case StatLink of + no_link -> ?XCT("td", L); + {fixed_url, Fixedurl} -> ?XE("td", [?AC(Fixedurl, L)]); + _ -> ?XE("td", [?AC(make_url(StatLink, L), L)]) + end, + case N of + {url, NUrl, NName} -> ?XAE("td", [{"class", "alignright"}], [?AC(NUrl, NName)]); + _ -> ?XAC("td", [{"class", "alignright"}], N) + end + ]). + +make_url(StatLink, L) -> + List = case string:tokens(StatLink, "_") of + [Stat] -> + [Stat, L]; + [Stat1, Stat2] -> + [L1, L2] = string:tokens(L, "/"), + [Stat1, L1, Stat2, L2] + end, + string:join(List, "/"). + +do_stat_table(global, Lang, Stat, Host) -> + Os = mod_statsdx:get_statistic(global, [Stat, Host]), + lists:map( + fun({L, N}) -> + do_table_element(Lang, L, Stat, io_lib:format("~p", [N])) + end, + lists:reverse(lists:keysort(2, Os)) + ). + +do_sessions_table(_Node, _Lang, Filter, {Sort_direction, Sort_column}, Host) -> + Sessions = get_sessions_filtered(Filter, Host), + SessionsSorted = sort_sessions(Sort_direction, Sort_column, Sessions), + lists:map( + fun( {{session, JID}, Client_id, OS_id, Lang, ConnType, Client, Version, OS} ) -> + User = JID#jid.luser, + Server = JID#jid.lserver, + UserURL = "/admin/server/" ++ Server ++ "/user/" ++ User ++ "/", + ?XE("tr", [ + ?XE("td", [?AC(UserURL, jlib:jid_to_string(JID))]), + ?XCT("td", atom_to_list(Client_id)), + ?XCT("td", atom_to_list(OS_id)), + ?XCT("td", Lang), + ?XCT("td", atom_to_list(ConnType)), + ?XCT("td", Client), + ?XCT("td", Version), + ?XCT("td", OS) + ]) + end, + SessionsSorted + ). + +%% Code copied from mod_muc_admin.erl +sort_sessions(Direction, Column, Rooms) -> + Rooms2 = lists:keysort(Column, Rooms), + case Direction of + normal -> Rooms2; + reverse -> lists:reverse(Rooms2) + end. + +get_sessions_filtered(Filter, server) -> + lists:foldl( + fun(Host, Res) -> + try get_sessions_filtered(Filter, Host) of + List when is_list(List) -> List ++ Res + catch + _:_ -> Res + end + end, + [], + ?MYHOSTS); +get_sessions_filtered(Filter, Host) -> + Match = case Filter of + [{"client", Client}] -> {{session, '$1'}, list_to_atom(Client), '$2', '$3', '$4', '$5', '$6', '$7'}; + [{"os", OS}] -> {{session, '$1'}, '$2', list_to_atom(OS), '$3', '$4', '$5', '$6', '$7'}; + [{"conntype", ConnType}] -> {{session, '$1'}, '$2', '$3', '$4', list_to_atom(ConnType), '$5', '$6', '$7'}; + [{"languages", Lang}] -> {{session, '$1'}, '$2', '$3', Lang, '$4', '$5', '$6', '$7'}; + [{"client", Client}, {"os", OS}] -> {{session, '$1'}, list_to_atom(Client), list_to_atom(OS), '$3', '$4', '$5', '$6', '$7'}; + [{"client", Client}, {"conntype", ConnType}] -> {{session, '$1'}, list_to_atom(Client), '$2', '$3', list_to_atom(ConnType), '$5', '$6', '$7'}; + _ -> {{session, '$1'}, '$2', '$3', '$4', '$5'} + end, + ets:match_object(table_name(Host), Match). + +do_stat(Node, Lang, Stat) -> + ?XE("tr", [ + ?XCT("td", get_stat_n(Stat)), + ?XAC("td", [{"class", "alignright"}], + get_stat_v(Node, [Stat]))]). + +do_stat(Node, Lang, Stat, Host) -> + %%[Res] = get_stat_v(Node, [Stat, Host]), + %%do_table_element(Lang, get_stat_n(Stat), Res). + do_table_element(Lang, get_stat_n(Stat), no_link, get_stat_v(Node, [Stat, Host])). + +%% Get a stat name +get_stat_n(Stat) -> + mod_statsdx:get_statistic(foo, [Stat, title]). +%% Get a stat value +get_stat_v(Node, Stat) -> get_stat_v2(mod_statsdx:get_statistic(Node, Stat)). +get_stat_v2(Value) when is_list(Value) -> Value; +get_stat_v2(Value) when is_float(Value) -> io_lib:format("~.4f", [Value]); +get_stat_v2(Value) when is_integer(Value) -> + [Str] = io_lib:format("~p", [Value]), + pretty_string_int(Str); +get_stat_v2(Value) -> io_lib:format("~p", [Value]). + +%% Transform "1234567890" into "1,234,567,890" +pretty_string_int(String) -> + {_, Result} = lists:foldl( + fun(NewNumber, {3, Result}) -> + {1, [NewNumber, $, | Result]}; + (NewNumber, {CountAcc, Result}) -> + {CountAcc+1, [NewNumber | Result]} + end, + {0, ""}, + lists:reverse(String)), + Result. + +%%%================================== +%%%% Commands + +commands() -> + [ + #ejabberd_commands{name = get_top_users, tags = [stats], + desc = "Get top X users with larger offlinemsg, vcard or roster.", + module = ?MODULE, function = get_top_users, + args = [{topnumber, integer}, {topic, string}], + result = {top, {list, + {user, {tuple, [ + {value, integer}, + {user, string}, + {server, string} + ]}} + }}}, + #ejabberd_commands{name = getstatsdx, tags = [stats], + desc = "Get statistical value.", + module = ?MODULE, function = getstatsdx, + args = [{name, string}], + result = {stat, integer}}, + #ejabberd_commands{name = getstatsdx_host, tags = [stats], + desc = "Get statistical value for this host.", + module = ?MODULE, function = getstatsdx, + args = [{name, string}, {host, string}], + result = {stat, integer}} + ]. + +getstatsdx(Name) -> + get_statistic(global, [Name]). + +getstatsdx(Name, Host) -> + get_statistic(global, [Name, Host]). + +get_top_users(Number, Topic) -> + get_top_users(server, Number, Topic). + +%% Returns: [{Integer, User, Server}] +get_top_users(Host, Number, "vcard") -> + get_top_users_vcard(Host, Number); +get_top_users(Host, Number, "offlinemsg") -> + get_top_users(Host, Number, offline_msg, #offline_msg.us); +get_top_users(Host, Number, "roster") -> + get_top_users(Host, Number, roster, #roster.us). + + +get_top_users(Host, TopX, Table, RecordUserPos) -> + F = fun() -> + F2 = fun(R, {H, Dict}) -> + {LUser, LServer} = element(RecordUserPos, R), + case H of + server -> + {Host, dict:update_counter({LUser, LServer}, 1, Dict)}; + LServer -> + {Host, dict:update_counter({LUser, LServer}, 1, Dict)}; + _ -> + {Host, Dict} + end + end, + mnesia:foldl(F2, {Host, dict:new()}, Table) + end, + {atomic, {Host, DictRes}} = mnesia:transaction(F), + {_, _, Result} = dict:fold( + fun({User, Server}, Num, {EntryNumber, Size, TopList}) -> + case {Num > EntryNumber, Size < TopX} of + {false, true} -> + {Num, Size+1, lists:keymerge(1, TopList, [{Num, User, Server}])}; + {true, true} -> + {EntryNumber, Size+1, lists:keymerge(1, TopList, [{Num, User, Server}])}; + {true, false} -> + [{NewEntryNumber, _, _} | _] = TopList2 = lists:keydelete(EntryNumber, 1, TopList), + {NewEntryNumber, Size, lists:keymerge(1, TopList2, [{Num, User, Server}])}; + {false, false} -> + {EntryNumber, Size, TopList} + end + end, + {10000000000000000, 0, []}, + DictRes), + lists:reverse(Result). + +get_top_users_vcard(Host, Number) -> + F = fun() -> + B = fun get_users_vcard_fun/2, + {_Host, _NumSelects, _MinSize, _Sizes, Selects} = mnesia:foldl(B, {Host, Number, -1, [], []}, vcard), %+++ + Selects + end, + {atomic, Result} = mnesia:transaction(F), + lists:reverse(Result). + +%% Selects = [{Size, Vcard}] sorted from smaller to larger +get_users_vcard_fun(#vcard{us = {_, Host1}}, {HostReq, NumRemaining, MinSize, Sizes, Selects}) + when (Host1 /= HostReq) and (HostReq /= server) -> + {HostReq, NumRemaining, MinSize, Sizes, Selects}; +get_users_vcard_fun(Vcard, {HostReq, NumRemaining, MinSize, Sizes, Selects}) -> + String = lists:flatten(xml:element_to_string(Vcard#vcard.vcard)), + Size = length(String), + case {Size > MinSize, NumRemaining > 0} of + {true, true} -> + {User, Host} = Vcard#vcard.us, + Selects2 = lists:umerge(Selects, [{Size, User, Host}]), + Sizes2 = lists:umerge(Sizes, [Size]), + MinSize2 = lists:min(Sizes2), + {HostReq, NumRemaining-1, MinSize2, Sizes2, Selects2}; + {true, false} -> + [_ | SelectsReduced] = Selects, + [_ | SizesReduced] = Sizes, + Sizes2 = lists:umerge(SizesReduced, [Size]), + MinSize2 = lists:min(Sizes2), + {User, Host} = Vcard#vcard.us, + Selects2 = lists:umerge(SelectsReduced, [{Size, User, Host}]), + {HostReq, NumRemaining, MinSize2, Sizes2, Selects2}; + {false, _} -> + {HostReq, NumRemaining, MinSize, Sizes, Selects} + end. + + +%%%================================== + +%%% vim: set foldmethod=marker foldmarker=%%%%,%%%=: diff --git a/mod_webpresence/AUTHORS b/mod_webpresence/AUTHORS new file mode 100644 index 0000000..6ec6f81 --- /dev/null +++ b/mod_webpresence/AUTHORS @@ -0,0 +1,46 @@ + + CODE + ---- + +Igor Goryachev: author of the original mod_presence + +Badlop: added WebAdmin features + +Tobias Markmann: example PHP code + + + PIXMAPS + ------- + +This module includes pixmap artwork from many different authors. +The list of known authors in alphabetical order is shown below. +Your help to complete this information is appreciated. + +Everaldo Coelho: crystal + +Jason Kim: stellar + +NoAlWin: jsf-jabber-text, jsf-text and jsf-text2 + +Unknown author: +alphamod +amibulb +amiglobe +dcraven +dudes +frickenhuge +gabber +gnome +goojim +gossip +gota +gtalk +invision +jabberbulb +msn_amicons +nuvola +phpbb +simplebulb +star_amicons +sun +zyx diff --git a/mod_webpresence/COPYING b/mod_webpresence/COPYING new file mode 100644 index 0000000..8b325f5 --- /dev/null +++ b/mod_webpresence/COPYING @@ -0,0 +1,343 @@ +As a special exception, the authors give permission to link this program +with the OpenSSL library and distribute the resulting binary. + + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Library General +Public License instead of this License. diff --git a/mod_webpresence/ChangeLog b/mod_webpresence/ChangeLog new file mode 100644 index 0000000..b2f09f4 --- /dev/null +++ b/mod_webpresence/ChangeLog @@ -0,0 +1,169 @@ +2009-11-16 Badlop + + * src/mod_webpresence.erl: New option baseurl, allows to specify + the exact base url, useful when using a proxy. + * README.txt: Likewise + +2009-06-16 Badlop + + * AUTHORS: As mod_webpresence is a rename of mod_presence, Igor + Goryachev is the original author and Badlop added new features. + * src/mod_webpresence.erl: Likewise + * README.txt: Likewise + +2009-05-02 Badlop + + * src/mod_webpresence.erl: Minor change in string + * src/msgs/*: Likewise + + * src/msgs/wa.mod_webpresence.po: Added Walon translation (thanks + to Pablo Saratxaga) + + * src/msgs/es.mod_webpresence.po: Added Spanish translation + + * src/mod_webpresence.erl: Update translatable strings + * src/msgs/mod_webpresence.pot: Likewise + + * src/msgs/: New directory for module translations (EJAB-925) + +2008-11-08 Badlop + + * src/mod_webpresence.erl (escape): Escape also newlines (thanks + to Florian Zeitz) + +2008-09-10 Badlop + + * src/mod_webpresence.erl (get_presences): Return only sessions + that have presence + +2008-07-29 Badlop + + * src/mod_webpresence.erl: Fix calls to supervisor:terminate_child + and ejabberd_hooks:delete + +2008-07-25 Badlop + + * src/mod_webpresence.erl: Changed the syntax of image request + with custom theme: the URI must contain the word "theme" before + the theme name. Allow to request image and avatar providing an + additional element in the URI, which is interesting for some + restrictive public forums. Changed category and type in disco + queries. Fix iconset verification. + * README.txt: Likewise + +2008-01-29 Badlop + + * src/mod_webpresence.erl: Support 'cb' parameter in js requests, + which is later added in the result as a callback (thanks to Eric + Sessoms) + * README.txt: Likewise + +2007-12-26 Badlop + + * src/mod_webpresence.erl: Translate menu items of webadmin hooks + in each module (EJAB-485) + +2007-09-22 Badlop + + * src/mod_webpresence.erl: Added new ACCESS for automatic + enable. Added image.php URL, useful in some cases. + + * README.txt: Added usage description. + + * TODO.txt: Added feature requests proposed by Pablo Saratxaga. + + * msgs/wa.msg: Added Walloon translation (thanks to Pablo + Saratxaga). + + * src/mod_webpresence.erl: Better i18n support (thanks to Pablo + Saratxaga). + +2007-09-21 Badlop + + * src/mod_webpresence.erl: Better i18n support. + +2007-08-31 Badlop + + * src/mod_webpresence.erl: Escape ' in status message. Fix bug + while cheking jidurl and ridurl options. Small fix in PHP example. + +2007-08-30 Badlop + + * src/mod_webpresence.erl: Support for JavaScript output. Request + for image of an unavailable resource results in an unavailable + type of image. + +2007-08-29 Badlop + + * src/mod_webpresence.erl: Set jsf-jabber-text as default icon + theme in the registration form. Fixed bug in data forms (thanks to + NoAlWin). + + * README.txt: Improved example PHP script (thanks to NoAlWin). + + * data/pixmaps/*: Added many Icon Themes from Elmer NxG (thanks to + Zbyszek Zolkiewski). + +2007-08-28 Badlop + + * AUTHORS: Added Badlop as main author, and mention Igor Goryachev + as author of mod_presence. + * README.txt: Same. + * src/mod_webpresence.erl: Same. + + * LICENSE: Deleted, since all modules are by default published + with the GPL license for compatibility with ejabberd. + + * src/mod_webpresence.erl: Added registration and theme + statistics. Improved statistics: icon theme, and + percentages. Added support for Text output. Changed order of + options. + +2007-08-27 Badlop + + * README.txt: Updated description of configurable options. + + * src/mod_webpresence.erl: Enforce Hash and JID options in a web + request. The Hash is now a pseudo random integer. Added support + for avatar output. Added support to show image of a specific + resource. Improved Form, Message and Web. Added module options + Port and Path. Improved message with instructions. Replaced the + misleading term 'Hash' with 'Random ID'. Only show the JID in XML + output if JabberID is used in the URI. If user does not select at + least an ID and Output, he is unregistered. Added Unregistration + message. Remove registration is Jabber account is deleted. + +2007-08-26 Badlop + + * src/mod_webpresence.erl: Improved table format and conversion + from old mod_presence. Small fixes. Replace nested cases with call + to funcions. + + * src/mod_webpresence.erl: Renamed mod_presence.erl to + mod_webpresence.erl. Updated table format. XForm support for Hash + and JID URLs. Added complete support for 'jid' and 'hash' URIs. + * README.txt: Document 'jid' and 'hash' type of URIs. + + * Emakefile: Idem. + * README.txt: Idem. + + * src/mod_presence.erl: Preliminary support for hashed JIDs. Added + support for HOST keyword, and removed unusued Access + variables. Added basic page to Web Admin. + +2007-08-25 Badlop + + * src/mod_presence.erl: Fixed compilation warnings. Fixed two + warnings reported by Dialyzer. Renamed Status and Text to Show and + Status. Table index key only needs User and Server. Added id table + field and update_table function. XML option is stored as atom + instead of string. Cleaned process/2 and show_presence/1. New home + page, and icon themes quickview. + +2007-08-05 Badlop + + * ChangeLog: New file to track changes. + + * src/mod_presence.erl: Fix indentation. + + * src/Makefile.in: Removed unusued file. diff --git a/mod_webpresence/Emakefile b/mod_webpresence/Emakefile new file mode 100644 index 0000000..6d14f70 --- /dev/null +++ b/mod_webpresence/Emakefile @@ -0,0 +1,2 @@ +{'../ejabberd-dev/src/gen_mod', [{outdir, "../ejabberd-dev/ebin"},{i,"../ejabberd-dev/include"}]}. +{'src/mod_webpresence', [{outdir, "ebin"},{i,"../ejabberd-dev/include"}]}. diff --git a/mod_webpresence/README.txt b/mod_webpresence/README.txt new file mode 100644 index 0000000..310debf --- /dev/null +++ b/mod_webpresence/README.txt @@ -0,0 +1,276 @@ + + mod_webpresence - Presence on the Web + + Authors: Igor Goryachev, Badlop + Requires: ejabberd SVN (not possible with 1.1.x) + http://www.ejabberd.im/mod_webpresence + + + DESCRIPTION + ----------- + +This module allows any local user of the ejabberd server to publish his +presence information in the web. +This module is the succesor of Igor Goryachev's mod_presence. + +Allowed output methods are + * Icons (various themes available) + * Status text + * Raw XML + * Avatar, stored in the user's vCard + +No web server, database, additional libraries or programs are required. + + + + INSTALL + ------- + +1. Compile the module + * On Windows: build.bat + * On other systems: ./build.sh + +2. Copy ebin/mod_webpresence.beam to your ejabberd ebin directory. + +3. Copy the directory data/pixmaps to a directory you prefer. + +4. Edit ejabberd.cfg and add the HTTP and module definitions: +{listen, [ + ... + {5280, ejabberd_http, [ + ... + {request_handlers, [ + ... + {["presence"], mod_webpresence} + ]} + ]} +]}. + +{modules, [ + ... + {mod_webpresence, [ + {pixmaps_path, "/path/to/pixmaps"} + ]} +]}. + +5. Restart ejabberd. +If problems appear, remember to always look first the ejabberd log files +ejabberd.log and sasl.log since they may provide some valuable information. + + + CONFIGURABLE PARAMETERS + ----------------------- + +host + Define the hostname of the service. + You can use the keyword @HOST@. + Default value: "webpresence.@HOST@" +access: + Specify who can register in the webpresence service. + Don't bother to specify 'all' because this module can only show presence of + local users. + Default value: local +pixmaps_path: + Take special care with commas and dots: if this module does not seem to work + correctly, the problem may be that the configuration file has syntax errors. + Remember to put the correct path to the pixmaps directory, + and make sure the user than runs ejabberd has read access to that directory. + Default value: "./pixmaps" +port: + This informational option is used only when sending a message to the user. + If you set a different port in the 'listen' section, set this option too. + Default value: 5280 +path: + This informational option is used only when sending a message to the user. + If you set a different path in the 'listen' section, set this option too. + Default value: "presence" +baseurl: + This informational option is used only when sending a message to the user + and when building the JavaScript code. + It is the base part of the URL of the webpresence HTTP content. + If the option is not specified, it takes as default value: http://host:port/path/ + + + AUTOMATIC ENABLE + ---------------- + +If you want certain Jabber accounts to be automatically accepted, +without requiring the user to register in the service, you can user ACL+ACCESS. +The ACCESSNAME 'webpresence_auto' is available for that purpose. + +In that case, all the output methods are enabled, the icon theme is +'jsf-jabber-text' and RandomID is disabled. + +The default behaviour is to not have automatic webpresence: + {access, webpresence_auto, [{deny, all}]}. + +For example, if you want all the local users to be automatically enabled in the service: + {access, webpresence_auto, [{allow, local}]}. + +Note that this ACCESS rule is only checked if the user is not registered. +So, if the user registers and disables all output methods, +his registration prevails over your setup. If you want to ensure the users do not +register and disable output methods, you can use the Access configurable parameter. + + + EXAMPLE CONFIGURATION + --------------------- + + Example 1 + --------- + +{listen, [ + ... + {5280, ejabberd_http, [ + ... + {request_handlers, [ + ... + {["presence"], mod_webpresence} + ]} + ]} +]}. + +{modules, [ + ... + {mod_webpresence, [ + {pixmaps_path, "/path/to/pixmaps"} + ]} +]}. + + + Example 2 + --------- + +{listen, [ + ... + {80, ejabberd_http, [ + ... + {request_handlers, [ + ... + {["status"], mod_webpresence} + ]} + ]} +]}. + +{modules, [ + ... + {mod_webpresence, [ + {host, "webstatus.@HOST@"}, + {access, local}, + {pixmaps_path, "/path/to/pixmaps"}, + {port, 80}, + {path, "status"}, + {baseurl, "http://www.example.org/status/"} + ]} +]}. + + + USAGE + ----- + +The web-presence feature by default is switched off for every user. If +user wants to use it, he should register on service webpresence.example.org, +which is accessible from Service Discovery. +There are several switches for web-presence: + * Jabber ID: publish the presence in URIs that use the user's Jabber ID. + * Random ID: publish the presence in URIs that use a Random ID. + * XML: allow XML output. + * Icon: allow icon output. + * Avatar: allow Avatar output. + +Login to an account on your ejabberd server using a powerful Jabber client. +Open the Service Discovery on your Jabber client, and you should see +a new service called "webpresence.example.org". +Try to register on it. A formulary appears allowing the user to +allow image publishing, and XML publishing. + +Once you enabled some of those options, +on a web browser open the corresponding URI: + * for XML output: + http://example.org:5280/presence/jid///xml/ + * for image output: + http://example.org:5280/presence/jid///image/ + * for image output with theme: + http://example.org:5280/presence/jid///image/theme// + * for avatar output: + http://example.org:5280/presence/jid///avatar/ + +If you want to show the image or text outputs of a specific resource, add /res/ +to the URI: + http://example.org:5280/presence/jid///text/res/ + http://example.org:5280/presence/jid///image/res/ + http://example.org:5280/presence/jid///image/theme//res/ + +For output types image and avatar, you can append any string to a valid URI. +For example, you can use this URI: + http://example.org:5280/presence/jid///image/theme//myimage.jpeg +The response is exactly the same than the regular image/theme// + +If you don't want to reveal your Jabber ID, you can enable Random ID URI. +After the registration the user gets a message with his a pseudo-random ID. +The URI can be formed this way: + http://example.org:5280/presence/rid//image/ +If the user forgets his Random ID, he can get another message by just registering again, +there is no need to change the values. +If the user wants to get a new Random ID, he must disable Random ID in the registration form, +and later enable Random ID again. A new Random ID will be generated for him. + + + EXAMPLE PHP CODE + ---------------- + +This PHP script generates HTML code. +Thanks to Tobias Markmann and NoAlWin. + +It assumes that the URI of the presence is: + http://example.org:5280/presence/jid/tom/example.org + +load('http://example.org:5280/presence/jid/tom/example.org/xml'); + $presences = $doc->getElementsByTagName("presence"); + foreach ($presences as $presence) { + echo "

    "; + echo ""; + echo "getAttribute('server')."'>"; + echo "Tobias Markmann
    "; + $resources = $presence->getElementsByTagName("resource"); + if($resources->length == 0){ + echo 'Unavailable'; + }else{ + foreach ($resources as $resource) { + echo "getAttribute('server').'/'.$resource->getAttribute('name')."'>".$resource->getAttribute('name')." > "; + switch($resource->getAttribute('show')){ + case 'chat': echo 'Free for chat'; break; + case 'xa': echo 'Extended away'; break; + case 'dnd': echo 'Do not disturb'; break; + default: echo ucfirst($resource->getAttribute('show')); + } + if($resource->nodeValue){ + echo ": ".$resource->nodeValue; + } + echo "
    "; + } + } + echo "

    "; + } +?> + + + JAVASCRIPT CALLBACK + ------------------- + +The JavaScript output supports cross-site AJAX calls. + +Basically, it allows to tack on a callback parameter to presence +requests like so: + http://example.org:5280/presence/jid///js?cb=doStuff +Which then gets fed back in the result as: + var jabber_resources = [...]; doStuff(); + +The motivation for this is to work around browser restrictions in +cross-site scripting. You can use it by adding a new