summaryrefslogtreecommitdiff
path: root/platform/www/lib/plugins
diff options
context:
space:
mode:
Diffstat (limited to 'platform/www/lib/plugins')
-rw-r--r--platform/www/lib/plugins/acl/action.php86
-rw-r--r--platform/www/lib/plugins/acl/admin.php858
-rw-r--r--platform/www/lib/plugins/acl/admin.svg1
-rw-r--r--platform/www/lib/plugins/acl/lang/en/help.txt9
-rw-r--r--platform/www/lib/plugins/acl/lang/en/lang.php46
-rw-r--r--platform/www/lib/plugins/acl/pix/group.pngbin0 -> 699 bytes
-rw-r--r--platform/www/lib/plugins/acl/pix/ns.pngbin0 -> 799 bytes
-rw-r--r--platform/www/lib/plugins/acl/pix/page.pngbin0 -> 582 bytes
-rw-r--r--platform/www/lib/plugins/acl/pix/user.pngbin0 -> 650 bytes
-rw-r--r--platform/www/lib/plugins/acl/plugin.info.txt7
-rw-r--r--platform/www/lib/plugins/acl/remote.php102
-rw-r--r--platform/www/lib/plugins/acl/script.js121
-rw-r--r--platform/www/lib/plugins/acl/style.css135
-rw-r--r--platform/www/lib/plugins/action.php2
-rw-r--r--platform/www/lib/plugins/admin.php2
-rw-r--r--platform/www/lib/plugins/auth.php2
-rw-r--r--platform/www/lib/plugins/authad/action.php90
-rw-r--r--platform/www/lib/plugins/authad/adLDAP/adLDAP.php949
-rw-r--r--platform/www/lib/plugins/authad/adLDAP/classes/adLDAPComputers.php153
-rw-r--r--platform/www/lib/plugins/authad/adLDAP/classes/adLDAPContacts.php294
-rw-r--r--platform/www/lib/plugins/authad/adLDAP/classes/adLDAPExchange.php390
-rw-r--r--platform/www/lib/plugins/authad/adLDAP/classes/adLDAPFolders.php179
-rw-r--r--platform/www/lib/plugins/authad/adLDAP/classes/adLDAPGroups.php631
-rw-r--r--platform/www/lib/plugins/authad/adLDAP/classes/adLDAPUsers.php682
-rw-r--r--platform/www/lib/plugins/authad/adLDAP/classes/adLDAPUtils.php268
-rw-r--r--platform/www/lib/plugins/authad/adLDAP/collections/adLDAPCollection.php137
-rw-r--r--platform/www/lib/plugins/authad/adLDAP/collections/adLDAPComputerCollection.php46
-rw-r--r--platform/www/lib/plugins/authad/adLDAP/collections/adLDAPContactCollection.php46
-rw-r--r--platform/www/lib/plugins/authad/adLDAP/collections/adLDAPGroupCollection.php46
-rw-r--r--platform/www/lib/plugins/authad/adLDAP/collections/adLDAPUserCollection.php46
-rw-r--r--platform/www/lib/plugins/authad/auth.php786
-rw-r--r--platform/www/lib/plugins/authad/conf/default.php18
-rw-r--r--platform/www/lib/plugins/authad/conf/metadata.php18
-rw-r--r--platform/www/lib/plugins/authad/lang/en/lang.php15
-rw-r--r--platform/www/lib/plugins/authad/lang/en/settings.php18
-rw-r--r--platform/www/lib/plugins/authad/plugin.info.txt7
-rw-r--r--platform/www/lib/plugins/authldap/auth.php698
-rw-r--r--platform/www/lib/plugins/authldap/conf/default.php23
-rw-r--r--platform/www/lib/plugins/authldap/conf/metadata.php22
-rw-r--r--platform/www/lib/plugins/authldap/lang/en/lang.php11
-rw-r--r--platform/www/lib/plugins/authldap/lang/en/settings.php30
-rw-r--r--platform/www/lib/plugins/authldap/plugin.info.txt7
-rw-r--r--platform/www/lib/plugins/authpdo/README27
-rw-r--r--platform/www/lib/plugins/authpdo/auth.php826
-rw-r--r--platform/www/lib/plugins/authpdo/conf/default.php118
-rw-r--r--platform/www/lib/plugins/authpdo/conf/metadata.php25
-rw-r--r--platform/www/lib/plugins/authpdo/lang/en/lang.php12
-rw-r--r--platform/www/lib/plugins/authpdo/lang/en/settings.php25
-rw-r--r--platform/www/lib/plugins/authpdo/plugin.info.txt7
-rw-r--r--platform/www/lib/plugins/authplain/auth.php494
-rw-r--r--platform/www/lib/plugins/authplain/lang/en/lang.php9
-rw-r--r--platform/www/lib/plugins/authplain/plugin.info.txt7
-rw-r--r--platform/www/lib/plugins/cli.php2
-rw-r--r--platform/www/lib/plugins/config/admin.php282
-rw-r--r--platform/www/lib/plugins/config/admin.svg1
-rw-r--r--platform/www/lib/plugins/config/core/ConfigParser.php90
-rw-r--r--platform/www/lib/plugins/config/core/Configuration.php219
-rw-r--r--platform/www/lib/plugins/config/core/Loader.php269
-rw-r--r--platform/www/lib/plugins/config/core/Setting/Setting.php294
-rw-r--r--platform/www/lib/plugins/config/core/Setting/SettingArray.php105
-rw-r--r--platform/www/lib/plugins/config/core/Setting/SettingAuthtype.php60
-rw-r--r--platform/www/lib/plugins/config/core/Setting/SettingCompression.php21
-rw-r--r--platform/www/lib/plugins/config/core/Setting/SettingDirchoice.php33
-rw-r--r--platform/www/lib/plugins/config/core/Setting/SettingDisableactions.php23
-rw-r--r--platform/www/lib/plugins/config/core/Setting/SettingEmail.php58
-rw-r--r--platform/www/lib/plugins/config/core/Setting/SettingFieldset.php17
-rw-r--r--platform/www/lib/plugins/config/core/Setting/SettingHidden.php10
-rw-r--r--platform/www/lib/plugins/config/core/Setting/SettingImConvert.php28
-rw-r--r--platform/www/lib/plugins/config/core/Setting/SettingLicense.php23
-rw-r--r--platform/www/lib/plugins/config/core/Setting/SettingMulticheckbox.php163
-rw-r--r--platform/www/lib/plugins/config/core/Setting/SettingMultichoice.php71
-rw-r--r--platform/www/lib/plugins/config/core/Setting/SettingNoClass.php12
-rw-r--r--platform/www/lib/plugins/config/core/Setting/SettingNoDefault.php13
-rw-r--r--platform/www/lib/plugins/config/core/Setting/SettingNoKnownClass.php11
-rw-r--r--platform/www/lib/plugins/config/core/Setting/SettingNumeric.php42
-rw-r--r--platform/www/lib/plugins/config/core/Setting/SettingNumericopt.php25
-rw-r--r--platform/www/lib/plugins/config/core/Setting/SettingOnoff.php57
-rw-r--r--platform/www/lib/plugins/config/core/Setting/SettingPassword.php39
-rw-r--r--platform/www/lib/plugins/config/core/Setting/SettingRegex.php34
-rw-r--r--platform/www/lib/plugins/config/core/Setting/SettingRenderer.php56
-rw-r--r--platform/www/lib/plugins/config/core/Setting/SettingSavedir.php26
-rw-r--r--platform/www/lib/plugins/config/core/Setting/SettingSepchar.php18
-rw-r--r--platform/www/lib/plugins/config/core/Setting/SettingString.php32
-rw-r--r--platform/www/lib/plugins/config/core/Setting/SettingUndefined.php40
-rw-r--r--platform/www/lib/plugins/config/core/Writer.php116
-rw-r--r--platform/www/lib/plugins/config/images/danger.pngbin0 -> 637 bytes
-rw-r--r--platform/www/lib/plugins/config/images/security.pngbin0 -> 682 bytes
-rw-r--r--platform/www/lib/plugins/config/images/warning.pngbin0 -> 606 bytes
-rw-r--r--platform/www/lib/plugins/config/lang/en/intro.txt7
-rw-r--r--platform/www/lib/plugins/config/lang/en/lang.php277
-rw-r--r--platform/www/lib/plugins/config/plugin.info.txt7
-rw-r--r--platform/www/lib/plugins/config/settings/config.metadata.php245
-rw-r--r--platform/www/lib/plugins/config/style.css167
-rw-r--r--platform/www/lib/plugins/extension/action.php82
-rw-r--r--platform/www/lib/plugins/extension/admin.php185
-rw-r--r--platform/www/lib/plugins/extension/admin.svg1
-rw-r--r--platform/www/lib/plugins/extension/all.less37
-rw-r--r--platform/www/lib/plugins/extension/cli.php372
-rw-r--r--platform/www/lib/plugins/extension/helper/extension.php1298
-rw-r--r--platform/www/lib/plugins/extension/helper/gui.php237
-rw-r--r--platform/www/lib/plugins/extension/helper/list.php674
-rw-r--r--platform/www/lib/plugins/extension/helper/repository.php203
-rw-r--r--platform/www/lib/plugins/extension/images/bug.gifbin0 -> 194 bytes
-rw-r--r--platform/www/lib/plugins/extension/images/disabled.pngbin0 -> 1163 bytes
-rw-r--r--platform/www/lib/plugins/extension/images/donate.pngbin0 -> 677 bytes
-rw-r--r--platform/www/lib/plugins/extension/images/down.pngbin0 -> 197 bytes
-rw-r--r--platform/www/lib/plugins/extension/images/enabled.pngbin0 -> 1172 bytes
-rw-r--r--platform/www/lib/plugins/extension/images/icons.xcfbin0 -> 67195 bytes
-rw-r--r--platform/www/lib/plugins/extension/images/license.txt4
-rw-r--r--platform/www/lib/plugins/extension/images/overlay.pngbin0 -> 68 bytes
-rw-r--r--platform/www/lib/plugins/extension/images/plugin.pngbin0 -> 4054 bytes
-rw-r--r--platform/www/lib/plugins/extension/images/tag.pngbin0 -> 341 bytes
-rw-r--r--platform/www/lib/plugins/extension/images/template.pngbin0 -> 5206 bytes
-rw-r--r--platform/www/lib/plugins/extension/images/up.pngbin0 -> 197 bytes
-rw-r--r--platform/www/lib/plugins/extension/images/warning.pngbin0 -> 606 bytes
-rw-r--r--platform/www/lib/plugins/extension/lang/en/intro_install.txt1
-rw-r--r--platform/www/lib/plugins/extension/lang/en/intro_plugins.txt1
-rw-r--r--platform/www/lib/plugins/extension/lang/en/intro_search.txt1
-rw-r--r--platform/www/lib/plugins/extension/lang/en/intro_templates.txt1
-rw-r--r--platform/www/lib/plugins/extension/lang/en/lang.php110
-rw-r--r--platform/www/lib/plugins/extension/plugin.info.txt7
-rw-r--r--platform/www/lib/plugins/extension/script.js145
-rw-r--r--platform/www/lib/plugins/extension/style.less386
-rw-r--r--platform/www/lib/plugins/farmer/.github/auto-comment.yml9
-rw-r--r--platform/www/lib/plugins/farmer/.travis.yml15
-rw-r--r--platform/www/lib/plugins/farmer/3rdparty/PHPIco.php248
-rw-r--r--platform/www/lib/plugins/farmer/3rdparty/RingIcon.php186
-rw-r--r--platform/www/lib/plugins/farmer/DokuWikiFarmCore.php375
-rw-r--r--platform/www/lib/plugins/farmer/README27
-rw-r--r--platform/www/lib/plugins/farmer/_animal/conf/acl.auth.php21
-rw-r--r--platform/www/lib/plugins/farmer/_animal/conf/local.php6
-rw-r--r--platform/www/lib/plugins/farmer/_animal/data/_dummy0
-rw-r--r--platform/www/lib/plugins/farmer/_animal/data/attic/_dummy0
-rw-r--r--platform/www/lib/plugins/farmer/_animal/data/cache/_dummy0
-rw-r--r--platform/www/lib/plugins/farmer/_animal/data/index/_dummy0
-rw-r--r--platform/www/lib/plugins/farmer/_animal/data/locks/_dummy0
-rw-r--r--platform/www/lib/plugins/farmer/_animal/data/log/_dummy0
-rw-r--r--platform/www/lib/plugins/farmer/_animal/data/media/wiki/dokuwiki-128.pngbin0 -> 33615 bytes
-rw-r--r--platform/www/lib/plugins/farmer/_animal/data/media_attic/_dummy0
-rw-r--r--platform/www/lib/plugins/farmer/_animal/data/media_meta/_dummy0
-rw-r--r--platform/www/lib/plugins/farmer/_animal/data/meta/_dummy0
-rw-r--r--platform/www/lib/plugins/farmer/_animal/data/pages/wiki/dokuwiki.txt64
-rw-r--r--platform/www/lib/plugins/farmer/_animal/data/pages/wiki/syntax.txt486
-rw-r--r--platform/www/lib/plugins/farmer/_animal/data/tmp/_dummy0
-rw-r--r--platform/www/lib/plugins/farmer/_test/core.test.php40
-rw-r--r--platform/www/lib/plugins/farmer/_test/general.test.php36
-rw-r--r--platform/www/lib/plugins/farmer/_test/getUserLine.test.php86
-rw-r--r--platform/www/lib/plugins/farmer/_test/helper.test.php64
-rw-r--r--platform/www/lib/plugins/farmer/action/ajax.php267
-rw-r--r--platform/www/lib/plugins/farmer/action/disable.php56
-rw-r--r--platform/www/lib/plugins/farmer/action/startup.php90
-rw-r--r--platform/www/lib/plugins/farmer/admin.php115
-rw-r--r--platform/www/lib/plugins/farmer/admin.svg1
-rw-r--r--platform/www/lib/plugins/farmer/admin/config.php126
-rw-r--r--platform/www/lib/plugins/farmer/admin/delete.php95
-rw-r--r--platform/www/lib/plugins/farmer/admin/info.php116
-rw-r--r--platform/www/lib/plugins/farmer/admin/new.php338
-rw-r--r--platform/www/lib/plugins/farmer/admin/plugins.php104
-rw-r--r--platform/www/lib/plugins/farmer/admin/setup.php151
-rw-r--r--platform/www/lib/plugins/farmer/all.less13
-rw-r--r--platform/www/lib/plugins/farmer/conf/default.php9
-rw-r--r--platform/www/lib/plugins/farmer/conf/metadata.php9
-rw-r--r--platform/www/lib/plugins/farmer/css/chosen-sprite.pngbin0 -> 538 bytes
-rw-r--r--platform/www/lib/plugins/farmer/css/chosen.less450
-rw-r--r--platform/www/lib/plugins/farmer/deleted.files11
-rw-r--r--platform/www/lib/plugins/farmer/helper.php334
-rw-r--r--platform/www/lib/plugins/farmer/includes/config.php13
-rw-r--r--platform/www/lib/plugins/farmer/includes/plugins.php12
-rw-r--r--platform/www/lib/plugins/farmer/includes/template.php27
-rw-r--r--platform/www/lib/plugins/farmer/lang/de/lang.php118
-rw-r--r--platform/www/lib/plugins/farmer/lang/de/notfound_404.txt3
-rw-r--r--platform/www/lib/plugins/farmer/lang/de/notfound_list.txt3
-rw-r--r--platform/www/lib/plugins/farmer/lang/de/settings.php13
-rw-r--r--platform/www/lib/plugins/farmer/lang/de/tab_config.txt1
-rw-r--r--platform/www/lib/plugins/farmer/lang/de/tab_delete.txt1
-rw-r--r--platform/www/lib/plugins/farmer/lang/de/tab_info.txt1
-rw-r--r--platform/www/lib/plugins/farmer/lang/de/tab_new.txt1
-rw-r--r--platform/www/lib/plugins/farmer/lang/de/tab_plugins.txt1
-rw-r--r--platform/www/lib/plugins/farmer/lang/de/tab_setup.txt1
-rw-r--r--platform/www/lib/plugins/farmer/lang/en/lang.php128
-rw-r--r--platform/www/lib/plugins/farmer/lang/en/notfound_404.txt3
-rw-r--r--platform/www/lib/plugins/farmer/lang/en/notfound_list.txt3
-rw-r--r--platform/www/lib/plugins/farmer/lang/en/settings.php13
-rw-r--r--platform/www/lib/plugins/farmer/lang/en/tab_config.txt1
-rw-r--r--platform/www/lib/plugins/farmer/lang/en/tab_config_help.txt26
-rw-r--r--platform/www/lib/plugins/farmer/lang/en/tab_delete.txt1
-rw-r--r--platform/www/lib/plugins/farmer/lang/en/tab_info.txt1
-rw-r--r--platform/www/lib/plugins/farmer/lang/en/tab_new.txt1
-rw-r--r--platform/www/lib/plugins/farmer/lang/en/tab_new_help.txt31
-rw-r--r--platform/www/lib/plugins/farmer/lang/en/tab_plugins.txt1
-rw-r--r--platform/www/lib/plugins/farmer/lang/en/tab_plugins_help.txt16
-rw-r--r--platform/www/lib/plugins/farmer/lang/en/tab_setup.txt1
-rw-r--r--platform/www/lib/plugins/farmer/lang/en/tab_setup_help.txt37
-rw-r--r--platform/www/lib/plugins/farmer/lang/fr/lang.php109
-rw-r--r--platform/www/lib/plugins/farmer/lang/fr/notfound_404.txt3
-rw-r--r--platform/www/lib/plugins/farmer/lang/fr/notfound_list.txt4
-rw-r--r--platform/www/lib/plugins/farmer/lang/fr/settings.php9
-rw-r--r--platform/www/lib/plugins/farmer/lang/fr/tab_config.txt1
-rw-r--r--platform/www/lib/plugins/farmer/lang/fr/tab_config_help.txt37
-rw-r--r--platform/www/lib/plugins/farmer/lang/fr/tab_delete.txt1
-rw-r--r--platform/www/lib/plugins/farmer/lang/fr/tab_info.txt1
-rw-r--r--platform/www/lib/plugins/farmer/lang/fr/tab_new.txt1
-rw-r--r--platform/www/lib/plugins/farmer/lang/fr/tab_new_help.txt43
-rw-r--r--platform/www/lib/plugins/farmer/lang/fr/tab_plugins.txt1
-rw-r--r--platform/www/lib/plugins/farmer/lang/fr/tab_plugins_help.txt22
-rw-r--r--platform/www/lib/plugins/farmer/lang/fr/tab_setup.txt1
-rw-r--r--platform/www/lib/plugins/farmer/lang/fr/tab_setup_help.txt52
-rw-r--r--platform/www/lib/plugins/farmer/lang/ja/lang.php106
-rw-r--r--platform/www/lib/plugins/farmer/lang/ja/notfound_404.txt3
-rw-r--r--platform/www/lib/plugins/farmer/lang/ja/notfound_list.txt3
-rw-r--r--platform/www/lib/plugins/farmer/lang/ja/settings.php9
-rw-r--r--platform/www/lib/plugins/farmer/lang/ja/tab_config.txt1
-rw-r--r--platform/www/lib/plugins/farmer/lang/ja/tab_config_help.txt25
-rw-r--r--platform/www/lib/plugins/farmer/lang/ja/tab_delete.txt1
-rw-r--r--platform/www/lib/plugins/farmer/lang/ja/tab_info.txt1
-rw-r--r--platform/www/lib/plugins/farmer/lang/ja/tab_new.txt1
-rw-r--r--platform/www/lib/plugins/farmer/lang/ja/tab_new_help.txt38
-rw-r--r--platform/www/lib/plugins/farmer/lang/ja/tab_plugins.txt1
-rw-r--r--platform/www/lib/plugins/farmer/lang/ja/tab_plugins_help.txt17
-rw-r--r--platform/www/lib/plugins/farmer/lang/ja/tab_setup.txt1
-rw-r--r--platform/www/lib/plugins/farmer/lang/ja/tab_setup_help.txt38
-rw-r--r--platform/www/lib/plugins/farmer/lang/nl/lang.php20
-rw-r--r--platform/www/lib/plugins/farmer/lang/pl/lang.php102
-rw-r--r--platform/www/lib/plugins/farmer/plugin.info.txt7
-rw-r--r--platform/www/lib/plugins/farmer/script.js2
-rw-r--r--platform/www/lib/plugins/farmer/script/jquery.chosen.js1257
-rw-r--r--platform/www/lib/plugins/farmer/script/plugins.js149
-rw-r--r--platform/www/lib/plugins/farmer/style.less104
m---------platform/www/lib/plugins/fastwiki0
-rw-r--r--platform/www/lib/plugins/index.html11
-rw-r--r--platform/www/lib/plugins/info/plugin.info.txt7
-rw-r--r--platform/www/lib/plugins/info/syntax.php302
-rw-r--r--platform/www/lib/plugins/markdowku/LICENSE27
-rw-r--r--platform/www/lib/plugins/markdowku/README.md2
-rw-r--r--platform/www/lib/plugins/markdowku/manager.dat2
-rw-r--r--platform/www/lib/plugins/markdowku/plugin.info.txt7
-rw-r--r--platform/www/lib/plugins/markdowku/syntax/anchorsinline.php46
-rw-r--r--platform/www/lib/plugins/markdowku/syntax/anchorsreference.php62
-rw-r--r--platform/www/lib/plugins/markdowku/syntax/autolinks.php40
-rw-r--r--platform/www/lib/plugins/markdowku/syntax/blockquotes.php115
-rw-r--r--platform/www/lib/plugins/markdowku/syntax/boldasterisk.php45
-rw-r--r--platform/www/lib/plugins/markdowku/syntax/codeblocks.php63
-rw-r--r--platform/www/lib/plugins/markdowku/syntax/codespans1.php35
-rw-r--r--platform/www/lib/plugins/markdowku/syntax/codespans2.php34
-rw-r--r--platform/www/lib/plugins/markdowku/syntax/codespans3.php34
-rw-r--r--platform/www/lib/plugins/markdowku/syntax/codespans4.php34
-rw-r--r--platform/www/lib/plugins/markdowku/syntax/codespans5.php34
-rw-r--r--platform/www/lib/plugins/markdowku/syntax/escapespecialchars.php94
-rw-r--r--platform/www/lib/plugins/markdowku/syntax/githubcodeblocks.php48
-rw-r--r--platform/www/lib/plugins/markdowku/syntax/headeratx.php57
-rw-r--r--platform/www/lib/plugins/markdowku/syntax/headersetext.php56
-rw-r--r--platform/www/lib/plugins/markdowku/syntax/hr.php47
-rw-r--r--platform/www/lib/plugins/markdowku/syntax/imagesinline.php43
-rw-r--r--platform/www/lib/plugins/markdowku/syntax/imagesreference.php58
-rw-r--r--platform/www/lib/plugins/markdowku/syntax/italicasterisk.php49
-rw-r--r--platform/www/lib/plugins/markdowku/syntax/italicunderline.php56
-rw-r--r--platform/www/lib/plugins/markdowku/syntax/linebreak.php32
-rw-r--r--platform/www/lib/plugins/markdowku/syntax/olists.php98
-rw-r--r--platform/www/lib/plugins/markdowku/syntax/references.php41
-rw-r--r--platform/www/lib/plugins/markdowku/syntax/ulists.php99
-rw-r--r--platform/www/lib/plugins/master.zipbin0 -> 139966 bytes
-rw-r--r--platform/www/lib/plugins/popularity/action.php66
-rw-r--r--platform/www/lib/plugins/popularity/admin.php157
-rw-r--r--platform/www/lib/plugins/popularity/admin.svg1
-rw-r--r--platform/www/lib/plugins/popularity/helper.php292
-rw-r--r--platform/www/lib/plugins/popularity/lang/en/intro.txt11
-rw-r--r--platform/www/lib/plugins/popularity/lang/en/lang.php9
-rw-r--r--platform/www/lib/plugins/popularity/lang/en/submitted.txt3
-rw-r--r--platform/www/lib/plugins/popularity/plugin.info.txt7
-rw-r--r--platform/www/lib/plugins/remote.php2
-rw-r--r--platform/www/lib/plugins/revert/admin.php193
-rw-r--r--platform/www/lib/plugins/revert/admin.svg1
-rw-r--r--platform/www/lib/plugins/revert/lang/en/intro.txt3
-rw-r--r--platform/www/lib/plugins/revert/lang/en/lang.php23
-rw-r--r--platform/www/lib/plugins/revert/plugin.info.txt7
-rw-r--r--platform/www/lib/plugins/safefnrecode/action.php68
-rw-r--r--platform/www/lib/plugins/safefnrecode/plugin.info.txt7
-rw-r--r--platform/www/lib/plugins/styling/README27
-rw-r--r--platform/www/lib/plugins/styling/action.php51
-rw-r--r--platform/www/lib/plugins/styling/admin.php224
-rw-r--r--platform/www/lib/plugins/styling/admin.svg1
-rw-r--r--platform/www/lib/plugins/styling/lang/en/intro.txt2
-rw-r--r--platform/www/lib/plugins/styling/lang/en/lang.php35
-rw-r--r--platform/www/lib/plugins/styling/plugin.info.txt7
-rw-r--r--platform/www/lib/plugins/styling/popup.php31
-rw-r--r--platform/www/lib/plugins/styling/script.js92
-rw-r--r--platform/www/lib/plugins/styling/style.less13
-rw-r--r--platform/www/lib/plugins/syntax.php2
-rw-r--r--platform/www/lib/plugins/textinsert/README55
-rw-r--r--platform/www/lib/plugins/textinsert/admin.php292
-rw-r--r--platform/www/lib/plugins/textinsert/conf/default.php5
-rw-r--r--platform/www/lib/plugins/textinsert/conf/metadata.php3
-rw-r--r--platform/www/lib/plugins/textinsert/lang/de/lang.php22
-rw-r--r--platform/www/lib/plugins/textinsert/lang/de/settings.php8
-rw-r--r--platform/www/lib/plugins/textinsert/lang/en/intro.txt16
-rw-r--r--platform/www/lib/plugins/textinsert/lang/en/lang.php31
l---------platform/www/lib/plugins/textinsert/lang/en/macros.php1
-rw-r--r--platform/www/lib/plugins/textinsert/lang/en/settings.php3
-rw-r--r--platform/www/lib/plugins/textinsert/lang/es/intro.txt16
-rw-r--r--platform/www/lib/plugins/textinsert/lang/es/lang.php31
l---------platform/www/lib/plugins/textinsert/lang/es/macros.php1
-rw-r--r--platform/www/lib/plugins/textinsert/lang/es/settings.php3
-rw-r--r--platform/www/lib/plugins/textinsert/lang/fr/intro.txt10
-rw-r--r--platform/www/lib/plugins/textinsert/lang/fr/lang.php25
-rw-r--r--platform/www/lib/plugins/textinsert/lang/fr/settings.php10
-rw-r--r--platform/www/lib/plugins/textinsert/lang/ja/intro.txt16
-rw-r--r--platform/www/lib/plugins/textinsert/lang/ja/lang.php23
-rw-r--r--platform/www/lib/plugins/textinsert/lang/ja/settings.php9
-rw-r--r--platform/www/lib/plugins/textinsert/lang/nl/intro.txt11
-rw-r--r--platform/www/lib/plugins/textinsert/lang/nl/lang.php24
-rw-r--r--platform/www/lib/plugins/textinsert/lang/nl/settings.php8
-rw-r--r--platform/www/lib/plugins/textinsert/lang/pt-br/intro.txt13
-rw-r--r--platform/www/lib/plugins/textinsert/lang/pt-br/lang.php23
-rw-r--r--platform/www/lib/plugins/textinsert/lang/pt-br/settings.php9
-rw-r--r--platform/www/lib/plugins/textinsert/lang/ru/intro.txt10
-rw-r--r--platform/www/lib/plugins/textinsert/lang/ru/lang.php25
-rw-r--r--platform/www/lib/plugins/textinsert/lang/ru/settings.php9
-rw-r--r--platform/www/lib/plugins/textinsert/lang/zh/intro.txt11
-rw-r--r--platform/www/lib/plugins/textinsert/lang/zh/lang.php23
-rw-r--r--platform/www/lib/plugins/textinsert/manager.dat2
-rw-r--r--platform/www/lib/plugins/textinsert/plugin.info.txt9
-rw-r--r--platform/www/lib/plugins/textinsert/syntax.php261
-rw-r--r--platform/www/lib/plugins/textinsert/version2
-rw-r--r--platform/www/lib/plugins/translation/.travis.yml13
-rw-r--r--platform/www/lib/plugins/translation/README25
-rw-r--r--platform/www/lib/plugins/translation/_test/basic.test.php113
-rw-r--r--platform/www/lib/plugins/translation/_test/general.test.php61
-rw-r--r--platform/www/lib/plugins/translation/action.php302
-rw-r--r--platform/www/lib/plugins/translation/admin.php101
-rw-r--r--platform/www/lib/plugins/translation/admin.svg1
-rw-r--r--platform/www/lib/plugins/translation/conf/default.php19
-rw-r--r--platform/www/lib/plugins/translation/conf/metadata.php21
-rw-r--r--platform/www/lib/plugins/translation/flags/af.gifbin0 -> 369 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/ar.gifbin0 -> 370 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/da.gifbin0 -> 374 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/de.gifbin0 -> 362 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/el.gifbin0 -> 368 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/en.gifbin0 -> 260 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/es.gifbin0 -> 360 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/et.gifbin0 -> 364 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/fa.gifbin0 -> 366 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/fr.gifbin0 -> 366 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/ga.gifbin0 -> 371 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/he.gifbin0 -> 366 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/hu.gifbin0 -> 357 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/it.gifbin0 -> 366 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/ja.gifbin0 -> 366 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/ko.gifbin0 -> 385 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/ad.gifbin0 -> 371 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/ae.gifbin0 -> 361 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/ag.gifbin0 -> 361 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/ai.gifbin0 -> 369 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/al.gifbin0 -> 370 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/am.gifbin0 -> 363 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/an.gifbin0 -> 368 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/ao.gifbin0 -> 244 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/ar.gifbin0 -> 366 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/as.gifbin0 -> 365 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/at.gifbin0 -> 361 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/au.gifbin0 -> 378 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/aw.gifbin0 -> 365 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/ax.gifbin0 -> 376 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/az.gifbin0 -> 370 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/ba.gifbin0 -> 363 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/bb.gifbin0 -> 368 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/bd.gifbin0 -> 361 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/be.gifbin0 -> 359 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/bf.gifbin0 -> 358 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/bg.gifbin0 -> 360 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/bh.gifbin0 -> 367 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/bi.gifbin0 -> 374 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/bj.gifbin0 -> 368 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/blankflag.gifbin0 -> 42 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/bm.gifbin0 -> 367 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/bn.gifbin0 -> 373 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/bo.gifbin0 -> 359 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/bs.gifbin0 -> 351 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/bt.gifbin0 -> 377 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/bv.gifbin0 -> 376 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/bw.gifbin0 -> 364 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/by.gifbin0 -> 361 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/bz.gifbin0 -> 368 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/ca.gifbin0 -> 376 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/catalonia.gifbin0 -> 238 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/cc.gifbin0 -> 371 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/cd.gifbin0 -> 243 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/cf.gifbin0 -> 364 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/cg.gifbin0 -> 359 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/ch.gifbin0 -> 332 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/ci.gifbin0 -> 368 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/ck.gifbin0 -> 362 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/cl.gifbin0 -> 364 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/cm.gifbin0 -> 369 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/co.gifbin0 -> 353 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/cr.gifbin0 -> 359 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/cs.gifbin0 -> 364 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/cu.gifbin0 -> 367 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/cv.gifbin0 -> 367 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/cx.gifbin0 -> 363 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/cy.gifbin0 -> 365 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/cz.gifbin0 -> 362 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/dj.gifbin0 -> 369 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/dm.gifbin0 -> 368 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/do.gifbin0 -> 362 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/dz.gifbin0 -> 370 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/ec.gifbin0 -> 362 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/eg.gifbin0 -> 363 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/eh.gifbin0 -> 359 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/england.gifbin0 -> 367 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/er.gifbin0 -> 361 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/et.gifbin0 -> 364 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/europeanunion.gifbin0 -> 171 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/fam.gifbin0 -> 370 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/fi.gifbin0 -> 371 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/fj.gifbin0 -> 370 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/fk.gifbin0 -> 372 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/fm.gifbin0 -> 377 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/fo.gifbin0 -> 370 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/ga.gifbin0 -> 359 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/gd.gifbin0 -> 364 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/ge.gifbin0 -> 379 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/gf.gifbin0 -> 366 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/gh.gifbin0 -> 358 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/gi.gifbin0 -> 370 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/gl.gifbin0 -> 368 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/gm.gifbin0 -> 362 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/gn.gifbin0 -> 363 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/gp.gifbin0 -> 357 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/gq.gifbin0 -> 361 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/gs.gifbin0 -> 363 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/gt.gifbin0 -> 374 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/gu.gifbin0 -> 370 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/gw.gifbin0 -> 358 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/gy.gifbin0 -> 367 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/hk.gifbin0 -> 373 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/hm.gifbin0 -> 378 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/hn.gifbin0 -> 367 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/hr.gifbin0 -> 364 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/ht.gifbin0 -> 361 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/id.gifbin0 -> 362 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/in.gifbin0 -> 363 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/io.gifbin0 -> 373 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/iq.gifbin0 -> 361 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/is.gifbin0 -> 373 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/ja.gifbin0 -> 366 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/jm.gifbin0 -> 365 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/jo.gifbin0 -> 360 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/ke.gifbin0 -> 360 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/kg.gifbin0 -> 373 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/kh.gifbin0 -> 367 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/ki.gifbin0 -> 371 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/km.gifbin0 -> 358 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/kn.gifbin0 -> 370 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/ko.gifbin0 -> 385 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/kp.gifbin0 -> 366 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/kw.gifbin0 -> 362 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/ky.gifbin0 -> 373 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/kz.gifbin0 -> 374 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/la.gifbin0 -> 366 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/lb.gifbin0 -> 366 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/lc.gifbin0 -> 259 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/li.gifbin0 -> 359 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/lk.gifbin0 -> 377 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/lr.gifbin0 -> 360 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/ls.gifbin0 -> 369 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/lt.gifbin0 -> 362 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/lu.gifbin0 -> 368 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/lv.gifbin0 -> 363 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/ly.gifbin0 -> 362 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/ma.gifbin0 -> 367 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/mc.gifbin0 -> 359 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/md.gifbin0 -> 367 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/me.gifbin0 -> 238 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/mg.gifbin0 -> 372 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/mh.gifbin0 -> 370 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/mk.gifbin0 -> 382 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/ml.gifbin0 -> 363 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/mm.gifbin0 -> 365 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/mn.gifbin0 -> 368 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/mo.gifbin0 -> 378 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/mp.gifbin0 -> 368 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/mq.gifbin0 -> 379 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/mr.gifbin0 -> 377 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/ms.gifbin0 -> 371 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/mt.gifbin0 -> 369 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/mu.gifbin0 -> 358 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/mv.gifbin0 -> 372 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/mw.gifbin0 -> 364 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/mx.gifbin0 -> 366 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/my.gifbin0 -> 375 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/mz.gifbin0 -> 366 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/na.gifbin0 -> 371 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/nc.gifbin0 -> 364 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/ne.gifbin0 -> 366 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/nf.gifbin0 -> 375 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/ng.gifbin0 -> 371 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/ni.gifbin0 -> 366 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/np.gifbin0 -> 302 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/nr.gifbin0 -> 364 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/nu.gifbin0 -> 369 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/nz.gifbin0 -> 369 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/om.gifbin0 -> 364 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/pa.gifbin0 -> 367 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/pe.gifbin0 -> 361 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/pf.gifbin0 -> 366 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/pg.gifbin0 -> 360 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/ph.gifbin0 -> 361 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/pk.gifbin0 -> 377 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/pl.gifbin0 -> 360 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/pm.gifbin0 -> 374 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/pn.gifbin0 -> 367 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/pr.gifbin0 -> 369 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/ps.gifbin0 -> 358 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/pw.gifbin0 -> 374 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/py.gifbin0 -> 363 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/qa.gifbin0 -> 364 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/re.gifbin0 -> 366 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/rs.gifbin0 -> 238 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/rw.gifbin0 -> 361 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/sb.gifbin0 -> 366 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/sc.gifbin0 -> 357 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/scotland.gifbin0 -> 378 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/sd.gifbin0 -> 355 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/sg.gifbin0 -> 364 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/sh.gifbin0 -> 371 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/si.gifbin0 -> 362 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/sj.gifbin0 -> 376 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/sk.gifbin0 -> 361 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/sl.gifbin0 -> 363 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/sm.gifbin0 -> 367 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/sn.gifbin0 -> 364 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/so.gifbin0 -> 376 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/sr.gifbin0 -> 361 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/st.gifbin0 -> 367 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/sv.gifbin0 -> 363 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/sy.gifbin0 -> 361 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/sz.gifbin0 -> 363 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/tc.gifbin0 -> 366 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/td.gifbin0 -> 368 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/tf.gifbin0 -> 365 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/tg.gifbin0 -> 366 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/tj.gifbin0 -> 361 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/tk.gifbin0 -> 372 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/tl.gifbin0 -> 360 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/tm.gifbin0 -> 367 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/tn.gifbin0 -> 375 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/to.gifbin0 -> 367 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/tt.gifbin0 -> 377 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/tv.gifbin0 -> 361 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/tw.gifbin0 -> 367 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/tz.gifbin0 -> 366 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/ua.gifbin0 -> 360 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/ug.gifbin0 -> 359 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/um.gifbin0 -> 371 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/us.gifbin0 -> 367 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/uy.gifbin0 -> 373 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/uz.gifbin0 -> 364 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/va.gifbin0 -> 369 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/vc.gifbin0 -> 370 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/ve.gifbin0 -> 364 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/vg.gifbin0 -> 368 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/vi.gifbin0 -> 376 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/vu.gifbin0 -> 365 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/wales.gifbin0 -> 372 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/wf.gifbin0 -> 377 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/ws.gifbin0 -> 365 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/ye.gifbin0 -> 356 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/yt.gifbin0 -> 382 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/za.gifbin0 -> 363 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/zm.gifbin0 -> 358 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/more/zw.gifbin0 -> 365 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/nl.gifbin0 -> 360 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/no.gifbin0 -> 376 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/pt-br.gifbin0 -> 367 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/pt.gifbin0 -> 369 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/ro.gifbin0 -> 363 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/ru.gifbin0 -> 361 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/sv.gifbin0 -> 367 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/th.gifbin0 -> 360 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/tr.gifbin0 -> 371 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/vi.gifbin0 -> 370 bytes
-rw-r--r--platform/www/lib/plugins/translation/flags/zh.gifbin0 -> 366 bytes
-rw-r--r--platform/www/lib/plugins/translation/helper.php446
-rw-r--r--platform/www/lib/plugins/translation/lang/be/lang.php19
-rw-r--r--platform/www/lib/plugins/translation/lang/be/settings.php23
-rw-r--r--platform/www/lib/plugins/translation/lang/be/totranslate.txt1
-rw-r--r--platform/www/lib/plugins/translation/lang/bn/lang.php10
-rw-r--r--platform/www/lib/plugins/translation/lang/bn/settings.php10
-rw-r--r--platform/www/lib/plugins/translation/lang/bn/totranslate.txt1
-rw-r--r--platform/www/lib/plugins/translation/lang/ca/lang.php16
-rw-r--r--platform/www/lib/plugins/translation/lang/ca/settings.php19
-rw-r--r--platform/www/lib/plugins/translation/lang/ca/totranslate.txt1
-rw-r--r--platform/www/lib/plugins/translation/lang/cs/lang.php16
-rw-r--r--platform/www/lib/plugins/translation/lang/cs/settings.php19
-rw-r--r--platform/www/lib/plugins/translation/lang/cs/totranslate.txt1
-rw-r--r--platform/www/lib/plugins/translation/lang/cy/lang.php11
-rw-r--r--platform/www/lib/plugins/translation/lang/cy/settings.php18
-rw-r--r--platform/www/lib/plugins/translation/lang/cy/totranslate.txt1
-rw-r--r--platform/www/lib/plugins/translation/lang/da/lang.php12
-rw-r--r--platform/www/lib/plugins/translation/lang/da/settings.php20
-rw-r--r--platform/www/lib/plugins/translation/lang/da/totranslate.txt1
-rw-r--r--platform/www/lib/plugins/translation/lang/de-informal/lang.php16
-rw-r--r--platform/www/lib/plugins/translation/lang/de-informal/settings.php19
-rw-r--r--platform/www/lib/plugins/translation/lang/de-informal/totranslate.txt1
-rw-r--r--platform/www/lib/plugins/translation/lang/de/lang.php16
-rw-r--r--platform/www/lib/plugins/translation/lang/de/settings.php19
-rw-r--r--platform/www/lib/plugins/translation/lang/de/totranslate.txt1
-rw-r--r--platform/www/lib/plugins/translation/lang/en/lang.php11
-rw-r--r--platform/www/lib/plugins/translation/lang/en/settings.php20
-rw-r--r--platform/www/lib/plugins/translation/lang/en/totranslate.txt1
-rw-r--r--platform/www/lib/plugins/translation/lang/eo/lang.php11
-rw-r--r--platform/www/lib/plugins/translation/lang/eo/settings.php18
-rw-r--r--platform/www/lib/plugins/translation/lang/eo/totranslate.txt1
-rw-r--r--platform/www/lib/plugins/translation/lang/es/lang.php18
-rw-r--r--platform/www/lib/plugins/translation/lang/es/settings.php22
-rw-r--r--platform/www/lib/plugins/translation/lang/es/totranslate.txt1
-rw-r--r--platform/www/lib/plugins/translation/lang/fa/lang.php16
-rw-r--r--platform/www/lib/plugins/translation/lang/fa/settings.php19
-rw-r--r--platform/www/lib/plugins/translation/lang/fa/totranslate.txt1
-rw-r--r--platform/www/lib/plugins/translation/lang/fr/lang.php19
-rw-r--r--platform/www/lib/plugins/translation/lang/fr/settings.php22
-rw-r--r--platform/www/lib/plugins/translation/lang/fr/totranslate.txt1
-rw-r--r--platform/www/lib/plugins/translation/lang/hr/lang.php16
-rw-r--r--platform/www/lib/plugins/translation/lang/hr/settings.php19
-rw-r--r--platform/www/lib/plugins/translation/lang/hr/totranslate.txt1
-rw-r--r--platform/www/lib/plugins/translation/lang/hu/lang.php11
-rw-r--r--platform/www/lib/plugins/translation/lang/hu/settings.php18
-rw-r--r--platform/www/lib/plugins/translation/lang/hu/totranslate.txt1
-rw-r--r--platform/www/lib/plugins/translation/lang/it/lang.php10
-rw-r--r--platform/www/lib/plugins/translation/lang/it/settings.php20
-rw-r--r--platform/www/lib/plugins/translation/lang/it/totranslate.txt1
-rw-r--r--platform/www/lib/plugins/translation/lang/ja/lang.php16
-rw-r--r--platform/www/lib/plugins/translation/lang/ja/settings.php19
-rw-r--r--platform/www/lib/plugins/translation/lang/ja/totranslate.txt1
-rw-r--r--platform/www/lib/plugins/translation/lang/ko/lang.php16
-rw-r--r--platform/www/lib/plugins/translation/lang/ko/settings.php19
-rw-r--r--platform/www/lib/plugins/translation/lang/ko/totranslate.txt1
-rw-r--r--platform/www/lib/plugins/translation/lang/langnames.txt188
-rw-r--r--platform/www/lib/plugins/translation/lang/lv/lang.php11
-rw-r--r--platform/www/lib/plugins/translation/lang/lv/settings.php19
-rw-r--r--platform/www/lib/plugins/translation/lang/lv/totranslate.txt1
-rw-r--r--platform/www/lib/plugins/translation/lang/nl/lang.php17
-rw-r--r--platform/www/lib/plugins/translation/lang/nl/settings.php21
-rw-r--r--platform/www/lib/plugins/translation/lang/nl/totranslate.txt1
-rw-r--r--platform/www/lib/plugins/translation/lang/pt-br/lang.php17
-rw-r--r--platform/www/lib/plugins/translation/lang/pt-br/settings.php21
-rw-r--r--platform/www/lib/plugins/translation/lang/pt-br/totranslate.txt1
-rw-r--r--platform/www/lib/plugins/translation/lang/pt/lang.php11
-rw-r--r--platform/www/lib/plugins/translation/lang/pt/settings.php19
-rw-r--r--platform/www/lib/plugins/translation/lang/ru/lang.php18
-rw-r--r--platform/www/lib/plugins/translation/lang/ru/settings.php21
-rw-r--r--platform/www/lib/plugins/translation/lang/ru/totranslate.txt1
-rw-r--r--platform/www/lib/plugins/translation/lang/sl/lang.php9
-rw-r--r--platform/www/lib/plugins/translation/lang/sl/settings.php18
-rw-r--r--platform/www/lib/plugins/translation/lang/sv/lang.php16
-rw-r--r--platform/www/lib/plugins/translation/lang/sv/settings.php19
-rw-r--r--platform/www/lib/plugins/translation/lang/sv/totranslate.txt1
-rw-r--r--platform/www/lib/plugins/translation/lang/tr/lang.php10
-rw-r--r--platform/www/lib/plugins/translation/lang/tr/settings.php15
-rw-r--r--platform/www/lib/plugins/translation/lang/tr/totranslate.txt1
-rw-r--r--platform/www/lib/plugins/translation/lang/uk/lang.php17
-rw-r--r--platform/www/lib/plugins/translation/lang/uk/settings.php23
-rw-r--r--platform/www/lib/plugins/translation/lang/uk/totranslate.txt1
-rw-r--r--platform/www/lib/plugins/translation/lang/zh-tw/lang.php6
-rw-r--r--platform/www/lib/plugins/translation/lang/zh-tw/settings.php16
-rw-r--r--platform/www/lib/plugins/translation/lang/zh/lang.php18
-rw-r--r--platform/www/lib/plugins/translation/lang/zh/settings.php21
-rw-r--r--platform/www/lib/plugins/translation/lang/zh/totranslate.txt1
-rw-r--r--platform/www/lib/plugins/translation/manager.dat2
-rw-r--r--platform/www/lib/plugins/translation/plugin.info.txt8
-rw-r--r--platform/www/lib/plugins/translation/print.css1
-rw-r--r--platform/www/lib/plugins/translation/script.js20
-rw-r--r--platform/www/lib/plugins/translation/style.css114
-rw-r--r--platform/www/lib/plugins/translation/syntax/notrans.php92
-rw-r--r--platform/www/lib/plugins/translation/syntax/trans.php72
-rw-r--r--platform/www/lib/plugins/usermanager/admin.php1235
-rw-r--r--platform/www/lib/plugins/usermanager/admin.svg1
-rw-r--r--platform/www/lib/plugins/usermanager/images/search.pngbin0 -> 549 bytes
-rw-r--r--platform/www/lib/plugins/usermanager/lang/en/add.txt1
-rw-r--r--platform/www/lib/plugins/usermanager/lang/en/delete.txt1
-rw-r--r--platform/www/lib/plugins/usermanager/lang/en/edit.txt1
-rw-r--r--platform/www/lib/plugins/usermanager/lang/en/import.txt9
-rw-r--r--platform/www/lib/plugins/usermanager/lang/en/intro.txt1
-rw-r--r--platform/www/lib/plugins/usermanager/lang/en/lang.php86
-rw-r--r--platform/www/lib/plugins/usermanager/lang/en/list.txt1
-rw-r--r--platform/www/lib/plugins/usermanager/plugin.info.txt7
-rw-r--r--platform/www/lib/plugins/usermanager/script.js8
-rw-r--r--platform/www/lib/plugins/usermanager/style.css33
688 files changed, 29854 insertions, 0 deletions
diff --git a/platform/www/lib/plugins/acl/action.php b/platform/www/lib/plugins/acl/action.php
new file mode 100644
index 0000000..86e5870
--- /dev/null
+++ b/platform/www/lib/plugins/acl/action.php
@@ -0,0 +1,86 @@
+<?php
+/**
+ * AJAX call handler for ACL plugin
+ *
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+
+/**
+ * Register handler
+ */
+class action_plugin_acl extends DokuWiki_Action_Plugin
+{
+
+ /**
+ * Registers a callback function for a given event
+ *
+ * @param Doku_Event_Handler $controller DokuWiki's event controller object
+ * @return void
+ */
+ public function register(Doku_Event_Handler $controller)
+ {
+
+ $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, 'handleAjaxCallAcl');
+ }
+
+ /**
+ * AJAX call handler for ACL plugin
+ *
+ * @param Doku_Event $event event object by reference
+ * @param mixed $param empty
+ * @return void
+ */
+ public function handleAjaxCallAcl(Doku_Event $event, $param)
+ {
+ if ($event->data !== 'plugin_acl') {
+ return;
+ }
+ $event->stopPropagation();
+ $event->preventDefault();
+
+ global $ID;
+ global $INPUT;
+
+ /** @var $acl admin_plugin_acl */
+ $acl = plugin_load('admin', 'acl');
+ if (!$acl->isAccessibleByCurrentUser()) {
+ echo 'for admins only';
+ return;
+ }
+ if (!checkSecurityToken()) {
+ echo 'CRSF Attack';
+ return;
+ }
+
+ $ID = getID();
+ $acl->handle();
+
+ $ajax = $INPUT->str('ajax');
+ header('Content-Type: text/html; charset=utf-8');
+
+ if ($ajax == 'info') {
+ $acl->printInfo();
+ } elseif ($ajax == 'tree') {
+ $ns = $INPUT->str('ns');
+ if ($ns == '*') {
+ $ns = '';
+ }
+ $ns = cleanID($ns);
+ $lvl = count(explode(':', $ns));
+ $ns = utf8_encodeFN(str_replace(':', '/', $ns));
+
+ $data = $acl->makeTree($ns, $ns);
+
+ foreach (array_keys($data) as $item) {
+ $data[$item]['level'] = $lvl + 1;
+ }
+ echo html_buildlist(
+ $data,
+ 'acl',
+ array($acl, 'makeTreeItem'),
+ array($acl, 'makeListItem')
+ );
+ }
+ }
+}
diff --git a/platform/www/lib/plugins/acl/admin.php b/platform/www/lib/plugins/acl/admin.php
new file mode 100644
index 0000000..02842fd
--- /dev/null
+++ b/platform/www/lib/plugins/acl/admin.php
@@ -0,0 +1,858 @@
+<?php
+/**
+ * ACL administration functions
+ *
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @author Anika Henke <anika@selfthinker.org> (concepts)
+ * @author Frank Schubert <frank@schokilade.de> (old version)
+ */
+
+/**
+ * All DokuWiki plugins to extend the admin function
+ * need to inherit from this class
+ */
+class admin_plugin_acl extends DokuWiki_Admin_Plugin
+{
+ public $acl = null;
+ protected $ns = null;
+ /**
+ * The currently selected item, associative array with id and type.
+ * Populated from (in this order):
+ * $_REQUEST['current_ns']
+ * $_REQUEST['current_id']
+ * $ns
+ * $ID
+ */
+ protected $current_item = null;
+ protected $who = '';
+ protected $usersgroups = array();
+ protected $specials = array();
+
+ /**
+ * return prompt for admin menu
+ */
+ public function getMenuText($language)
+ {
+ return $this->getLang('admin_acl');
+ }
+
+ /**
+ * return sort order for position in admin menu
+ */
+ public function getMenuSort()
+ {
+ return 1;
+ }
+
+ /**
+ * handle user request
+ *
+ * Initializes internal vars and handles modifications
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+ public function handle()
+ {
+ global $AUTH_ACL;
+ global $ID;
+ global $auth;
+ global $config_cascade;
+ global $INPUT;
+
+ // fresh 1:1 copy without replacements
+ $AUTH_ACL = file($config_cascade['acl']['default']);
+
+ // namespace given?
+ if ($INPUT->str('ns') == '*') {
+ $this->ns = '*';
+ } else {
+ $this->ns = cleanID($INPUT->str('ns'));
+ }
+
+ if ($INPUT->str('current_ns')) {
+ $this->current_item = array('id' => cleanID($INPUT->str('current_ns')), 'type' => 'd');
+ } elseif ($INPUT->str('current_id')) {
+ $this->current_item = array('id' => cleanID($INPUT->str('current_id')), 'type' => 'f');
+ } elseif ($this->ns) {
+ $this->current_item = array('id' => $this->ns, 'type' => 'd');
+ } else {
+ $this->current_item = array('id' => $ID, 'type' => 'f');
+ }
+
+ // user or group choosen?
+ $who = trim($INPUT->str('acl_w'));
+ if ($INPUT->str('acl_t') == '__g__' && $who) {
+ $this->who = '@'.ltrim($auth->cleanGroup($who), '@');
+ } elseif ($INPUT->str('acl_t') == '__u__' && $who) {
+ $this->who = ltrim($who, '@');
+ if ($this->who != '%USER%' && $this->who != '%GROUP%') { #keep wildcard as is
+ $this->who = $auth->cleanUser($this->who);
+ }
+ } elseif ($INPUT->str('acl_t') &&
+ $INPUT->str('acl_t') != '__u__' &&
+ $INPUT->str('acl_t') != '__g__') {
+ $this->who = $INPUT->str('acl_t');
+ } elseif ($who) {
+ $this->who = $who;
+ }
+
+ // handle modifications
+ if ($INPUT->has('cmd') && checkSecurityToken()) {
+ $cmd = $INPUT->extract('cmd')->str('cmd');
+
+ // scope for modifications
+ if ($this->ns) {
+ if ($this->ns == '*') {
+ $scope = '*';
+ } else {
+ $scope = $this->ns.':*';
+ }
+ } else {
+ $scope = $ID;
+ }
+
+ if ($cmd == 'save' && $scope && $this->who && $INPUT->has('acl')) {
+ // handle additions or single modifications
+ $this->deleteACL($scope, $this->who);
+ $this->addOrUpdateACL($scope, $this->who, $INPUT->int('acl'));
+ } elseif ($cmd == 'del' && $scope && $this->who) {
+ // handle single deletions
+ $this->deleteACL($scope, $this->who);
+ } elseif ($cmd == 'update') {
+ $acl = $INPUT->arr('acl');
+
+ // handle update of the whole file
+ foreach ($INPUT->arr('del') as $where => $names) {
+ // remove all rules marked for deletion
+ foreach ($names as $who)
+ unset($acl[$where][$who]);
+ }
+ // prepare lines
+ $lines = array();
+ // keep header
+ foreach ($AUTH_ACL as $line) {
+ if ($line[0] == '#') {
+ $lines[] = $line;
+ } else {
+ break;
+ }
+ }
+ // re-add all rules
+ foreach ($acl as $where => $opt) {
+ foreach ($opt as $who => $perm) {
+ if ($who[0]=='@') {
+ if ($who!='@ALL') {
+ $who = '@'.ltrim($auth->cleanGroup($who), '@');
+ }
+ } elseif ($who != '%USER%' && $who != '%GROUP%') { #keep wildcard as is
+ $who = $auth->cleanUser($who);
+ }
+ $who = auth_nameencode($who, true);
+ $lines[] = "$where\t$who\t$perm\n";
+ }
+ }
+ // save it
+ io_saveFile($config_cascade['acl']['default'], join('', $lines));
+ }
+
+ // reload ACL config
+ $AUTH_ACL = file($config_cascade['acl']['default']);
+ }
+
+ // initialize ACL array
+ $this->initAclConfig();
+ }
+
+ /**
+ * ACL Output function
+ *
+ * print a table with all significant permissions for the
+ * current id
+ *
+ * @author Frank Schubert <frank@schokilade.de>
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+ public function html()
+ {
+ echo '<div id="acl_manager">'.NL;
+ echo '<h1>'.$this->getLang('admin_acl').'</h1>'.NL;
+ echo '<div class="level1">'.NL;
+
+ echo '<div id="acl__tree">'.NL;
+ $this->makeExplorer();
+ echo '</div>'.NL;
+
+ echo '<div id="acl__detail">'.NL;
+ $this->printDetail();
+ echo '</div>'.NL;
+ echo '</div>'.NL;
+
+ echo '<div class="clearer"></div>';
+ echo '<h2>'.$this->getLang('current').'</h2>'.NL;
+ echo '<div class="level2">'.NL;
+ $this->printAclTable();
+ echo '</div>'.NL;
+
+ echo '<div class="footnotes"><div class="fn">'.NL;
+ echo '<sup><a id="fn__1" class="fn_bot" href="#fnt__1">1)</a></sup>'.NL;
+ echo '<div class="content">'.$this->getLang('p_include').'</div>';
+ echo '</div></div>';
+
+ echo '</div>'.NL;
+ }
+
+ /**
+ * returns array with set options for building links
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+ protected function getLinkOptions($addopts = null)
+ {
+ $opts = array(
+ 'do'=>'admin',
+ 'page'=>'acl',
+ );
+ if ($this->ns) $opts['ns'] = $this->ns;
+ if ($this->who) $opts['acl_w'] = $this->who;
+
+ if (is_null($addopts)) return $opts;
+ return array_merge($opts, $addopts);
+ }
+
+ /**
+ * Display a tree menu to select a page or namespace
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+ protected function makeExplorer()
+ {
+ global $conf;
+ global $ID;
+ global $lang;
+
+ $ns = $this->ns;
+ if (empty($ns)) {
+ $ns = dirname(str_replace(':', '/', $ID));
+ if ($ns == '.') $ns ='';
+ } elseif ($ns == '*') {
+ $ns ='';
+ }
+ $ns = utf8_encodeFN(str_replace(':', '/', $ns));
+
+ $data = $this->makeTree($ns);
+
+ // wrap a list with the root level around the other namespaces
+ array_unshift($data, array( 'level' => 0, 'id' => '*', 'type' => 'd',
+ 'open' =>'true', 'label' => '['.$lang['mediaroot'].']'));
+
+ echo html_buildlist(
+ $data,
+ 'acl',
+ array($this, 'makeTreeItem'),
+ array($this, 'makeListItem')
+ );
+ }
+
+ /**
+ * get a combined list of media and page files
+ *
+ * also called via AJAX
+ *
+ * @param string $folder an already converted filesystem folder of the current namespace
+ * @param string $limit limit the search to this folder
+ * @return array
+ */
+ public function makeTree($folder, $limit = '')
+ {
+ global $conf;
+
+ // read tree structure from pages and media
+ $data = array();
+ search($data, $conf['datadir'], 'search_index', array('ns' => $folder), $limit);
+ $media = array();
+ search($media, $conf['mediadir'], 'search_index', array('ns' => $folder, 'nofiles' => true), $limit);
+ $data = array_merge($data, $media);
+ unset($media);
+
+ // combine by sorting and removing duplicates
+ usort($data, array($this, 'treeSort'));
+ $count = count($data);
+ if ($count>0) for ($i=1; $i<$count; $i++) {
+ if ($data[$i-1]['id'] == $data[$i]['id'] && $data[$i-1]['type'] == $data[$i]['type']) {
+ unset($data[$i]);
+ $i++; // duplicate found, next $i can't be a duplicate, so skip forward one
+ }
+ }
+ return $data;
+ }
+
+ /**
+ * usort callback
+ *
+ * Sorts the combined trees of media and page files
+ */
+ public function treeSort($a, $b)
+ {
+ // handle the trivial cases first
+ if ($a['id'] == '') return -1;
+ if ($b['id'] == '') return 1;
+ // split up the id into parts
+ $a_ids = explode(':', $a['id']);
+ $b_ids = explode(':', $b['id']);
+ // now loop through the parts
+ while (count($a_ids) && count($b_ids)) {
+ // compare each level from upper to lower
+ // until a non-equal component is found
+ $cur_result = strcmp(array_shift($a_ids), array_shift($b_ids));
+ if ($cur_result) {
+ // if one of the components is the last component and is a file
+ // and the other one is either of a deeper level or a directory,
+ // the file has to come after the deeper level or directory
+ if (empty($a_ids) && $a['type'] == 'f' && (count($b_ids) || $b['type'] == 'd')) return 1;
+ if (empty($b_ids) && $b['type'] == 'f' && (count($a_ids) || $a['type'] == 'd')) return -1;
+ return $cur_result;
+ }
+ }
+ // The two ids seem to be equal. One of them might however refer
+ // to a page, one to a namespace, the namespace needs to be first.
+ if (empty($a_ids) && empty($b_ids)) {
+ if ($a['type'] == $b['type']) return 0;
+ if ($a['type'] == 'f') return 1;
+ return -1;
+ }
+ // Now the empty part is either a page in the parent namespace
+ // that obviously needs to be after the namespace
+ // Or it is the namespace that contains the other part and should be
+ // before that other part.
+ if (empty($a_ids)) return ($a['type'] == 'd') ? -1 : 1;
+ if (empty($b_ids)) return ($b['type'] == 'd') ? 1 : -1;
+ return 0; //shouldn't happen
+ }
+
+ /**
+ * Display the current ACL for selected where/who combination with
+ * selectors and modification form
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+ protected function printDetail()
+ {
+ global $ID;
+
+ echo '<form action="'.wl().'" method="post" accept-charset="utf-8"><div class="no">'.NL;
+
+ echo '<div id="acl__user">';
+ echo $this->getLang('acl_perms').' ';
+ $inl = $this->makeSelect();
+ echo '<input type="text" name="acl_w" class="edit" value="'.(($inl)?'':hsc(ltrim($this->who, '@'))).'" />'.NL;
+ echo '<button type="submit">'.$this->getLang('btn_select').'</button>'.NL;
+ echo '</div>'.NL;
+
+ echo '<div id="acl__info">';
+ $this->printInfo();
+ echo '</div>';
+
+ echo '<input type="hidden" name="ns" value="'.hsc($this->ns).'" />'.NL;
+ echo '<input type="hidden" name="id" value="'.hsc($ID).'" />'.NL;
+ echo '<input type="hidden" name="do" value="admin" />'.NL;
+ echo '<input type="hidden" name="page" value="acl" />'.NL;
+ echo '<input type="hidden" name="sectok" value="'.getSecurityToken().'" />'.NL;
+ echo '</div></form>'.NL;
+ }
+
+ /**
+ * Print info and editor
+ *
+ * also loaded via Ajax
+ */
+ public function printInfo()
+ {
+ global $ID;
+
+ if ($this->who) {
+ $current = $this->getExactPermisson();
+
+ // explain current permissions
+ $this->printExplanation($current);
+ // load editor
+ $this->printAclEditor($current);
+ } else {
+ echo '<p>';
+ if ($this->ns) {
+ printf($this->getLang('p_choose_ns'), hsc($this->ns));
+ } else {
+ printf($this->getLang('p_choose_id'), hsc($ID));
+ }
+ echo '</p>';
+
+ echo $this->locale_xhtml('help');
+ }
+ }
+
+ /**
+ * Display the ACL editor
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+ protected function printAclEditor($current)
+ {
+ global $lang;
+
+ echo '<fieldset>';
+ if (is_null($current)) {
+ echo '<legend>'.$this->getLang('acl_new').'</legend>';
+ } else {
+ echo '<legend>'.$this->getLang('acl_mod').'</legend>';
+ }
+
+ echo $this->makeCheckboxes($current, empty($this->ns), 'acl');
+
+ if (is_null($current)) {
+ echo '<button type="submit" name="cmd[save]">'.$lang['btn_save'].'</button>'.NL;
+ } else {
+ echo '<button type="submit" name="cmd[save]">'.$lang['btn_update'].'</button>'.NL;
+ echo '<button type="submit" name="cmd[del]">'.$lang['btn_delete'].'</button>'.NL;
+ }
+
+ echo '</fieldset>';
+ }
+
+ /**
+ * Explain the currently set permissions in plain english/$lang
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+ protected function printExplanation($current)
+ {
+ global $ID;
+ global $auth;
+
+ $who = $this->who;
+ $ns = $this->ns;
+
+ // prepare where to check
+ if ($ns) {
+ if ($ns == '*') {
+ $check='*';
+ } else {
+ $check=$ns.':*';
+ }
+ } else {
+ $check = $ID;
+ }
+
+ // prepare who to check
+ if ($who[0] == '@') {
+ $user = '';
+ $groups = array(ltrim($who, '@'));
+ } else {
+ $user = $who;
+ $info = $auth->getUserData($user);
+ if ($info === false) {
+ $groups = array();
+ } else {
+ $groups = $info['grps'];
+ }
+ }
+
+ // check the permissions
+ $perm = auth_aclcheck($check, $user, $groups);
+
+ // build array of named permissions
+ $names = array();
+ if ($perm) {
+ if ($ns) {
+ if ($perm >= AUTH_DELETE) $names[] = $this->getLang('acl_perm16');
+ if ($perm >= AUTH_UPLOAD) $names[] = $this->getLang('acl_perm8');
+ if ($perm >= AUTH_CREATE) $names[] = $this->getLang('acl_perm4');
+ }
+ if ($perm >= AUTH_EDIT) $names[] = $this->getLang('acl_perm2');
+ if ($perm >= AUTH_READ) $names[] = $this->getLang('acl_perm1');
+ $names = array_reverse($names);
+ } else {
+ $names[] = $this->getLang('acl_perm0');
+ }
+
+ // print permission explanation
+ echo '<p>';
+ if ($user) {
+ if ($ns) {
+ printf($this->getLang('p_user_ns'), hsc($who), hsc($ns), join(', ', $names));
+ } else {
+ printf($this->getLang('p_user_id'), hsc($who), hsc($ID), join(', ', $names));
+ }
+ } else {
+ if ($ns) {
+ printf($this->getLang('p_group_ns'), hsc(ltrim($who, '@')), hsc($ns), join(', ', $names));
+ } else {
+ printf($this->getLang('p_group_id'), hsc(ltrim($who, '@')), hsc($ID), join(', ', $names));
+ }
+ }
+ echo '</p>';
+
+ // add note if admin
+ if ($perm == AUTH_ADMIN) {
+ echo '<p>'.$this->getLang('p_isadmin').'</p>';
+ } elseif (is_null($current)) {
+ echo '<p>'.$this->getLang('p_inherited').'</p>';
+ }
+ }
+
+
+ /**
+ * Item formatter for the tree view
+ *
+ * User function for html_buildlist()
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+ public function makeTreeItem($item)
+ {
+ $ret = '';
+ // what to display
+ if (!empty($item['label'])) {
+ $base = $item['label'];
+ } else {
+ $base = ':'.$item['id'];
+ $base = substr($base, strrpos($base, ':')+1);
+ }
+
+ // highlight?
+ if (($item['type']== $this->current_item['type'] && $item['id'] == $this->current_item['id'])) {
+ $cl = ' cur';
+ } else {
+ $cl = '';
+ }
+
+ // namespace or page?
+ if ($item['type']=='d') {
+ if ($item['open']) {
+ $img = DOKU_BASE.'lib/images/minus.gif';
+ $alt = '−';
+ } else {
+ $img = DOKU_BASE.'lib/images/plus.gif';
+ $alt = '+';
+ }
+ $ret .= '<img src="'.$img.'" alt="'.$alt.'" />';
+ $ret .= '<a href="'.
+ wl('', $this->getLinkOptions(array('ns'=> $item['id'], 'sectok'=>getSecurityToken()))).
+ '" class="idx_dir'.$cl.'">';
+ $ret .= $base;
+ $ret .= '</a>';
+ } else {
+ $ret .= '<a href="'.
+ wl('', $this->getLinkOptions(array('id'=> $item['id'], 'ns'=>'', 'sectok'=>getSecurityToken()))).
+ '" class="wikilink1'.$cl.'">';
+ $ret .= noNS($item['id']);
+ $ret .= '</a>';
+ }
+ return $ret;
+ }
+
+ /**
+ * List Item formatter
+ *
+ * @param array $item
+ * @return string
+ */
+ public function makeListItem($item)
+ {
+ return '<li class="level' . $item['level'] . ' ' .
+ ($item['open'] ? 'open' : 'closed') . '">';
+ }
+
+
+ /**
+ * Get current ACL settings as multidim array
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+ public function initAclConfig()
+ {
+ global $AUTH_ACL;
+ global $conf;
+ $acl_config=array();
+ $usersgroups = array();
+
+ // get special users and groups
+ $this->specials[] = '@ALL';
+ $this->specials[] = '@'.$conf['defaultgroup'];
+ if ($conf['manager'] != '!!not set!!') {
+ $this->specials = array_merge(
+ $this->specials,
+ array_map(
+ 'trim',
+ explode(',', $conf['manager'])
+ )
+ );
+ }
+ $this->specials = array_filter($this->specials);
+ $this->specials = array_unique($this->specials);
+ sort($this->specials);
+
+ foreach ($AUTH_ACL as $line) {
+ $line = trim(preg_replace('/#.*$/', '', $line)); //ignore comments
+ if (!$line) continue;
+
+ $acl = preg_split('/[ \t]+/', $line);
+ //0 is pagename, 1 is user, 2 is acl
+
+ $acl[1] = rawurldecode($acl[1]);
+ $acl_config[$acl[0]][$acl[1]] = $acl[2];
+
+ // store non-special users and groups for later selection dialog
+ $ug = $acl[1];
+ if (in_array($ug, $this->specials)) continue;
+ $usersgroups[] = $ug;
+ }
+
+ $usersgroups = array_unique($usersgroups);
+ sort($usersgroups);
+ ksort($acl_config);
+
+ $this->acl = $acl_config;
+ $this->usersgroups = $usersgroups;
+ }
+
+ /**
+ * Display all currently set permissions in a table
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+ protected function printAclTable()
+ {
+ global $lang;
+ global $ID;
+
+ echo '<form action="'.wl().'" method="post" accept-charset="utf-8"><div class="no">'.NL;
+ if ($this->ns) {
+ echo '<input type="hidden" name="ns" value="'.hsc($this->ns).'" />'.NL;
+ } else {
+ echo '<input type="hidden" name="id" value="'.hsc($ID).'" />'.NL;
+ }
+ echo '<input type="hidden" name="acl_w" value="'.hsc($this->who).'" />'.NL;
+ echo '<input type="hidden" name="do" value="admin" />'.NL;
+ echo '<input type="hidden" name="page" value="acl" />'.NL;
+ echo '<input type="hidden" name="sectok" value="'.getSecurityToken().'" />'.NL;
+ echo '<div class="table">';
+ echo '<table class="inline">';
+ echo '<tr>';
+ echo '<th>'.$this->getLang('where').'</th>';
+ echo '<th>'.$this->getLang('who').'</th>';
+ echo '<th>'.$this->getLang('perm').'<sup><a id="fnt__1" class="fn_top" href="#fn__1">1)</a></sup></th>';
+ echo '<th>'.$lang['btn_delete'].'</th>';
+ echo '</tr>';
+ foreach ($this->acl as $where => $set) {
+ foreach ($set as $who => $perm) {
+ echo '<tr>';
+ echo '<td>';
+ if (substr($where, -1) == '*') {
+ echo '<span class="aclns">'.hsc($where).'</span>';
+ $ispage = false;
+ } else {
+ echo '<span class="aclpage">'.hsc($where).'</span>';
+ $ispage = true;
+ }
+ echo '</td>';
+
+ echo '<td>';
+ if ($who[0] == '@') {
+ echo '<span class="aclgroup">'.hsc($who).'</span>';
+ } else {
+ echo '<span class="acluser">'.hsc($who).'</span>';
+ }
+ echo '</td>';
+
+ echo '<td>';
+ echo $this->makeCheckboxes($perm, $ispage, 'acl['.$where.']['.$who.']');
+ echo '</td>';
+
+ echo '<td class="check">';
+ echo '<input type="checkbox" name="del['.hsc($where).'][]" value="'.hsc($who).'" />';
+ echo '</td>';
+ echo '</tr>';
+ }
+ }
+
+ echo '<tr>';
+ echo '<th class="action" colspan="4">';
+ echo '<button type="submit" name="cmd[update]">'.$lang['btn_update'].'</button>';
+ echo '</th>';
+ echo '</tr>';
+ echo '</table>';
+ echo '</div>';
+ echo '</div></form>'.NL;
+ }
+
+ /**
+ * Returns the permission which were set for exactly the given user/group
+ * and page/namespace. Returns null if no exact match is available
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+ protected function getExactPermisson()
+ {
+ global $ID;
+ if ($this->ns) {
+ if ($this->ns == '*') {
+ $check = '*';
+ } else {
+ $check = $this->ns.':*';
+ }
+ } else {
+ $check = $ID;
+ }
+
+ if (isset($this->acl[$check][$this->who])) {
+ return $this->acl[$check][$this->who];
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * adds new acl-entry to conf/acl.auth.php
+ *
+ * @author Frank Schubert <frank@schokilade.de>
+ */
+ public function addOrUpdateACL($acl_scope, $acl_user, $acl_level)
+ {
+ global $config_cascade;
+
+ // first make sure we won't end up with 2 lines matching this user and scope. See issue #1115
+ $this->deleteACL($acl_scope, $acl_user);
+ $acl_user = auth_nameencode($acl_user, true);
+
+ // max level for pagenames is edit
+ if (strpos($acl_scope, '*') === false) {
+ if ($acl_level > AUTH_EDIT) $acl_level = AUTH_EDIT;
+ }
+
+ $new_acl = "$acl_scope\t$acl_user\t$acl_level\n";
+
+ return io_saveFile($config_cascade['acl']['default'], $new_acl, true);
+ }
+
+ /**
+ * remove acl-entry from conf/acl.auth.php
+ *
+ * @author Frank Schubert <frank@schokilade.de>
+ */
+ public function deleteACL($acl_scope, $acl_user)
+ {
+ global $config_cascade;
+ $acl_user = auth_nameencode($acl_user, true);
+
+ $acl_pattern = '^'.preg_quote($acl_scope, '/').'[ \t]+'.$acl_user.'[ \t]+[0-8].*$';
+
+ return io_deleteFromFile($config_cascade['acl']['default'], "/$acl_pattern/", true);
+ }
+
+ /**
+ * print the permission radio boxes
+ *
+ * @author Frank Schubert <frank@schokilade.de>
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+ protected function makeCheckboxes($setperm, $ispage, $name)
+ {
+ global $lang;
+
+ static $label = 0; //number labels
+ $ret = '';
+
+ if ($ispage && $setperm > AUTH_EDIT) $setperm = AUTH_EDIT;
+
+ foreach (array(AUTH_NONE,AUTH_READ,AUTH_EDIT,AUTH_CREATE,AUTH_UPLOAD,AUTH_DELETE) as $perm) {
+ $label += 1;
+
+ //general checkbox attributes
+ $atts = array( 'type' => 'radio',
+ 'id' => 'pbox'.$label,
+ 'name' => $name,
+ 'value' => $perm );
+ //dynamic attributes
+ if (!is_null($setperm) && $setperm == $perm) $atts['checked'] = 'checked';
+ if ($ispage && $perm > AUTH_EDIT) {
+ $atts['disabled'] = 'disabled';
+ $class = ' class="disabled"';
+ } else {
+ $class = '';
+ }
+
+ //build code
+ $ret .= '<label for="pbox'.$label.'"'.$class.'>';
+ $ret .= '<input '.buildAttributes($atts).' />&#160;';
+ $ret .= $this->getLang('acl_perm'.$perm);
+ $ret .= '</label>'.NL;
+ }
+ return $ret;
+ }
+
+ /**
+ * Print a user/group selector (reusing already used users and groups)
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+ protected function makeSelect()
+ {
+ $inlist = false;
+ $usel = '';
+ $gsel = '';
+
+ if ($this->who &&
+ !in_array($this->who, $this->usersgroups) &&
+ !in_array($this->who, $this->specials)) {
+ if ($this->who[0] == '@') {
+ $gsel = ' selected="selected"';
+ } else {
+ $usel = ' selected="selected"';
+ }
+ } else {
+ $inlist = true;
+ }
+
+ echo '<select name="acl_t" class="edit">'.NL;
+ echo ' <option value="__g__" class="aclgroup"'.$gsel.'>'.$this->getLang('acl_group').'</option>'.NL;
+ echo ' <option value="__u__" class="acluser"'.$usel.'>'.$this->getLang('acl_user').'</option>'.NL;
+ if (!empty($this->specials)) {
+ echo ' <optgroup label="&#160;">'.NL;
+ foreach ($this->specials as $ug) {
+ if ($ug == $this->who) {
+ $sel = ' selected="selected"';
+ $inlist = true;
+ } else {
+ $sel = '';
+ }
+
+ if ($ug[0] == '@') {
+ echo ' <option value="'.hsc($ug).'" class="aclgroup"'.$sel.'>'.hsc($ug).'</option>'.NL;
+ } else {
+ echo ' <option value="'.hsc($ug).'" class="acluser"'.$sel.'>'.hsc($ug).'</option>'.NL;
+ }
+ }
+ echo ' </optgroup>'.NL;
+ }
+ if (!empty($this->usersgroups)) {
+ echo ' <optgroup label="&#160;">'.NL;
+ foreach ($this->usersgroups as $ug) {
+ if ($ug == $this->who) {
+ $sel = ' selected="selected"';
+ $inlist = true;
+ } else {
+ $sel = '';
+ }
+
+ if ($ug[0] == '@') {
+ echo ' <option value="'.hsc($ug).'" class="aclgroup"'.$sel.'>'.hsc($ug).'</option>'.NL;
+ } else {
+ echo ' <option value="'.hsc($ug).'" class="acluser"'.$sel.'>'.hsc($ug).'</option>'.NL;
+ }
+ }
+ echo ' </optgroup>'.NL;
+ }
+ echo '</select>'.NL;
+ return $inlist;
+ }
+}
diff --git a/platform/www/lib/plugins/acl/admin.svg b/platform/www/lib/plugins/acl/admin.svg
new file mode 100644
index 0000000..b5cf001
--- /dev/null
+++ b/platform/www/lib/plugins/acl/admin.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M22 18v4h-4v-3h-3v-3h-3l-2.26-2.26c-.55.17-1.13.26-1.74.26a6 6 0 0 1-6-6 6 6 0 0 1 6-6 6 6 0 0 1 6 6c0 .61-.09 1.19-.26 1.74L22 18M7 5a2 2 0 0 0-2 2 2 2 0 0 0 2 2 2 2 0 0 0 2-2 2 2 0 0 0-2-2z"/></svg> \ No newline at end of file
diff --git a/platform/www/lib/plugins/acl/lang/en/help.txt b/platform/www/lib/plugins/acl/lang/en/help.txt
new file mode 100644
index 0000000..e865bbb
--- /dev/null
+++ b/platform/www/lib/plugins/acl/lang/en/help.txt
@@ -0,0 +1,9 @@
+=== Quick Help: ===
+
+On this page you can add and remove permissions for namespaces and pages in your wiki.
+ * The left pane displays all available namespaces and pages.
+ * The form above allows you to see and modify the permissions of a selected user or group.
+ * In the table below all currently set access control rules are shown. You can use it to quickly delete or change multiple rules.
+
+Reading the [[doku>acl|official documentation on ACL]] might help you to fully understand how access control works in DokuWiki.
+
diff --git a/platform/www/lib/plugins/acl/lang/en/lang.php b/platform/www/lib/plugins/acl/lang/en/lang.php
new file mode 100644
index 0000000..0c86489
--- /dev/null
+++ b/platform/www/lib/plugins/acl/lang/en/lang.php
@@ -0,0 +1,46 @@
+<?php
+/**
+ * english language file
+ *
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @author Anika Henke <anika@selfthinker.org>
+ * @author Matthias Grimm <matthiasgrimm@users.sourceforge.net>
+ */
+
+$lang['admin_acl'] = 'Access Control List Management';
+$lang['acl_group'] = 'Group:';
+$lang['acl_user'] = 'User:';
+$lang['acl_perms'] = 'Permissions for';
+$lang['page'] = 'Page';
+$lang['namespace'] = 'Namespace';
+
+$lang['btn_select'] = 'Select';
+
+$lang['p_user_id'] = 'User <b class="acluser">%s</b> currently has the following permissions on page <b class="aclpage">%s</b>: <i>%s</i>.';
+$lang['p_user_ns'] = 'User <b class="acluser">%s</b> currently has the following permissions in namespace <b class="aclns">%s</b>: <i>%s</i>.';
+$lang['p_group_id'] = 'Members of group <b class="aclgroup">%s</b> currently have the following permissions on page <b class="aclpage">%s</b>: <i>%s</i>.';
+$lang['p_group_ns'] = 'Members of group <b class="aclgroup">%s</b> currently have the following permissions in namespace <b class="aclns">%s</b>: <i>%s</i>.';
+
+$lang['p_choose_id'] = 'Please <b>enter a user or group</b> in the form above to view or edit the permissions set for the page <b class="aclpage">%s</b>.';
+$lang['p_choose_ns'] = 'Please <b>enter a user or group</b> in the form above to view or edit the permissions set for the namespace <b class="aclns">%s</b>.';
+
+
+$lang['p_inherited'] = 'Note: Those permissions were not set explicitly but were inherited from other groups or higher namespaces.';
+$lang['p_isadmin'] = 'Note: The selected group or user has always full permissions because it is configured as superuser.';
+$lang['p_include'] = 'Higher permissions include lower ones. Create, Upload and Delete permissions only apply to namespaces, not pages.';
+
+$lang['current'] = 'Current ACL Rules';
+$lang['where'] = 'Page/Namespace';
+$lang['who'] = 'User/Group';
+$lang['perm'] = 'Permissions';
+
+$lang['acl_perm0'] = 'None';
+$lang['acl_perm1'] = 'Read';
+$lang['acl_perm2'] = 'Edit';
+$lang['acl_perm4'] = 'Create';
+$lang['acl_perm8'] = 'Upload';
+$lang['acl_perm16'] = 'Delete';
+$lang['acl_new'] = 'Add new Entry';
+$lang['acl_mod'] = 'Modify Entry';
+//Setup VIM: ex: et ts=2 :
diff --git a/platform/www/lib/plugins/acl/pix/group.png b/platform/www/lib/plugins/acl/pix/group.png
new file mode 100644
index 0000000..348d4e5
--- /dev/null
+++ b/platform/www/lib/plugins/acl/pix/group.png
Binary files differ
diff --git a/platform/www/lib/plugins/acl/pix/ns.png b/platform/www/lib/plugins/acl/pix/ns.png
new file mode 100644
index 0000000..77e03b1
--- /dev/null
+++ b/platform/www/lib/plugins/acl/pix/ns.png
Binary files differ
diff --git a/platform/www/lib/plugins/acl/pix/page.png b/platform/www/lib/plugins/acl/pix/page.png
new file mode 100644
index 0000000..b1b7ebe
--- /dev/null
+++ b/platform/www/lib/plugins/acl/pix/page.png
Binary files differ
diff --git a/platform/www/lib/plugins/acl/pix/user.png b/platform/www/lib/plugins/acl/pix/user.png
new file mode 100644
index 0000000..8d5d1c2
--- /dev/null
+++ b/platform/www/lib/plugins/acl/pix/user.png
Binary files differ
diff --git a/platform/www/lib/plugins/acl/plugin.info.txt b/platform/www/lib/plugins/acl/plugin.info.txt
new file mode 100644
index 0000000..1b2c82c
--- /dev/null
+++ b/platform/www/lib/plugins/acl/plugin.info.txt
@@ -0,0 +1,7 @@
+base acl
+author Andreas Gohr
+email andi@splitbrain.org
+date 2015-07-25
+name ACL Manager
+desc Manage Page Access Control Lists
+url http://dokuwiki.org/plugin:acl
diff --git a/platform/www/lib/plugins/acl/remote.php b/platform/www/lib/plugins/acl/remote.php
new file mode 100644
index 0000000..8d19add
--- /dev/null
+++ b/platform/www/lib/plugins/acl/remote.php
@@ -0,0 +1,102 @@
+<?php
+
+use dokuwiki\Remote\AccessDeniedException;
+
+/**
+ * Class remote_plugin_acl
+ */
+class remote_plugin_acl extends DokuWiki_Remote_Plugin
+{
+
+ /**
+ * Returns details about the remote plugin methods
+ *
+ * @return array Information about all provided methods. {@see dokuwiki\Remote\RemoteAPI}
+ */
+ public function _getMethods()
+ {
+ return array(
+ 'listAcls' => array(
+ 'args' => array(),
+ 'return' => 'Array of ACLs {scope, user, permission}',
+ 'name' => 'listAcls',
+ 'doc' => 'Get the list of all ACLs',
+ ),'addAcl' => array(
+ 'args' => array('string','string','int'),
+ 'return' => 'int',
+ 'name' => 'addAcl',
+ 'doc' => 'Adds a new ACL rule.'
+ ), 'delAcl' => array(
+ 'args' => array('string','string'),
+ 'return' => 'int',
+ 'name' => 'delAcl',
+ 'doc' => 'Delete an existing ACL rule.'
+ ),
+ );
+ }
+
+ /**
+ * List all ACL config entries
+ *
+ * @throws AccessDeniedException
+ * @return dictionary {Scope: ACL}, where ACL = dictionnary {user/group: permissions_int}
+ */
+ public function listAcls()
+ {
+ if (!auth_isadmin()) {
+ throw new AccessDeniedException(
+ 'You are not allowed to access ACLs, superuser permission is required',
+ 114
+ );
+ }
+ /** @var admin_plugin_acl $apa */
+ $apa = plugin_load('admin', 'acl');
+ $apa->initAclConfig();
+ return $apa->acl;
+ }
+
+ /**
+ * Add a new entry to ACL config
+ *
+ * @param string $scope
+ * @param string $user
+ * @param int $level see also inc/auth.php
+ * @throws AccessDeniedException
+ * @return bool
+ */
+ public function addAcl($scope, $user, $level)
+ {
+ if (!auth_isadmin()) {
+ throw new AccessDeniedException(
+ 'You are not allowed to access ACLs, superuser permission is required',
+ 114
+ );
+ }
+
+ /** @var admin_plugin_acl $apa */
+ $apa = plugin_load('admin', 'acl');
+ return $apa->addOrUpdateACL($scope, $user, $level);
+ }
+
+ /**
+ * Remove an entry from ACL config
+ *
+ * @param string $scope
+ * @param string $user
+ * @throws AccessDeniedException
+ * @return bool
+ */
+ public function delAcl($scope, $user)
+ {
+ if (!auth_isadmin()) {
+ throw new AccessDeniedException(
+ 'You are not allowed to access ACLs, superuser permission is required',
+ 114
+ );
+ }
+
+ /** @var admin_plugin_acl $apa */
+ $apa = plugin_load('admin', 'acl');
+ return $apa->deleteACL($scope, $user);
+ }
+}
diff --git a/platform/www/lib/plugins/acl/script.js b/platform/www/lib/plugins/acl/script.js
new file mode 100644
index 0000000..95621a2
--- /dev/null
+++ b/platform/www/lib/plugins/acl/script.js
@@ -0,0 +1,121 @@
+/**
+ * ACL Manager AJAX enhancements
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+var dw_acl = {
+ /**
+ * Initialize the object and attach the event handlers
+ */
+ init: function () {
+ var $tree;
+
+ //FIXME only one underscore!!
+ if (jQuery('#acl_manager').length === 0) {
+ return;
+ }
+
+ jQuery('#acl__user select').on('change', dw_acl.userselhandler);
+ jQuery('#acl__user button').on('click', dw_acl.loadinfo);
+
+ $tree = jQuery('#acl__tree');
+ $tree.dw_tree({toggle_selector: 'img',
+ load_data: function (show_sublist, $clicky) {
+ // get the enclosed link and the edit form
+ var $frm = jQuery('#acl__detail form');
+
+ jQuery.post(
+ DOKU_BASE + 'lib/exe/ajax.php',
+ jQuery.extend(dw_acl.parseatt($clicky.parent().find('a')[0].search),
+ {call: 'plugin_acl',
+ ajax: 'tree',
+ current_ns: $frm.find('input[name=ns]').val(),
+ current_id: $frm.find('input[name=id]').val()}),
+ show_sublist,
+ 'html'
+ );
+ },
+
+ toggle_display: function ($clicky, opening) {
+ $clicky.attr('src',
+ DOKU_BASE + 'lib/images/' +
+ (opening ? 'minus' : 'plus') + '.gif');
+ }});
+ $tree.delegate('a', 'click', dw_acl.treehandler);
+ },
+
+ /**
+ * Handle user dropdown
+ *
+ * Hides or shows the user/group entry box depending on what was selected in the
+ * dropdown element
+ */
+ userselhandler: function () {
+ // make entry field visible/invisible
+ jQuery('#acl__user input').toggle(this.value === '__g__' ||
+ this.value === '__u__');
+ dw_acl.loadinfo();
+ },
+
+ /**
+ * Load the current permission info and edit form
+ */
+ loadinfo: function () {
+ jQuery('#acl__info')
+ .attr('role', 'alert')
+ .html('<img src="'+DOKU_BASE+'lib/images/throbber.gif" alt="..." />')
+ .load(
+ DOKU_BASE + 'lib/exe/ajax.php',
+ jQuery('#acl__detail form').serialize() + '&call=plugin_acl&ajax=info'
+ );
+ return false;
+ },
+
+ /**
+ * parse URL attributes into a associative array
+ *
+ * @todo put into global script lib?
+ */
+ parseatt: function (str) {
+ if (str[0] === '?') {
+ str = str.substr(1);
+ }
+ var attributes = {};
+ var all = str.split('&');
+ for (var i = 0; i < all.length; i++) {
+ var att = all[i].split('=');
+ attributes[att[0]] = decodeURIComponent(att[1]);
+ }
+ return attributes;
+ },
+
+ /**
+ * Handles clicks to the tree nodes
+ */
+ treehandler: function () {
+ var $link, $frm;
+
+ $link = jQuery(this);
+
+ // remove highlighting
+ jQuery('#acl__tree a.cur').removeClass('cur');
+
+ // add new highlighting
+ $link.addClass('cur');
+
+ // set new page to detail form
+ $frm = jQuery('#acl__detail form');
+ if ($link.hasClass('wikilink1')) {
+ $frm.find('input[name=ns]').val('');
+ $frm.find('input[name=id]').val(dw_acl.parseatt($link[0].search).id);
+ } else if ($link.hasClass('idx_dir')) {
+ $frm.find('input[name=ns]').val(dw_acl.parseatt($link[0].search).ns);
+ $frm.find('input[name=id]').val('');
+ }
+ dw_acl.loadinfo();
+
+ return false;
+ }
+};
+
+jQuery(dw_acl.init);
diff --git a/platform/www/lib/plugins/acl/style.css b/platform/www/lib/plugins/acl/style.css
new file mode 100644
index 0000000..4233cd3
--- /dev/null
+++ b/platform/www/lib/plugins/acl/style.css
@@ -0,0 +1,135 @@
+#acl__tree {
+ font-size: 90%;
+ width: 25%;
+ height: 300px;
+ float: left;
+ overflow: auto;
+ border: 1px solid __border__;
+ text-align: left;
+}
+[dir=rtl] #acl__tree {
+ float: right;
+ text-align: right;
+}
+
+#acl__tree a.cur {
+ background-color: __highlight__;
+ font-weight: bold;
+}
+
+#acl__tree ul {
+ list-style-type: none;
+ margin: 0;
+ padding: 0;
+}
+
+#acl__tree li {
+ padding-left: 1em;
+ list-style-image: none;
+}
+[dir=rtl] #acl__tree li {
+ padding-left: 0em;
+ padding-right: 1em;
+}
+
+#acl__tree ul img {
+ margin-right: 0.25em;
+ cursor: pointer;
+}
+[dir=rtl] #acl__tree ul img {
+ margin-left: 0.25em;
+ margin-right: 0em;
+}
+
+#acl__detail {
+ width: 73%;
+ height: 300px;
+ float: right;
+ overflow: auto;
+}
+[dir=rtl] #acl__detail {
+ float: left;
+}
+
+#acl__detail fieldset {
+ width: 90%;
+}
+
+#acl__detail div#acl__user {
+ border: 1px solid __border__;
+ padding: 0.5em;
+ margin-bottom: 0.6em;
+}
+
+#acl_manager table.inline {
+ width: 100%;
+ margin: 0;
+}
+
+#acl_manager table .check {
+ text-align: center;
+}
+
+#acl_manager table .action {
+ text-align: right;
+}
+
+#acl_manager .aclgroup {
+ background: transparent url(pix/group.png) 0px 1px no-repeat;
+ padding: 1px 0px 1px 18px;
+}
+[dir=rtl] #acl_manager .aclgroup {
+ background: transparent url(pix/group.png) right 1px no-repeat;
+ padding: 1px 18px 1px 0px;
+}
+
+#acl_manager .acluser {
+ background: transparent url(pix/user.png) 0px 1px no-repeat;
+ padding: 1px 0px 1px 18px;
+}
+[dir=rtl] #acl_manager .acluser {
+ background: transparent url(pix/user.png) right 1px no-repeat;
+ padding: 1px 18px 1px 0px;
+}
+
+#acl_manager .aclpage {
+ background: transparent url(pix/page.png) 0px 1px no-repeat;
+ padding: 1px 0px 1px 18px;
+}
+[dir=rtl] #acl_manager .aclpage {
+ background: transparent url(pix/page.png) right 1px no-repeat;
+ padding: 1px 18px 1px 0px;
+}
+
+#acl_manager .aclns {
+ background: transparent url(pix/ns.png) 0px 1px no-repeat;
+ padding: 1px 0px 1px 18px;
+}
+[dir=rtl] #acl_manager .aclns {
+ background: transparent url(pix/ns.png) right 1px no-repeat;
+ padding: 1px 18px 1px 0px;
+}
+
+#acl_manager label.disabled {
+ opacity: .5;
+ cursor: auto;
+}
+
+#acl_manager label {
+ text-align: left;
+ font-weight: normal;
+ display: inline;
+}
+
+#acl_manager table {
+ margin-left: 10%;
+ width: 80%;
+}
+
+#acl_manager table tr {
+ background-color: inherit;
+}
+
+#acl_manager table tr:hover {
+ background-color: __background_alt__;
+}
diff --git a/platform/www/lib/plugins/action.php b/platform/www/lib/plugins/action.php
new file mode 100644
index 0000000..a3cbec7
--- /dev/null
+++ b/platform/www/lib/plugins/action.php
@@ -0,0 +1,2 @@
+<?php
+dbg_deprecated('Autoloading. Do not require() files yourself.');
diff --git a/platform/www/lib/plugins/admin.php b/platform/www/lib/plugins/admin.php
new file mode 100644
index 0000000..a3cbec7
--- /dev/null
+++ b/platform/www/lib/plugins/admin.php
@@ -0,0 +1,2 @@
+<?php
+dbg_deprecated('Autoloading. Do not require() files yourself.');
diff --git a/platform/www/lib/plugins/auth.php b/platform/www/lib/plugins/auth.php
new file mode 100644
index 0000000..a3cbec7
--- /dev/null
+++ b/platform/www/lib/plugins/auth.php
@@ -0,0 +1,2 @@
+<?php
+dbg_deprecated('Autoloading. Do not require() files yourself.');
diff --git a/platform/www/lib/plugins/authad/action.php b/platform/www/lib/plugins/authad/action.php
new file mode 100644
index 0000000..a9fc01c
--- /dev/null
+++ b/platform/www/lib/plugins/authad/action.php
@@ -0,0 +1,90 @@
+<?php
+/**
+ * DokuWiki Plugin addomain (Action Component)
+ *
+ * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
+ * @author Andreas Gohr <gohr@cosmocode.de>
+ */
+
+/**
+ * Class action_plugin_addomain
+ */
+class action_plugin_authad extends DokuWiki_Action_Plugin
+{
+
+ /**
+ * Registers a callback function for a given event
+ */
+ public function register(Doku_Event_Handler $controller)
+ {
+
+ $controller->register_hook('AUTH_LOGIN_CHECK', 'BEFORE', $this, 'handleAuthLoginCheck');
+ $controller->register_hook('HTML_LOGINFORM_OUTPUT', 'BEFORE', $this, 'handleHtmlLoginformOutput');
+ }
+
+ /**
+ * Adds the selected domain as user postfix when attempting a login
+ *
+ * @param Doku_Event $event
+ * @param array $param
+ */
+ public function handleAuthLoginCheck(Doku_Event $event, $param)
+ {
+ global $INPUT;
+
+ /** @var auth_plugin_authad $auth */
+ global $auth;
+ if (!is_a($auth, 'auth_plugin_authad')) return; // AD not even used
+
+ if ($INPUT->str('dom')) {
+ $usr = $auth->cleanUser($event->data['user']);
+ $dom = $auth->getUserDomain($usr);
+ if (!$dom) {
+ $usr = "$usr@".$INPUT->str('dom');
+ }
+ $INPUT->post->set('u', $usr);
+ $event->data['user'] = $usr;
+ }
+ }
+
+ /**
+ * Shows a domain selection in the login form when more than one domain is configured
+ *
+ * @param Doku_Event $event
+ * @param array $param
+ */
+ public function handleHtmlLoginformOutput(Doku_Event $event, $param)
+ {
+ global $INPUT;
+ /** @var auth_plugin_authad $auth */
+ global $auth;
+ if (!is_a($auth, 'auth_plugin_authad')) return; // AD not even used
+ $domains = $auth->getConfiguredDomains();
+ if (count($domains) <= 1) return; // no choice at all
+
+ /** @var Doku_Form $form */
+ $form =& $event->data;
+
+ // any default?
+ $dom = '';
+ if ($INPUT->has('u')) {
+ $usr = $auth->cleanUser($INPUT->str('u'));
+ $dom = $auth->getUserDomain($usr);
+
+ // update user field value
+ if ($dom) {
+ $usr = $auth->getUserName($usr);
+ $pos = $form->findElementByAttribute('name', 'u');
+ $ele =& $form->getElementAt($pos);
+ $ele['value'] = $usr;
+ }
+ }
+
+ // add select box
+ $element = form_makeListboxField('dom', $domains, $dom, $this->getLang('domain'), '', 'block');
+ $pos = $form->findElementByAttribute('name', 'p');
+ $form->insertElement($pos + 1, $element);
+ }
+}
+
+// vim:ts=4:sw=4:et:
diff --git a/platform/www/lib/plugins/authad/adLDAP/adLDAP.php b/platform/www/lib/plugins/authad/adLDAP/adLDAP.php
new file mode 100644
index 0000000..c84a4f4
--- /dev/null
+++ b/platform/www/lib/plugins/authad/adLDAP/adLDAP.php
@@ -0,0 +1,949 @@
+<?php
+/**
+ * PHP LDAP CLASS FOR MANIPULATING ACTIVE DIRECTORY
+ * Version 4.0.4
+ *
+ * PHP Version 5 with SSL and LDAP support
+ *
+ * Written by Scott Barnett, Richard Hyland
+ * email: scott@wiggumworld.com, adldap@richardhyland.com
+ * http://adldap.sourceforge.net/
+ *
+ * Copyright (c) 2006-2012 Scott Barnett, Richard Hyland
+ *
+ * We'd appreciate any improvements or additions to be submitted back
+ * to benefit the entire community :)
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * @category ToolsAndUtilities
+ * @package adLDAP
+ * @author Scott Barnett, Richard Hyland
+ * @copyright (c) 2006-2012 Scott Barnett, Richard Hyland
+ * @license http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html LGPLv2.1
+ * @revision $Revision: 169 $
+ * @version 4.0.4
+ * @link http://adldap.sourceforge.net/
+ */
+
+/**
+* Main adLDAP class
+*
+* Can be initialised using $adldap = new adLDAP();
+*
+* Something to keep in mind is that Active Directory is a permissions
+* based directory. If you bind as a domain user, you can't fetch as
+* much information on other users as you could as a domain admin.
+*
+* Before asking questions, please read the Documentation at
+* http://adldap.sourceforge.net/wiki/doku.php?id=api
+*/
+require_once(dirname(__FILE__) . '/collections/adLDAPCollection.php');
+require_once(dirname(__FILE__) . '/classes/adLDAPGroups.php');
+require_once(dirname(__FILE__) . '/classes/adLDAPUsers.php');
+require_once(dirname(__FILE__) . '/classes/adLDAPFolders.php');
+require_once(dirname(__FILE__) . '/classes/adLDAPUtils.php');
+require_once(dirname(__FILE__) . '/classes/adLDAPContacts.php');
+require_once(dirname(__FILE__) . '/classes/adLDAPExchange.php');
+require_once(dirname(__FILE__) . '/classes/adLDAPComputers.php');
+
+class adLDAP {
+
+ /**
+ * Define the different types of account in AD
+ */
+ const ADLDAP_NORMAL_ACCOUNT = 805306368;
+ const ADLDAP_WORKSTATION_TRUST = 805306369;
+ const ADLDAP_INTERDOMAIN_TRUST = 805306370;
+ const ADLDAP_SECURITY_GLOBAL_GROUP = 268435456;
+ const ADLDAP_DISTRIBUTION_GROUP = 268435457;
+ const ADLDAP_SECURITY_LOCAL_GROUP = 536870912;
+ const ADLDAP_DISTRIBUTION_LOCAL_GROUP = 536870913;
+ const ADLDAP_FOLDER = 'OU';
+ const ADLDAP_CONTAINER = 'CN';
+
+ /**
+ * The default port for LDAP non-SSL connections
+ */
+ const ADLDAP_LDAP_PORT = '389';
+ /**
+ * The default port for LDAPS SSL connections
+ */
+ const ADLDAP_LDAPS_PORT = '636';
+
+ /**
+ * The account suffix for your domain, can be set when the class is invoked
+ *
+ * @var string
+ */
+ protected $accountSuffix = "@mydomain.local";
+
+ /**
+ * The base dn for your domain
+ *
+ * If this is set to null then adLDAP will attempt to obtain this automatically from the rootDSE
+ *
+ * @var string
+ */
+ protected $baseDn = "DC=mydomain,DC=local";
+
+ /**
+ * Port used to talk to the domain controllers.
+ *
+ * @var int
+ */
+ protected $adPort = self::ADLDAP_LDAP_PORT;
+
+ /**
+ * Array of domain controllers. Specifiy multiple controllers if you
+ * would like the class to balance the LDAP queries amongst multiple servers
+ *
+ * @var array
+ */
+ protected $domainControllers = array("dc01.mydomain.local");
+
+ /**
+ * Optional account with higher privileges for searching
+ * This should be set to a domain admin account
+ *
+ * @var string
+ * @var string
+ */
+ protected $adminUsername = NULL;
+ protected $adminPassword = NULL;
+
+ /**
+ * AD does not return the primary group. http://support.microsoft.com/?kbid=321360
+ * This tweak will resolve the real primary group.
+ * Setting to false will fudge "Domain Users" and is much faster. Keep in mind though that if
+ * someone's primary group is NOT domain users, this is obviously going to mess up the results
+ *
+ * @var bool
+ */
+ protected $realPrimaryGroup = true;
+
+ /**
+ * Use SSL (LDAPS), your server needs to be setup, please see
+ * http://adldap.sourceforge.net/wiki/doku.php?id=ldap_over_ssl
+ *
+ * @var bool
+ */
+ protected $useSSL = false;
+
+ /**
+ * Use TLS
+ * If you wish to use TLS you should ensure that $useSSL is set to false and vice-versa
+ *
+ * @var bool
+ */
+ protected $useTLS = false;
+
+ /**
+ * Use SSO
+ * To indicate to adLDAP to reuse password set by the brower through NTLM or Kerberos
+ *
+ * @var bool
+ */
+ protected $useSSO = false;
+
+ /**
+ * When querying group memberships, do it recursively
+ * eg. User Fred is a member of Group A, which is a member of Group B, which is a member of Group C
+ * user_ingroup("Fred","C") will returns true with this option turned on, false if turned off
+ *
+ * @var bool
+ */
+ protected $recursiveGroups = true;
+
+ // You should not need to edit anything below this line
+ //******************************************************************************************
+
+ /**
+ * Connection and bind default variables
+ *
+ * @var mixed
+ * @var mixed
+ */
+ protected $ldapConnection;
+ protected $ldapBind;
+
+ /**
+ * Get the active LDAP Connection
+ *
+ * @return resource
+ */
+ public function getLdapConnection() {
+ if ($this->ldapConnection) {
+ return $this->ldapConnection;
+ }
+ return false;
+ }
+
+ /**
+ * Get the bind status
+ *
+ * @return bool
+ */
+ public function getLdapBind() {
+ return $this->ldapBind;
+ }
+
+ /**
+ * Get the current base DN
+ *
+ * @return string
+ */
+ public function getBaseDn() {
+ return $this->baseDn;
+ }
+
+ /**
+ * The group class
+ *
+ * @var adLDAPGroups
+ */
+ protected $groupClass;
+
+ /**
+ * Get the group class interface
+ *
+ * @return adLDAPGroups
+ */
+ public function group() {
+ if (!$this->groupClass) {
+ $this->groupClass = new adLDAPGroups($this);
+ }
+ return $this->groupClass;
+ }
+
+ /**
+ * The user class
+ *
+ * @var adLDAPUsers
+ */
+ protected $userClass;
+
+ /**
+ * Get the userclass interface
+ *
+ * @return adLDAPUsers
+ */
+ public function user() {
+ if (!$this->userClass) {
+ $this->userClass = new adLDAPUsers($this);
+ }
+ return $this->userClass;
+ }
+
+ /**
+ * The folders class
+ *
+ * @var adLDAPFolders
+ */
+ protected $folderClass;
+
+ /**
+ * Get the folder class interface
+ *
+ * @return adLDAPFolders
+ */
+ public function folder() {
+ if (!$this->folderClass) {
+ $this->folderClass = new adLDAPFolders($this);
+ }
+ return $this->folderClass;
+ }
+
+ /**
+ * The utils class
+ *
+ * @var adLDAPUtils
+ */
+ protected $utilClass;
+
+ /**
+ * Get the utils class interface
+ *
+ * @return adLDAPUtils
+ */
+ public function utilities() {
+ if (!$this->utilClass) {
+ $this->utilClass = new adLDAPUtils($this);
+ }
+ return $this->utilClass;
+ }
+
+ /**
+ * The contacts class
+ *
+ * @var adLDAPContacts
+ */
+ protected $contactClass;
+
+ /**
+ * Get the contacts class interface
+ *
+ * @return adLDAPContacts
+ */
+ public function contact() {
+ if (!$this->contactClass) {
+ $this->contactClass = new adLDAPContacts($this);
+ }
+ return $this->contactClass;
+ }
+
+ /**
+ * The exchange class
+ *
+ * @var adLDAPExchange
+ */
+ protected $exchangeClass;
+
+ /**
+ * Get the exchange class interface
+ *
+ * @return adLDAPExchange
+ */
+ public function exchange() {
+ if (!$this->exchangeClass) {
+ $this->exchangeClass = new adLDAPExchange($this);
+ }
+ return $this->exchangeClass;
+ }
+
+ /**
+ * The computers class
+ *
+ * @var adLDAPComputers
+ */
+ protected $computersClass;
+
+ /**
+ * Get the computers class interface
+ *
+ * @return adLDAPComputers
+ */
+ public function computer() {
+ if (!$this->computerClass) {
+ $this->computerClass = new adLDAPComputers($this);
+ }
+ return $this->computerClass;
+ }
+
+ /**
+ * Getters and Setters
+ */
+
+ /**
+ * Set the account suffix
+ *
+ * @param string $accountSuffix
+ * @return void
+ */
+ public function setAccountSuffix($accountSuffix)
+ {
+ $this->accountSuffix = $accountSuffix;
+ }
+
+ /**
+ * Get the account suffix
+ *
+ * @return string
+ */
+ public function getAccountSuffix()
+ {
+ return $this->accountSuffix;
+ }
+
+ /**
+ * Set the domain controllers array
+ *
+ * @param array $domainControllers
+ * @return void
+ */
+ public function setDomainControllers(array $domainControllers)
+ {
+ $this->domainControllers = $domainControllers;
+ }
+
+ /**
+ * Get the list of domain controllers
+ *
+ * @return void
+ */
+ public function getDomainControllers()
+ {
+ return $this->domainControllers;
+ }
+
+ /**
+ * Sets the port number your domain controller communicates over
+ *
+ * @param int $adPort
+ */
+ public function setPort($adPort)
+ {
+ $this->adPort = $adPort;
+ }
+
+ /**
+ * Gets the port number your domain controller communicates over
+ *
+ * @return int
+ */
+ public function getPort()
+ {
+ return $this->adPort;
+ }
+
+ /**
+ * Set the username of an account with higher priviledges
+ *
+ * @param string $adminUsername
+ * @return void
+ */
+ public function setAdminUsername($adminUsername)
+ {
+ $this->adminUsername = $adminUsername;
+ }
+
+ /**
+ * Get the username of the account with higher priviledges
+ *
+ * This will throw an exception for security reasons
+ */
+ public function getAdminUsername()
+ {
+ throw new adLDAPException('For security reasons you cannot access the domain administrator account details');
+ }
+
+ /**
+ * Set the password of an account with higher priviledges
+ *
+ * @param string $adminPassword
+ * @return void
+ */
+ public function setAdminPassword($adminPassword)
+ {
+ $this->adminPassword = $adminPassword;
+ }
+
+ /**
+ * Get the password of the account with higher priviledges
+ *
+ * This will throw an exception for security reasons
+ */
+ public function getAdminPassword()
+ {
+ throw new adLDAPException('For security reasons you cannot access the domain administrator account details');
+ }
+
+ /**
+ * Set whether to detect the true primary group
+ *
+ * @param bool $realPrimaryGroup
+ * @return void
+ */
+ public function setRealPrimaryGroup($realPrimaryGroup)
+ {
+ $this->realPrimaryGroup = $realPrimaryGroup;
+ }
+
+ /**
+ * Get the real primary group setting
+ *
+ * @return bool
+ */
+ public function getRealPrimaryGroup()
+ {
+ return $this->realPrimaryGroup;
+ }
+
+ /**
+ * Set whether to use SSL
+ *
+ * @param bool $useSSL
+ * @return void
+ */
+ public function setUseSSL($useSSL)
+ {
+ $this->useSSL = $useSSL;
+ // Set the default port correctly
+ if($this->useSSL) {
+ $this->setPort(self::ADLDAP_LDAPS_PORT);
+ }
+ else {
+ $this->setPort(self::ADLDAP_LDAP_PORT);
+ }
+ }
+
+ /**
+ * Get the SSL setting
+ *
+ * @return bool
+ */
+ public function getUseSSL()
+ {
+ return $this->useSSL;
+ }
+
+ /**
+ * Set whether to use TLS
+ *
+ * @param bool $useTLS
+ * @return void
+ */
+ public function setUseTLS($useTLS)
+ {
+ $this->useTLS = $useTLS;
+ }
+
+ /**
+ * Get the TLS setting
+ *
+ * @return bool
+ */
+ public function getUseTLS()
+ {
+ return $this->useTLS;
+ }
+
+ /**
+ * Set whether to use SSO
+ * Requires ldap_sasl_bind support. Be sure --with-ldap-sasl is used when configuring PHP otherwise this function will be undefined.
+ *
+ * @param bool $useSSO
+ * @return void
+ */
+ public function setUseSSO($useSSO)
+ {
+ if ($useSSO === true && !$this->ldapSaslSupported()) {
+ throw new adLDAPException('No LDAP SASL support for PHP. See: http://php.net/ldap_sasl_bind');
+ }
+ $this->useSSO = $useSSO;
+ }
+
+ /**
+ * Get the SSO setting
+ *
+ * @return bool
+ */
+ public function getUseSSO()
+ {
+ return $this->useSSO;
+ }
+
+ /**
+ * Set whether to lookup recursive groups
+ *
+ * @param bool $recursiveGroups
+ * @return void
+ */
+ public function setRecursiveGroups($recursiveGroups)
+ {
+ $this->recursiveGroups = $recursiveGroups;
+ }
+
+ /**
+ * Get the recursive groups setting
+ *
+ * @return bool
+ */
+ public function getRecursiveGroups()
+ {
+ return $this->recursiveGroups;
+ }
+
+ /**
+ * Default Constructor
+ *
+ * Tries to bind to the AD domain over LDAP or LDAPs
+ *
+ * @param array $options Array of options to pass to the constructor
+ * @throws Exception - if unable to bind to Domain Controller
+ * @return bool
+ */
+ function __construct($options = array()) {
+ // You can specifically overide any of the default configuration options setup above
+ if (count($options) > 0) {
+ if (array_key_exists("account_suffix",$options)){ $this->accountSuffix = $options["account_suffix"]; }
+ if (array_key_exists("base_dn",$options)){ $this->baseDn = $options["base_dn"]; }
+ if (array_key_exists("domain_controllers",$options)){
+ if (!is_array($options["domain_controllers"])) {
+ throw new adLDAPException('[domain_controllers] option must be an array');
+ }
+ $this->domainControllers = $options["domain_controllers"];
+ }
+ if (array_key_exists("admin_username",$options)){ $this->adminUsername = $options["admin_username"]; }
+ if (array_key_exists("admin_password",$options)){ $this->adminPassword = $options["admin_password"]; }
+ if (array_key_exists("real_primarygroup",$options)){ $this->realPrimaryGroup = $options["real_primarygroup"]; }
+ if (array_key_exists("use_ssl",$options)){ $this->setUseSSL($options["use_ssl"]); }
+ if (array_key_exists("use_tls",$options)){ $this->useTLS = $options["use_tls"]; }
+ if (array_key_exists("recursive_groups",$options)){ $this->recursiveGroups = $options["recursive_groups"]; }
+ if (array_key_exists("ad_port",$options)){ $this->setPort($options["ad_port"]); }
+ if (array_key_exists("sso",$options)) {
+ $this->setUseSSO($options["sso"]);
+ if (!$this->ldapSaslSupported()) {
+ $this->setUseSSO(false);
+ }
+ }
+ }
+
+ if ($this->ldapSupported() === false) {
+ throw new adLDAPException('No LDAP support for PHP. See: http://php.net/ldap');
+ }
+
+ return $this->connect();
+ }
+
+ /**
+ * Default Destructor
+ *
+ * Closes the LDAP connection
+ *
+ * @return void
+ */
+ function __destruct() {
+ $this->close();
+ }
+
+ /**
+ * Connects and Binds to the Domain Controller
+ *
+ * @return bool
+ */
+ public function connect()
+ {
+ // Connect to the AD/LDAP server as the username/password
+ $domainController = $this->randomController();
+ if ($this->useSSL) {
+ $this->ldapConnection = ldap_connect("ldaps://" . $domainController, $this->adPort);
+ } else {
+ $this->ldapConnection = ldap_connect($domainController, $this->adPort);
+ }
+
+ // Set some ldap options for talking to AD
+ ldap_set_option($this->ldapConnection, LDAP_OPT_PROTOCOL_VERSION, 3);
+ ldap_set_option($this->ldapConnection, LDAP_OPT_REFERRALS, 0);
+
+ if ($this->useTLS) {
+ ldap_start_tls($this->ldapConnection);
+ }
+
+ // Bind as a domain admin if they've set it up
+ if ($this->adminUsername !== NULL && $this->adminPassword !== NULL) {
+ $this->ldapBind = @ldap_bind($this->ldapConnection, $this->adminUsername . $this->accountSuffix, $this->adminPassword);
+ if (!$this->ldapBind) {
+ if ($this->useSSL && !$this->useTLS) {
+ // If you have problems troubleshooting, remove the @ character from the ldapldapBind command above to get the actual error message
+ throw new adLDAPException('Bind to Active Directory failed. Either the LDAPs connection failed or the login credentials are incorrect. AD said: ' . $this->getLastError());
+ }
+ else {
+ throw new adLDAPException('Bind to Active Directory failed. Check the login credentials and/or server details. AD said: ' . $this->getLastError());
+ }
+ }
+ }
+ if ($this->useSSO && $_SERVER['REMOTE_USER'] && $this->adminUsername === null && $_SERVER['KRB5CCNAME']) {
+ putenv("KRB5CCNAME=" . $_SERVER['KRB5CCNAME']);
+ $this->ldapBind = @ldap_sasl_bind($this->ldapConnection, NULL, NULL, "GSSAPI");
+ if (!$this->ldapBind){
+ throw new adLDAPException('Rebind to Active Directory failed. AD said: ' . $this->getLastError());
+ }
+ else {
+ return true;
+ }
+ }
+
+
+ if ($this->baseDn == NULL) {
+ $this->baseDn = $this->findBaseDn();
+ }
+
+ return true;
+ }
+
+ /**
+ * Closes the LDAP connection
+ *
+ * @return void
+ */
+ public function close() {
+ if ($this->ldapConnection) {
+ @ldap_close($this->ldapConnection);
+ }
+ }
+
+ /**
+ * Validate a user's login credentials
+ *
+ * @param string $username A user's AD username
+ * @param string $password A user's AD password
+ * @param bool optional $preventRebind
+ * @return bool
+ */
+ public function authenticate($username, $password, $preventRebind = false) {
+ // Prevent null binding
+ if ($username === NULL || $password === NULL) { return false; }
+ if (empty($username) || empty($password)) { return false; }
+
+ // Allow binding over SSO for Kerberos
+ if ($this->useSSO && $_SERVER['REMOTE_USER'] && $_SERVER['REMOTE_USER'] == $username && $this->adminUsername === NULL && $_SERVER['KRB5CCNAME']) {
+ putenv("KRB5CCNAME=" . $_SERVER['KRB5CCNAME']);
+ $this->ldapBind = @ldap_sasl_bind($this->ldapConnection, NULL, NULL, "GSSAPI");
+ if (!$this->ldapBind) {
+ throw new adLDAPException('Rebind to Active Directory failed. AD said: ' . $this->getLastError());
+ }
+ else {
+ return true;
+ }
+ }
+
+ // Bind as the user
+ $ret = true;
+ $this->ldapBind = @ldap_bind($this->ldapConnection, $username . $this->accountSuffix, $password);
+ if (!$this->ldapBind){
+ $ret = false;
+ }
+
+ // Cnce we've checked their details, kick back into admin mode if we have it
+ if ($this->adminUsername !== NULL && !$preventRebind) {
+ $this->ldapBind = @ldap_bind($this->ldapConnection, $this->adminUsername . $this->accountSuffix , $this->adminPassword);
+ if (!$this->ldapBind){
+ // This should never happen in theory
+ throw new adLDAPException('Rebind to Active Directory failed. AD said: ' . $this->getLastError());
+ }
+ }
+
+ return $ret;
+ }
+
+ /**
+ * Find the Base DN of your domain controller
+ *
+ * @return string
+ */
+ public function findBaseDn()
+ {
+ $namingContext = $this->getRootDse(array('defaultnamingcontext'));
+ return $namingContext[0]['defaultnamingcontext'][0];
+ }
+
+ /**
+ * Get the RootDSE properties from a domain controller
+ *
+ * @param array $attributes The attributes you wish to query e.g. defaultnamingcontext
+ * @return array
+ */
+ public function getRootDse($attributes = array("*", "+")) {
+ if (!$this->ldapBind){ return (false); }
+
+ $sr = @ldap_read($this->ldapConnection, NULL, 'objectClass=*', $attributes);
+ $entries = @ldap_get_entries($this->ldapConnection, $sr);
+ return $entries;
+ }
+
+ /**
+ * Get last error from Active Directory
+ *
+ * This function gets the last message from Active Directory
+ * This may indeed be a 'Success' message but if you get an unknown error
+ * it might be worth calling this function to see what errors were raised
+ *
+ * return string
+ */
+ public function getLastError() {
+ return @ldap_error($this->ldapConnection);
+ }
+
+ /**
+ * Detect LDAP support in php
+ *
+ * @return bool
+ */
+ protected function ldapSupported()
+ {
+ if (!function_exists('ldap_connect')) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Detect ldap_sasl_bind support in PHP
+ *
+ * @return bool
+ */
+ protected function ldapSaslSupported()
+ {
+ if (!function_exists('ldap_sasl_bind')) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Schema
+ *
+ * @param array $attributes Attributes to be queried
+ * @return array
+ */
+ public function adldap_schema($attributes){
+
+ // LDAP doesn't like NULL attributes, only set them if they have values
+ // If you wish to remove an attribute you should set it to a space
+ // TO DO: Adapt user_modify to use ldap_mod_delete to remove a NULL attribute
+ $mod=array();
+
+ // Check every attribute to see if it contains 8bit characters and then UTF8 encode them
+ array_walk($attributes, array($this, 'encode8bit'));
+
+ if ($attributes["address_city"]){ $mod["l"][0]=$attributes["address_city"]; }
+ if ($attributes["address_code"]){ $mod["postalCode"][0]=$attributes["address_code"]; }
+ //if ($attributes["address_country"]){ $mod["countryCode"][0]=$attributes["address_country"]; } // use country codes?
+ if ($attributes["address_country"]){ $mod["c"][0]=$attributes["address_country"]; }
+ if ($attributes["address_pobox"]){ $mod["postOfficeBox"][0]=$attributes["address_pobox"]; }
+ if ($attributes["address_state"]){ $mod["st"][0]=$attributes["address_state"]; }
+ if ($attributes["address_street"]){ $mod["streetAddress"][0]=$attributes["address_street"]; }
+ if ($attributes["company"]){ $mod["company"][0]=$attributes["company"]; }
+ if ($attributes["change_password"]){ $mod["pwdLastSet"][0]=0; }
+ if ($attributes["department"]){ $mod["department"][0]=$attributes["department"]; }
+ if ($attributes["description"]){ $mod["description"][0]=$attributes["description"]; }
+ if ($attributes["display_name"]){ $mod["displayName"][0]=$attributes["display_name"]; }
+ if ($attributes["email"]){ $mod["mail"][0]=$attributes["email"]; }
+ if ($attributes["expires"]){ $mod["accountExpires"][0]=$attributes["expires"]; } //unix epoch format?
+ if ($attributes["firstname"]){ $mod["givenName"][0]=$attributes["firstname"]; }
+ if ($attributes["home_directory"]){ $mod["homeDirectory"][0]=$attributes["home_directory"]; }
+ if ($attributes["home_drive"]){ $mod["homeDrive"][0]=$attributes["home_drive"]; }
+ if ($attributes["initials"]){ $mod["initials"][0]=$attributes["initials"]; }
+ if ($attributes["logon_name"]){ $mod["userPrincipalName"][0]=$attributes["logon_name"]; }
+ if ($attributes["manager"]){ $mod["manager"][0]=$attributes["manager"]; } //UNTESTED ***Use DistinguishedName***
+ if ($attributes["office"]){ $mod["physicalDeliveryOfficeName"][0]=$attributes["office"]; }
+ if ($attributes["password"]){ $mod["unicodePwd"][0]=$this->user()->encodePassword($attributes["password"]); }
+ if ($attributes["profile_path"]){ $mod["profilepath"][0]=$attributes["profile_path"]; }
+ if ($attributes["script_path"]){ $mod["scriptPath"][0]=$attributes["script_path"]; }
+ if ($attributes["surname"]){ $mod["sn"][0]=$attributes["surname"]; }
+ if ($attributes["title"]){ $mod["title"][0]=$attributes["title"]; }
+ if ($attributes["telephone"]){ $mod["telephoneNumber"][0]=$attributes["telephone"]; }
+ if ($attributes["mobile"]){ $mod["mobile"][0]=$attributes["mobile"]; }
+ if ($attributes["pager"]){ $mod["pager"][0]=$attributes["pager"]; }
+ if ($attributes["ipphone"]){ $mod["ipphone"][0]=$attributes["ipphone"]; }
+ if ($attributes["web_page"]){ $mod["wWWHomePage"][0]=$attributes["web_page"]; }
+ if ($attributes["fax"]){ $mod["facsimileTelephoneNumber"][0]=$attributes["fax"]; }
+ if ($attributes["enabled"]){ $mod["userAccountControl"][0]=$attributes["enabled"]; }
+ if ($attributes["homephone"]){ $mod["homephone"][0]=$attributes["homephone"]; }
+
+ // Distribution List specific schema
+ if ($attributes["group_sendpermission"]){ $mod["dlMemSubmitPerms"][0]=$attributes["group_sendpermission"]; }
+ if ($attributes["group_rejectpermission"]){ $mod["dlMemRejectPerms"][0]=$attributes["group_rejectpermission"]; }
+
+ // Exchange Schema
+ if ($attributes["exchange_homemdb"]){ $mod["homeMDB"][0]=$attributes["exchange_homemdb"]; }
+ if ($attributes["exchange_mailnickname"]){ $mod["mailNickname"][0]=$attributes["exchange_mailnickname"]; }
+ if ($attributes["exchange_proxyaddress"]){ $mod["proxyAddresses"][0]=$attributes["exchange_proxyaddress"]; }
+ if ($attributes["exchange_usedefaults"]){ $mod["mDBUseDefaults"][0]=$attributes["exchange_usedefaults"]; }
+ if ($attributes["exchange_policyexclude"]){ $mod["msExchPoliciesExcluded"][0]=$attributes["exchange_policyexclude"]; }
+ if ($attributes["exchange_policyinclude"]){ $mod["msExchPoliciesIncluded"][0]=$attributes["exchange_policyinclude"]; }
+ if ($attributes["exchange_addressbook"]){ $mod["showInAddressBook"][0]=$attributes["exchange_addressbook"]; }
+ if ($attributes["exchange_altrecipient"]){ $mod["altRecipient"][0]=$attributes["exchange_altrecipient"]; }
+ if ($attributes["exchange_deliverandredirect"]){ $mod["deliverAndRedirect"][0]=$attributes["exchange_deliverandredirect"]; }
+
+ // This schema is designed for contacts
+ if ($attributes["exchange_hidefromlists"]){ $mod["msExchHideFromAddressLists"][0]=$attributes["exchange_hidefromlists"]; }
+ if ($attributes["contact_email"]){ $mod["targetAddress"][0]=$attributes["contact_email"]; }
+
+ //echo ("<pre>"); print_r($mod);
+ /*
+ // modifying a name is a bit fiddly
+ if ($attributes["firstname"] && $attributes["surname"]){
+ $mod["cn"][0]=$attributes["firstname"]." ".$attributes["surname"];
+ $mod["displayname"][0]=$attributes["firstname"]." ".$attributes["surname"];
+ $mod["name"][0]=$attributes["firstname"]." ".$attributes["surname"];
+ }
+ */
+
+ if (count($mod)==0){ return (false); }
+ return ($mod);
+ }
+
+ /**
+ * Convert 8bit characters e.g. accented characters to UTF8 encoded characters
+ */
+ protected function encode8Bit(&$item, $key) {
+ $encode = false;
+ if (is_string($item)) {
+ for ($i=0; $i<strlen($item); $i++) {
+ if (ord($item[$i]) >> 7) {
+ $encode = true;
+ }
+ }
+ }
+ if ($encode === true && $key != 'password') {
+ $item = utf8_encode($item);
+ }
+ }
+
+ /**
+ * Select a random domain controller from your domain controller array
+ *
+ * @return string
+ */
+ protected function randomController()
+ {
+ mt_srand(doubleval(microtime()) * 100000000); // For older PHP versions
+ /*if (sizeof($this->domainControllers) > 1) {
+ $adController = $this->domainControllers[array_rand($this->domainControllers)];
+ // Test if the controller is responding to pings
+ $ping = $this->pingController($adController);
+ if ($ping === false) {
+ // Find the current key in the domain controllers array
+ $key = array_search($adController, $this->domainControllers);
+ // Remove it so that we don't end up in a recursive loop
+ unset($this->domainControllers[$key]);
+ // Select a new controller
+ return $this->randomController();
+ }
+ else {
+ return ($adController);
+ }
+ } */
+ return $this->domainControllers[array_rand($this->domainControllers)];
+ }
+
+ /**
+ * Test basic connectivity to controller
+ *
+ * @return bool
+ */
+ protected function pingController($host) {
+ $port = $this->adPort;
+ fsockopen($host, $port, $errno, $errstr, 10);
+ if ($errno > 0) {
+ return false;
+ }
+ return true;
+ }
+
+}
+
+/**
+* adLDAP Exception Handler
+*
+* Exceptions of this type are thrown on bind failure or when SSL is required but not configured
+* Example:
+* try {
+* $adldap = new adLDAP();
+* }
+* catch (adLDAPException $e) {
+* echo $e;
+* exit();
+* }
+*/
+class adLDAPException extends Exception {}
diff --git a/platform/www/lib/plugins/authad/adLDAP/classes/adLDAPComputers.php b/platform/www/lib/plugins/authad/adLDAP/classes/adLDAPComputers.php
new file mode 100644
index 0000000..aabd88f
--- /dev/null
+++ b/platform/www/lib/plugins/authad/adLDAP/classes/adLDAPComputers.php
@@ -0,0 +1,153 @@
+<?php
+/**
+ * PHP LDAP CLASS FOR MANIPULATING ACTIVE DIRECTORY
+ * Version 4.0.4
+ *
+ * PHP Version 5 with SSL and LDAP support
+ *
+ * Written by Scott Barnett, Richard Hyland
+ * email: scott@wiggumworld.com, adldap@richardhyland.com
+ * http://adldap.sourceforge.net/
+ *
+ * Copyright (c) 2006-2012 Scott Barnett, Richard Hyland
+ *
+ * We'd appreciate any improvements or additions to be submitted back
+ * to benefit the entire community :)
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * @category ToolsAndUtilities
+ * @package adLDAP
+ * @subpackage Computers
+ * @author Scott Barnett, Richard Hyland
+ * @copyright (c) 2006-2012 Scott Barnett, Richard Hyland
+ * @license http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html LGPLv2.1
+ * @revision $Revision: 97 $
+ * @version 4.0.4
+ * @link http://adldap.sourceforge.net/
+ */
+require_once(dirname(__FILE__) . '/../adLDAP.php');
+require_once(dirname(__FILE__) . '/../collections/adLDAPComputerCollection.php');
+
+/**
+* COMPUTER MANAGEMENT FUNCTIONS
+*/
+class adLDAPComputers {
+
+ /**
+ * The current adLDAP connection via dependency injection
+ *
+ * @var adLDAP
+ */
+ protected $adldap;
+
+ public function __construct(adLDAP $adldap) {
+ $this->adldap = $adldap;
+ }
+
+ /**
+ * Get information about a specific computer. Returned in a raw array format from AD
+ *
+ * @param string $computerName The name of the computer
+ * @param array $fields Attributes to return
+ * @return array
+ */
+ public function info($computerName, $fields = NULL)
+ {
+ if ($computerName === NULL) { return false; }
+ if (!$this->adldap->getLdapBind()) { return false; }
+
+ $filter = "(&(objectClass=computer)(cn=" . $computerName . "))";
+ if ($fields === NULL) {
+ $fields = array("memberof","cn","displayname","dnshostname","distinguishedname","objectcategory","operatingsystem","operatingsystemservicepack","operatingsystemversion");
+ }
+ $sr = ldap_search($this->adldap->getLdapConnection(), $this->adldap->getBaseDn(), $filter, $fields);
+ $entries = ldap_get_entries($this->adldap->getLdapConnection(), $sr);
+
+ return $entries;
+ }
+
+ /**
+ * Find information about the computers. Returned in a raw array format from AD
+ *
+ * @param string $computerName The name of the computer
+ * @param array $fields Array of parameters to query
+ * @return mixed
+ */
+ public function infoCollection($computerName, $fields = NULL)
+ {
+ if ($computerName === NULL) { return false; }
+ if (!$this->adldap->getLdapBind()) { return false; }
+
+ $info = $this->info($computerName, $fields);
+
+ if ($info !== false) {
+ $collection = new adLDAPComputerCollection($info, $this->adldap);
+ return $collection;
+ }
+ return false;
+ }
+
+ /**
+ * Check if a computer is in a group
+ *
+ * @param string $computerName The name of the computer
+ * @param string $group The group to check
+ * @param bool $recursive Whether to check recursively
+ * @return array
+ */
+ public function inGroup($computerName, $group, $recursive = NULL)
+ {
+ if ($computerName === NULL) { return false; }
+ if ($group === NULL) { return false; }
+ if (!$this->adldap->getLdapBind()) { return false; }
+ if ($recursive === NULL) { $recursive = $this->adldap->getRecursiveGroups(); } // use the default option if they haven't set it
+
+ //get a list of the groups
+ $groups = $this->groups($computerName, array("memberof"), $recursive);
+
+ //return true if the specified group is in the group list
+ if (in_array($group, $groups)){
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Get the groups a computer is in
+ *
+ * @param string $computerName The name of the computer
+ * @param bool $recursive Whether to check recursively
+ * @return array
+ */
+ public function groups($computerName, $recursive = NULL)
+ {
+ if ($computerName === NULL) { return false; }
+ if ($recursive === NULL) { $recursive = $this->adldap->getRecursiveGroups(); } //use the default option if they haven't set it
+ if (!$this->adldap->getLdapBind()){ return false; }
+
+ //search the directory for their information
+ $info = @$this->info($computerName, array("memberof", "primarygroupid"));
+ $groups = $this->adldap->utilities()->niceNames($info[0]["memberof"]); //presuming the entry returned is our guy (unique usernames)
+
+ if ($recursive === true) {
+ foreach ($groups as $id => $groupName){
+ $extraGroups = $this->adldap->group()->recursiveGroups($groupName);
+ $groups = array_merge($groups, $extraGroups);
+ }
+ }
+
+ return $groups;
+ }
+
+}
+?> \ No newline at end of file
diff --git a/platform/www/lib/plugins/authad/adLDAP/classes/adLDAPContacts.php b/platform/www/lib/plugins/authad/adLDAP/classes/adLDAPContacts.php
new file mode 100644
index 0000000..42a0d75
--- /dev/null
+++ b/platform/www/lib/plugins/authad/adLDAP/classes/adLDAPContacts.php
@@ -0,0 +1,294 @@
+<?php
+/**
+ * PHP LDAP CLASS FOR MANIPULATING ACTIVE DIRECTORY
+ * Version 4.0.4
+ *
+ * PHP Version 5 with SSL and LDAP support
+ *
+ * Written by Scott Barnett, Richard Hyland
+ * email: scott@wiggumworld.com, adldap@richardhyland.com
+ * http://adldap.sourceforge.net/
+ *
+ * Copyright (c) 2006-2012 Scott Barnett, Richard Hyland
+ *
+ * We'd appreciate any improvements or additions to be submitted back
+ * to benefit the entire community :)
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * @category ToolsAndUtilities
+ * @package adLDAP
+ * @subpackage Contacts
+ * @author Scott Barnett, Richard Hyland
+ * @copyright (c) 2006-2012 Scott Barnett, Richard Hyland
+ * @license http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html LGPLv2.1
+ * @revision $Revision: 97 $
+ * @version 4.0.4
+ * @link http://adldap.sourceforge.net/
+ */
+
+require_once(dirname(__FILE__) . '/../adLDAP.php');
+require_once(dirname(__FILE__) . '/../collections/adLDAPContactCollection.php');
+
+class adLDAPContacts {
+ /**
+ * The current adLDAP connection via dependency injection
+ *
+ * @var adLDAP
+ */
+ protected $adldap;
+
+ public function __construct(adLDAP $adldap) {
+ $this->adldap = $adldap;
+ }
+
+ //*****************************************************************************************************************
+ // CONTACT FUNCTIONS
+ // * Still work to do in this area, and new functions to write
+
+ /**
+ * Create a contact
+ *
+ * @param array $attributes The attributes to set to the contact
+ * @return bool
+ */
+ public function create($attributes)
+ {
+ // Check for compulsory fields
+ if (!array_key_exists("display_name", $attributes)) { return "Missing compulsory field [display_name]"; }
+ if (!array_key_exists("email", $attributes)) { return "Missing compulsory field [email]"; }
+ if (!array_key_exists("container", $attributes)) { return "Missing compulsory field [container]"; }
+ if (!is_array($attributes["container"])) { return "Container attribute must be an array."; }
+
+ // Translate the schema
+ $add = $this->adldap->adldap_schema($attributes);
+
+ // Additional stuff only used for adding contacts
+ $add["cn"][0] = $attributes["display_name"];
+ $add["objectclass"][0] = "top";
+ $add["objectclass"][1] = "person";
+ $add["objectclass"][2] = "organizationalPerson";
+ $add["objectclass"][3] = "contact";
+ if (!isset($attributes['exchange_hidefromlists'])) {
+ $add["msExchHideFromAddressLists"][0] = "TRUE";
+ }
+
+ // Determine the container
+ $attributes["container"] = array_reverse($attributes["container"]);
+ $container= "OU=" . implode(",OU=", $attributes["container"]);
+
+ // Add the entry
+ $result = @ldap_add($this->adldap->getLdapConnection(), "CN=" . $this->adldap->utilities()->escapeCharacters($add["cn"][0]) . ", " . $container . "," . $this->adldap->getBaseDn(), $add);
+ if ($result != true) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Determine the list of groups a contact is a member of
+ *
+ * @param string $distinguisedname The full DN of a contact
+ * @param bool $recursive Recursively check groups
+ * @return array
+ */
+ public function groups($distinguishedName, $recursive = NULL)
+ {
+ if ($distinguishedName === NULL) { return false; }
+ if ($recursive === NULL) { $recursive = $this->adldap->getRecursiveGroups(); } //use the default option if they haven't set it
+ if (!$this->adldap->getLdapBind()){ return false; }
+
+ // Search the directory for their information
+ $info = @$this->info($distinguishedName, array("memberof", "primarygroupid"));
+ $groups = $this->adldap->utilities()->niceNames($info[0]["memberof"]); //presuming the entry returned is our contact
+
+ if ($recursive === true){
+ foreach ($groups as $id => $groupName){
+ $extraGroups = $this->adldap->group()->recursiveGroups($groupName);
+ $groups = array_merge($groups, $extraGroups);
+ }
+ }
+
+ return $groups;
+ }
+
+ /**
+ * Get contact information. Returned in a raw array format from AD
+ *
+ * @param string $distinguisedname The full DN of a contact
+ * @param array $fields Attributes to be returned
+ * @return array
+ */
+ public function info($distinguishedName, $fields = NULL)
+ {
+ if ($distinguishedName === NULL) { return false; }
+ if (!$this->adldap->getLdapBind()) { return false; }
+
+ $filter = "distinguishedName=" . $distinguishedName;
+ if ($fields === NULL) {
+ $fields = array("distinguishedname", "mail", "memberof", "department", "displayname", "telephonenumber", "primarygroupid", "objectsid");
+ }
+ $sr = ldap_search($this->adldap->getLdapConnection(), $this->adldap->getBaseDn(), $filter, $fields);
+ $entries = ldap_get_entries($this->adldap->getLdapConnection(), $sr);
+
+ if ($entries[0]['count'] >= 1) {
+ // AD does not return the primary group in the ldap query, we may need to fudge it
+ if ($this->adldap->getRealPrimaryGroup() && isset($entries[0]["primarygroupid"][0]) && isset($entries[0]["primarygroupid"][0])){
+ //$entries[0]["memberof"][]=$this->group_cn($entries[0]["primarygroupid"][0]);
+ $entries[0]["memberof"][] = $this->adldap->group()->getPrimaryGroup($entries[0]["primarygroupid"][0], $entries[0]["objectsid"][0]);
+ } else {
+ $entries[0]["memberof"][] = "CN=Domain Users,CN=Users," . $this->adldap->getBaseDn();
+ }
+ }
+
+ $entries[0]["memberof"]["count"]++;
+ return $entries;
+ }
+
+ /**
+ * Find information about the contacts. Returned in a raw array format from AD
+ *
+ * @param string $distinguishedName The full DN of a contact
+ * @param array $fields Array of parameters to query
+ * @return mixed
+ */
+ public function infoCollection($distinguishedName, $fields = NULL)
+ {
+ if ($distinguishedName === NULL) { return false; }
+ if (!$this->adldap->getLdapBind()) { return false; }
+
+ $info = $this->info($distinguishedName, $fields);
+
+ if ($info !== false) {
+ $collection = new adLDAPContactCollection($info, $this->adldap);
+ return $collection;
+ }
+ return false;
+ }
+
+ /**
+ * Determine if a contact is a member of a group
+ *
+ * @param string $distinguisedName The full DN of a contact
+ * @param string $group The group name to query
+ * @param bool $recursive Recursively check groups
+ * @return bool
+ */
+ public function inGroup($distinguisedName, $group, $recursive = NULL)
+ {
+ if ($distinguisedName === NULL) { return false; }
+ if ($group === NULL) { return false; }
+ if (!$this->adldap->getLdapBind()) { return false; }
+ if ($recursive === NULL) { $recursive = $this->adldap->getRecursiveGroups(); } //use the default option if they haven't set it
+
+ // Get a list of the groups
+ $groups = $this->groups($distinguisedName, array("memberof"), $recursive);
+
+ // Return true if the specified group is in the group list
+ if (in_array($group, $groups)){
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Modify a contact
+ *
+ * @param string $distinguishedName The contact to query
+ * @param array $attributes The attributes to modify. Note if you set the enabled attribute you must not specify any other attributes
+ * @return bool
+ */
+ public function modify($distinguishedName, $attributes) {
+ if ($distinguishedName === NULL) { return "Missing compulsory field [distinguishedname]"; }
+
+ // Translate the update to the LDAP schema
+ $mod = $this->adldap->adldap_schema($attributes);
+
+ // Check to see if this is an enabled status update
+ if (!$mod) {
+ return false;
+ }
+
+ // Do the update
+ $result = ldap_modify($this->adldap->getLdapConnection(), $distinguishedName, $mod);
+ if ($result == false) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Delete a contact
+ *
+ * @param string $distinguishedName The contact dn to delete (please be careful here!)
+ * @return array
+ */
+ public function delete($distinguishedName)
+ {
+ $result = $this->folder()->delete($distinguishedName);
+ if ($result != true) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Return a list of all contacts
+ *
+ * @param bool $includeDescription Include a description of a contact
+ * @param string $search The search parameters
+ * @param bool $sorted Whether to sort the results
+ * @return array
+ */
+ public function all($includeDescription = false, $search = "*", $sorted = true) {
+ if (!$this->adldap->getLdapBind()) { return false; }
+
+ // Perform the search and grab all their details
+ $filter = "(&(objectClass=contact)(cn=" . $search . "))";
+ $fields = array("displayname","distinguishedname");
+ $sr = ldap_search($this->adldap->getLdapConnection(), $this->adldap->getBaseDn(), $filter, $fields);
+ $entries = ldap_get_entries($this->adldap->getLdapConnection(), $sr);
+
+ $usersArray = array();
+ for ($i=0; $i<$entries["count"]; $i++){
+ if ($includeDescription && strlen($entries[$i]["displayname"][0])>0){
+ $usersArray[$entries[$i]["distinguishedname"][0]] = $entries[$i]["displayname"][0];
+ } elseif ($includeDescription){
+ $usersArray[$entries[$i]["distinguishedname"][0]] = $entries[$i]["distinguishedname"][0];
+ } else {
+ array_push($usersArray, $entries[$i]["distinguishedname"][0]);
+ }
+ }
+ if ($sorted) {
+ asort($usersArray);
+ }
+ return $usersArray;
+ }
+
+ /**
+ * Mail enable a contact
+ * Allows email to be sent to them through Exchange
+ *
+ * @param string $distinguishedname The contact to mail enable
+ * @param string $emailaddress The email address to allow emails to be sent through
+ * @param string $mailnickname The mailnickname for the contact in Exchange. If NULL this will be set to the display name
+ * @return bool
+ */
+ public function contactMailEnable($distinguishedName, $emailAddress, $mailNickname = NULL){
+ return $this->adldap->exchange()->contactMailEnable($distinguishedName, $emailAddress, $mailNickname);
+ }
+
+
+}
+?>
diff --git a/platform/www/lib/plugins/authad/adLDAP/classes/adLDAPExchange.php b/platform/www/lib/plugins/authad/adLDAP/classes/adLDAPExchange.php
new file mode 100644
index 0000000..d70aac7
--- /dev/null
+++ b/platform/www/lib/plugins/authad/adLDAP/classes/adLDAPExchange.php
@@ -0,0 +1,390 @@
+<?php
+/**
+ * PHP LDAP CLASS FOR MANIPULATING ACTIVE DIRECTORY
+ * Version 4.0.4
+ *
+ * PHP Version 5 with SSL and LDAP support
+ *
+ * Written by Scott Barnett, Richard Hyland
+ * email: scott@wiggumworld.com, adldap@richardhyland.com
+ * http://adldap.sourceforge.net/
+ *
+ * Copyright (c) 2006-2012 Scott Barnett, Richard Hyland
+ *
+ * We'd appreciate any improvements or additions to be submitted back
+ * to benefit the entire community :)
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * @category ToolsAndUtilities
+ * @package adLDAP
+ * @subpackage Exchange
+ * @author Scott Barnett, Richard Hyland
+ * @copyright (c) 2006-2012 Scott Barnett, Richard Hyland
+ * @license http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html LGPLv2.1
+ * @revision $Revision: 97 $
+ * @version 4.0.4
+ * @link http://adldap.sourceforge.net/
+ */
+require_once(dirname(__FILE__) . '/../adLDAP.php');
+
+/**
+* MICROSOFT EXCHANGE FUNCTIONS
+*/
+class adLDAPExchange {
+ /**
+ * The current adLDAP connection via dependency injection
+ *
+ * @var adLDAP
+ */
+ protected $adldap;
+
+ public function __construct(adLDAP $adldap) {
+ $this->adldap = $adldap;
+ }
+
+ /**
+ * Create an Exchange account
+ *
+ * @param string $username The username of the user to add the Exchange account to
+ * @param array $storageGroup The mailbox, Exchange Storage Group, for the user account, this must be a full CN
+ * If the storage group has a different base_dn to the adLDAP configuration, set it using $base_dn
+ * @param string $emailAddress The primary email address to add to this user
+ * @param string $mailNickname The mail nick name. If mail nickname is blank, the username will be used
+ * @param bool $mdbUseDefaults Indicates whether the store should use the default quota, rather than the per-mailbox quota.
+ * @param string $baseDn Specify an alternative base_dn for the Exchange storage group
+ * @param bool $isGUID Is the username passed a GUID or a samAccountName
+ * @return bool
+ */
+ public function createMailbox($username, $storageGroup, $emailAddress, $mailNickname=NULL, $useDefaults=TRUE, $baseDn=NULL, $isGUID=false)
+ {
+ if ($username === NULL){ return "Missing compulsory field [username]"; }
+ if ($storageGroup === NULL) { return "Missing compulsory array [storagegroup]"; }
+ if (!is_array($storageGroup)) { return "[storagegroup] must be an array"; }
+ if ($emailAddress === NULL) { return "Missing compulsory field [emailAddress]"; }
+
+ if ($baseDn === NULL) {
+ $baseDn = $this->adldap->getBaseDn();
+ }
+
+ $container = "CN=" . implode(",CN=", $storageGroup);
+
+ if ($mailNickname === NULL) {
+ $mailNickname = $username;
+ }
+ $mdbUseDefaults = $this->adldap->utilities()->boolToString($useDefaults);
+
+ $attributes = array(
+ 'exchange_homemdb'=>$container.",".$baseDn,
+ 'exchange_proxyaddress'=>'SMTP:' . $emailAddress,
+ 'exchange_mailnickname'=>$mailNickname,
+ 'exchange_usedefaults'=>$mdbUseDefaults
+ );
+ $result = $this->adldap->user()->modify($username, $attributes, $isGUID);
+ if ($result == false) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Add an X400 address to Exchange
+ * See http://tools.ietf.org/html/rfc1685 for more information.
+ * An X400 Address looks similar to this X400:c=US;a= ;p=Domain;o=Organization;s=Doe;g=John;
+ *
+ * @param string $username The username of the user to add the X400 to to
+ * @param string $country Country
+ * @param string $admd Administration Management Domain
+ * @param string $pdmd Private Management Domain (often your AD domain)
+ * @param string $org Organization
+ * @param string $surname Surname
+ * @param string $givenName Given name
+ * @param bool $isGUID Is the username passed a GUID or a samAccountName
+ * @return bool
+ */
+ public function addX400($username, $country, $admd, $pdmd, $org, $surname, $givenName, $isGUID=false)
+ {
+ if ($username === NULL){ return "Missing compulsory field [username]"; }
+
+ $proxyValue = 'X400:';
+
+ // Find the dn of the user
+ $user = $this->adldap->user()->info($username, array("cn","proxyaddresses"), $isGUID);
+ if ($user[0]["dn"] === NULL) { return false; }
+ $userDn = $user[0]["dn"];
+
+ // We do not have to demote an email address from the default so we can just add the new proxy address
+ $attributes['exchange_proxyaddress'] = $proxyValue . 'c=' . $country . ';a=' . $admd . ';p=' . $pdmd . ';o=' . $org . ';s=' . $surname . ';g=' . $givenName . ';';
+
+ // Translate the update to the LDAP schema
+ $add = $this->adldap->adldap_schema($attributes);
+
+ if (!$add) { return false; }
+
+ // Do the update
+ // Take out the @ to see any errors, usually this error might occur because the address already
+ // exists in the list of proxyAddresses
+ $result = @ldap_mod_add($this->adldap->getLdapConnection(), $userDn, $add);
+ if ($result == false) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Add an address to Exchange
+ *
+ * @param string $username The username of the user to add the Exchange account to
+ * @param string $emailAddress The email address to add to this user
+ * @param bool $default Make this email address the default address, this is a bit more intensive as we have to demote any existing default addresses
+ * @param bool $isGUID Is the username passed a GUID or a samAccountName
+ * @return bool
+ */
+ public function addAddress($username, $emailAddress, $default = FALSE, $isGUID = false)
+ {
+ if ($username === NULL) { return "Missing compulsory field [username]"; }
+ if ($emailAddress === NULL) { return "Missing compulsory fields [emailAddress]"; }
+
+ $proxyValue = 'smtp:';
+ if ($default === true) {
+ $proxyValue = 'SMTP:';
+ }
+
+ // Find the dn of the user
+ $user = $this->adldap->user()->info($username, array("cn","proxyaddresses"), $isGUID);
+ if ($user[0]["dn"] === NULL){ return false; }
+ $userDn = $user[0]["dn"];
+
+ // We need to scan existing proxy addresses and demote the default one
+ if (is_array($user[0]["proxyaddresses"]) && $default === true) {
+ $modAddresses = array();
+ for ($i=0;$i<sizeof($user[0]['proxyaddresses']);$i++) {
+ if (strstr($user[0]['proxyaddresses'][$i], 'SMTP:') !== false) {
+ $user[0]['proxyaddresses'][$i] = str_replace('SMTP:', 'smtp:', $user[0]['proxyaddresses'][$i]);
+ }
+ if ($user[0]['proxyaddresses'][$i] != '') {
+ $modAddresses['proxyAddresses'][$i] = $user[0]['proxyaddresses'][$i];
+ }
+ }
+ $modAddresses['proxyAddresses'][(sizeof($user[0]['proxyaddresses'])-1)] = 'SMTP:' . $emailAddress;
+
+ $result = @ldap_mod_replace($this->adldap->getLdapConnection(), $userDn, $modAddresses);
+ if ($result == false) {
+ return false;
+ }
+
+ return true;
+ }
+ else {
+ // We do not have to demote an email address from the default so we can just add the new proxy address
+ $attributes['exchange_proxyaddress'] = $proxyValue . $emailAddress;
+
+ // Translate the update to the LDAP schema
+ $add = $this->adldap->adldap_schema($attributes);
+
+ if (!$add) {
+ return false;
+ }
+
+ // Do the update
+ // Take out the @ to see any errors, usually this error might occur because the address already
+ // exists in the list of proxyAddresses
+ $result = @ldap_mod_add($this->adldap->getLdapConnection(), $userDn,$add);
+ if ($result == false) {
+ return false;
+ }
+
+ return true;
+ }
+ }
+
+ /**
+ * Remove an address to Exchange
+ * If you remove a default address the account will no longer have a default,
+ * we recommend changing the default address first
+ *
+ * @param string $username The username of the user to add the Exchange account to
+ * @param string $emailAddress The email address to add to this user
+ * @param bool $isGUID Is the username passed a GUID or a samAccountName
+ * @return bool
+ */
+ public function deleteAddress($username, $emailAddress, $isGUID=false)
+ {
+ if ($username === NULL) { return "Missing compulsory field [username]"; }
+ if ($emailAddress === NULL) { return "Missing compulsory fields [emailAddress]"; }
+
+ // Find the dn of the user
+ $user = $this->adldap->user()->info($username, array("cn","proxyaddresses"), $isGUID);
+ if ($user[0]["dn"] === NULL) { return false; }
+ $userDn = $user[0]["dn"];
+
+ if (is_array($user[0]["proxyaddresses"])) {
+ $mod = array();
+ for ($i=0;$i<sizeof($user[0]['proxyaddresses']);$i++) {
+ if (strstr($user[0]['proxyaddresses'][$i], 'SMTP:') !== false && $user[0]['proxyaddresses'][$i] == 'SMTP:' . $emailAddress) {
+ $mod['proxyAddresses'][0] = 'SMTP:' . $emailAddress;
+ }
+ elseif (strstr($user[0]['proxyaddresses'][$i], 'smtp:') !== false && $user[0]['proxyaddresses'][$i] == 'smtp:' . $emailAddress) {
+ $mod['proxyAddresses'][0] = 'smtp:' . $emailAddress;
+ }
+ }
+
+ $result = @ldap_mod_del($this->adldap->getLdapConnection(), $userDn,$mod);
+ if ($result == false) {
+ return false;
+ }
+
+ return true;
+ }
+ else {
+ return false;
+ }
+ }
+ /**
+ * Change the default address
+ *
+ * @param string $username The username of the user to add the Exchange account to
+ * @param string $emailAddress The email address to make default
+ * @param bool $isGUID Is the username passed a GUID or a samAccountName
+ * @return bool
+ */
+ public function primaryAddress($username, $emailAddress, $isGUID = false)
+ {
+ if ($username === NULL) { return "Missing compulsory field [username]"; }
+ if ($emailAddress === NULL) { return "Missing compulsory fields [emailAddress]"; }
+
+ // Find the dn of the user
+ $user = $this->adldap->user()->info($username, array("cn","proxyaddresses"), $isGUID);
+ if ($user[0]["dn"] === NULL){ return false; }
+ $userDn = $user[0]["dn"];
+
+ if (is_array($user[0]["proxyaddresses"])) {
+ $modAddresses = array();
+ for ($i=0;$i<sizeof($user[0]['proxyaddresses']);$i++) {
+ if (strstr($user[0]['proxyaddresses'][$i], 'SMTP:') !== false) {
+ $user[0]['proxyaddresses'][$i] = str_replace('SMTP:', 'smtp:', $user[0]['proxyaddresses'][$i]);
+ }
+ if ($user[0]['proxyaddresses'][$i] == 'smtp:' . $emailAddress) {
+ $user[0]['proxyaddresses'][$i] = str_replace('smtp:', 'SMTP:', $user[0]['proxyaddresses'][$i]);
+ }
+ if ($user[0]['proxyaddresses'][$i] != '') {
+ $modAddresses['proxyAddresses'][$i] = $user[0]['proxyaddresses'][$i];
+ }
+ }
+
+ $result = @ldap_mod_replace($this->adldap->getLdapConnection(), $userDn, $modAddresses);
+ if ($result == false) {
+ return false;
+ }
+
+ return true;
+ }
+
+ }
+
+ /**
+ * Mail enable a contact
+ * Allows email to be sent to them through Exchange
+ *
+ * @param string $distinguishedName The contact to mail enable
+ * @param string $emailAddress The email address to allow emails to be sent through
+ * @param string $mailNickname The mailnickname for the contact in Exchange. If NULL this will be set to the display name
+ * @return bool
+ */
+ public function contactMailEnable($distinguishedName, $emailAddress, $mailNickname = NULL)
+ {
+ if ($distinguishedName === NULL) { return "Missing compulsory field [distinguishedName]"; }
+ if ($emailAddress === NULL) { return "Missing compulsory field [emailAddress]"; }
+
+ if ($mailNickname !== NULL) {
+ // Find the dn of the user
+ $user = $this->adldap->contact()->info($distinguishedName, array("cn","displayname"));
+ if ($user[0]["displayname"] === NULL) { return false; }
+ $mailNickname = $user[0]['displayname'][0];
+ }
+
+ $attributes = array("email"=>$emailAddress,"contact_email"=>"SMTP:" . $emailAddress,"exchange_proxyaddress"=>"SMTP:" . $emailAddress,"exchange_mailnickname" => $mailNickname);
+
+ // Translate the update to the LDAP schema
+ $mod = $this->adldap->adldap_schema($attributes);
+
+ // Check to see if this is an enabled status update
+ if (!$mod) { return false; }
+
+ // Do the update
+ $result = ldap_modify($this->adldap->getLdapConnection(), $distinguishedName, $mod);
+ if ($result == false) { return false; }
+
+ return true;
+ }
+
+ /**
+ * Returns a list of Exchange Servers in the ConfigurationNamingContext of the domain
+ *
+ * @param array $attributes An array of the AD attributes you wish to return
+ * @return array
+ */
+ public function servers($attributes = array('cn','distinguishedname','serialnumber'))
+ {
+ if (!$this->adldap->getLdapBind()){ return false; }
+
+ $configurationNamingContext = $this->adldap->getRootDse(array('configurationnamingcontext'));
+ $sr = @ldap_search($this->adldap->getLdapConnection(), $configurationNamingContext[0]['configurationnamingcontext'][0],'(&(objectCategory=msExchExchangeServer))', $attributes);
+ $entries = @ldap_get_entries($this->adldap->getLdapConnection(), $sr);
+ return $entries;
+ }
+
+ /**
+ * Returns a list of Storage Groups in Exchange for a given mail server
+ *
+ * @param string $exchangeServer The full DN of an Exchange server. You can use exchange_servers() to find the DN for your server
+ * @param array $attributes An array of the AD attributes you wish to return
+ * @param bool $recursive If enabled this will automatically query the databases within a storage group
+ * @return array
+ */
+ public function storageGroups($exchangeServer, $attributes = array('cn','distinguishedname'), $recursive = NULL)
+ {
+ if (!$this->adldap->getLdapBind()){ return false; }
+ if ($exchangeServer === NULL) { return "Missing compulsory field [exchangeServer]"; }
+ if ($recursive === NULL) { $recursive = $this->adldap->getRecursiveGroups(); }
+
+ $filter = '(&(objectCategory=msExchStorageGroup))';
+ $sr = @ldap_search($this->adldap->getLdapConnection(), $exchangeServer, $filter, $attributes);
+ $entries = @ldap_get_entries($this->adldap->getLdapConnection(), $sr);
+
+ if ($recursive === true) {
+ for ($i=0; $i<$entries['count']; $i++) {
+ $entries[$i]['msexchprivatemdb'] = $this->storageDatabases($entries[$i]['distinguishedname'][0]);
+ }
+ }
+
+ return $entries;
+ }
+
+ /**
+ * Returns a list of Databases within any given storage group in Exchange for a given mail server
+ *
+ * @param string $storageGroup The full DN of an Storage Group. You can use exchange_storage_groups() to find the DN
+ * @param array $attributes An array of the AD attributes you wish to return
+ * @return array
+ */
+ public function storageDatabases($storageGroup, $attributes = array('cn','distinguishedname','displayname')) {
+ if (!$this->adldap->getLdapBind()){ return false; }
+ if ($storageGroup === NULL) { return "Missing compulsory field [storageGroup]"; }
+
+ $filter = '(&(objectCategory=msExchPrivateMDB))';
+ $sr = @ldap_search($this->adldap->getLdapConnection(), $storageGroup, $filter, $attributes);
+ $entries = @ldap_get_entries($this->adldap->getLdapConnection(), $sr);
+ return $entries;
+ }
+}
+?> \ No newline at end of file
diff --git a/platform/www/lib/plugins/authad/adLDAP/classes/adLDAPFolders.php b/platform/www/lib/plugins/authad/adLDAP/classes/adLDAPFolders.php
new file mode 100644
index 0000000..67b1474
--- /dev/null
+++ b/platform/www/lib/plugins/authad/adLDAP/classes/adLDAPFolders.php
@@ -0,0 +1,179 @@
+<?php
+/**
+ * PHP LDAP CLASS FOR MANIPULATING ACTIVE DIRECTORY
+ * Version 4.0.4
+ *
+ * PHP Version 5 with SSL and LDAP support
+ *
+ * Written by Scott Barnett, Richard Hyland
+ * email: scott@wiggumworld.com, adldap@richardhyland.com
+ * http://adldap.sourceforge.net/
+ *
+ * Copyright (c) 2006-2012 Scott Barnett, Richard Hyland
+ *
+ * We'd appreciate any improvements or additions to be submitted back
+ * to benefit the entire community :)
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * @category ToolsAndUtilities
+ * @package adLDAP
+ * @subpackage Folders
+ * @author Scott Barnett, Richard Hyland
+ * @copyright (c) 2006-2012 Scott Barnett, Richard Hyland
+ * @license http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html LGPLv2.1
+ * @revision $Revision: 97 $
+ * @version 4.0.4
+ * @link http://adldap.sourceforge.net/
+ */
+require_once(dirname(__FILE__) . '/../adLDAP.php');
+
+/**
+* FOLDER / OU MANAGEMENT FUNCTIONS
+*/
+class adLDAPFolders {
+ /**
+ * The current adLDAP connection via dependency injection
+ *
+ * @var adLDAP
+ */
+ protected $adldap;
+
+ public function __construct(adLDAP $adldap) {
+ $this->adldap = $adldap;
+ }
+
+ /**
+ * Delete a distinguished name from Active Directory
+ * You should never need to call this yourself, just use the wrapper functions user_delete and contact_delete
+ *
+ * @param string $dn The distinguished name to delete
+ * @return bool
+ */
+ public function delete($dn){
+ $result = ldap_delete($this->adldap->getLdapConnection(), $dn);
+ if ($result != true) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Returns a folder listing for a specific OU
+ * See http://adldap.sourceforge.net/wiki/doku.php?id=api_folder_functions
+ *
+ * @param array $folderName An array to the OU you wish to list.
+ * If set to NULL will list the root, strongly recommended to set
+ * $recursive to false in that instance!
+ * @param string $dnType The type of record to list. This can be ADLDAP_FOLDER or ADLDAP_CONTAINER.
+ * @param bool $recursive Recursively search sub folders
+ * @param bool $type Specify a type of object to search for
+ * @return array
+ */
+ public function listing($folderName = NULL, $dnType = adLDAP::ADLDAP_FOLDER, $recursive = NULL, $type = NULL)
+ {
+ if ($recursive === NULL) { $recursive = $this->adldap->getRecursiveGroups(); } //use the default option if they haven't set it
+ if (!$this->adldap->getLdapBind()) { return false; }
+
+ $filter = '(&';
+ if ($type !== NULL) {
+ switch ($type) {
+ case 'contact':
+ $filter .= '(objectClass=contact)';
+ break;
+ case 'computer':
+ $filter .= '(objectClass=computer)';
+ break;
+ case 'group':
+ $filter .= '(objectClass=group)';
+ break;
+ case 'folder':
+ $filter .= '(objectClass=organizationalUnit)';
+ break;
+ case 'container':
+ $filter .= '(objectClass=container)';
+ break;
+ case 'domain':
+ $filter .= '(objectClass=builtinDomain)';
+ break;
+ default:
+ $filter .= '(objectClass=user)';
+ break;
+ }
+ }
+ else {
+ $filter .= '(objectClass=*)';
+ }
+ // If the folder name is null then we will search the root level of AD
+ // This requires us to not have an OU= part, just the base_dn
+ $searchOu = $this->adldap->getBaseDn();
+ if (is_array($folderName)) {
+ $ou = $dnType . "=" . implode("," . $dnType . "=", $folderName);
+ $filter .= '(!(distinguishedname=' . $ou . ',' . $this->adldap->getBaseDn() . ')))';
+ $searchOu = $ou . ',' . $this->adldap->getBaseDn();
+ }
+ else {
+ $filter .= '(!(distinguishedname=' . $this->adldap->getBaseDn() . ')))';
+ }
+
+ if ($recursive === true) {
+ $sr = ldap_search($this->adldap->getLdapConnection(), $searchOu, $filter, array('objectclass', 'distinguishedname', 'samaccountname'));
+ $entries = @ldap_get_entries($this->adldap->getLdapConnection(), $sr);
+ if (is_array($entries)) {
+ return $entries;
+ }
+ }
+ else {
+ $sr = ldap_list($this->adldap->getLdapConnection(), $searchOu, $filter, array('objectclass', 'distinguishedname', 'samaccountname'));
+ $entries = @ldap_get_entries($this->adldap->getLdapConnection(), $sr);
+ if (is_array($entries)) {
+ return $entries;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Create an organizational unit
+ *
+ * @param array $attributes Default attributes of the ou
+ * @return bool
+ */
+ public function create($attributes)
+ {
+ if (!is_array($attributes)){ return "Attributes must be an array"; }
+ if (!is_array($attributes["container"])) { return "Container attribute must be an array."; }
+ if (!array_key_exists("ou_name",$attributes)) { return "Missing compulsory field [ou_name]"; }
+ if (!array_key_exists("container",$attributes)) { return "Missing compulsory field [container]"; }
+
+ $attributes["container"] = array_reverse($attributes["container"]);
+
+ $add=array();
+ $add["objectClass"] = "organizationalUnit";
+ $add["OU"] = $attributes['ou_name'];
+ $containers = "";
+ if (count($attributes['container']) > 0) {
+ $containers = "OU=" . implode(",OU=", $attributes["container"]) . ",";
+ }
+
+ $containers = "OU=" . implode(",OU=", $attributes["container"]);
+ $result = ldap_add($this->adldap->getLdapConnection(), "OU=" . $add["OU"] . ", " . $containers . $this->adldap->getBaseDn(), $add);
+ if ($result != true) {
+ return false;
+ }
+
+ return true;
+ }
+
+}
+
+?> \ No newline at end of file
diff --git a/platform/www/lib/plugins/authad/adLDAP/classes/adLDAPGroups.php b/platform/www/lib/plugins/authad/adLDAP/classes/adLDAPGroups.php
new file mode 100644
index 0000000..94bc048
--- /dev/null
+++ b/platform/www/lib/plugins/authad/adLDAP/classes/adLDAPGroups.php
@@ -0,0 +1,631 @@
+<?php
+/**
+ * PHP LDAP CLASS FOR MANIPULATING ACTIVE DIRECTORY
+ * Version 4.0.4
+ *
+ * PHP Version 5 with SSL and LDAP support
+ *
+ * Written by Scott Barnett, Richard Hyland
+ * email: scott@wiggumworld.com, adldap@richardhyland.com
+ * http://adldap.sourceforge.net/
+ *
+ * Copyright (c) 2006-2012 Scott Barnett, Richard Hyland
+ *
+ * We'd appreciate any improvements or additions to be submitted back
+ * to benefit the entire community :)
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * @category ToolsAndUtilities
+ * @package adLDAP
+ * @subpackage Groups
+ * @author Scott Barnett, Richard Hyland
+ * @copyright (c) 2006-2012 Scott Barnett, Richard Hyland
+ * @license http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html LGPLv2.1
+ * @revision $Revision: 97 $
+ * @version 4.0.4
+ * @link http://adldap.sourceforge.net/
+ */
+require_once(dirname(__FILE__) . '/../adLDAP.php');
+require_once(dirname(__FILE__) . '/../collections/adLDAPGroupCollection.php');
+
+/**
+* GROUP FUNCTIONS
+*/
+class adLDAPGroups {
+ /**
+ * The current adLDAP connection via dependency injection
+ *
+ * @var adLDAP
+ */
+ protected $adldap;
+
+ public function __construct(adLDAP $adldap) {
+ $this->adldap = $adldap;
+ }
+
+ /**
+ * Add a group to a group
+ *
+ * @param string $parent The parent group name
+ * @param string $child The child group name
+ * @return bool
+ */
+ public function addGroup($parent,$child){
+
+ // Find the parent group's dn
+ $parentGroup = $this->ginfo($parent, array("cn"));
+ if ($parentGroup[0]["dn"] === NULL){
+ return false;
+ }
+ $parentDn = $parentGroup[0]["dn"];
+
+ // Find the child group's dn
+ $childGroup = $this->info($child, array("cn"));
+ if ($childGroup[0]["dn"] === NULL){
+ return false;
+ }
+ $childDn = $childGroup[0]["dn"];
+
+ $add = array();
+ $add["member"] = $childDn;
+
+ $result = @ldap_mod_add($this->adldap->getLdapConnection(), $parentDn, $add);
+ if ($result == false) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Add a user to a group
+ *
+ * @param string $group The group to add the user to
+ * @param string $user The user to add to the group
+ * @param bool $isGUID Is the username passed a GUID or a samAccountName
+ * @return bool
+ */
+ public function addUser($group, $user, $isGUID = false)
+ {
+ // Adding a user is a bit fiddly, we need to get the full DN of the user
+ // and add it using the full DN of the group
+
+ // Find the user's dn
+ $userDn = $this->adldap->user()->dn($user, $isGUID);
+ if ($userDn === false) {
+ return false;
+ }
+
+ // Find the group's dn
+ $groupInfo = $this->info($group, array("cn"));
+ if ($groupInfo[0]["dn"] === NULL) {
+ return false;
+ }
+ $groupDn = $groupInfo[0]["dn"];
+
+ $add = array();
+ $add["member"] = $userDn;
+
+ $result = @ldap_mod_add($this->adldap->getLdapConnection(), $groupDn, $add);
+ if ($result == false) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Add a contact to a group
+ *
+ * @param string $group The group to add the contact to
+ * @param string $contactDn The DN of the contact to add
+ * @return bool
+ */
+ public function addContact($group, $contactDn)
+ {
+ // To add a contact we take the contact's DN
+ // and add it using the full DN of the group
+
+ // Find the group's dn
+ $groupInfo = $this->info($group, array("cn"));
+ if ($groupInfo[0]["dn"] === NULL) {
+ return false;
+ }
+ $groupDn = $groupInfo[0]["dn"];
+
+ $add = array();
+ $add["member"] = $contactDn;
+
+ $result = @ldap_mod_add($this->adldap->getLdapConnection(), $groupDn, $add);
+ if ($result == false) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Create a group
+ *
+ * @param array $attributes Default attributes of the group
+ * @return bool
+ */
+ public function create($attributes)
+ {
+ if (!is_array($attributes)){ return "Attributes must be an array"; }
+ if (!array_key_exists("group_name", $attributes)){ return "Missing compulsory field [group_name]"; }
+ if (!array_key_exists("container", $attributes)){ return "Missing compulsory field [container]"; }
+ if (!array_key_exists("description", $attributes)){ return "Missing compulsory field [description]"; }
+ if (!is_array($attributes["container"])){ return "Container attribute must be an array."; }
+ $attributes["container"] = array_reverse($attributes["container"]);
+
+ //$member_array = array();
+ //$member_array[0] = "cn=user1,cn=Users,dc=yourdomain,dc=com";
+ //$member_array[1] = "cn=administrator,cn=Users,dc=yourdomain,dc=com";
+
+ $add = array();
+ $add["cn"] = $attributes["group_name"];
+ $add["samaccountname"] = $attributes["group_name"];
+ $add["objectClass"] = "Group";
+ $add["description"] = $attributes["description"];
+ //$add["member"] = $member_array; UNTESTED
+
+ $container = "OU=" . implode(",OU=", $attributes["container"]);
+ $result = ldap_add($this->adldap->getLdapConnection(), "CN=" . $add["cn"] . ", " . $container . "," . $this->adldap->getBaseDn(), $add);
+ if ($result != true) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Delete a group account
+ *
+ * @param string $group The group to delete (please be careful here!)
+ *
+ * @return array
+ */
+ public function delete($group) {
+ if (!$this->adldap->getLdapBind()){ return false; }
+ if ($group === null){ return "Missing compulsory field [group]"; }
+
+ $groupInfo = $this->info($group, array("*"));
+ $dn = $groupInfo[0]['distinguishedname'][0];
+ $result = $this->adldap->folder()->delete($dn);
+ if ($result !== true) {
+ return false;
+ } return true;
+ }
+
+ /**
+ * Remove a group from a group
+ *
+ * @param string $parent The parent group name
+ * @param string $child The child group name
+ * @return bool
+ */
+ public function removeGroup($parent , $child)
+ {
+
+ // Find the parent dn
+ $parentGroup = $this->info($parent, array("cn"));
+ if ($parentGroup[0]["dn"] === NULL) {
+ return false;
+ }
+ $parentDn = $parentGroup[0]["dn"];
+
+ // Find the child dn
+ $childGroup = $this->info($child, array("cn"));
+ if ($childGroup[0]["dn"] === NULL) {
+ return false;
+ }
+ $childDn = $childGroup[0]["dn"];
+
+ $del = array();
+ $del["member"] = $childDn;
+
+ $result = @ldap_mod_del($this->adldap->getLdapConnection(), $parentDn, $del);
+ if ($result == false) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Remove a user from a group
+ *
+ * @param string $group The group to remove a user from
+ * @param string $user The AD user to remove from the group
+ * @param bool $isGUID Is the username passed a GUID or a samAccountName
+ * @return bool
+ */
+ public function removeUser($group, $user, $isGUID = false)
+ {
+
+ // Find the parent dn
+ $groupInfo = $this->info($group, array("cn"));
+ if ($groupInfo[0]["dn"] === NULL){
+ return false;
+ }
+ $groupDn = $groupInfo[0]["dn"];
+
+ // Find the users dn
+ $userDn = $this->adldap->user()->dn($user, $isGUID);
+ if ($userDn === false) {
+ return false;
+ }
+
+ $del = array();
+ $del["member"] = $userDn;
+
+ $result = @ldap_mod_del($this->adldap->getLdapConnection(), $groupDn, $del);
+ if ($result == false) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Remove a contact from a group
+ *
+ * @param string $group The group to remove a user from
+ * @param string $contactDn The DN of a contact to remove from the group
+ * @return bool
+ */
+ public function removeContact($group, $contactDn)
+ {
+
+ // Find the parent dn
+ $groupInfo = $this->info($group, array("cn"));
+ if ($groupInfo[0]["dn"] === NULL) {
+ return false;
+ }
+ $groupDn = $groupInfo[0]["dn"];
+
+ $del = array();
+ $del["member"] = $contactDn;
+
+ $result = @ldap_mod_del($this->adldap->getLdapConnection(), $groupDn, $del);
+ if ($result == false) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Return a list of groups in a group
+ *
+ * @param string $group The group to query
+ * @param bool $recursive Recursively get groups
+ * @return array
+ */
+ public function inGroup($group, $recursive = NULL)
+ {
+ if (!$this->adldap->getLdapBind()){ return false; }
+ if ($recursive === NULL){ $recursive = $this->adldap->getRecursiveGroups(); } // Use the default option if they haven't set it
+
+ // Search the directory for the members of a group
+ $info = $this->info($group, array("member","cn"));
+ $groups = $info[0]["member"];
+ if (!is_array($groups)) {
+ return false;
+ }
+
+ $groupArray = array();
+
+ for ($i=0; $i<$groups["count"]; $i++){
+ $filter = "(&(objectCategory=group)(distinguishedName=" . $this->adldap->utilities()->ldapSlashes($groups[$i]) . "))";
+ $fields = array("samaccountname", "distinguishedname", "objectClass");
+ $sr = ldap_search($this->adldap->getLdapConnection(), $this->adldap->getBaseDn(), $filter, $fields);
+ $entries = ldap_get_entries($this->adldap->getLdapConnection(), $sr);
+
+ // not a person, look for a group
+ if ($entries['count'] == 0 && $recursive == true) {
+ $filter = "(&(objectCategory=group)(distinguishedName=" . $this->adldap->utilities()->ldapSlashes($groups[$i]) . "))";
+ $fields = array("distinguishedname");
+ $sr = ldap_search($this->adldap->getLdapConnection(), $this->adldap->getBaseDn(), $filter, $fields);
+ $entries = ldap_get_entries($this->adldap->getLdapConnection(), $sr);
+ if (!isset($entries[0]['distinguishedname'][0])) {
+ continue;
+ }
+ $subGroups = $this->inGroup($entries[0]['distinguishedname'][0], $recursive);
+ if (is_array($subGroups)) {
+ $groupArray = array_merge($groupArray, $subGroups);
+ $groupArray = array_unique($groupArray);
+ }
+ continue;
+ }
+
+ $groupArray[] = $entries[0]['distinguishedname'][0];
+ }
+ return $groupArray;
+ }
+
+ /**
+ * Return a list of members in a group
+ *
+ * @param string $group The group to query
+ * @param bool $recursive Recursively get group members
+ * @return array
+ */
+ public function members($group, $recursive = NULL)
+ {
+ if (!$this->adldap->getLdapBind()){ return false; }
+ if ($recursive === NULL){ $recursive = $this->adldap->getRecursiveGroups(); } // Use the default option if they haven't set it
+ // Search the directory for the members of a group
+ $info = $this->info($group, array("member","cn"));
+ $users = $info[0]["member"];
+ if (!is_array($users)) {
+ return false;
+ }
+
+ $userArray = array();
+
+ for ($i=0; $i<$users["count"]; $i++){
+ $filter = "(&(objectCategory=person)(distinguishedName=" . $this->adldap->utilities()->ldapSlashes($users[$i]) . "))";
+ $fields = array("samaccountname", "distinguishedname", "objectClass");
+ $sr = ldap_search($this->adldap->getLdapConnection(), $this->adldap->getBaseDn(), $filter, $fields);
+ $entries = ldap_get_entries($this->adldap->getLdapConnection(), $sr);
+
+ // not a person, look for a group
+ if ($entries['count'] == 0 && $recursive == true) {
+ $filter = "(&(objectCategory=group)(distinguishedName=" . $this->adldap->utilities()->ldapSlashes($users[$i]) . "))";
+ $fields = array("samaccountname");
+ $sr = ldap_search($this->adldap->getLdapConnection(), $this->adldap->getBaseDn(), $filter, $fields);
+ $entries = ldap_get_entries($this->adldap->getLdapConnection(), $sr);
+ if (!isset($entries[0]['samaccountname'][0])) {
+ continue;
+ }
+ $subUsers = $this->members($entries[0]['samaccountname'][0], $recursive);
+ if (is_array($subUsers)) {
+ $userArray = array_merge($userArray, $subUsers);
+ $userArray = array_unique($userArray);
+ }
+ continue;
+ }
+ else if ($entries['count'] == 0) {
+ continue;
+ }
+
+ if ((!isset($entries[0]['samaccountname'][0]) || $entries[0]['samaccountname'][0] === NULL) && $entries[0]['distinguishedname'][0] !== NULL) {
+ $userArray[] = $entries[0]['distinguishedname'][0];
+ }
+ else if ($entries[0]['samaccountname'][0] !== NULL) {
+ $userArray[] = $entries[0]['samaccountname'][0];
+ }
+ }
+ return $userArray;
+ }
+
+ /**
+ * Group Information. Returns an array of raw information about a group.
+ * The group name is case sensitive
+ *
+ * @param string $groupName The group name to retrieve info about
+ * @param array $fields Fields to retrieve
+ * @return array
+ */
+ public function info($groupName, $fields = NULL)
+ {
+ if ($groupName === NULL) { return false; }
+ if (!$this->adldap->getLdapBind()) { return false; }
+
+ if (stristr($groupName, '+')) {
+ $groupName = stripslashes($groupName);
+ }
+
+ $filter = "(&(objectCategory=group)(name=" . $this->adldap->utilities()->ldapSlashes($groupName) . "))";
+ if ($fields === NULL) {
+ $fields = array("member","memberof","cn","description","distinguishedname","objectcategory","samaccountname");
+ }
+ $sr = ldap_search($this->adldap->getLdapConnection(), $this->adldap->getBaseDn(), $filter, $fields);
+ $entries = ldap_get_entries($this->adldap->getLdapConnection(), $sr);
+
+ return $entries;
+ }
+
+ /**
+ * Group Information. Returns an collection
+ * The group name is case sensitive
+ *
+ * @param string $groupName The group name to retrieve info about
+ * @param array $fields Fields to retrieve
+ * @return adLDAPGroupCollection
+ */
+ public function infoCollection($groupName, $fields = NULL)
+ {
+ if ($groupName === NULL) { return false; }
+ if (!$this->adldap->getLdapBind()) { return false; }
+
+ $info = $this->info($groupName, $fields);
+ if ($info !== false) {
+ $collection = new adLDAPGroupCollection($info, $this->adldap);
+ return $collection;
+ }
+ return false;
+ }
+
+ /**
+ * Return a complete list of "groups in groups"
+ *
+ * @param string $group The group to get the list from
+ * @return array
+ */
+ public function recursiveGroups($group)
+ {
+ if ($group === NULL) { return false; }
+
+ $stack = array();
+ $processed = array();
+ $retGroups = array();
+
+ array_push($stack, $group); // Initial Group to Start with
+ while (count($stack) > 0) {
+ $parent = array_pop($stack);
+ array_push($processed, $parent);
+
+ $info = $this->info($parent, array("memberof"));
+
+ if (isset($info[0]["memberof"]) && is_array($info[0]["memberof"])) {
+ $groups = $info[0]["memberof"];
+ if ($groups) {
+ $groupNames = $this->adldap->utilities()->niceNames($groups);
+ $retGroups = array_merge($retGroups, $groupNames); //final groups to return
+ foreach ($groupNames as $id => $groupName) {
+ if (!in_array($groupName, $processed)) {
+ array_push($stack, $groupName);
+ }
+ }
+ }
+ }
+ }
+
+ return $retGroups;
+ }
+
+ /**
+ * Returns a complete list of the groups in AD based on a SAM Account Type
+ *
+ * @param string $sAMAaccountType The account type to return
+ * @param bool $includeDescription Whether to return a description
+ * @param string $search Search parameters
+ * @param bool $sorted Whether to sort the results
+ * @return array
+ */
+ public function search($sAMAaccountType = adLDAP::ADLDAP_SECURITY_GLOBAL_GROUP, $includeDescription = false, $search = "*", $sorted = true) {
+ if (!$this->adldap->getLdapBind()) { return false; }
+
+ $filter = '(&(objectCategory=group)';
+ if ($sAMAaccountType !== null) {
+ $filter .= '(samaccounttype='. $sAMAaccountType .')';
+ }
+ $filter .= '(cn=' . $search . '))';
+ // Perform the search and grab all their details
+ $fields = array("samaccountname", "description");
+ $sr = ldap_search($this->adldap->getLdapConnection(), $this->adldap->getBaseDn(), $filter, $fields);
+ $entries = ldap_get_entries($this->adldap->getLdapConnection(), $sr);
+
+ $groupsArray = array();
+ for ($i=0; $i<$entries["count"]; $i++){
+ if ($includeDescription && strlen($entries[$i]["description"][0]) > 0 ) {
+ $groupsArray[$entries[$i]["samaccountname"][0]] = $entries[$i]["description"][0];
+ }
+ else if ($includeDescription){
+ $groupsArray[$entries[$i]["samaccountname"][0]] = $entries[$i]["samaccountname"][0];
+ }
+ else {
+ array_push($groupsArray, $entries[$i]["samaccountname"][0]);
+ }
+ }
+ if ($sorted) {
+ asort($groupsArray);
+ }
+ return $groupsArray;
+ }
+
+ /**
+ * Returns a complete list of all groups in AD
+ *
+ * @param bool $includeDescription Whether to return a description
+ * @param string $search Search parameters
+ * @param bool $sorted Whether to sort the results
+ * @return array
+ */
+ public function all($includeDescription = false, $search = "*", $sorted = true){
+ $groupsArray = $this->search(null, $includeDescription, $search, $sorted);
+ return $groupsArray;
+ }
+
+ /**
+ * Returns a complete list of security groups in AD
+ *
+ * @param bool $includeDescription Whether to return a description
+ * @param string $search Search parameters
+ * @param bool $sorted Whether to sort the results
+ * @return array
+ */
+ public function allSecurity($includeDescription = false, $search = "*", $sorted = true){
+ $groupsArray = $this->search(adLDAP::ADLDAP_SECURITY_GLOBAL_GROUP, $includeDescription, $search, $sorted);
+ return $groupsArray;
+ }
+
+ /**
+ * Returns a complete list of distribution lists in AD
+ *
+ * @param bool $includeDescription Whether to return a description
+ * @param string $search Search parameters
+ * @param bool $sorted Whether to sort the results
+ * @return array
+ */
+ public function allDistribution($includeDescription = false, $search = "*", $sorted = true){
+ $groupsArray = $this->search(adLDAP::ADLDAP_DISTRIBUTION_GROUP, $includeDescription, $search, $sorted);
+ return $groupsArray;
+ }
+
+ /**
+ * Coping with AD not returning the primary group
+ * http://support.microsoft.com/?kbid=321360
+ *
+ * This is a re-write based on code submitted by Bruce which prevents the
+ * need to search each security group to find the true primary group
+ *
+ * @param string $gid Group ID
+ * @param string $usersid User's Object SID
+ * @return mixed
+ */
+ public function getPrimaryGroup($gid, $usersid)
+ {
+ if ($gid === NULL || $usersid === NULL) { return false; }
+ $sr = false;
+
+ $gsid = substr_replace($usersid, pack('V',$gid), strlen($usersid)-4,4);
+ $filter = '(objectsid=' . $this->adldap->utilities()->getTextSID($gsid).')';
+ $fields = array("samaccountname","distinguishedname");
+ $sr = ldap_search($this->adldap->getLdapConnection(), $this->adldap->getBaseDn(), $filter, $fields);
+ $entries = ldap_get_entries($this->adldap->getLdapConnection(), $sr);
+
+ if (isset($entries[0]['distinguishedname'][0])) {
+ return $entries[0]['distinguishedname'][0];
+ }
+ return false;
+ }
+
+ /**
+ * Coping with AD not returning the primary group
+ * http://support.microsoft.com/?kbid=321360
+ *
+ * For some reason it's not possible to search on primarygrouptoken=XXX
+ * If someone can show otherwise, I'd like to know about it :)
+ * this way is resource intensive and generally a pain in the @#%^
+ *
+ * @deprecated deprecated since version 3.1, see get get_primary_group
+ * @param string $gid Group ID
+ * @return string
+ */
+ public function cn($gid){
+ if ($gid === NULL) { return false; }
+ $sr = false;
+ $r = '';
+
+ $filter = "(&(objectCategory=group)(samaccounttype=" . adLDAP::ADLDAP_SECURITY_GLOBAL_GROUP . "))";
+ $fields = array("primarygrouptoken", "samaccountname", "distinguishedname");
+ $sr = ldap_search($this->adldap->getLdapConnection(), $this->adldap->getBaseDn(), $filter, $fields);
+ $entries = ldap_get_entries($this->adldap->getLdapConnection(), $sr);
+
+ for ($i=0; $i<$entries["count"]; $i++){
+ if ($entries[$i]["primarygrouptoken"][0] == $gid) {
+ $r = $entries[$i]["distinguishedname"][0];
+ $i = $entries["count"];
+ }
+ }
+
+ return $r;
+ }
+}
+?>
diff --git a/platform/www/lib/plugins/authad/adLDAP/classes/adLDAPUsers.php b/platform/www/lib/plugins/authad/adLDAP/classes/adLDAPUsers.php
new file mode 100644
index 0000000..dc3ebd7
--- /dev/null
+++ b/platform/www/lib/plugins/authad/adLDAP/classes/adLDAPUsers.php
@@ -0,0 +1,682 @@
+<?php
+/**
+ * PHP LDAP CLASS FOR MANIPULATING ACTIVE DIRECTORY
+ * Version 4.0.4
+ *
+ * PHP Version 5 with SSL and LDAP support
+ *
+ * Written by Scott Barnett, Richard Hyland
+ * email: scott@wiggumworld.com, adldap@richardhyland.com
+ * http://adldap.sourceforge.net/
+ *
+ * Copyright (c) 2006-2012 Scott Barnett, Richard Hyland
+ *
+ * We'd appreciate any improvements or additions to be submitted back
+ * to benefit the entire community :)
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * @category ToolsAndUtilities
+ * @package adLDAP
+ * @subpackage User
+ * @author Scott Barnett, Richard Hyland
+ * @copyright (c) 2006-2012 Scott Barnett, Richard Hyland
+ * @license http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html LGPLv2.1
+ * @revision $Revision: 97 $
+ * @version 4.0.4
+ * @link http://adldap.sourceforge.net/
+ */
+require_once(dirname(__FILE__) . '/../adLDAP.php');
+require_once(dirname(__FILE__) . '/../collections/adLDAPUserCollection.php');
+
+/**
+* USER FUNCTIONS
+*/
+class adLDAPUsers {
+ /**
+ * The current adLDAP connection via dependency injection
+ *
+ * @var adLDAP
+ */
+ protected $adldap;
+
+ public function __construct(adLDAP $adldap) {
+ $this->adldap = $adldap;
+ }
+
+ /**
+ * Validate a user's login credentials
+ *
+ * @param string $username A user's AD username
+ * @param string $password A user's AD password
+ * @param bool optional $prevent_rebind
+ * @return bool
+ */
+ public function authenticate($username, $password, $preventRebind = false) {
+ return $this->adldap->authenticate($username, $password, $preventRebind);
+ }
+
+ /**
+ * Create a user
+ *
+ * If you specify a password here, this can only be performed over SSL
+ *
+ * @param array $attributes The attributes to set to the user account
+ * @return bool
+ */
+ public function create($attributes)
+ {
+ // Check for compulsory fields
+ if (!array_key_exists("username", $attributes)){ return "Missing compulsory field [username]"; }
+ if (!array_key_exists("firstname", $attributes)){ return "Missing compulsory field [firstname]"; }
+ if (!array_key_exists("surname", $attributes)){ return "Missing compulsory field [surname]"; }
+ if (!array_key_exists("email", $attributes)){ return "Missing compulsory field [email]"; }
+ if (!array_key_exists("container", $attributes)){ return "Missing compulsory field [container]"; }
+ if (!is_array($attributes["container"])){ return "Container attribute must be an array."; }
+
+ if (array_key_exists("password",$attributes) && (!$this->adldap->getUseSSL() && !$this->adldap->getUseTLS())){
+ throw new adLDAPException('SSL must be configured on your webserver and enabled in the class to set passwords.');
+ }
+
+ if (!array_key_exists("display_name", $attributes)) {
+ $attributes["display_name"] = $attributes["firstname"] . " " . $attributes["surname"];
+ }
+
+ // Translate the schema
+ $add = $this->adldap->adldap_schema($attributes);
+
+ // Additional stuff only used for adding accounts
+ $add["cn"][0] = $attributes["display_name"];
+ $add["samaccountname"][0] = $attributes["username"];
+ $add["objectclass"][0] = "top";
+ $add["objectclass"][1] = "person";
+ $add["objectclass"][2] = "organizationalPerson";
+ $add["objectclass"][3] = "user"; //person?
+ //$add["name"][0]=$attributes["firstname"]." ".$attributes["surname"];
+
+ // Set the account control attribute
+ $control_options = array("NORMAL_ACCOUNT");
+ if (!$attributes["enabled"]) {
+ $control_options[] = "ACCOUNTDISABLE";
+ }
+ $add["userAccountControl"][0] = $this->accountControl($control_options);
+
+ // Determine the container
+ $attributes["container"] = array_reverse($attributes["container"]);
+ $container = "OU=" . implode(", OU=",$attributes["container"]);
+
+ // Add the entry
+ $result = @ldap_add($this->adldap->getLdapConnection(), "CN=" . $add["cn"][0] . ", " . $container . "," . $this->adldap->getBaseDn(), $add);
+ if ($result != true) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Account control options
+ *
+ * @param array $options The options to convert to int
+ * @return int
+ */
+ protected function accountControl($options)
+ {
+ $val=0;
+
+ if (is_array($options)) {
+ if (in_array("SCRIPT",$options)){ $val=$val+1; }
+ if (in_array("ACCOUNTDISABLE",$options)){ $val=$val+2; }
+ if (in_array("HOMEDIR_REQUIRED",$options)){ $val=$val+8; }
+ if (in_array("LOCKOUT",$options)){ $val=$val+16; }
+ if (in_array("PASSWD_NOTREQD",$options)){ $val=$val+32; }
+ //PASSWD_CANT_CHANGE Note You cannot assign this permission by directly modifying the UserAccountControl attribute.
+ //For information about how to set the permission programmatically, see the "Property flag descriptions" section.
+ if (in_array("ENCRYPTED_TEXT_PWD_ALLOWED",$options)){ $val=$val+128; }
+ if (in_array("TEMP_DUPLICATE_ACCOUNT",$options)){ $val=$val+256; }
+ if (in_array("NORMAL_ACCOUNT",$options)){ $val=$val+512; }
+ if (in_array("INTERDOMAIN_TRUST_ACCOUNT",$options)){ $val=$val+2048; }
+ if (in_array("WORKSTATION_TRUST_ACCOUNT",$options)){ $val=$val+4096; }
+ if (in_array("SERVER_TRUST_ACCOUNT",$options)){ $val=$val+8192; }
+ if (in_array("DONT_EXPIRE_PASSWORD",$options)){ $val=$val+65536; }
+ if (in_array("MNS_LOGON_ACCOUNT",$options)){ $val=$val+131072; }
+ if (in_array("SMARTCARD_REQUIRED",$options)){ $val=$val+262144; }
+ if (in_array("TRUSTED_FOR_DELEGATION",$options)){ $val=$val+524288; }
+ if (in_array("NOT_DELEGATED",$options)){ $val=$val+1048576; }
+ if (in_array("USE_DES_KEY_ONLY",$options)){ $val=$val+2097152; }
+ if (in_array("DONT_REQ_PREAUTH",$options)){ $val=$val+4194304; }
+ if (in_array("PASSWORD_EXPIRED",$options)){ $val=$val+8388608; }
+ if (in_array("TRUSTED_TO_AUTH_FOR_DELEGATION",$options)){ $val=$val+16777216; }
+ }
+ return $val;
+ }
+
+ /**
+ * Delete a user account
+ *
+ * @param string $username The username to delete (please be careful here!)
+ * @param bool $isGUID Is the username a GUID or a samAccountName
+ * @return array
+ */
+ public function delete($username, $isGUID = false)
+ {
+ $userinfo = $this->info($username, array("*"), $isGUID);
+ $dn = $userinfo[0]['distinguishedname'][0];
+ $result = $this->adldap->folder()->delete($dn);
+ if ($result != true) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Groups the user is a member of
+ *
+ * @param string $username The username to query
+ * @param bool $recursive Recursive list of groups
+ * @param bool $isGUID Is the username passed a GUID or a samAccountName
+ * @return array
+ */
+ public function groups($username, $recursive = NULL, $isGUID = false)
+ {
+ if ($username === NULL) { return false; }
+ if ($recursive === NULL) { $recursive = $this->adldap->getRecursiveGroups(); } // Use the default option if they haven't set it
+ if (!$this->adldap->getLdapBind()) { return false; }
+
+ // Search the directory for their information
+ $info = @$this->info($username, array("memberof", "primarygroupid"), $isGUID);
+ $groups = $this->adldap->utilities()->niceNames($info[0]["memberof"]); // Presuming the entry returned is our guy (unique usernames)
+
+ if ($recursive === true){
+ foreach ($groups as $id => $groupName){
+ $extraGroups = $this->adldap->group()->recursiveGroups($groupName);
+ $groups = array_merge($groups, $extraGroups);
+ }
+ }
+
+ return $groups;
+ }
+
+ /**
+ * Find information about the users. Returned in a raw array format from AD
+ *
+ * @param string $username The username to query
+ * @param array $fields Array of parameters to query
+ * @param bool $isGUID Is the username passed a GUID or a samAccountName
+ * @return array
+ */
+ public function info($username, $fields = NULL, $isGUID = false)
+ {
+ if ($username === NULL) { return false; }
+ if (!$this->adldap->getLdapBind()) { return false; }
+
+ if ($isGUID === true) {
+ $username = $this->adldap->utilities()->strGuidToHex($username);
+ $filter = "objectguid=" . $username;
+ }
+ else if (strstr($username, "@")) {
+ $filter = "userPrincipalName=" . $username;
+ }
+ else {
+ $filter = "samaccountname=" . $username;
+ }
+ $filter = "(&(objectCategory=person)({$filter}))";
+ if ($fields === NULL) {
+ $fields = array("samaccountname","mail","memberof","department","displayname","telephonenumber","primarygroupid","objectsid");
+ }
+ if (!in_array("objectsid", $fields)) {
+ $fields[] = "objectsid";
+ }
+ $sr = ldap_search($this->adldap->getLdapConnection(), $this->adldap->getBaseDn(), $filter, $fields);
+ $entries = ldap_get_entries($this->adldap->getLdapConnection(), $sr);
+
+ if (isset($entries[0])) {
+ if ($entries[0]['count'] >= 1) {
+ if (in_array("memberof", $fields)) {
+ // AD does not return the primary group in the ldap query, we may need to fudge it
+ if ($this->adldap->getRealPrimaryGroup() && isset($entries[0]["primarygroupid"][0]) && isset($entries[0]["objectsid"][0])){
+ //$entries[0]["memberof"][]=$this->group_cn($entries[0]["primarygroupid"][0]);
+ $entries[0]["memberof"][] = $this->adldap->group()->getPrimaryGroup($entries[0]["primarygroupid"][0], $entries[0]["objectsid"][0]);
+ } else {
+ $entries[0]["memberof"][] = "CN=Domain Users,CN=Users," . $this->adldap->getBaseDn();
+ }
+ if (!isset($entries[0]["memberof"]["count"])) {
+ $entries[0]["memberof"]["count"] = 0;
+ }
+ $entries[0]["memberof"]["count"]++;
+ }
+ }
+
+ return $entries;
+ }
+ return false;
+ }
+
+ /**
+ * Find information about the users. Returned in a raw array format from AD
+ *
+ * @param string $username The username to query
+ * @param array $fields Array of parameters to query
+ * @param bool $isGUID Is the username passed a GUID or a samAccountName
+ * @return mixed
+ */
+ public function infoCollection($username, $fields = NULL, $isGUID = false)
+ {
+ if ($username === NULL) { return false; }
+ if (!$this->adldap->getLdapBind()) { return false; }
+
+ $info = $this->info($username, $fields, $isGUID);
+
+ if ($info !== false) {
+ $collection = new adLDAPUserCollection($info, $this->adldap);
+ return $collection;
+ }
+ return false;
+ }
+
+ /**
+ * Determine if a user is in a specific group
+ *
+ * @param string $username The username to query
+ * @param string $group The name of the group to check against
+ * @param bool $recursive Check groups recursively
+ * @param bool $isGUID Is the username passed a GUID or a samAccountName
+ * @return bool
+ */
+ public function inGroup($username, $group, $recursive = NULL, $isGUID = false)
+ {
+ if ($username === NULL) { return false; }
+ if ($group === NULL) { return false; }
+ if (!$this->adldap->getLdapBind()) { return false; }
+ if ($recursive === NULL) { $recursive = $this->adldap->getRecursiveGroups(); } // Use the default option if they haven't set it
+
+ // Get a list of the groups
+ $groups = $this->groups($username, $recursive, $isGUID);
+
+ // Return true if the specified group is in the group list
+ if (in_array($group, $groups)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Determine a user's password expiry date
+ *
+ * @param string $username The username to query
+ * @param book $isGUID Is the username passed a GUID or a samAccountName
+ * @requires bcmath http://php.net/manual/en/book.bc.php
+ * @return array
+ */
+ public function passwordExpiry($username, $isGUID = false)
+ {
+ if ($username === NULL) { return "Missing compulsory field [username]"; }
+ if (!$this->adldap->getLdapBind()) { return false; }
+ if (!function_exists('bcmod')) { throw new adLDAPException("Missing function support [bcmod] http://php.net/manual/en/book.bc.php"); };
+
+ $userInfo = $this->info($username, array("pwdlastset", "useraccountcontrol"), $isGUID);
+ $pwdLastSet = $userInfo[0]['pwdlastset'][0];
+ $status = array();
+
+ if ($userInfo[0]['useraccountcontrol'][0] == '66048') {
+ // Password does not expire
+ return "Does not expire";
+ }
+ if ($pwdLastSet === '0') {
+ // Password has already expired
+ return "Password has expired";
+ }
+
+ // Password expiry in AD can be calculated from TWO values:
+ // - User's own pwdLastSet attribute: stores the last time the password was changed
+ // - Domain's maxPwdAge attribute: how long passwords last in the domain
+ //
+ // Although Microsoft chose to use a different base and unit for time measurements.
+ // This function will convert them to Unix timestamps
+ $sr = ldap_read($this->adldap->getLdapConnection(), $this->adldap->getBaseDn(), 'objectclass=*', array('maxPwdAge'));
+ if (!$sr) {
+ return false;
+ }
+ $info = ldap_get_entries($this->adldap->getLdapConnection(), $sr);
+ $maxPwdAge = $info[0]['maxpwdage'][0];
+
+
+ // See MSDN: http://msdn.microsoft.com/en-us/library/ms974598.aspx
+ //
+ // pwdLastSet contains the number of 100 nanosecond intervals since January 1, 1601 (UTC),
+ // stored in a 64 bit integer.
+ //
+ // The number of seconds between this date and Unix epoch is 11644473600.
+ //
+ // maxPwdAge is stored as a large integer that represents the number of 100 nanosecond
+ // intervals from the time the password was set before the password expires.
+ //
+ // We also need to scale this to seconds but also this value is a _negative_ quantity!
+ //
+ // If the low 32 bits of maxPwdAge are equal to 0 passwords do not expire
+ //
+ // Unfortunately the maths involved are too big for PHP integers, so I've had to require
+ // BCMath functions to work with arbitrary precision numbers.
+ if (bcmod($maxPwdAge, 4294967296) === '0') {
+ return "Domain does not expire passwords";
+ }
+
+ // Add maxpwdage and pwdlastset and we get password expiration time in Microsoft's
+ // time units. Because maxpwd age is negative we need to subtract it.
+ $pwdExpire = bcsub($pwdLastSet, $maxPwdAge);
+
+ // Convert MS's time to Unix time
+ $status['expiryts'] = bcsub(bcdiv($pwdExpire, '10000000'), '11644473600');
+ $status['expiryformat'] = date('Y-m-d H:i:s', bcsub(bcdiv($pwdExpire, '10000000'), '11644473600'));
+
+ return $status;
+ }
+
+ /**
+ * Modify a user
+ *
+ * @param string $username The username to query
+ * @param array $attributes The attributes to modify. Note if you set the enabled attribute you must not specify any other attributes
+ * @param bool $isGUID Is the username passed a GUID or a samAccountName
+ * @return bool
+ */
+ public function modify($username, $attributes, $isGUID = false)
+ {
+ if ($username === NULL) { return "Missing compulsory field [username]"; }
+ if (array_key_exists("password", $attributes) && !$this->adldap->getUseSSL() && !$this->adldap->getUseTLS()) {
+ throw new adLDAPException('SSL/TLS must be configured on your webserver and enabled in the class to set passwords.');
+ }
+
+ // Find the dn of the user
+ $userDn = $this->dn($username, $isGUID);
+ if ($userDn === false) {
+ return false;
+ }
+
+ // Translate the update to the LDAP schema
+ $mod = $this->adldap->adldap_schema($attributes);
+
+ // Check to see if this is an enabled status update
+ if (!$mod && !array_key_exists("enabled", $attributes)){
+ return false;
+ }
+
+ // Set the account control attribute (only if specified)
+ if (array_key_exists("enabled", $attributes)){
+ if ($attributes["enabled"]){
+ $controlOptions = array("NORMAL_ACCOUNT");
+ }
+ else {
+ $controlOptions = array("NORMAL_ACCOUNT", "ACCOUNTDISABLE");
+ }
+ $mod["userAccountControl"][0] = $this->accountControl($controlOptions);
+ }
+
+ // Do the update
+ $result = @ldap_modify($this->adldap->getLdapConnection(), $userDn, $mod);
+ if ($result == false) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Disable a user account
+ *
+ * @param string $username The username to disable
+ * @param bool $isGUID Is the username passed a GUID or a samAccountName
+ * @return bool
+ */
+ public function disable($username, $isGUID = false)
+ {
+ if ($username === NULL) { return "Missing compulsory field [username]"; }
+ $attributes = array("enabled" => 0);
+ $result = $this->modify($username, $attributes, $isGUID);
+ if ($result == false) { return false; }
+
+ return true;
+ }
+
+ /**
+ * Enable a user account
+ *
+ * @param string $username The username to enable
+ * @param bool $isGUID Is the username passed a GUID or a samAccountName
+ * @return bool
+ */
+ public function enable($username, $isGUID = false)
+ {
+ if ($username === NULL) { return "Missing compulsory field [username]"; }
+ $attributes = array("enabled" => 1);
+ $result = $this->modify($username, $attributes, $isGUID);
+ if ($result == false) { return false; }
+
+ return true;
+ }
+
+ /**
+ * Set the password of a user - This must be performed over SSL
+ *
+ * @param string $username The username to modify
+ * @param string $password The new password
+ * @param bool $isGUID Is the username passed a GUID or a samAccountName
+ * @return bool
+ */
+ public function password($username, $password, $isGUID = false)
+ {
+ if ($username === NULL) { return false; }
+ if ($password === NULL) { return false; }
+ if (!$this->adldap->getLdapBind()) { return false; }
+ if (!$this->adldap->getUseSSL() && !$this->adldap->getUseTLS()) {
+ throw new adLDAPException('SSL must be configured on your webserver and enabled in the class to set passwords.');
+ }
+
+ $userDn = $this->dn($username, $isGUID);
+ if ($userDn === false) {
+ return false;
+ }
+
+ $add=array();
+ $add["unicodePwd"][0] = $this->encodePassword($password);
+
+ $result = @ldap_mod_replace($this->adldap->getLdapConnection(), $userDn, $add);
+ if ($result === false){
+ $err = ldap_errno($this->adldap->getLdapConnection());
+ if ($err) {
+ $msg = 'Error ' . $err . ': ' . ldap_err2str($err) . '.';
+ if($err == 53) {
+ $msg .= ' Your password might not match the password policy.';
+ }
+ throw new adLDAPException($msg);
+ }
+ else {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Encode a password for transmission over LDAP
+ *
+ * @param string $password The password to encode
+ * @return string
+ */
+ public function encodePassword($password)
+ {
+ $password="\"".$password."\"";
+ $encoded="";
+ for ($i=0; $i <strlen($password); $i++){ $encoded.="{$password{$i}}\000"; }
+ return $encoded;
+ }
+
+ /**
+ * Obtain the user's distinguished name based on their userid
+ *
+ *
+ * @param string $username The username
+ * @param bool $isGUID Is the username passed a GUID or a samAccountName
+ * @return string
+ */
+ public function dn($username, $isGUID=false)
+ {
+ $user = $this->info($username, array("cn"), $isGUID);
+ if ($user[0]["dn"] === NULL) {
+ return false;
+ }
+ $userDn = $user[0]["dn"];
+ return $userDn;
+ }
+
+ /**
+ * Return a list of all users in AD
+ *
+ * @param bool $includeDescription Return a description of the user
+ * @param string $search Search parameter
+ * @param bool $sorted Sort the user accounts
+ * @return array
+ */
+ public function all($includeDescription = false, $search = "*", $sorted = true)
+ {
+ if (!$this->adldap->getLdapBind()) { return false; }
+
+ // Perform the search and grab all their details
+ $filter = "(&(objectClass=user)(samaccounttype=" . adLDAP::ADLDAP_NORMAL_ACCOUNT .")(objectCategory=person)(cn=" . $search . "))";
+ $fields = array("samaccountname","displayname");
+ $sr = ldap_search($this->adldap->getLdapConnection(), $this->adldap->getBaseDn(), $filter, $fields);
+ $entries = ldap_get_entries($this->adldap->getLdapConnection(), $sr);
+
+ $usersArray = array();
+ for ($i=0; $i<$entries["count"]; $i++){
+ if ($includeDescription && strlen($entries[$i]["displayname"][0])>0){
+ $usersArray[$entries[$i]["samaccountname"][0]] = $entries[$i]["displayname"][0];
+ } elseif ($includeDescription){
+ $usersArray[$entries[$i]["samaccountname"][0]] = $entries[$i]["samaccountname"][0];
+ } else {
+ array_push($usersArray, $entries[$i]["samaccountname"][0]);
+ }
+ }
+ if ($sorted) {
+ asort($usersArray);
+ }
+ return $usersArray;
+ }
+
+ /**
+ * Converts a username (samAccountName) to a GUID
+ *
+ * @param string $username The username to query
+ * @return string
+ */
+ public function usernameToGuid($username)
+ {
+ if (!$this->adldap->getLdapBind()){ return false; }
+ if ($username === null){ return "Missing compulsory field [username]"; }
+
+ $filter = "samaccountname=" . $username;
+ $fields = array("objectGUID");
+ $sr = @ldap_search($this->adldap->getLdapConnection(), $this->adldap->getBaseDn(), $filter, $fields);
+ if (ldap_count_entries($this->adldap->getLdapConnection(), $sr) > 0) {
+ $entry = @ldap_first_entry($this->adldap->getLdapConnection(), $sr);
+ $guid = @ldap_get_values_len($this->adldap->getLdapConnection(), $entry, 'objectGUID');
+ $strGUID = $this->adldap->utilities()->binaryToText($guid[0]);
+ return $strGUID;
+ }
+ return false;
+ }
+
+ /**
+ * Return a list of all users in AD that have a specific value in a field
+ *
+ * @param bool $includeDescription Return a description of the user
+ * @param string $searchField Field to search search for
+ * @param string $searchFilter Value to search for in the specified field
+ * @param bool $sorted Sort the user accounts
+ * @return array
+ */
+ public function find($includeDescription = false, $searchField = false, $searchFilter = false, $sorted = true){
+ if (!$this->adldap->getLdapBind()){ return false; }
+
+ // Perform the search and grab all their details
+ $searchParams = "";
+ if ($searchField) {
+ $searchParams = "(" . $searchField . "=" . $searchFilter . ")";
+ }
+ $filter = "(&(objectClass=user)(samaccounttype=" . adLDAP::ADLDAP_NORMAL_ACCOUNT .")(objectCategory=person)" . $searchParams . ")";
+ $fields = array("samaccountname","displayname");
+ $sr = ldap_search($this->adldap->getLdapConnection(), $this->adldap->getBaseDn(), $filter, $fields);
+ $entries = ldap_get_entries($this->adldap->getLdapConnection(), $sr);
+
+ $usersArray = array();
+ for ($i=0; $i < $entries["count"]; $i++) {
+ if ($includeDescription && strlen($entries[$i]["displayname"][0]) > 0) {
+ $usersArray[$entries[$i]["samaccountname"][0]] = $entries[$i]["displayname"][0];
+ }
+ else if ($includeDescription) {
+ $usersArray[$entries[$i]["samaccountname"][0]] = $entries[$i]["samaccountname"][0];
+ }
+ else {
+ array_push($usersArray, $entries[$i]["samaccountname"][0]);
+ }
+ }
+ if ($sorted){
+ asort($usersArray);
+ }
+ return ($usersArray);
+ }
+
+ /**
+ * Move a user account to a different OU
+ *
+ * @param string $username The username to move (please be careful here!)
+ * @param array $container The container or containers to move the user to (please be careful here!).
+ * accepts containers in 1. parent 2. child order
+ * @return array
+ */
+ public function move($username, $container)
+ {
+ if (!$this->adldap->getLdapBind()) { return false; }
+ if ($username === null) { return "Missing compulsory field [username]"; }
+ if ($container === null) { return "Missing compulsory field [container]"; }
+ if (!is_array($container)) { return "Container must be an array"; }
+
+ $userInfo = $this->info($username, array("*"));
+ $dn = $userInfo[0]['distinguishedname'][0];
+ $newRDn = "cn=" . $username;
+ $container = array_reverse($container);
+ $newContainer = "ou=" . implode(",ou=",$container);
+ $newBaseDn = strtolower($newContainer) . "," . $this->adldap->getBaseDn();
+ $result = @ldap_rename($this->adldap->getLdapConnection(), $dn, $newRDn, $newBaseDn, true);
+ if ($result !== true) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Get the last logon time of any user as a Unix timestamp
+ *
+ * @param string $username
+ * @return long $unixTimestamp
+ */
+ public function getLastLogon($username) {
+ if (!$this->adldap->getLdapBind()) { return false; }
+ if ($username === null) { return "Missing compulsory field [username]"; }
+ $userInfo = $this->info($username, array("lastLogonTimestamp"));
+ $lastLogon = adLDAPUtils::convertWindowsTimeToUnixTime($userInfo[0]['lastLogonTimestamp'][0]);
+ return $lastLogon;
+ }
+
+}
+?>
diff --git a/platform/www/lib/plugins/authad/adLDAP/classes/adLDAPUtils.php b/platform/www/lib/plugins/authad/adLDAP/classes/adLDAPUtils.php
new file mode 100644
index 0000000..6f94fe2
--- /dev/null
+++ b/platform/www/lib/plugins/authad/adLDAP/classes/adLDAPUtils.php
@@ -0,0 +1,268 @@
+<?php
+/**
+ * PHP LDAP CLASS FOR MANIPULATING ACTIVE DIRECTORY
+ * Version 4.0.4
+ *
+ * PHP Version 5 with SSL and LDAP support
+ *
+ * Written by Scott Barnett, Richard Hyland
+ * email: scott@wiggumworld.com, adldap@richardhyland.com
+ * http://adldap.sourceforge.net/
+ *
+ * Copyright (c) 2006-2012 Scott Barnett, Richard Hyland
+ *
+ * We'd appreciate any improvements or additions to be submitted back
+ * to benefit the entire community :)
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * @category ToolsAndUtilities
+ * @package adLDAP
+ * @subpackage Utils
+ * @author Scott Barnett, Richard Hyland
+ * @copyright (c) 2006-2012 Scott Barnett, Richard Hyland
+ * @license http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html LGPLv2.1
+ * @revision $Revision: 97 $
+ * @version 4.0.4
+ * @link http://adldap.sourceforge.net/
+ */
+require_once(dirname(__FILE__) . '/../adLDAP.php');
+
+/**
+* UTILITY FUNCTIONS
+*/
+class adLDAPUtils {
+ const ADLDAP_VERSION = '4.0.4';
+
+ /**
+ * The current adLDAP connection via dependency injection
+ *
+ * @var adLDAP
+ */
+ protected $adldap;
+
+ public function __construct(adLDAP $adldap) {
+ $this->adldap = $adldap;
+ }
+
+
+ /**
+ * Take an LDAP query and return the nice names, without all the LDAP prefixes (eg. CN, DN)
+ *
+ * @param array $groups
+ * @return array
+ */
+ public function niceNames($groups)
+ {
+
+ $groupArray = array();
+ for ($i=0; $i<$groups["count"]; $i++){ // For each group
+ $line = $groups[$i];
+
+ if (strlen($line)>0) {
+ // More presumptions, they're all prefixed with CN=
+ // so we ditch the first three characters and the group
+ // name goes up to the first comma
+ $bits=explode(",", $line);
+ $groupArray[] = substr($bits[0], 3, (strlen($bits[0])-3));
+ }
+ }
+ return $groupArray;
+ }
+
+ /**
+ * Escape characters for use in an ldap_create function
+ *
+ * @param string $str
+ * @return string
+ */
+ public function escapeCharacters($str) {
+ $str = str_replace(",", "\,", $str);
+ return $str;
+ }
+
+ /**
+ * Escape strings for the use in LDAP filters
+ *
+ * DEVELOPERS SHOULD BE DOING PROPER FILTERING IF THEY'RE ACCEPTING USER INPUT
+ * Ported from Perl's Net::LDAP::Util escape_filter_value
+ *
+ * @param string $str The string the parse
+ * @author Port by Andreas Gohr <andi@splitbrain.org>
+ * @return string
+ */
+ public function ldapSlashes($str) {
+ // see https://github.com/adldap/adLDAP/issues/22
+ return preg_replace_callback(
+ '/([\x00-\x1F\*\(\)\\\\])/',
+ function ($matches) {
+ return "\\".join("", unpack("H2", $matches[1]));
+ },
+ $str
+ );
+ }
+ /**
+ * Converts a string GUID to a hexdecimal value so it can be queried
+ *
+ * @param string $strGUID A string representation of a GUID
+ * @return string
+ */
+ public function strGuidToHex($strGUID)
+ {
+ $strGUID = str_replace('-', '', $strGUID);
+
+ $octet_str = '\\' . substr($strGUID, 6, 2);
+ $octet_str .= '\\' . substr($strGUID, 4, 2);
+ $octet_str .= '\\' . substr($strGUID, 2, 2);
+ $octet_str .= '\\' . substr($strGUID, 0, 2);
+ $octet_str .= '\\' . substr($strGUID, 10, 2);
+ $octet_str .= '\\' . substr($strGUID, 8, 2);
+ $octet_str .= '\\' . substr($strGUID, 14, 2);
+ $octet_str .= '\\' . substr($strGUID, 12, 2);
+ //$octet_str .= '\\' . substr($strGUID, 16, strlen($strGUID));
+ for ($i=16; $i<=(strlen($strGUID)-2); $i++) {
+ if (($i % 2) == 0) {
+ $octet_str .= '\\' . substr($strGUID, $i, 2);
+ }
+ }
+
+ return $octet_str;
+ }
+
+ /**
+ * Convert a binary SID to a text SID
+ *
+ * @param string $binsid A Binary SID
+ * @return string
+ */
+ public function getTextSID($binsid) {
+ $hex_sid = bin2hex($binsid);
+ $rev = hexdec(substr($hex_sid, 0, 2));
+ $subcount = hexdec(substr($hex_sid, 2, 2));
+ $auth = hexdec(substr($hex_sid, 4, 12));
+ $result = "$rev-$auth";
+
+ for ($x=0;$x < $subcount; $x++) {
+ $subauth[$x] =
+ hexdec($this->littleEndian(substr($hex_sid, 16 + ($x * 8), 8)));
+ $result .= "-" . $subauth[$x];
+ }
+
+ // Cheat by tacking on the S-
+ return 'S-' . $result;
+ }
+
+ /**
+ * Converts a little-endian hex number to one that hexdec() can convert
+ *
+ * @param string $hex A hex code
+ * @return string
+ */
+ public function littleEndian($hex)
+ {
+ $result = '';
+ for ($x = strlen($hex) - 2; $x >= 0; $x = $x - 2) {
+ $result .= substr($hex, $x, 2);
+ }
+ return $result;
+ }
+
+ /**
+ * Converts a binary attribute to a string
+ *
+ * @param string $bin A binary LDAP attribute
+ * @return string
+ */
+ public function binaryToText($bin)
+ {
+ $hex_guid = bin2hex($bin);
+ $hex_guid_to_guid_str = '';
+ for($k = 1; $k <= 4; ++$k) {
+ $hex_guid_to_guid_str .= substr($hex_guid, 8 - 2 * $k, 2);
+ }
+ $hex_guid_to_guid_str .= '-';
+ for($k = 1; $k <= 2; ++$k) {
+ $hex_guid_to_guid_str .= substr($hex_guid, 12 - 2 * $k, 2);
+ }
+ $hex_guid_to_guid_str .= '-';
+ for($k = 1; $k <= 2; ++$k) {
+ $hex_guid_to_guid_str .= substr($hex_guid, 16 - 2 * $k, 2);
+ }
+ $hex_guid_to_guid_str .= '-' . substr($hex_guid, 16, 4);
+ $hex_guid_to_guid_str .= '-' . substr($hex_guid, 20);
+ return strtoupper($hex_guid_to_guid_str);
+ }
+
+ /**
+ * Converts a binary GUID to a string GUID
+ *
+ * @param string $binaryGuid The binary GUID attribute to convert
+ * @return string
+ */
+ public function decodeGuid($binaryGuid)
+ {
+ if ($binaryGuid === null){ return "Missing compulsory field [binaryGuid]"; }
+
+ $strGUID = $this->binaryToText($binaryGuid);
+ return $strGUID;
+ }
+
+ /**
+ * Convert a boolean value to a string
+ * You should never need to call this yourself
+ *
+ * @param bool $bool Boolean value
+ * @return string
+ */
+ public function boolToStr($bool)
+ {
+ return ($bool) ? 'TRUE' : 'FALSE';
+ }
+
+ /**
+ * Convert 8bit characters e.g. accented characters to UTF8 encoded characters
+ */
+ public function encode8Bit(&$item, $key) {
+ $encode = false;
+ if (is_string($item)) {
+ for ($i=0; $i<strlen($item); $i++) {
+ if (ord($item[$i]) >> 7) {
+ $encode = true;
+ }
+ }
+ }
+ if ($encode === true && $key != 'password') {
+ $item = utf8_encode($item);
+ }
+ }
+
+ /**
+ * Get the current class version number
+ *
+ * @return string
+ */
+ public function getVersion() {
+ return self::ADLDAP_VERSION;
+ }
+
+ /**
+ * Round a Windows timestamp down to seconds and remove the seconds between 1601-01-01 and 1970-01-01
+ *
+ * @param long $windowsTime
+ * @return long $unixTime
+ */
+ public static function convertWindowsTimeToUnixTime($windowsTime) {
+ $unixTime = round($windowsTime / 10000000) - 11644477200;
+ return $unixTime;
+ }
+}
+
+?>
diff --git a/platform/www/lib/plugins/authad/adLDAP/collections/adLDAPCollection.php b/platform/www/lib/plugins/authad/adLDAP/collections/adLDAPCollection.php
new file mode 100644
index 0000000..433d39f
--- /dev/null
+++ b/platform/www/lib/plugins/authad/adLDAP/collections/adLDAPCollection.php
@@ -0,0 +1,137 @@
+<?php
+/**
+ * PHP LDAP CLASS FOR MANIPULATING ACTIVE DIRECTORY
+ * Version 4.0.4
+ *
+ * PHP Version 5 with SSL and LDAP support
+ *
+ * Written by Scott Barnett, Richard Hyland
+ * email: scott@wiggumworld.com, adldap@richardhyland.com
+ * http://adldap.sourceforge.net/
+ *
+ * Copyright (c) 2006-2012 Scott Barnett, Richard Hyland
+ *
+ * We'd appreciate any improvements or additions to be submitted back
+ * to benefit the entire community :)
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * @category ToolsAndUtilities
+ * @package adLDAP
+ * @subpackage Collection
+ * @author Scott Barnett, Richard Hyland
+ * @copyright (c) 2006-2012 Scott Barnett, Richard Hyland
+ * @license http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html LGPLv2.1
+ * @revision $Revision: 97 $
+ * @version 4.0.4
+ * @link http://adldap.sourceforge.net/
+*/
+
+abstract class adLDAPCollection
+{
+ /**
+ * The current adLDAP connection via dependency injection
+ *
+ * @var adLDAP
+ */
+ protected $adldap;
+
+ /**
+ * The current object being modifed / called
+ *
+ * @var mixed
+ */
+ protected $currentObject;
+
+ /**
+ * The raw info array from Active Directory
+ *
+ * @var array
+ */
+ protected $info;
+
+ public function __construct($info, adLDAP $adldap)
+ {
+ $this->setInfo($info);
+ $this->adldap = $adldap;
+ }
+
+ /**
+ * Set the raw info array from Active Directory
+ *
+ * @param array $info
+ */
+ public function setInfo(array $info)
+ {
+ if ($this->info && sizeof($info) >= 1) {
+ unset($this->info);
+ }
+ $this->info = $info;
+ }
+
+ /**
+ * Magic get method to retrieve data from the raw array in a formatted way
+ *
+ * @param string $attribute
+ * @return mixed
+ */
+ public function __get($attribute)
+ {
+ if (isset($this->info[0]) && is_array($this->info[0])) {
+ foreach ($this->info[0] as $keyAttr => $valueAttr) {
+ if (strtolower($keyAttr) == strtolower($attribute)) {
+ if ($this->info[0][strtolower($attribute)]['count'] == 1) {
+ return $this->info[0][strtolower($attribute)][0];
+ }
+ else {
+ $array = array();
+ foreach ($this->info[0][strtolower($attribute)] as $key => $value) {
+ if ((string)$key != 'count') {
+ $array[$key] = $value;
+ }
+ }
+ return $array;
+ }
+ }
+ }
+ }
+ else {
+ return NULL;
+ }
+ }
+
+ /**
+ * Magic set method to update an attribute
+ *
+ * @param string $attribute
+ * @param string $value
+ * @return bool
+ */
+ abstract public function __set($attribute, $value);
+
+ /**
+ * Magic isset method to check for the existence of an attribute
+ *
+ * @param string $attribute
+ * @return bool
+ */
+ public function __isset($attribute) {
+ if (isset($this->info[0]) && is_array($this->info[0])) {
+ foreach ($this->info[0] as $keyAttr => $valueAttr) {
+ if (strtolower($keyAttr) == strtolower($attribute)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+}
+?>
diff --git a/platform/www/lib/plugins/authad/adLDAP/collections/adLDAPComputerCollection.php b/platform/www/lib/plugins/authad/adLDAP/collections/adLDAPComputerCollection.php
new file mode 100644
index 0000000..09f82ca
--- /dev/null
+++ b/platform/www/lib/plugins/authad/adLDAP/collections/adLDAPComputerCollection.php
@@ -0,0 +1,46 @@
+<?php
+/**
+ * PHP LDAP CLASS FOR MANIPULATING ACTIVE DIRECTORY
+ * Version 4.0.4
+ *
+ * PHP Version 5 with SSL and LDAP support
+ *
+ * Written by Scott Barnett, Richard Hyland
+ * email: scott@wiggumworld.com, adldap@richardhyland.com
+ * http://adldap.sourceforge.net/
+ *
+ * Copyright (c) 2006-2012 Scott Barnett, Richard Hyland
+ *
+ * We'd appreciate any improvements or additions to be submitted back
+ * to benefit the entire community :)
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * @category ToolsAndUtilities
+ * @package adLDAP
+ * @subpackage ComputerCollection
+ * @author Scott Barnett, Richard Hyland
+ * @copyright (c) 2006-2012 Scott Barnett, Richard Hyland
+ * @license http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html LGPLv2.1
+ * @revision $Revision: 97 $
+ * @version 4.0.4
+ * @link http://adldap.sourceforge.net/
+*/
+
+class adLDAPComputerCollection extends adLDAPCollection
+{
+
+ public function __set($attribute, $value)
+ {
+
+ }
+}
+?>
diff --git a/platform/www/lib/plugins/authad/adLDAP/collections/adLDAPContactCollection.php b/platform/www/lib/plugins/authad/adLDAP/collections/adLDAPContactCollection.php
new file mode 100644
index 0000000..a9efad5
--- /dev/null
+++ b/platform/www/lib/plugins/authad/adLDAP/collections/adLDAPContactCollection.php
@@ -0,0 +1,46 @@
+<?php
+/**
+ * PHP LDAP CLASS FOR MANIPULATING ACTIVE DIRECTORY
+ * Version 4.0.4
+ *
+ * PHP Version 5 with SSL and LDAP support
+ *
+ * Written by Scott Barnett, Richard Hyland
+ * email: scott@wiggumworld.com, adldap@richardhyland.com
+ * http://adldap.sourceforge.net/
+ *
+ * Copyright (c) 2006-2012 Scott Barnett, Richard Hyland
+ *
+ * We'd appreciate any improvements or additions to be submitted back
+ * to benefit the entire community :)
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * @category ToolsAndUtilities
+ * @package adLDAP
+ * @subpackage ContactCollection
+ * @author Scott Barnett, Richard Hyland
+ * @copyright (c) 2006-2012 Scott Barnett, Richard Hyland
+ * @license http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html LGPLv2.1
+ * @revision $Revision: 97 $
+ * @version 4.0.4
+ * @link http://adldap.sourceforge.net/
+*/
+
+class adLDAPContactCollection extends adLDAPCollection
+{
+
+ public function __set($attribute, $value)
+ {
+
+ }
+}
+?>
diff --git a/platform/www/lib/plugins/authad/adLDAP/collections/adLDAPGroupCollection.php b/platform/www/lib/plugins/authad/adLDAP/collections/adLDAPGroupCollection.php
new file mode 100644
index 0000000..ef4af8d
--- /dev/null
+++ b/platform/www/lib/plugins/authad/adLDAP/collections/adLDAPGroupCollection.php
@@ -0,0 +1,46 @@
+<?php
+/**
+ * PHP LDAP CLASS FOR MANIPULATING ACTIVE DIRECTORY
+ * Version 4.0.4
+ *
+ * PHP Version 5 with SSL and LDAP support
+ *
+ * Written by Scott Barnett, Richard Hyland
+ * email: scott@wiggumworld.com, adldap@richardhyland.com
+ * http://adldap.sourceforge.net/
+ *
+ * Copyright (c) 2006-2012 Scott Barnett, Richard Hyland
+ *
+ * We'd appreciate any improvements or additions to be submitted back
+ * to benefit the entire community :)
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * @category ToolsAndUtilities
+ * @package adLDAP
+ * @subpackage GroupCollection
+ * @author Scott Barnett, Richard Hyland
+ * @copyright (c) 2006-2012 Scott Barnett, Richard Hyland
+ * @license http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html LGPLv2.1
+ * @revision $Revision: 97 $
+ * @version 4.0.4
+ * @link http://adldap.sourceforge.net/
+*/
+
+class adLDAPGroupCollection extends adLDAPCollection
+{
+
+ public function __set($attribute, $value)
+ {
+
+ }
+}
+?>
diff --git a/platform/www/lib/plugins/authad/adLDAP/collections/adLDAPUserCollection.php b/platform/www/lib/plugins/authad/adLDAP/collections/adLDAPUserCollection.php
new file mode 100644
index 0000000..63fce5f
--- /dev/null
+++ b/platform/www/lib/plugins/authad/adLDAP/collections/adLDAPUserCollection.php
@@ -0,0 +1,46 @@
+<?php
+/**
+ * PHP LDAP CLASS FOR MANIPULATING ACTIVE DIRECTORY
+ * Version 4.0.4
+ *
+ * PHP Version 5 with SSL and LDAP support
+ *
+ * Written by Scott Barnett, Richard Hyland
+ * email: scott@wiggumworld.com, adldap@richardhyland.com
+ * http://adldap.sourceforge.net/
+ *
+ * Copyright (c) 2006-2012 Scott Barnett, Richard Hyland
+ *
+ * We'd appreciate any improvements or additions to be submitted back
+ * to benefit the entire community :)
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * @category ToolsAndUtilities
+ * @package adLDAP
+ * @subpackage UserCollection
+ * @author Scott Barnett, Richard Hyland
+ * @copyright (c) 2006-2012 Scott Barnett, Richard Hyland
+ * @license http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html LGPLv2.1
+ * @revision $Revision: 97 $
+ * @version 4.0.4
+ * @link http://adldap.sourceforge.net/
+*/
+
+class adLDAPUserCollection extends adLDAPCollection
+{
+
+ public function __set($attribute, $value)
+ {
+
+ }
+}
+?>
diff --git a/platform/www/lib/plugins/authad/auth.php b/platform/www/lib/plugins/authad/auth.php
new file mode 100644
index 0000000..27a6b22
--- /dev/null
+++ b/platform/www/lib/plugins/authad/auth.php
@@ -0,0 +1,786 @@
+<?php
+
+/**
+ * Active Directory authentication backend for DokuWiki
+ *
+ * This makes authentication with a Active Directory server much easier
+ * than when using the normal LDAP backend by utilizing the adLDAP library
+ *
+ * Usage:
+ * Set DokuWiki's local.protected.php auth setting to read
+ *
+ * $conf['authtype'] = 'authad';
+ *
+ * $conf['plugin']['authad']['account_suffix'] = '@my.domain.org';
+ * $conf['plugin']['authad']['base_dn'] = 'DC=my,DC=domain,DC=org';
+ * $conf['plugin']['authad']['domain_controllers'] = 'srv1.domain.org,srv2.domain.org';
+ *
+ * //optional:
+ * $conf['plugin']['authad']['sso'] = 1;
+ * $conf['plugin']['authad']['admin_username'] = 'root';
+ * $conf['plugin']['authad']['admin_password'] = 'pass';
+ * $conf['plugin']['authad']['real_primarygroup'] = 1;
+ * $conf['plugin']['authad']['use_ssl'] = 1;
+ * $conf['plugin']['authad']['use_tls'] = 1;
+ * $conf['plugin']['authad']['debug'] = 1;
+ * // warn user about expiring password this many days in advance:
+ * $conf['plugin']['authad']['expirywarn'] = 5;
+ *
+ * // get additional information to the userinfo array
+ * // add a list of comma separated ldap contact fields.
+ * $conf['plugin']['authad']['additional'] = 'field1,field2';
+ *
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ * @author James Van Lommel <jamesvl@gmail.com>
+ * @link http://www.nosq.com/blog/2005/08/ldap-activedirectory-and-dokuwiki/
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @author Jan Schumann <js@schumann-it.com>
+ */
+class auth_plugin_authad extends DokuWiki_Auth_Plugin
+{
+
+ /**
+ * @var array hold connection data for a specific AD domain
+ */
+ protected $opts = array();
+
+ /**
+ * @var array open connections for each AD domain, as adLDAP objects
+ */
+ protected $adldap = array();
+
+ /**
+ * @var bool message state
+ */
+ protected $msgshown = false;
+
+ /**
+ * @var array user listing cache
+ */
+ protected $users = array();
+
+ /**
+ * @var array filter patterns for listing users
+ */
+ protected $pattern = array();
+
+ protected $grpsusers = array();
+
+ /**
+ * Constructor
+ */
+ public function __construct()
+ {
+ global $INPUT;
+ parent::__construct();
+
+ require_once(DOKU_PLUGIN.'authad/adLDAP/adLDAP.php');
+ require_once(DOKU_PLUGIN.'authad/adLDAP/classes/adLDAPUtils.php');
+
+ // we load the config early to modify it a bit here
+ $this->loadConfig();
+
+ // additional information fields
+ if (isset($this->conf['additional'])) {
+ $this->conf['additional'] = str_replace(' ', '', $this->conf['additional']);
+ $this->conf['additional'] = explode(',', $this->conf['additional']);
+ } else $this->conf['additional'] = array();
+
+ // ldap extension is needed
+ if (!function_exists('ldap_connect')) {
+ if ($this->conf['debug'])
+ msg("AD Auth: PHP LDAP extension not found.", -1);
+ $this->success = false;
+ return;
+ }
+
+ // Prepare SSO
+ if (!empty($_SERVER['REMOTE_USER'])) {
+ // make sure the right encoding is used
+ if ($this->getConf('sso_charset')) {
+ $_SERVER['REMOTE_USER'] = iconv($this->getConf('sso_charset'), 'UTF-8', $_SERVER['REMOTE_USER']);
+ } elseif (!\dokuwiki\Utf8\Clean::isUtf8($_SERVER['REMOTE_USER'])) {
+ $_SERVER['REMOTE_USER'] = utf8_encode($_SERVER['REMOTE_USER']);
+ }
+
+ // trust the incoming user
+ if ($this->conf['sso']) {
+ $_SERVER['REMOTE_USER'] = $this->cleanUser($_SERVER['REMOTE_USER']);
+
+ // we need to simulate a login
+ if (empty($_COOKIE[DOKU_COOKIE])) {
+ $INPUT->set('u', $_SERVER['REMOTE_USER']);
+ $INPUT->set('p', 'sso_only');
+ }
+ }
+ }
+
+ // other can do's are changed in $this->_loadServerConfig() base on domain setup
+ $this->cando['modName'] = (bool)$this->conf['update_name'];
+ $this->cando['modMail'] = (bool)$this->conf['update_mail'];
+ $this->cando['getUserCount'] = true;
+ }
+
+ /**
+ * Load domain config on capability check
+ *
+ * @param string $cap
+ * @return bool
+ */
+ public function canDo($cap)
+ {
+ //capabilities depend on config, which may change depending on domain
+ $domain = $this->getUserDomain($_SERVER['REMOTE_USER']);
+ $this->loadServerConfig($domain);
+ return parent::canDo($cap);
+ }
+
+ /**
+ * Check user+password [required auth function]
+ *
+ * Checks if the given user exists and the given
+ * plaintext password is correct by trying to bind
+ * to the LDAP server
+ *
+ * @author James Van Lommel <james@nosq.com>
+ * @param string $user
+ * @param string $pass
+ * @return bool
+ */
+ public function checkPass($user, $pass)
+ {
+ if ($_SERVER['REMOTE_USER'] &&
+ $_SERVER['REMOTE_USER'] == $user &&
+ $this->conf['sso']
+ ) return true;
+
+ $adldap = $this->initAdLdap($this->getUserDomain($user));
+ if (!$adldap) return false;
+
+ try {
+ return $adldap->authenticate($this->getUserName($user), $pass);
+ } catch (adLDAPException $e) {
+ // shouldn't really happen
+ return false;
+ }
+ }
+
+ /**
+ * Return user info [required auth function]
+ *
+ * Returns info about the given user needs to contain
+ * at least these fields:
+ *
+ * name string full name of the user
+ * mail string email address of the user
+ * grps array list of groups the user is in
+ *
+ * This AD specific function returns the following
+ * addional fields:
+ *
+ * dn string distinguished name (DN)
+ * uid string samaccountname
+ * lastpwd int timestamp of the date when the password was set
+ * expires true if the password expires
+ * expiresin int seconds until the password expires
+ * any fields specified in the 'additional' config option
+ *
+ * @author James Van Lommel <james@nosq.com>
+ * @param string $user
+ * @param bool $requireGroups (optional) - ignored, groups are always supplied by this plugin
+ * @return array
+ */
+ public function getUserData($user, $requireGroups = true)
+ {
+ global $conf;
+ global $lang;
+ global $ID;
+ $adldap = $this->initAdLdap($this->getUserDomain($user));
+ if (!$adldap) return array();
+
+ if ($user == '') return array();
+
+ $fields = array('mail', 'displayname', 'samaccountname', 'lastpwd', 'pwdlastset', 'useraccountcontrol');
+
+ // add additional fields to read
+ $fields = array_merge($fields, $this->conf['additional']);
+ $fields = array_unique($fields);
+ $fields = array_filter($fields);
+
+ //get info for given user
+ $result = $adldap->user()->info($this->getUserName($user), $fields);
+ if ($result == false) {
+ return array();
+ }
+
+ //general user info
+ $info = array();
+ $info['name'] = $result[0]['displayname'][0];
+ $info['mail'] = $result[0]['mail'][0];
+ $info['uid'] = $result[0]['samaccountname'][0];
+ $info['dn'] = $result[0]['dn'];
+ //last password set (Windows counts from January 1st 1601)
+ $info['lastpwd'] = $result[0]['pwdlastset'][0] / 10000000 - 11644473600;
+ //will it expire?
+ $info['expires'] = !($result[0]['useraccountcontrol'][0] & 0x10000); //ADS_UF_DONT_EXPIRE_PASSWD
+
+ // additional information
+ foreach ($this->conf['additional'] as $field) {
+ if (isset($result[0][strtolower($field)])) {
+ $info[$field] = $result[0][strtolower($field)][0];
+ }
+ }
+
+ // handle ActiveDirectory memberOf
+ $info['grps'] = $adldap->user()->groups($this->getUserName($user), (bool) $this->opts['recursive_groups']);
+
+ if (is_array($info['grps'])) {
+ foreach ($info['grps'] as $ndx => $group) {
+ $info['grps'][$ndx] = $this->cleanGroup($group);
+ }
+ }
+
+ // always add the default group to the list of groups
+ if (!is_array($info['grps']) || !in_array($conf['defaultgroup'], $info['grps'])) {
+ $info['grps'][] = $conf['defaultgroup'];
+ }
+
+ // add the user's domain to the groups
+ $domain = $this->getUserDomain($user);
+ if ($domain && !in_array("domain-$domain", (array) $info['grps'])) {
+ $info['grps'][] = $this->cleanGroup("domain-$domain");
+ }
+
+ // check expiry time
+ if ($info['expires'] && $this->conf['expirywarn']) {
+ try {
+ $expiry = $adldap->user()->passwordExpiry($user);
+ if (is_array($expiry)) {
+ $info['expiresat'] = $expiry['expiryts'];
+ $info['expiresin'] = round(($info['expiresat'] - time())/(24*60*60));
+
+ // if this is the current user, warn him (once per request only)
+ if (($_SERVER['REMOTE_USER'] == $user) &&
+ ($info['expiresin'] <= $this->conf['expirywarn']) &&
+ !$this->msgshown
+ ) {
+ $msg = sprintf($this->getLang('authpwdexpire'), $info['expiresin']);
+ if ($this->canDo('modPass')) {
+ $url = wl($ID, array('do'=> 'profile'));
+ $msg .= ' <a href="'.$url.'">'.$lang['btn_profile'].'</a>';
+ }
+ msg($msg);
+ $this->msgshown = true;
+ }
+ }
+ } catch (adLDAPException $e) {
+ // ignore. should usually not happen
+ }
+ }
+
+ return $info;
+ }
+
+ /**
+ * Make AD group names usable by DokuWiki.
+ *
+ * Removes backslashes ('\'), pound signs ('#'), and converts spaces to underscores.
+ *
+ * @author James Van Lommel (jamesvl@gmail.com)
+ * @param string $group
+ * @return string
+ */
+ public function cleanGroup($group)
+ {
+ $group = str_replace('\\', '', $group);
+ $group = str_replace('#', '', $group);
+ $group = preg_replace('[\s]', '_', $group);
+ $group = \dokuwiki\Utf8\PhpString::strtolower(trim($group));
+ return $group;
+ }
+
+ /**
+ * Sanitize user names
+ *
+ * Normalizes domain parts, does not modify the user name itself (unlike cleanGroup)
+ *
+ * @author Andreas Gohr <gohr@cosmocode.de>
+ * @param string $user
+ * @return string
+ */
+ public function cleanUser($user)
+ {
+ $domain = '';
+
+ // get NTLM or Kerberos domain part
+ list($dom, $user) = explode('\\', $user, 2);
+ if (!$user) $user = $dom;
+ if ($dom) $domain = $dom;
+ list($user, $dom) = explode('@', $user, 2);
+ if ($dom) $domain = $dom;
+
+ // clean up both
+ $domain = \dokuwiki\Utf8\PhpString::strtolower(trim($domain));
+ $user = \dokuwiki\Utf8\PhpString::strtolower(trim($user));
+
+ // is this a known, valid domain or do we work without account suffix? if not discard
+ if (!is_array($this->conf[$domain]) && $this->conf['account_suffix'] !== '') {
+ $domain = '';
+ }
+
+ // reattach domain
+ if ($domain) $user = "$user@$domain";
+ return $user;
+ }
+
+ /**
+ * Most values in LDAP are case-insensitive
+ *
+ * @return bool
+ */
+ public function isCaseSensitive()
+ {
+ return false;
+ }
+
+ /**
+ * Create a Search-String useable by adLDAPUsers::all($includeDescription = false, $search = "*", $sorted = true)
+ *
+ * @param array $filter
+ * @return string
+ */
+ protected function constructSearchString($filter)
+ {
+ if (!$filter) {
+ return '*';
+ }
+ $adldapUtils = new adLDAPUtils($this->initAdLdap(null));
+ $result = '*';
+ if (isset($filter['name'])) {
+ $result .= ')(displayname=*' . $adldapUtils->ldapSlashes($filter['name']) . '*';
+ unset($filter['name']);
+ }
+
+ if (isset($filter['user'])) {
+ $result .= ')(samAccountName=*' . $adldapUtils->ldapSlashes($filter['user']) . '*';
+ unset($filter['user']);
+ }
+
+ if (isset($filter['mail'])) {
+ $result .= ')(mail=*' . $adldapUtils->ldapSlashes($filter['mail']) . '*';
+ unset($filter['mail']);
+ }
+ return $result;
+ }
+
+ /**
+ * Return a count of the number of user which meet $filter criteria
+ *
+ * @param array $filter $filter array of field/pattern pairs, empty array for no filter
+ * @return int number of users
+ */
+ public function getUserCount($filter = array())
+ {
+ $adldap = $this->initAdLdap(null);
+ if (!$adldap) {
+ dbglog("authad/auth.php getUserCount(): _adldap not set.");
+ return -1;
+ }
+ if ($filter == array()) {
+ $result = $adldap->user()->all();
+ } else {
+ $searchString = $this->constructSearchString($filter);
+ $result = $adldap->user()->all(false, $searchString);
+ if (isset($filter['grps'])) {
+ $this->users = array_fill_keys($result, false);
+ /** @var admin_plugin_usermanager $usermanager */
+ $usermanager = plugin_load("admin", "usermanager", false);
+ $usermanager->setLastdisabled(true);
+ if (!isset($this->grpsusers[$this->filterToString($filter)])) {
+ $this->fillGroupUserArray($filter, $usermanager->getStart() + 3*$usermanager->getPagesize());
+ } elseif (count($this->grpsusers[$this->filterToString($filter)]) <
+ $usermanager->getStart() + 3*$usermanager->getPagesize()
+ ) {
+ $this->fillGroupUserArray(
+ $filter,
+ $usermanager->getStart() +
+ 3*$usermanager->getPagesize() -
+ count($this->grpsusers[$this->filterToString($filter)])
+ );
+ }
+ $result = $this->grpsusers[$this->filterToString($filter)];
+ } else {
+ /** @var admin_plugin_usermanager $usermanager */
+ $usermanager = plugin_load("admin", "usermanager", false);
+ $usermanager->setLastdisabled(false);
+ }
+ }
+
+ if (!$result) {
+ return 0;
+ }
+ return count($result);
+ }
+
+ /**
+ *
+ * create a unique string for each filter used with a group
+ *
+ * @param array $filter
+ * @return string
+ */
+ protected function filterToString($filter)
+ {
+ $result = '';
+ if (isset($filter['user'])) {
+ $result .= 'user-' . $filter['user'];
+ }
+ if (isset($filter['name'])) {
+ $result .= 'name-' . $filter['name'];
+ }
+ if (isset($filter['mail'])) {
+ $result .= 'mail-' . $filter['mail'];
+ }
+ if (isset($filter['grps'])) {
+ $result .= 'grps-' . $filter['grps'];
+ }
+ return $result;
+ }
+
+ /**
+ * Create an array of $numberOfAdds users passing a certain $filter, including belonging
+ * to a certain group and save them to a object-wide array. If the array
+ * already exists try to add $numberOfAdds further users to it.
+ *
+ * @param array $filter
+ * @param int $numberOfAdds additional number of users requested
+ * @return int number of Users actually add to Array
+ */
+ protected function fillGroupUserArray($filter, $numberOfAdds)
+ {
+ if (isset($this->grpsusers[$this->filterToString($filter)])) {
+ $actualstart = count($this->grpsusers[$this->filterToString($filter)]);
+ } else {
+ $this->grpsusers[$this->filterToString($filter)] = [];
+ $actualstart = 0;
+ }
+
+ $i=0;
+ $count = 0;
+ $this->constructPattern($filter);
+ foreach ($this->users as $user => &$info) {
+ if ($i++ < $actualstart) {
+ continue;
+ }
+ if ($info === false) {
+ $info = $this->getUserData($user);
+ }
+ if ($this->filter($user, $info)) {
+ $this->grpsusers[$this->filterToString($filter)][$user] = $info;
+ if (($numberOfAdds > 0) && (++$count >= $numberOfAdds)) break;
+ }
+ }
+ return $count;
+ }
+
+ /**
+ * Bulk retrieval of user data
+ *
+ * @author Dominik Eckelmann <dokuwiki@cosmocode.de>
+ *
+ * @param int $start index of first user to be returned
+ * @param int $limit max number of users to be returned
+ * @param array $filter array of field/pattern pairs, null for no filter
+ * @return array userinfo (refer getUserData for internal userinfo details)
+ */
+ public function retrieveUsers($start = 0, $limit = 0, $filter = array())
+ {
+ $adldap = $this->initAdLdap(null);
+ if (!$adldap) return array();
+
+ //if (!$this->users) {
+ //get info for given user
+ $result = $adldap->user()->all(false, $this->constructSearchString($filter));
+ if (!$result) return array();
+ $this->users = array_fill_keys($result, false);
+ //}
+
+ $i = 0;
+ $count = 0;
+ $result = array();
+
+ if (!isset($filter['grps'])) {
+ /** @var admin_plugin_usermanager $usermanager */
+ $usermanager = plugin_load("admin", "usermanager", false);
+ $usermanager->setLastdisabled(false);
+ $this->constructPattern($filter);
+ foreach ($this->users as $user => &$info) {
+ if ($i++ < $start) {
+ continue;
+ }
+ if ($info === false) {
+ $info = $this->getUserData($user);
+ }
+ $result[$user] = $info;
+ if (($limit > 0) && (++$count >= $limit)) break;
+ }
+ } else {
+ /** @var admin_plugin_usermanager $usermanager */
+ $usermanager = plugin_load("admin", "usermanager", false);
+ $usermanager->setLastdisabled(true);
+ if (!isset($this->grpsusers[$this->filterToString($filter)]) ||
+ count($this->grpsusers[$this->filterToString($filter)]) < ($start+$limit)
+ ) {
+ if(!isset($this->grpsusers[$this->filterToString($filter)])) {
+ $this->grpsusers[$this->filterToString($filter)] = [];
+ }
+
+ $this->fillGroupUserArray(
+ $filter,
+ $start+$limit - count($this->grpsusers[$this->filterToString($filter)]) +1
+ );
+ }
+ if (!$this->grpsusers[$this->filterToString($filter)]) return array();
+ foreach ($this->grpsusers[$this->filterToString($filter)] as $user => &$info) {
+ if ($i++ < $start) {
+ continue;
+ }
+ $result[$user] = $info;
+ if (($limit > 0) && (++$count >= $limit)) break;
+ }
+ }
+ return $result;
+ }
+
+ /**
+ * Modify user data
+ *
+ * @param string $user nick of the user to be changed
+ * @param array $changes array of field/value pairs to be changed
+ * @return bool
+ */
+ public function modifyUser($user, $changes)
+ {
+ $return = true;
+ $adldap = $this->initAdLdap($this->getUserDomain($user));
+ if (!$adldap) {
+ msg($this->getLang('connectfail'), -1);
+ return false;
+ }
+
+ // password changing
+ if (isset($changes['pass'])) {
+ try {
+ $return = $adldap->user()->password($this->getUserName($user), $changes['pass']);
+ } catch (adLDAPException $e) {
+ if ($this->conf['debug']) msg('AD Auth: '.$e->getMessage(), -1);
+ $return = false;
+ }
+ if (!$return) msg($this->getLang('passchangefail'), -1);
+ }
+
+ // changing user data
+ $adchanges = array();
+ if (isset($changes['name'])) {
+ // get first and last name
+ $parts = explode(' ', $changes['name']);
+ $adchanges['surname'] = array_pop($parts);
+ $adchanges['firstname'] = join(' ', $parts);
+ $adchanges['display_name'] = $changes['name'];
+ }
+ if (isset($changes['mail'])) {
+ $adchanges['email'] = $changes['mail'];
+ }
+ if (count($adchanges)) {
+ try {
+ $return = $return & $adldap->user()->modify($this->getUserName($user), $adchanges);
+ } catch (adLDAPException $e) {
+ if ($this->conf['debug']) msg('AD Auth: '.$e->getMessage(), -1);
+ $return = false;
+ }
+ if (!$return) msg($this->getLang('userchangefail'), -1);
+ }
+
+ return $return;
+ }
+
+ /**
+ * Initialize the AdLDAP library and connect to the server
+ *
+ * When you pass null as domain, it will reuse any existing domain.
+ * Eg. the one of the logged in user. It falls back to the default
+ * domain if no current one is available.
+ *
+ * @param string|null $domain The AD domain to use
+ * @return adLDAP|bool true if a connection was established
+ */
+ protected function initAdLdap($domain)
+ {
+ if (is_null($domain) && is_array($this->opts)) {
+ $domain = $this->opts['domain'];
+ }
+
+ $this->opts = $this->loadServerConfig((string) $domain);
+ if (isset($this->adldap[$domain])) return $this->adldap[$domain];
+
+ // connect
+ try {
+ $this->adldap[$domain] = new adLDAP($this->opts);
+ return $this->adldap[$domain];
+ } catch (Exception $e) {
+ if ($this->conf['debug']) {
+ msg('AD Auth: '.$e->getMessage(), -1);
+ }
+ $this->success = false;
+ $this->adldap[$domain] = null;
+ }
+ return false;
+ }
+
+ /**
+ * Get the domain part from a user
+ *
+ * @param string $user
+ * @return string
+ */
+ public function getUserDomain($user)
+ {
+ list(, $domain) = explode('@', $user, 2);
+ return $domain;
+ }
+
+ /**
+ * Get the user part from a user
+ *
+ * When an account suffix is set, we strip the domain part from the user
+ *
+ * @param string $user
+ * @return string
+ */
+ public function getUserName($user)
+ {
+ if ($this->conf['account_suffix'] !== '') {
+ list($user) = explode('@', $user, 2);
+ }
+ return $user;
+ }
+
+ /**
+ * Fetch the configuration for the given AD domain
+ *
+ * @param string $domain current AD domain
+ * @return array
+ */
+ protected function loadServerConfig($domain)
+ {
+ // prepare adLDAP standard configuration
+ $opts = $this->conf;
+
+ $opts['domain'] = $domain;
+
+ // add possible domain specific configuration
+ if ($domain && is_array($this->conf[$domain])) foreach ($this->conf[$domain] as $key => $val) {
+ $opts[$key] = $val;
+ }
+
+ // handle multiple AD servers
+ $opts['domain_controllers'] = explode(',', $opts['domain_controllers']);
+ $opts['domain_controllers'] = array_map('trim', $opts['domain_controllers']);
+ $opts['domain_controllers'] = array_filter($opts['domain_controllers']);
+
+ // compatibility with old option name
+ if (empty($opts['admin_username']) && !empty($opts['ad_username'])) {
+ $opts['admin_username'] = $opts['ad_username'];
+ }
+ if (empty($opts['admin_password']) && !empty($opts['ad_password'])) {
+ $opts['admin_password'] = $opts['ad_password'];
+ }
+ $opts['admin_password'] = conf_decodeString($opts['admin_password']); // deobfuscate
+
+ // we can change the password if SSL is set
+ if ($opts['use_ssl'] || $opts['use_tls']) {
+ $this->cando['modPass'] = true;
+ } else {
+ $this->cando['modPass'] = false;
+ }
+
+ // adLDAP expects empty user/pass as NULL, we're less strict FS#2781
+ if (empty($opts['admin_username'])) $opts['admin_username'] = null;
+ if (empty($opts['admin_password'])) $opts['admin_password'] = null;
+
+ // user listing needs admin priviledges
+ if (!empty($opts['admin_username']) && !empty($opts['admin_password'])) {
+ $this->cando['getUsers'] = true;
+ } else {
+ $this->cando['getUsers'] = false;
+ }
+
+ return $opts;
+ }
+
+ /**
+ * Returns a list of configured domains
+ *
+ * The default domain has an empty string as key
+ *
+ * @return array associative array(key => domain)
+ */
+ public function getConfiguredDomains()
+ {
+ $domains = array();
+ if (empty($this->conf['account_suffix'])) return $domains; // not configured yet
+
+ // add default domain, using the name from account suffix
+ $domains[''] = ltrim($this->conf['account_suffix'], '@');
+
+ // find additional domains
+ foreach ($this->conf as $key => $val) {
+ if (is_array($val) && isset($val['account_suffix'])) {
+ $domains[$key] = ltrim($val['account_suffix'], '@');
+ }
+ }
+ ksort($domains);
+
+ return $domains;
+ }
+
+ /**
+ * Check provided user and userinfo for matching patterns
+ *
+ * The patterns are set up with $this->_constructPattern()
+ *
+ * @author Chris Smith <chris@jalakai.co.uk>
+ *
+ * @param string $user
+ * @param array $info
+ * @return bool
+ */
+ protected function filter($user, $info)
+ {
+ foreach ($this->pattern as $item => $pattern) {
+ if ($item == 'user') {
+ if (!preg_match($pattern, $user)) return false;
+ } elseif ($item == 'grps') {
+ if (!count(preg_grep($pattern, $info['grps']))) return false;
+ } else {
+ if (!preg_match($pattern, $info[$item])) return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Create a pattern for $this->_filter()
+ *
+ * @author Chris Smith <chris@jalakai.co.uk>
+ *
+ * @param array $filter
+ */
+ protected function constructPattern($filter)
+ {
+ $this->pattern = array();
+ foreach ($filter as $item => $pattern) {
+ $this->pattern[$item] = '/'.str_replace('/', '\/', $pattern).'/i'; // allow regex characters
+ }
+ }
+}
diff --git a/platform/www/lib/plugins/authad/conf/default.php b/platform/www/lib/plugins/authad/conf/default.php
new file mode 100644
index 0000000..84094cc
--- /dev/null
+++ b/platform/www/lib/plugins/authad/conf/default.php
@@ -0,0 +1,18 @@
+<?php
+
+$conf['account_suffix'] = '';
+$conf['base_dn'] = '';
+$conf['domain_controllers'] = '';
+$conf['sso'] = 0;
+$conf['sso_charset'] = '';
+$conf['admin_username'] = '';
+$conf['admin_password'] = '';
+$conf['real_primarygroup'] = 0;
+$conf['use_ssl'] = 0;
+$conf['use_tls'] = 0;
+$conf['debug'] = 0;
+$conf['expirywarn'] = 0;
+$conf['additional'] = '';
+$conf['update_name'] = 0;
+$conf['update_mail'] = 0;
+$conf['recursive_groups'] = 0;
diff --git a/platform/www/lib/plugins/authad/conf/metadata.php b/platform/www/lib/plugins/authad/conf/metadata.php
new file mode 100644
index 0000000..945474c
--- /dev/null
+++ b/platform/www/lib/plugins/authad/conf/metadata.php
@@ -0,0 +1,18 @@
+<?php
+
+$meta['account_suffix'] = array('string','_caution' => 'danger');
+$meta['base_dn'] = array('string','_caution' => 'danger');
+$meta['domain_controllers'] = array('string','_caution' => 'danger');
+$meta['sso'] = array('onoff','_caution' => 'danger');
+$meta['sso_charset'] = array('string','_caution' => 'danger');
+$meta['admin_username'] = array('string','_caution' => 'danger');
+$meta['admin_password'] = array('password','_caution' => 'danger','_code' => 'base64');
+$meta['real_primarygroup'] = array('onoff','_caution' => 'danger');
+$meta['use_ssl'] = array('onoff','_caution' => 'danger');
+$meta['use_tls'] = array('onoff','_caution' => 'danger');
+$meta['debug'] = array('onoff','_caution' => 'security');
+$meta['expirywarn'] = array('numeric', '_min'=>0,'_caution' => 'danger');
+$meta['additional'] = array('string','_caution' => 'danger');
+$meta['update_name'] = array('onoff','_caution' => 'danger');
+$meta['update_mail'] = array('onoff','_caution' => 'danger');
+$meta['recursive_groups'] = array('onoff','_caution' => 'danger');
diff --git a/platform/www/lib/plugins/authad/lang/en/lang.php b/platform/www/lib/plugins/authad/lang/en/lang.php
new file mode 100644
index 0000000..3e8a9e2
--- /dev/null
+++ b/platform/www/lib/plugins/authad/lang/en/lang.php
@@ -0,0 +1,15 @@
+<?php
+/**
+ * English language file for addomain plugin
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ * @author Andreas Gohr <gohr@cosmocode.de>
+ */
+
+$lang['domain'] = 'Logon Domain';
+$lang['authpwdexpire'] = 'Your password will expire in %d days, you should change it soon.';
+$lang['passchangefail'] = 'Failed to change the password. Maybe the password policy was not met?';
+$lang['userchangefail'] = 'Failed to change user attributes. Maybe your account does not have permissions to make changes?';
+$lang['connectfail'] = 'Failed to connect to Active Directory server.';
+
+//Setup VIM: ex: et ts=4 :
diff --git a/platform/www/lib/plugins/authad/lang/en/settings.php b/platform/www/lib/plugins/authad/lang/en/settings.php
new file mode 100644
index 0000000..3de9a72
--- /dev/null
+++ b/platform/www/lib/plugins/authad/lang/en/settings.php
@@ -0,0 +1,18 @@
+<?php
+
+$lang['account_suffix'] = 'Your account suffix. Eg. <code>@my.domain.org</code>';
+$lang['base_dn'] = 'Your base DN. Eg. <code>DC=my,DC=domain,DC=org</code>';
+$lang['domain_controllers'] = 'A comma separated list of Domain controllers. Eg. <code>srv1.domain.org,srv2.domain.org</code>';
+$lang['admin_username'] = 'A privileged Active Directory user with access to all other user\'s data. Optional, but needed for certain actions like sending subscription mails.';
+$lang['admin_password'] = 'The password of the above user.';
+$lang['sso'] = 'Should Single-Sign-On via Kerberos or NTLM be used?';
+$lang['sso_charset'] = 'The charset your webserver will pass the Kerberos or NTLM username in. Empty for UTF-8 or latin-1. Requires the iconv extension.';
+$lang['real_primarygroup'] = 'Should the real primary group be resolved instead of assuming "Domain Users" (slower).';
+$lang['use_ssl'] = 'Use SSL connection? If used, do not enable TLS below.';
+$lang['use_tls'] = 'Use TLS connection? If used, do not enable SSL above.';
+$lang['debug'] = 'Display additional debugging output on errors?';
+$lang['expirywarn'] = 'Days in advance to warn user about expiring password. 0 to disable.';
+$lang['additional'] = 'A comma separated list of additional AD attributes to fetch from user data. Used by some plugins.';
+$lang['update_name'] = 'Allow users to update their AD display name?';
+$lang['update_mail'] = 'Allow users to update their email address?';
+$lang['recursive_groups'] = 'Resolve nested groups to their respective members (slower).';
diff --git a/platform/www/lib/plugins/authad/plugin.info.txt b/platform/www/lib/plugins/authad/plugin.info.txt
new file mode 100644
index 0000000..57e1387
--- /dev/null
+++ b/platform/www/lib/plugins/authad/plugin.info.txt
@@ -0,0 +1,7 @@
+base authad
+author Andreas Gohr
+email andi@splitbrain.org
+date 2015-07-13
+name Active Directory Auth Plugin
+desc Provides user authentication against a Microsoft Active Directory
+url http://www.dokuwiki.org/plugin:authad
diff --git a/platform/www/lib/plugins/authldap/auth.php b/platform/www/lib/plugins/authldap/auth.php
new file mode 100644
index 0000000..68d1dad
--- /dev/null
+++ b/platform/www/lib/plugins/authldap/auth.php
@@ -0,0 +1,698 @@
+<?php
+
+/**
+ * LDAP authentication backend
+ *
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @author Chris Smith <chris@jalakaic.co.uk>
+ * @author Jan Schumann <js@schumann-it.com>
+ */
+class auth_plugin_authldap extends DokuWiki_Auth_Plugin
+{
+ /* @var resource $con holds the LDAP connection */
+ protected $con = null;
+
+ /* @var int $bound What type of connection does already exist? */
+ protected $bound = 0; // 0: anonymous, 1: user, 2: superuser
+
+ /* @var array $users User data cache */
+ protected $users = null;
+
+ /* @var array $pattern User filter pattern */
+ protected $pattern = null;
+
+ /**
+ * Constructor
+ */
+ public function __construct()
+ {
+ parent::__construct();
+
+ // ldap extension is needed
+ if (!function_exists('ldap_connect')) {
+ $this->debug("LDAP err: PHP LDAP extension not found.", -1, __LINE__, __FILE__);
+ $this->success = false;
+ return;
+ }
+
+ // Add the capabilities to change the password
+ $this->cando['modPass'] = $this->getConf('modPass');
+ }
+
+ /**
+ * Check user+password
+ *
+ * Checks if the given user exists and the given
+ * plaintext password is correct by trying to bind
+ * to the LDAP server
+ *
+ * @param string $user
+ * @param string $pass
+ * @return bool
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+ public function checkPass($user, $pass)
+ {
+ // reject empty password
+ if (empty($pass)) return false;
+ if (!$this->openLDAP()) return false;
+
+ // indirect user bind
+ if ($this->getConf('binddn') && $this->getConf('bindpw')) {
+ // use superuser credentials
+ if (!@ldap_bind($this->con, $this->getConf('binddn'), conf_decodeString($this->getConf('bindpw')))) {
+ $this->debug('LDAP bind as superuser: ' . hsc(ldap_error($this->con)), 0, __LINE__, __FILE__);
+ return false;
+ }
+ $this->bound = 2;
+ } elseif ($this->getConf('binddn') &&
+ $this->getConf('usertree') &&
+ $this->getConf('userfilter')
+ ) {
+ // special bind string
+ $dn = $this->makeFilter(
+ $this->getConf('binddn'),
+ array('user' => $user, 'server' => $this->getConf('server'))
+ );
+ } elseif (strpos($this->getConf('usertree'), '%{user}')) {
+ // direct user bind
+ $dn = $this->makeFilter(
+ $this->getConf('usertree'),
+ array('user' => $user, 'server' => $this->getConf('server'))
+ );
+ } else {
+ // Anonymous bind
+ if (!@ldap_bind($this->con)) {
+ msg("LDAP: can not bind anonymously", -1);
+ $this->debug('LDAP anonymous bind: ' . hsc(ldap_error($this->con)), 0, __LINE__, __FILE__);
+ return false;
+ }
+ }
+
+ // Try to bind to with the dn if we have one.
+ if (!empty($dn)) {
+ // User/Password bind
+ if (!@ldap_bind($this->con, $dn, $pass)) {
+ $this->debug("LDAP: bind with $dn failed", -1, __LINE__, __FILE__);
+ $this->debug('LDAP user dn bind: ' . hsc(ldap_error($this->con)), 0, __LINE__, __FILE__);
+ return false;
+ }
+ $this->bound = 1;
+ return true;
+ } else {
+ // See if we can find the user
+ $info = $this->fetchUserData($user, true);
+ if (empty($info['dn'])) {
+ return false;
+ } else {
+ $dn = $info['dn'];
+ }
+
+ // Try to bind with the dn provided
+ if (!@ldap_bind($this->con, $dn, $pass)) {
+ $this->debug("LDAP: bind with $dn failed", -1, __LINE__, __FILE__);
+ $this->debug('LDAP user bind: ' . hsc(ldap_error($this->con)), 0, __LINE__, __FILE__);
+ return false;
+ }
+ $this->bound = 1;
+ return true;
+ }
+ }
+
+ /**
+ * Return user info
+ *
+ * Returns info about the given user needs to contain
+ * at least these fields:
+ *
+ * name string full name of the user
+ * mail string email addres of the user
+ * grps array list of groups the user is in
+ *
+ * This LDAP specific function returns the following
+ * addional fields:
+ *
+ * dn string distinguished name (DN)
+ * uid string Posix User ID
+ * inbind bool for internal use - avoid loop in binding
+ *
+ * @param string $user
+ * @param bool $requireGroups (optional) - ignored, groups are always supplied by this plugin
+ * @return array containing user data or false
+ * @author <evaldas.auryla@pheur.org>
+ * @author Stephane Chazelas <stephane.chazelas@emerson.com>
+ * @author Steffen Schoch <schoch@dsb.net>
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @author Trouble
+ * @author Dan Allen <dan.j.allen@gmail.com>
+ */
+ public function getUserData($user, $requireGroups = true)
+ {
+ return $this->fetchUserData($user);
+ }
+
+ /**
+ * @param string $user
+ * @param bool $inbind authldap specific, true if in bind phase
+ * @return array containing user data or false
+ */
+ protected function fetchUserData($user, $inbind = false)
+ {
+ global $conf;
+ if (!$this->openLDAP()) return array();
+
+ // force superuser bind if wanted and not bound as superuser yet
+ if ($this->getConf('binddn') && $this->getConf('bindpw') && $this->bound < 2) {
+ // use superuser credentials
+ if (!@ldap_bind($this->con, $this->getConf('binddn'), conf_decodeString($this->getConf('bindpw')))) {
+ $this->debug('LDAP bind as superuser: ' . hsc(ldap_error($this->con)), 0, __LINE__, __FILE__);
+ return array();
+ }
+ $this->bound = 2;
+ } elseif ($this->bound == 0 && !$inbind) {
+ // in some cases getUserData is called outside the authentication workflow
+ // eg. for sending email notification on subscribed pages. This data might not
+ // be accessible anonymously, so we try to rebind the current user here
+ list($loginuser, $loginsticky, $loginpass) = auth_getCookie();
+ if ($loginuser && $loginpass) {
+ $loginpass = auth_decrypt($loginpass, auth_cookiesalt(!$loginsticky, true));
+ $this->checkPass($loginuser, $loginpass);
+ }
+ }
+
+ $info = array();
+ $info['user'] = $user;
+ $this->debug('LDAP user to find: ' . hsc($info['user']), 0, __LINE__, __FILE__);
+
+ $info['server'] = $this->getConf('server');
+ $this->debug('LDAP Server: ' . hsc($info['server']), 0, __LINE__, __FILE__);
+
+ //get info for given user
+ $base = $this->makeFilter($this->getConf('usertree'), $info);
+ if ($this->getConf('userfilter')) {
+ $filter = $this->makeFilter($this->getConf('userfilter'), $info);
+ } else {
+ $filter = "(ObjectClass=*)";
+ }
+
+ $this->debug('LDAP Filter: ' . hsc($filter), 0, __LINE__, __FILE__);
+
+ $this->debug('LDAP user search: ' . hsc(ldap_error($this->con)), 0, __LINE__, __FILE__);
+ $this->debug('LDAP search at: ' . hsc($base . ' ' . $filter), 0, __LINE__, __FILE__);
+ $sr = $this->ldapSearch($this->con, $base, $filter, $this->getConf('userscope'), $this->getConf('attributes'));
+
+
+ $result = @ldap_get_entries($this->con, $sr);
+
+ // if result is not an array
+ if (!is_array($result)) {
+ // no objects found
+ $this->debug('LDAP search returned non-array result: ' . hsc(print($result)), -1, __LINE__, __FILE__);
+ return array();
+ }
+
+ // Don't accept more or less than one response
+ if ($result['count'] != 1) {
+ $this->debug(
+ 'LDAP search returned ' . hsc($result['count']) . ' results while it should return 1!',
+ -1,
+ __LINE__,
+ __FILE__
+ );
+ //for($i = 0; $i < $result["count"]; $i++) {
+ //$this->_debug('result: '.hsc(print_r($result[$i])), 0, __LINE__, __FILE__);
+ //}
+ return array();
+ }
+
+ $this->debug('LDAP search found single result !', 0, __LINE__, __FILE__);
+
+ $user_result = $result[0];
+ ldap_free_result($sr);
+
+ // general user info
+ $info['dn'] = $user_result['dn'];
+ $info['gid'] = $user_result['gidnumber'][0];
+ $info['mail'] = $user_result['mail'][0];
+ $info['name'] = $user_result['cn'][0];
+ $info['grps'] = array();
+
+ // overwrite if other attribs are specified.
+ if (is_array($this->getConf('mapping'))) {
+ foreach ($this->getConf('mapping') as $localkey => $key) {
+ if (is_array($key)) {
+ // use regexp to clean up user_result
+ // $key = array($key=>$regexp), only handles the first key-value
+ $regexp = current($key);
+ $key = key($key);
+ if ($user_result[$key]) foreach ($user_result[$key] as $grpkey => $grp) {
+ if ($grpkey !== 'count' && preg_match($regexp, $grp, $match)) {
+ if ($localkey == 'grps') {
+ $info[$localkey][] = $match[1];
+ } else {
+ $info[$localkey] = $match[1];
+ }
+ }
+ }
+ } else {
+ $info[$localkey] = $user_result[$key][0];
+ }
+ }
+ }
+ $user_result = array_merge($info, $user_result);
+
+ //get groups for given user if grouptree is given
+ if ($this->getConf('grouptree') || $this->getConf('groupfilter')) {
+ $base = $this->makeFilter($this->getConf('grouptree'), $user_result);
+ $filter = $this->makeFilter($this->getConf('groupfilter'), $user_result);
+ $sr = $this->ldapSearch(
+ $this->con,
+ $base,
+ $filter,
+ $this->getConf('groupscope'),
+ array($this->getConf('groupkey'))
+ );
+ $this->debug('LDAP group search: ' . hsc(ldap_error($this->con)), 0, __LINE__, __FILE__);
+ $this->debug('LDAP search at: ' . hsc($base . ' ' . $filter), 0, __LINE__, __FILE__);
+
+ if (!$sr) {
+ msg("LDAP: Reading group memberships failed", -1);
+ return array();
+ }
+ $result = ldap_get_entries($this->con, $sr);
+ ldap_free_result($sr);
+
+ if (is_array($result)) foreach ($result as $grp) {
+ if (!empty($grp[$this->getConf('groupkey')])) {
+ $group = $grp[$this->getConf('groupkey')];
+ if (is_array($group)) {
+ $group = $group[0];
+ } else {
+ $this->debug('groupkey did not return a detailled result', 0, __LINE__, __FILE__);
+ }
+ if ($group === '') continue;
+
+ $this->debug('LDAP usergroup: ' . hsc($group), 0, __LINE__, __FILE__);
+ $info['grps'][] = $group;
+ }
+ }
+ }
+
+ // always add the default group to the list of groups
+ if (!$info['grps'] or !in_array($conf['defaultgroup'], $info['grps'])) {
+ $info['grps'][] = $conf['defaultgroup'];
+ }
+ return $info;
+ }
+
+ /**
+ * Definition of the function modifyUser in order to modify the password
+ *
+ * @param string $user nick of the user to be changed
+ * @param array $changes array of field/value pairs to be changed (password will be clear text)
+ * @return bool true on success, false on error
+ */
+ public function modifyUser($user, $changes)
+ {
+
+ // open the connection to the ldap
+ if (!$this->openLDAP()) {
+ $this->debug('LDAP cannot connect: ' . hsc(ldap_error($this->con)), 0, __LINE__, __FILE__);
+ return false;
+ }
+
+ // find the information about the user, in particular the "dn"
+ $info = $this->getUserData($user, true);
+ if (empty($info['dn'])) {
+ $this->debug('LDAP cannot find your user dn', 0, __LINE__, __FILE__);
+ return false;
+ }
+ $dn = $info['dn'];
+
+ // find the old password of the user
+ list($loginuser, $loginsticky, $loginpass) = auth_getCookie();
+ if ($loginuser !== null) { // the user is currently logged in
+ $secret = auth_cookiesalt(!$loginsticky, true);
+ $pass = auth_decrypt($loginpass, $secret);
+
+ // bind with the ldap
+ if (!@ldap_bind($this->con, $dn, $pass)) {
+ $this->debug(
+ 'LDAP user bind failed: ' . hsc($dn) . ': ' . hsc(ldap_error($this->con)),
+ 0,
+ __LINE__,
+ __FILE__
+ );
+ return false;
+ }
+ } elseif ($this->getConf('binddn') && $this->getConf('bindpw')) {
+ // we are changing the password on behalf of the user (eg: forgotten password)
+ // bind with the superuser ldap
+ if (!@ldap_bind($this->con, $this->getConf('binddn'), conf_decodeString($this->getConf('bindpw')))) {
+ $this->debug('LDAP bind as superuser: ' . hsc(ldap_error($this->con)), 0, __LINE__, __FILE__);
+ return false;
+ }
+ } else {
+ return false; // no otherway
+ }
+
+ // Generate the salted hashed password for LDAP
+ $phash = new \dokuwiki\PassHash();
+ $hash = $phash->hash_ssha($changes['pass']);
+
+ // change the password
+ if (!@ldap_mod_replace($this->con, $dn, array('userpassword' => $hash))) {
+ $this->debug(
+ 'LDAP mod replace failed: ' . hsc($dn) . ': ' . hsc(ldap_error($this->con)),
+ 0,
+ __LINE__,
+ __FILE__
+ );
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Most values in LDAP are case-insensitive
+ *
+ * @return bool
+ */
+ public function isCaseSensitive()
+ {
+ return false;
+ }
+
+ /**
+ * Bulk retrieval of user data
+ *
+ * @param int $start index of first user to be returned
+ * @param int $limit max number of users to be returned
+ * @param array $filter array of field/pattern pairs, null for no filter
+ * @return array of userinfo (refer getUserData for internal userinfo details)
+ * @author Dominik Eckelmann <dokuwiki@cosmocode.de>
+ */
+ public function retrieveUsers($start = 0, $limit = 0, $filter = array())
+ {
+ if (!$this->openLDAP()) return array();
+
+ if (is_null($this->users)) {
+ // Perform the search and grab all their details
+ if ($this->getConf('userfilter')) {
+ $all_filter = str_replace('%{user}', '*', $this->getConf('userfilter'));
+ } else {
+ $all_filter = "(ObjectClass=*)";
+ }
+ $sr = ldap_search($this->con, $this->getConf('usertree'), $all_filter);
+ $entries = ldap_get_entries($this->con, $sr);
+ $users_array = array();
+ $userkey = $this->getConf('userkey');
+ for ($i = 0; $i < $entries["count"]; $i++) {
+ array_push($users_array, $entries[$i][$userkey][0]);
+ }
+ asort($users_array);
+ $result = $users_array;
+ if (!$result) return array();
+ $this->users = array_fill_keys($result, false);
+ }
+ $i = 0;
+ $count = 0;
+ $this->constructPattern($filter);
+ $result = array();
+
+ foreach ($this->users as $user => &$info) {
+ if ($i++ < $start) {
+ continue;
+ }
+ if ($info === false) {
+ $info = $this->getUserData($user);
+ }
+ if ($this->filter($user, $info)) {
+ $result[$user] = $info;
+ if (($limit > 0) && (++$count >= $limit)) break;
+ }
+ }
+ return $result;
+ }
+
+ /**
+ * Make LDAP filter strings.
+ *
+ * Used by auth_getUserData to make the filter
+ * strings for grouptree and groupfilter
+ *
+ * @param string $filter ldap search filter with placeholders
+ * @param array $placeholders placeholders to fill in
+ * @return string
+ * @author Troels Liebe Bentsen <tlb@rapanden.dk>
+ */
+ protected function makeFilter($filter, $placeholders)
+ {
+ preg_match_all("/%{([^}]+)/", $filter, $matches, PREG_PATTERN_ORDER);
+ //replace each match
+ foreach ($matches[1] as $match) {
+ //take first element if array
+ if (is_array($placeholders[$match])) {
+ $value = $placeholders[$match][0];
+ } else {
+ $value = $placeholders[$match];
+ }
+ $value = $this->filterEscape($value);
+ $filter = str_replace('%{' . $match . '}', $value, $filter);
+ }
+ return $filter;
+ }
+
+ /**
+ * return true if $user + $info match $filter criteria, false otherwise
+ *
+ * @param string $user the user's login name
+ * @param array $info the user's userinfo array
+ * @return bool
+ * @author Chris Smith <chris@jalakai.co.uk>
+ *
+ */
+ protected function filter($user, $info)
+ {
+ foreach ($this->pattern as $item => $pattern) {
+ if ($item == 'user') {
+ if (!preg_match($pattern, $user)) return false;
+ } elseif ($item == 'grps') {
+ if (!count(preg_grep($pattern, $info['grps']))) return false;
+ } else {
+ if (!preg_match($pattern, $info[$item])) return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Set the filter pattern
+ *
+ * @param $filter
+ * @return void
+ * @author Chris Smith <chris@jalakai.co.uk>
+ *
+ */
+ protected function constructPattern($filter)
+ {
+ $this->pattern = array();
+ foreach ($filter as $item => $pattern) {
+ $this->pattern[$item] = '/' . str_replace('/', '\/', $pattern) . '/i'; // allow regex characters
+ }
+ }
+
+ /**
+ * Escape a string to be used in a LDAP filter
+ *
+ * Ported from Perl's Net::LDAP::Util escape_filter_value
+ *
+ * @param string $string
+ * @return string
+ * @author Andreas Gohr
+ */
+ protected function filterEscape($string)
+ {
+ // see https://github.com/adldap/adLDAP/issues/22
+ return preg_replace_callback(
+ '/([\x00-\x1F\*\(\)\\\\])/',
+ function ($matches) {
+ return "\\" . join("", unpack("H2", $matches[1]));
+ },
+ $string
+ );
+ }
+
+ /**
+ * Opens a connection to the configured LDAP server and sets the wanted
+ * option on the connection
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+ protected function openLDAP()
+ {
+ if ($this->con) return true; // connection already established
+
+ if ($this->getConf('debug')) {
+ ldap_set_option(null, LDAP_OPT_DEBUG_LEVEL, 7);
+ }
+
+ $this->bound = 0;
+
+ $port = $this->getConf('port');
+ $bound = false;
+ $servers = explode(',', $this->getConf('server'));
+ foreach ($servers as $server) {
+ $server = trim($server);
+ $this->con = @ldap_connect($server, $port);
+ if (!$this->con) {
+ continue;
+ }
+
+ /*
+ * When OpenLDAP 2.x.x is used, ldap_connect() will always return a resource as it does
+ * not actually connect but just initializes the connecting parameters. The actual
+ * connect happens with the next calls to ldap_* funcs, usually with ldap_bind().
+ *
+ * So we should try to bind to server in order to check its availability.
+ */
+
+ //set protocol version and dependend options
+ if ($this->getConf('version')) {
+ if (!@ldap_set_option(
+ $this->con,
+ LDAP_OPT_PROTOCOL_VERSION,
+ $this->getConf('version')
+ )
+ ) {
+ msg('Setting LDAP Protocol version ' . $this->getConf('version') . ' failed', -1);
+ $this->debug('LDAP version set: ' . hsc(ldap_error($this->con)), 0, __LINE__, __FILE__);
+ } else {
+ //use TLS (needs version 3)
+ if ($this->getConf('starttls')) {
+ if (!@ldap_start_tls($this->con)) {
+ msg('Starting TLS failed', -1);
+ $this->debug('LDAP TLS set: ' . hsc(ldap_error($this->con)), 0, __LINE__, __FILE__);
+ }
+ }
+ // needs version 3
+ if ($this->getConf('referrals') > -1) {
+ if (!@ldap_set_option(
+ $this->con,
+ LDAP_OPT_REFERRALS,
+ $this->getConf('referrals')
+ )
+ ) {
+ msg('Setting LDAP referrals failed', -1);
+ $this->debug('LDAP referal set: ' . hsc(ldap_error($this->con)), 0, __LINE__, __FILE__);
+ }
+ }
+ }
+ }
+
+ //set deref mode
+ if ($this->getConf('deref')) {
+ if (!@ldap_set_option($this->con, LDAP_OPT_DEREF, $this->getConf('deref'))) {
+ msg('Setting LDAP Deref mode ' . $this->getConf('deref') . ' failed', -1);
+ $this->debug('LDAP deref set: ' . hsc(ldap_error($this->con)), 0, __LINE__, __FILE__);
+ }
+ }
+ /* As of PHP 5.3.0 we can set timeout to speedup skipping of invalid servers */
+ if (defined('LDAP_OPT_NETWORK_TIMEOUT')) {
+ ldap_set_option($this->con, LDAP_OPT_NETWORK_TIMEOUT, 1);
+ }
+
+ if ($this->getConf('binddn') && $this->getConf('bindpw')) {
+ $bound = @ldap_bind($this->con, $this->getConf('binddn'), conf_decodeString($this->getConf('bindpw')));
+ $this->bound = 2;
+ } else {
+ $bound = @ldap_bind($this->con);
+ }
+ if ($bound) {
+ break;
+ }
+ }
+
+ if (!$bound) {
+ msg("LDAP: couldn't connect to LDAP server", -1);
+ $this->debug(ldap_error($this->con), 0, __LINE__, __FILE__);
+ return false;
+ }
+
+ $this->cando['getUsers'] = true;
+ return true;
+ }
+
+ /**
+ * Wraps around ldap_search, ldap_list or ldap_read depending on $scope
+ *
+ * @param resource $link_identifier
+ * @param string $base_dn
+ * @param string $filter
+ * @param string $scope can be 'base', 'one' or 'sub'
+ * @param null|array $attributes
+ * @param int $attrsonly
+ * @param int $sizelimit
+ * @return resource
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+ protected function ldapSearch(
+ $link_identifier,
+ $base_dn,
+ $filter,
+ $scope = 'sub',
+ $attributes = null,
+ $attrsonly = 0,
+ $sizelimit = 0
+ )
+ {
+ if (is_null($attributes)) $attributes = array();
+
+ if ($scope == 'base') {
+ return @ldap_read(
+ $link_identifier,
+ $base_dn,
+ $filter,
+ $attributes,
+ $attrsonly,
+ $sizelimit
+ );
+ } elseif ($scope == 'one') {
+ return @ldap_list(
+ $link_identifier,
+ $base_dn,
+ $filter,
+ $attributes,
+ $attrsonly,
+ $sizelimit
+ );
+ } else {
+ return @ldap_search(
+ $link_identifier,
+ $base_dn,
+ $filter,
+ $attributes,
+ $attrsonly,
+ $sizelimit
+ );
+ }
+ }
+
+ /**
+ * Wrapper around msg() but outputs only when debug is enabled
+ *
+ * @param string $message
+ * @param int $err
+ * @param int $line
+ * @param string $file
+ * @return void
+ */
+ protected function debug($message, $err, $line, $file)
+ {
+ if (!$this->getConf('debug')) return;
+ msg($message, $err, $line, $file);
+ }
+}
diff --git a/platform/www/lib/plugins/authldap/conf/default.php b/platform/www/lib/plugins/authldap/conf/default.php
new file mode 100644
index 0000000..52fa1e6
--- /dev/null
+++ b/platform/www/lib/plugins/authldap/conf/default.php
@@ -0,0 +1,23 @@
+<?php
+
+$conf['server'] = '';
+$conf['port'] = 389;
+$conf['usertree'] = '';
+$conf['grouptree'] = '';
+$conf['userfilter'] = '';
+$conf['groupfilter'] = '';
+$conf['version'] = 2;
+$conf['starttls'] = 0;
+$conf['referrals'] = -1;
+$conf['deref'] = 0;
+$conf['binddn'] = '';
+$conf['bindpw'] = '';
+//$conf['mapping']['name'] unsupported in config manager
+//$conf['mapping']['grps'] unsupported in config manager
+$conf['userscope'] = 'sub';
+$conf['groupscope'] = 'sub';
+$conf['userkey'] = 'uid';
+$conf['groupkey'] = 'cn';
+$conf['debug'] = 0;
+$conf['modPass'] = 1;
+$conf['attributes'] = array();
diff --git a/platform/www/lib/plugins/authldap/conf/metadata.php b/platform/www/lib/plugins/authldap/conf/metadata.php
new file mode 100644
index 0000000..3a58590
--- /dev/null
+++ b/platform/www/lib/plugins/authldap/conf/metadata.php
@@ -0,0 +1,22 @@
+<?php
+$meta['server'] = array('string','_caution' => 'danger');
+$meta['port'] = array('numeric','_caution' => 'danger');
+$meta['usertree'] = array('string','_caution' => 'danger');
+$meta['grouptree'] = array('string','_caution' => 'danger');
+$meta['userfilter'] = array('string','_caution' => 'danger');
+$meta['groupfilter'] = array('string','_caution' => 'danger');
+$meta['version'] = array('numeric','_caution' => 'danger');
+$meta['starttls'] = array('onoff','_caution' => 'danger');
+$meta['referrals'] = array('multichoice','_choices' => array(-1,0,1),'_caution' => 'danger');
+$meta['deref'] = array('multichoice','_choices' => array(0,1,2,3),'_caution' => 'danger');
+$meta['binddn'] = array('string','_caution' => 'danger');
+$meta['bindpw'] = array('password','_caution' => 'danger','_code'=>'base64');
+$meta['attributes'] = array('array');
+//$meta['mapping']['name'] unsupported in config manager
+//$meta['mapping']['grps'] unsupported in config manager
+$meta['userscope'] = array('multichoice','_choices' => array('sub','one','base'),'_caution' => 'danger');
+$meta['groupscope'] = array('multichoice','_choices' => array('sub','one','base'),'_caution' => 'danger');
+$meta['userkey'] = array('string','_caution' => 'danger');
+$meta['groupkey'] = array('string','_caution' => 'danger');
+$meta['debug'] = array('onoff','_caution' => 'security');
+$meta['modPass'] = array('onoff');
diff --git a/platform/www/lib/plugins/authldap/lang/en/lang.php b/platform/www/lib/plugins/authldap/lang/en/lang.php
new file mode 100644
index 0000000..8185a84
--- /dev/null
+++ b/platform/www/lib/plugins/authldap/lang/en/lang.php
@@ -0,0 +1,11 @@
+<?php
+/**
+ * English language file for authldap plugin
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ */
+
+$lang['connectfail'] = 'LDAP cannot connect: %s';
+$lang['domainfail'] = 'LDAP cannot find your user dn';
+
+//Setup VIM: ex: et ts=4 :
diff --git a/platform/www/lib/plugins/authldap/lang/en/settings.php b/platform/www/lib/plugins/authldap/lang/en/settings.php
new file mode 100644
index 0000000..9988474
--- /dev/null
+++ b/platform/www/lib/plugins/authldap/lang/en/settings.php
@@ -0,0 +1,30 @@
+<?php
+$lang['server'] = 'Your LDAP server. Either hostname (<code>localhost</code>) or full qualified URL (<code>ldap://server.tld:389</code>)';
+$lang['port'] = 'LDAP server port if no full URL was given above';
+$lang['usertree'] = 'Where to find the user accounts. Eg. <code>ou=People, dc=server, dc=tld</code>';
+$lang['grouptree'] = 'Where to find the user groups. Eg. <code>ou=Group, dc=server, dc=tld</code>';
+$lang['userfilter'] = 'LDAP filter to search for user accounts. Eg. <code>(&amp;(uid=%{user})(objectClass=posixAccount))</code>';
+$lang['groupfilter'] = 'LDAP filter to search for groups. Eg. <code>(&amp;(objectClass=posixGroup)(|(gidNumber=%{gid})(memberUID=%{user})))</code>';
+$lang['version'] = 'The protocol version to use. You may need to set this to <code>3</code>';
+$lang['starttls'] = 'Use TLS connections?';
+$lang['referrals'] = 'Shall referrals be followed?';
+$lang['deref'] = 'How to dereference aliases?';
+$lang['binddn'] = 'DN of an optional bind user if anonymous bind is not sufficient. Eg. <code>cn=admin, dc=my, dc=home</code>';
+$lang['bindpw'] = 'Password of above user';
+$lang['attributes'] = 'Attributes to retrieve with the LDAP search.';
+$lang['userscope'] = 'Limit search scope for user search';
+$lang['groupscope'] = 'Limit search scope for group search';
+$lang['userkey'] = 'Attribute denoting the username; must be consistent to userfilter.';
+$lang['groupkey'] = 'Group membership from any user attribute (instead of standard AD groups) e.g. group from department or telephone number';
+$lang['modPass'] = 'Can the LDAP password be changed via dokuwiki?';
+$lang['debug'] = 'Display additional debug information on errors';
+
+
+$lang['deref_o_0'] = 'LDAP_DEREF_NEVER';
+$lang['deref_o_1'] = 'LDAP_DEREF_SEARCHING';
+$lang['deref_o_2'] = 'LDAP_DEREF_FINDING';
+$lang['deref_o_3'] = 'LDAP_DEREF_ALWAYS';
+
+$lang['referrals_o_-1'] = 'use default';
+$lang['referrals_o_0'] = 'don\'t follow referrals';
+$lang['referrals_o_1'] = 'follow referrals'; \ No newline at end of file
diff --git a/platform/www/lib/plugins/authldap/plugin.info.txt b/platform/www/lib/plugins/authldap/plugin.info.txt
new file mode 100644
index 0000000..e0c6144
--- /dev/null
+++ b/platform/www/lib/plugins/authldap/plugin.info.txt
@@ -0,0 +1,7 @@
+base authldap
+author Andreas Gohr
+email andi@splitbrain.org
+date 2015-07-13
+name LDAP Auth Plugin
+desc Provides user authentication against an LDAP server
+url http://www.dokuwiki.org/plugin:authldap
diff --git a/platform/www/lib/plugins/authpdo/README b/platform/www/lib/plugins/authpdo/README
new file mode 100644
index 0000000..c99bfbf
--- /dev/null
+++ b/platform/www/lib/plugins/authpdo/README
@@ -0,0 +1,27 @@
+authpdo Plugin for DokuWiki
+
+Authenticate against a database via PDO
+
+All documentation for this plugin can be found at
+https://www.dokuwiki.org/plugin:authpdo
+
+If you install this plugin manually, make sure it is installed in
+lib/plugins/authpdo/ - if the folder is called different it
+will not work!
+
+Please refer to http://www.dokuwiki.org/plugins for additional info
+on how to install plugins in DokuWiki.
+
+----
+Copyright (C) Andreas Gohr <andi@splitbrain.org>
+
+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; version 2 of the License
+
+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.
+
+See the COPYING file in your DokuWiki folder for details
diff --git a/platform/www/lib/plugins/authpdo/auth.php b/platform/www/lib/plugins/authpdo/auth.php
new file mode 100644
index 0000000..9c0968e
--- /dev/null
+++ b/platform/www/lib/plugins/authpdo/auth.php
@@ -0,0 +1,826 @@
+<?php
+/**
+ * DokuWiki Plugin authpdo (Auth Component)
+ *
+ * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+
+/**
+ * Class auth_plugin_authpdo
+ */
+class auth_plugin_authpdo extends DokuWiki_Auth_Plugin
+{
+
+ /** @var PDO */
+ protected $pdo;
+
+ /** @var null|array The list of all groups */
+ protected $groupcache = null;
+
+ /**
+ * Constructor.
+ */
+ public function __construct()
+ {
+ parent::__construct(); // for compatibility
+
+ if (!class_exists('PDO')) {
+ $this->debugMsg('PDO extension for PHP not found.', -1, __LINE__);
+ $this->success = false;
+ return;
+ }
+
+ if (!$this->getConf('dsn')) {
+ $this->debugMsg('No DSN specified', -1, __LINE__);
+ $this->success = false;
+ return;
+ }
+
+ try {
+ $this->pdo = new PDO(
+ $this->getConf('dsn'),
+ $this->getConf('user'),
+ conf_decodeString($this->getConf('pass')),
+ array(
+ PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, // always fetch as array
+ PDO::ATTR_EMULATE_PREPARES => true, // emulating prepares allows us to reuse param names
+ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // we want exceptions, not error codes
+ )
+ );
+ } catch (PDOException $e) {
+ $this->debugMsg($e);
+ msg($this->getLang('connectfail'), -1);
+ $this->success = false;
+ return;
+ }
+
+ // can Users be created?
+ $this->cando['addUser'] = $this->checkConfig(
+ array(
+ 'select-user',
+ 'select-user-groups',
+ 'select-groups',
+ 'insert-user',
+ 'insert-group',
+ 'join-group'
+ )
+ );
+
+ // can Users be deleted?
+ $this->cando['delUser'] = $this->checkConfig(
+ array(
+ 'select-user',
+ 'select-user-groups',
+ 'select-groups',
+ 'leave-group',
+ 'delete-user'
+ )
+ );
+
+ // can login names be changed?
+ $this->cando['modLogin'] = $this->checkConfig(
+ array(
+ 'select-user',
+ 'select-user-groups',
+ 'update-user-login'
+ )
+ );
+
+ // can passwords be changed?
+ $this->cando['modPass'] = $this->checkConfig(
+ array(
+ 'select-user',
+ 'select-user-groups',
+ 'update-user-pass'
+ )
+ );
+
+ // can real names be changed?
+ $this->cando['modName'] = $this->checkConfig(
+ array(
+ 'select-user',
+ 'select-user-groups',
+ 'update-user-info:name'
+ )
+ );
+
+ // can real email be changed?
+ $this->cando['modMail'] = $this->checkConfig(
+ array(
+ 'select-user',
+ 'select-user-groups',
+ 'update-user-info:mail'
+ )
+ );
+
+ // can groups be changed?
+ $this->cando['modGroups'] = $this->checkConfig(
+ array(
+ 'select-user',
+ 'select-user-groups',
+ 'select-groups',
+ 'leave-group',
+ 'join-group',
+ 'insert-group'
+ )
+ );
+
+ // can a filtered list of users be retrieved?
+ $this->cando['getUsers'] = $this->checkConfig(
+ array(
+ 'list-users'
+ )
+ );
+
+ // can the number of users be retrieved?
+ $this->cando['getUserCount'] = $this->checkConfig(
+ array(
+ 'count-users'
+ )
+ );
+
+ // can a list of available groups be retrieved?
+ $this->cando['getGroups'] = $this->checkConfig(
+ array(
+ 'select-groups'
+ )
+ );
+
+ $this->success = true;
+ }
+
+ /**
+ * Check user+password
+ *
+ * @param string $user the user name
+ * @param string $pass the clear text password
+ * @return bool
+ */
+ public function checkPass($user, $pass)
+ {
+
+ $userdata = $this->selectUser($user);
+ if ($userdata == false) return false;
+
+ // password checking done in SQL?
+ if ($this->checkConfig(array('check-pass'))) {
+ $userdata['clear'] = $pass;
+ $userdata['hash'] = auth_cryptPassword($pass);
+ $result = $this->query($this->getConf('check-pass'), $userdata);
+ if ($result === false) return false;
+ return (count($result) == 1);
+ }
+
+ // we do password checking on our own
+ if (isset($userdata['hash'])) {
+ // hashed password
+ $passhash = new \dokuwiki\PassHash();
+ return $passhash->verify_hash($pass, $userdata['hash']);
+ } else {
+ // clear text password in the database O_o
+ return ($pass === $userdata['clear']);
+ }
+ }
+
+ /**
+ * Return user info
+ *
+ * Returns info about the given user needs to contain
+ * at least these fields:
+ *
+ * name string full name of the user
+ * mail string email addres of the user
+ * grps array list of groups the user is in
+ *
+ * @param string $user the user name
+ * @param bool $requireGroups whether or not the returned data must include groups
+ * @return array|bool containing user data or false
+ */
+ public function getUserData($user, $requireGroups = true)
+ {
+ $data = $this->selectUser($user);
+ if ($data == false) return false;
+
+ if (isset($data['hash'])) unset($data['hash']);
+ if (isset($data['clean'])) unset($data['clean']);
+
+ if ($requireGroups) {
+ $data['grps'] = $this->selectUserGroups($data);
+ if ($data['grps'] === false) return false;
+ }
+
+ return $data;
+ }
+
+ /**
+ * Create a new User [implement only where required/possible]
+ *
+ * Returns false if the user already exists, null when an error
+ * occurred and true if everything went well.
+ *
+ * The new user HAS TO be added to the default group by this
+ * function!
+ *
+ * Set addUser capability when implemented
+ *
+ * @param string $user
+ * @param string $clear
+ * @param string $name
+ * @param string $mail
+ * @param null|array $grps
+ * @return bool|null
+ */
+ public function createUser($user, $clear, $name, $mail, $grps = null)
+ {
+ global $conf;
+
+ if (($info = $this->getUserData($user, false)) !== false) {
+ msg($this->getLang('userexists'), -1);
+ return false; // user already exists
+ }
+
+ // prepare data
+ if ($grps == null) $grps = array();
+ array_unshift($grps, $conf['defaultgroup']);
+ $grps = array_unique($grps);
+ $hash = auth_cryptPassword($clear);
+ $userdata = compact('user', 'clear', 'hash', 'name', 'mail');
+
+ // action protected by transaction
+ $this->pdo->beginTransaction();
+ {
+ // insert the user
+ $ok = $this->query($this->getConf('insert-user'), $userdata);
+ if ($ok === false) goto FAIL;
+ $userdata = $this->getUserData($user, false);
+ if ($userdata === false) goto FAIL;
+
+ // create all groups that do not exist, the refetch the groups
+ $allgroups = $this->selectGroups();
+ foreach ($grps as $group) {
+ if (!isset($allgroups[$group])) {
+ $ok = $this->addGroup($group);
+ if ($ok === false) goto FAIL;
+ }
+ }
+ $allgroups = $this->selectGroups();
+
+ // add user to the groups
+ foreach ($grps as $group) {
+ $ok = $this->joinGroup($userdata, $allgroups[$group]);
+ if ($ok === false) goto FAIL;
+ }
+ }
+ $this->pdo->commit();
+ return true;
+
+ // something went wrong, rollback
+ FAIL:
+ $this->pdo->rollBack();
+ $this->debugMsg('Transaction rolled back', 0, __LINE__);
+ msg($this->getLang('writefail'), -1);
+ return null; // return error
+ }
+
+ /**
+ * Modify user data
+ *
+ * @param string $user nick of the user to be changed
+ * @param array $changes array of field/value pairs to be changed (password will be clear text)
+ * @return bool
+ */
+ public function modifyUser($user, $changes)
+ {
+ // secure everything in transaction
+ $this->pdo->beginTransaction();
+ {
+ $olddata = $this->getUserData($user);
+ $oldgroups = $olddata['grps'];
+ unset($olddata['grps']);
+
+ // changing the user name?
+ if (isset($changes['user'])) {
+ if ($this->getUserData($changes['user'], false)) goto FAIL;
+ $params = $olddata;
+ $params['newlogin'] = $changes['user'];
+
+ $ok = $this->query($this->getConf('update-user-login'), $params);
+ if ($ok === false) goto FAIL;
+ }
+
+ // changing the password?
+ if (isset($changes['pass'])) {
+ $params = $olddata;
+ $params['clear'] = $changes['pass'];
+ $params['hash'] = auth_cryptPassword($changes['pass']);
+
+ $ok = $this->query($this->getConf('update-user-pass'), $params);
+ if ($ok === false) goto FAIL;
+ }
+
+ // changing info?
+ if (isset($changes['mail']) || isset($changes['name'])) {
+ $params = $olddata;
+ if (isset($changes['mail'])) $params['mail'] = $changes['mail'];
+ if (isset($changes['name'])) $params['name'] = $changes['name'];
+
+ $ok = $this->query($this->getConf('update-user-info'), $params);
+ if ($ok === false) goto FAIL;
+ }
+
+ // changing groups?
+ if (isset($changes['grps'])) {
+ $allgroups = $this->selectGroups();
+
+ // remove membership for previous groups
+ foreach ($oldgroups as $group) {
+ if (!in_array($group, $changes['grps']) && isset($allgroups[$group])) {
+ $ok = $this->leaveGroup($olddata, $allgroups[$group]);
+ if ($ok === false) goto FAIL;
+ }
+ }
+
+ // create all new groups that are missing
+ $added = 0;
+ foreach ($changes['grps'] as $group) {
+ if (!isset($allgroups[$group])) {
+ $ok = $this->addGroup($group);
+ if ($ok === false) goto FAIL;
+ $added++;
+ }
+ }
+ // reload group info
+ if ($added > 0) $allgroups = $this->selectGroups();
+
+ // add membership for new groups
+ foreach ($changes['grps'] as $group) {
+ if (!in_array($group, $oldgroups)) {
+ $ok = $this->joinGroup($olddata, $allgroups[$group]);
+ if ($ok === false) goto FAIL;
+ }
+ }
+ }
+
+ }
+ $this->pdo->commit();
+ return true;
+
+ // something went wrong, rollback
+ FAIL:
+ $this->pdo->rollBack();
+ $this->debugMsg('Transaction rolled back', 0, __LINE__);
+ msg($this->getLang('writefail'), -1);
+ return false; // return error
+ }
+
+ /**
+ * Delete one or more users
+ *
+ * Set delUser capability when implemented
+ *
+ * @param array $users
+ * @return int number of users deleted
+ */
+ public function deleteUsers($users)
+ {
+ $count = 0;
+ foreach ($users as $user) {
+ if ($this->deleteUser($user)) $count++;
+ }
+ return $count;
+ }
+
+ /**
+ * Bulk retrieval of user data [implement only where required/possible]
+ *
+ * Set getUsers capability when implemented
+ *
+ * @param int $start index of first user to be returned
+ * @param int $limit max number of users to be returned
+ * @param array $filter array of field/pattern pairs, null for no filter
+ * @return array list of userinfo (refer getUserData for internal userinfo details)
+ */
+ public function retrieveUsers($start = 0, $limit = -1, $filter = null)
+ {
+ if ($limit < 0) $limit = 10000; // we don't support no limit
+ if (is_null($filter)) $filter = array();
+
+ if (isset($filter['grps'])) $filter['group'] = $filter['grps'];
+ foreach (array('user', 'name', 'mail', 'group') as $key) {
+ if (!isset($filter[$key])) {
+ $filter[$key] = '%';
+ } else {
+ $filter[$key] = '%' . $filter[$key] . '%';
+ }
+ }
+ $filter['start'] = (int)$start;
+ $filter['end'] = (int)$start + $limit;
+ $filter['limit'] = (int)$limit;
+
+ $result = $this->query($this->getConf('list-users'), $filter);
+ if (!$result) return array();
+ $users = array();
+ if (is_array($result)) {
+ foreach ($result as $row) {
+ if (!isset($row['user'])) {
+ $this->debugMsg("list-users statement did not return 'user' attribute", -1, __LINE__);
+ return array();
+ }
+ $users[] = $this->getUserData($row['user']);
+ }
+ } else {
+ $this->debugMsg("list-users statement did not return a list of result", -1, __LINE__);
+ }
+ return $users;
+ }
+
+ /**
+ * Return a count of the number of user which meet $filter criteria
+ *
+ * @param array $filter array of field/pattern pairs, empty array for no filter
+ * @return int
+ */
+ public function getUserCount($filter = array())
+ {
+ if (is_null($filter)) $filter = array();
+
+ if (isset($filter['grps'])) $filter['group'] = $filter['grps'];
+ foreach (array('user', 'name', 'mail', 'group') as $key) {
+ if (!isset($filter[$key])) {
+ $filter[$key] = '%';
+ } else {
+ $filter[$key] = '%' . $filter[$key] . '%';
+ }
+ }
+
+ $result = $this->query($this->getConf('count-users'), $filter);
+ if (!$result || !isset($result[0]['count'])) {
+ $this->debugMsg("Statement did not return 'count' attribute", -1, __LINE__);
+ }
+ return (int)$result[0]['count'];
+ }
+
+ /**
+ * Create a new group with the given name
+ *
+ * @param string $group
+ * @return bool
+ */
+ public function addGroup($group)
+ {
+ $sql = $this->getConf('insert-group');
+
+ $result = $this->query($sql, array(':group' => $group));
+ $this->clearGroupCache();
+ if ($result === false) return false;
+ return true;
+ }
+
+ /**
+ * Retrieve groups
+ *
+ * Set getGroups capability when implemented
+ *
+ * @param int $start
+ * @param int $limit
+ * @return array
+ */
+ public function retrieveGroups($start = 0, $limit = 0)
+ {
+ $groups = array_keys($this->selectGroups());
+ if ($groups === false) return array();
+
+ if (!$limit) {
+ return array_splice($groups, $start);
+ } else {
+ return array_splice($groups, $start, $limit);
+ }
+ }
+
+ /**
+ * Select data of a specified user
+ *
+ * @param string $user the user name
+ * @return bool|array user data, false on error
+ */
+ protected function selectUser($user)
+ {
+ $sql = $this->getConf('select-user');
+
+ $result = $this->query($sql, array(':user' => $user));
+ if (!$result) return false;
+
+ if (count($result) > 1) {
+ $this->debugMsg('Found more than one matching user', -1, __LINE__);
+ return false;
+ }
+
+ $data = array_shift($result);
+ $dataok = true;
+
+ if (!isset($data['user'])) {
+ $this->debugMsg("Statement did not return 'user' attribute", -1, __LINE__);
+ $dataok = false;
+ }
+ if (!isset($data['hash']) && !isset($data['clear']) && !$this->checkConfig(array('check-pass'))) {
+ $this->debugMsg("Statement did not return 'clear' or 'hash' attribute", -1, __LINE__);
+ $dataok = false;
+ }
+ if (!isset($data['name'])) {
+ $this->debugMsg("Statement did not return 'name' attribute", -1, __LINE__);
+ $dataok = false;
+ }
+ if (!isset($data['mail'])) {
+ $this->debugMsg("Statement did not return 'mail' attribute", -1, __LINE__);
+ $dataok = false;
+ }
+
+ if (!$dataok) return false;
+ return $data;
+ }
+
+ /**
+ * Delete a user after removing all their group memberships
+ *
+ * @param string $user
+ * @return bool true when the user was deleted
+ */
+ protected function deleteUser($user)
+ {
+ $this->pdo->beginTransaction();
+ {
+ $userdata = $this->getUserData($user);
+ if ($userdata === false) goto FAIL;
+ $allgroups = $this->selectGroups();
+
+ // remove group memberships (ignore errors)
+ foreach ($userdata['grps'] as $group) {
+ if (isset($allgroups[$group])) {
+ $this->leaveGroup($userdata, $allgroups[$group]);
+ }
+ }
+
+ $ok = $this->query($this->getConf('delete-user'), $userdata);
+ if ($ok === false) goto FAIL;
+ }
+ $this->pdo->commit();
+ return true;
+
+ FAIL:
+ $this->pdo->rollBack();
+ return false;
+ }
+
+ /**
+ * Select all groups of a user
+ *
+ * @param array $userdata The userdata as returned by _selectUser()
+ * @return array|bool list of group names, false on error
+ */
+ protected function selectUserGroups($userdata)
+ {
+ global $conf;
+ $sql = $this->getConf('select-user-groups');
+ $result = $this->query($sql, $userdata);
+ if ($result === false) return false;
+
+ $groups = array($conf['defaultgroup']); // always add default config
+ if (is_array($result)) {
+ foreach ($result as $row) {
+ if (!isset($row['group'])) {
+ $this->debugMsg("No 'group' field returned in select-user-groups statement", -1, __LINE__);
+ return false;
+ }
+ $groups[] = $row['group'];
+ }
+ } else {
+ $this->debugMsg("select-user-groups statement did not return a list of result", -1, __LINE__);
+ }
+
+ $groups = array_unique($groups);
+ sort($groups);
+ return $groups;
+ }
+
+ /**
+ * Select all available groups
+ *
+ * @return array|bool list of all available groups and their properties
+ */
+ protected function selectGroups()
+ {
+ if ($this->groupcache) return $this->groupcache;
+
+ $sql = $this->getConf('select-groups');
+ $result = $this->query($sql);
+ if ($result === false) return false;
+
+ $groups = array();
+ if (is_array($result)) {
+ foreach ($result as $row) {
+ if (!isset($row['group'])) {
+ $this->debugMsg("No 'group' field returned from select-groups statement", -1, __LINE__);
+ return false;
+ }
+
+ // relayout result with group name as key
+ $group = $row['group'];
+ $groups[$group] = $row;
+ }
+ } else {
+ $this->debugMsg("select-groups statement did not return a list of result", -1, __LINE__);
+ }
+
+ ksort($groups);
+ return $groups;
+ }
+
+ /**
+ * Remove all entries from the group cache
+ */
+ protected function clearGroupCache()
+ {
+ $this->groupcache = null;
+ }
+
+ /**
+ * Adds the user to the group
+ *
+ * @param array $userdata all the user data
+ * @param array $groupdata all the group data
+ * @return bool
+ */
+ protected function joinGroup($userdata, $groupdata)
+ {
+ $data = array_merge($userdata, $groupdata);
+ $sql = $this->getConf('join-group');
+ $result = $this->query($sql, $data);
+ if ($result === false) return false;
+ return true;
+ }
+
+ /**
+ * Removes the user from the group
+ *
+ * @param array $userdata all the user data
+ * @param array $groupdata all the group data
+ * @return bool
+ */
+ protected function leaveGroup($userdata, $groupdata)
+ {
+ $data = array_merge($userdata, $groupdata);
+ $sql = $this->getConf('leave-group');
+ $result = $this->query($sql, $data);
+ if ($result === false) return false;
+ return true;
+ }
+
+ /**
+ * Executes a query
+ *
+ * @param string $sql The SQL statement to execute
+ * @param array $arguments Named parameters to be used in the statement
+ * @return array|int|bool The result as associative array for SELECTs, affected rows for others, false on error
+ */
+ protected function query($sql, $arguments = array())
+ {
+ $sql = trim($sql);
+ if (empty($sql)) {
+ $this->debugMsg('No SQL query given', -1, __LINE__);
+ return false;
+ }
+
+ // execute
+ $params = array();
+ $sth = $this->pdo->prepare($sql);
+ $result = false;
+ try {
+ // prepare parameters - we only use those that exist in the SQL
+ foreach ($arguments as $key => $value) {
+ if (is_array($value)) continue;
+ if (is_object($value)) continue;
+ if ($key[0] != ':') $key = ":$key"; // prefix with colon if needed
+ if (strpos($sql, $key) === false) continue; // skip if parameter is missing
+
+ if (is_int($value)) {
+ $sth->bindValue($key, $value, PDO::PARAM_INT);
+ } else {
+ $sth->bindValue($key, $value);
+ }
+ $params[$key] = $value; //remember for debugging
+ }
+
+ $sth->execute();
+ // only report last line's result
+ $hasnextrowset = true;
+ $currentsql = $sql;
+ while ($hasnextrowset) {
+ if (strtolower(substr($currentsql, 0, 6)) == 'select') {
+ $result = $sth->fetchAll();
+ } else {
+ $result = $sth->rowCount();
+ }
+ $semi_pos = strpos($currentsql, ';');
+ if ($semi_pos) {
+ $currentsql = trim(substr($currentsql, $semi_pos + 1));
+ }
+ try {
+ $hasnextrowset = $sth->nextRowset(); // run next rowset
+ } catch (PDOException $rowset_e) {
+ $hasnextrowset = false; // driver does not support multi-rowset, should be executed in one time
+ }
+ }
+ } catch (Exception $e) {
+ // report the caller's line
+ $trace = debug_backtrace();
+ $line = $trace[0]['line'];
+ $dsql = $this->debugSQL($sql, $params, !defined('DOKU_UNITTEST'));
+ $this->debugMsg($e, -1, $line);
+ $this->debugMsg("SQL: <pre>$dsql</pre>", -1, $line);
+ }
+ $sth->closeCursor();
+ $sth = null;
+
+ return $result;
+ }
+
+ /**
+ * Wrapper around msg() but outputs only when debug is enabled
+ *
+ * @param string|Exception $message
+ * @param int $err
+ * @param int $line
+ */
+ protected function debugMsg($message, $err = 0, $line = 0)
+ {
+ if (!$this->getConf('debug')) return;
+ if (is_a($message, 'Exception')) {
+ $err = -1;
+ $msg = $message->getMessage();
+ if (!$line) $line = $message->getLine();
+ } else {
+ $msg = $message;
+ }
+
+ if (defined('DOKU_UNITTEST')) {
+ printf("\n%s, %s:%d\n", $msg, __FILE__, $line);
+ } else {
+ msg('authpdo: ' . $msg, $err, $line, __FILE__);
+ }
+ }
+
+ /**
+ * Check if the given config strings are set
+ *
+ * @param string[] $keys
+ * @return bool
+ * @author Matthias Grimm <matthiasgrimm@users.sourceforge.net>
+ *
+ */
+ protected function checkConfig($keys)
+ {
+ foreach ($keys as $key) {
+ $params = explode(':', $key);
+ $key = array_shift($params);
+ $sql = trim($this->getConf($key));
+
+ // check if sql is set
+ if (!$sql) return false;
+ // check if needed params are there
+ foreach ($params as $param) {
+ if (strpos($sql, ":$param") === false) return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * create an approximation of the SQL string with parameters replaced
+ *
+ * @param string $sql
+ * @param array $params
+ * @param bool $htmlescape Should the result be escaped for output in HTML?
+ * @return string
+ */
+ protected function debugSQL($sql, $params, $htmlescape = true)
+ {
+ foreach ($params as $key => $val) {
+ if (is_int($val)) {
+ $val = $this->pdo->quote($val, PDO::PARAM_INT);
+ } elseif (is_bool($val)) {
+ $val = $this->pdo->quote($val, PDO::PARAM_BOOL);
+ } elseif (is_null($val)) {
+ $val = 'NULL';
+ } else {
+ $val = $this->pdo->quote($val);
+ }
+ $sql = str_replace($key, $val, $sql);
+ }
+ if ($htmlescape) $sql = hsc($sql);
+ return $sql;
+ }
+}
+
+// vim:ts=4:sw=4:et:
diff --git a/platform/www/lib/plugins/authpdo/conf/default.php b/platform/www/lib/plugins/authpdo/conf/default.php
new file mode 100644
index 0000000..138ca2f
--- /dev/null
+++ b/platform/www/lib/plugins/authpdo/conf/default.php
@@ -0,0 +1,118 @@
+<?php
+/**
+ * Default settings for the authpdo plugin
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+
+$conf['debug'] = 0;
+$conf['dsn'] = '';
+$conf['user'] = '';
+$conf['pass'] = '';
+
+/**
+ * statement to select a single user identified by its login name
+ *
+ * input: :user
+ * return: user, name, mail, (clear|hash), [uid], [*]
+ */
+$conf['select-user'] = '';
+
+/**
+ * statement to check the password in SQL, optional when above returned clear or hash
+ *
+ * input: :user, :clear, :hash, [uid], [*]
+ * return: *
+ */
+$conf['check-pass'] = '';
+
+/**
+ * statement to select a single user identified by its login name
+ *
+ * input: :user, [uid]
+ * return: group
+ */
+$conf['select-user-groups'] = '';
+
+/**
+ * Select all the existing group names
+ *
+ * return: group, [gid], [*]
+ */
+$conf['select-groups'] = '';
+
+/**
+ * Create a new user
+ *
+ * input: :user, :name, :mail, (:clear|:hash)
+ */
+$conf['insert-user'] = '';
+
+/**
+ * Remove a user
+ *
+ * input: :user, [:uid], [*]
+ */
+$conf['delete-user'] = '';
+
+/**
+ * list user names matching the given criteria
+ *
+ * Make sure the list is distinct and sorted by user name. Apply the given limit and offset
+ *
+ * input: :user, :name, :mail, :group, :start, :end, :limit
+ * out: user
+ */
+$conf['list-users'] = '';
+
+/**
+ * count user names matching the given criteria
+ *
+ * Make sure the counted list is distinct
+ *
+ * input: :user, :name, :mail, :group
+ * out: count
+ */
+$conf['count-users'] = '';
+
+/**
+ * Update user data (except password and user name)
+ *
+ * input: :user, :name, :mail, [:uid], [*]
+ */
+$conf['update-user-info'] = '';
+
+/**
+ * Update user name aka login
+ *
+ * input: :user, :newlogin, [:uid], [*]
+ */
+$conf['update-user-login'] = '';
+
+/**
+ * Update user password
+ *
+ * input: :user, :clear, :hash, [:uid], [*]
+ */
+$conf['update-user-pass'] = '';
+
+/**
+ * Create a new group
+ *
+ * input: :group
+ */
+$conf['insert-group'] = '';
+
+/**
+ * Make user join group
+ *
+ * input: :user, [:uid], group, [:gid], [*]
+ */
+$conf['join-group'] = '';
+
+/**
+ * Make user leave group
+ *
+ * input: :user, [:uid], group, [:gid], [*]
+ */
+$conf['leave-group'] = '';
diff --git a/platform/www/lib/plugins/authpdo/conf/metadata.php b/platform/www/lib/plugins/authpdo/conf/metadata.php
new file mode 100644
index 0000000..34e60a4
--- /dev/null
+++ b/platform/www/lib/plugins/authpdo/conf/metadata.php
@@ -0,0 +1,25 @@
+<?php
+/**
+ * Options for the authpdo plugin
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+
+$meta['debug'] = array('onoff', '_caution' => 'security');
+$meta['dsn'] = array('string', '_caution' => 'danger');
+$meta['user'] = array('string', '_caution' => 'danger');
+$meta['pass'] = array('password', '_caution' => 'danger', '_code' => 'base64');
+$meta['select-user'] = array('', '_caution' => 'danger');
+$meta['check-pass'] = array('', '_caution' => 'danger');
+$meta['select-user-groups'] = array('', '_caution' => 'danger');
+$meta['select-groups'] = array('', '_caution' => 'danger');
+$meta['insert-user'] = array('', '_caution' => 'danger');
+$meta['delete-user'] = array('', '_caution' => 'danger');
+$meta['list-users'] = array('', '_caution' => 'danger');
+$meta['count-users'] = array('', '_caution' => 'danger');
+$meta['update-user-info'] = array('', '_caution' => 'danger');
+$meta['update-user-login'] = array('', '_caution' => 'danger');
+$meta['update-user-pass'] = array('', '_caution' => 'danger');
+$meta['insert-group'] = array('', '_caution' => 'danger');
+$meta['join-group'] = array('', '_caution' => 'danger');
+$meta['leave-group'] = array('', '_caution' => 'danger');
diff --git a/platform/www/lib/plugins/authpdo/lang/en/lang.php b/platform/www/lib/plugins/authpdo/lang/en/lang.php
new file mode 100644
index 0000000..3e1482e
--- /dev/null
+++ b/platform/www/lib/plugins/authpdo/lang/en/lang.php
@@ -0,0 +1,12 @@
+<?php
+/**
+ * English language file for authpdo plugin
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ */
+
+$lang['connectfail'] = 'Failed to connect to database.';
+$lang['userexists'] = 'Sorry, a user with this login already exists.';
+$lang['writefail'] = 'Unable to modify user data. Please inform the Wiki-Admin';
+
+//Setup VIM: ex: et ts=4 :
diff --git a/platform/www/lib/plugins/authpdo/lang/en/settings.php b/platform/www/lib/plugins/authpdo/lang/en/settings.php
new file mode 100644
index 0000000..1aaaec0
--- /dev/null
+++ b/platform/www/lib/plugins/authpdo/lang/en/settings.php
@@ -0,0 +1,25 @@
+<?php
+/**
+ * english language file for authpdo plugin
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+
+$lang['debug'] = 'Print out detailed error messages. Should be disabled after setup.';
+$lang['dsn'] = 'The DSN to connect to the database.';
+$lang['user'] = 'The user for the above database connection (empty for sqlite)';
+$lang['pass'] = 'The password for the above database connection (empty for sqlite)';
+$lang['select-user'] = 'SQL Statement to select the data of a single user';
+$lang['select-user-groups'] = 'SQL Statement to select all groups of a single user';
+$lang['select-groups'] = 'SQL Statement to select all available groups';
+$lang['insert-user'] = 'SQL Statement to insert a new user into the database';
+$lang['delete-user'] = 'SQL Statement to remove a single user from the database';
+$lang['list-users'] = 'SQL Statement to list users matching a filter';
+$lang['count-users'] = 'SQL Statement to count users matching a filter';
+$lang['update-user-info'] = 'SQL Statement to update the full name and email address of a single user';
+$lang['update-user-login'] = 'SQL Statement to update the login name of a single user';
+$lang['update-user-pass'] = 'SQL Statement to update the password of a single user';
+$lang['insert-group'] = 'SQL Statement to insert a new group into the database';
+$lang['join-group'] = 'SQL Statement to add a user to an existing group';
+$lang['leave-group'] = 'SQL Statement to remove a user from a group';
+$lang['check-pass'] = 'SQL Statement to check the password for a user. Can be left empty if password info is fetched in select-user.';
diff --git a/platform/www/lib/plugins/authpdo/plugin.info.txt b/platform/www/lib/plugins/authpdo/plugin.info.txt
new file mode 100644
index 0000000..e60ff0b
--- /dev/null
+++ b/platform/www/lib/plugins/authpdo/plugin.info.txt
@@ -0,0 +1,7 @@
+base authpdo
+author Andreas Gohr
+email andi@splitbrain.org
+date 2016-08-20
+name authpdo plugin
+desc Authenticate against a database via PDO
+url https://www.dokuwiki.org/plugin:authpdo
diff --git a/platform/www/lib/plugins/authplain/auth.php b/platform/www/lib/plugins/authplain/auth.php
new file mode 100644
index 0000000..421af88
--- /dev/null
+++ b/platform/www/lib/plugins/authplain/auth.php
@@ -0,0 +1,494 @@
+<?php
+
+/**
+ * Plaintext authentication backend
+ *
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @author Chris Smith <chris@jalakai.co.uk>
+ * @author Jan Schumann <js@schumann-it.com>
+ */
+class auth_plugin_authplain extends DokuWiki_Auth_Plugin
+{
+ /** @var array user cache */
+ protected $users = null;
+
+ /** @var array filter pattern */
+ protected $pattern = array();
+
+ /** @var bool safe version of preg_split */
+ protected $pregsplit_safe = false;
+
+ /**
+ * Constructor
+ *
+ * Carry out sanity checks to ensure the object is
+ * able to operate. Set capabilities.
+ *
+ * @author Christopher Smith <chris@jalakai.co.uk>
+ */
+ public function __construct()
+ {
+ parent::__construct();
+ global $config_cascade;
+
+ if (!@is_readable($config_cascade['plainauth.users']['default'])) {
+ $this->success = false;
+ } else {
+ if (@is_writable($config_cascade['plainauth.users']['default'])) {
+ $this->cando['addUser'] = true;
+ $this->cando['delUser'] = true;
+ $this->cando['modLogin'] = true;
+ $this->cando['modPass'] = true;
+ $this->cando['modName'] = true;
+ $this->cando['modMail'] = true;
+ $this->cando['modGroups'] = true;
+ }
+ $this->cando['getUsers'] = true;
+ $this->cando['getUserCount'] = true;
+ $this->cando['getGroups'] = true;
+ }
+
+ $this->pregsplit_safe = version_compare(PCRE_VERSION, '6.7', '>=');
+ }
+
+ /**
+ * Check user+password
+ *
+ * Checks if the given user exists and the given
+ * plaintext password is correct
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @param string $user
+ * @param string $pass
+ * @return bool
+ */
+ public function checkPass($user, $pass)
+ {
+ $userinfo = $this->getUserData($user);
+ if ($userinfo === false) return false;
+
+ return auth_verifyPassword($pass, $this->users[$user]['pass']);
+ }
+
+ /**
+ * Return user info
+ *
+ * Returns info about the given user needs to contain
+ * at least these fields:
+ *
+ * name string full name of the user
+ * mail string email addres of the user
+ * grps array list of groups the user is in
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @param string $user
+ * @param bool $requireGroups (optional) ignored by this plugin, grps info always supplied
+ * @return array|false
+ */
+ public function getUserData($user, $requireGroups = true)
+ {
+ if ($this->users === null) $this->loadUserData();
+ return isset($this->users[$user]) ? $this->users[$user] : false;
+ }
+
+ /**
+ * Creates a string suitable for saving as a line
+ * in the file database
+ * (delimiters escaped, etc.)
+ *
+ * @param string $user
+ * @param string $pass
+ * @param string $name
+ * @param string $mail
+ * @param array $grps list of groups the user is in
+ * @return string
+ */
+ protected function createUserLine($user, $pass, $name, $mail, $grps)
+ {
+ $groups = join(',', $grps);
+ $userline = array($user, $pass, $name, $mail, $groups);
+ $userline = str_replace('\\', '\\\\', $userline); // escape \ as \\
+ $userline = str_replace(':', '\\:', $userline); // escape : as \:
+ $userline = join(':', $userline)."\n";
+ return $userline;
+ }
+
+ /**
+ * Create a new User
+ *
+ * Returns false if the user already exists, null when an error
+ * occurred and true if everything went well.
+ *
+ * The new user will be added to the default group by this
+ * function if grps are not specified (default behaviour).
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @author Chris Smith <chris@jalakai.co.uk>
+ *
+ * @param string $user
+ * @param string $pwd
+ * @param string $name
+ * @param string $mail
+ * @param array $grps
+ * @return bool|null|string
+ */
+ public function createUser($user, $pwd, $name, $mail, $grps = null)
+ {
+ global $conf;
+ global $config_cascade;
+
+ // user mustn't already exist
+ if ($this->getUserData($user) !== false) {
+ msg($this->getLang('userexists'), -1);
+ return false;
+ }
+
+ $pass = auth_cryptPassword($pwd);
+
+ // set default group if no groups specified
+ if (!is_array($grps)) $grps = array($conf['defaultgroup']);
+
+ // prepare user line
+ $userline = $this->createUserLine($user, $pass, $name, $mail, $grps);
+
+ if (!io_saveFile($config_cascade['plainauth.users']['default'], $userline, true)) {
+ msg($this->getLang('writefail'), -1);
+ return null;
+ }
+
+ $this->users[$user] = compact('pass', 'name', 'mail', 'grps');
+ return $pwd;
+ }
+
+ /**
+ * Modify user data
+ *
+ * @author Chris Smith <chris@jalakai.co.uk>
+ * @param string $user nick of the user to be changed
+ * @param array $changes array of field/value pairs to be changed (password will be clear text)
+ * @return bool
+ */
+ public function modifyUser($user, $changes)
+ {
+ global $ACT;
+ global $config_cascade;
+
+ // sanity checks, user must already exist and there must be something to change
+ if (($userinfo = $this->getUserData($user)) === false) {
+ msg($this->getLang('usernotexists'), -1);
+ return false;
+ }
+
+ // don't modify protected users
+ if (!empty($userinfo['protected'])) {
+ msg(sprintf($this->getLang('protected'), hsc($user)), -1);
+ return false;
+ }
+
+ if (!is_array($changes) || !count($changes)) return true;
+
+ // update userinfo with new data, remembering to encrypt any password
+ $newuser = $user;
+ foreach ($changes as $field => $value) {
+ if ($field == 'user') {
+ $newuser = $value;
+ continue;
+ }
+ if ($field == 'pass') $value = auth_cryptPassword($value);
+ $userinfo[$field] = $value;
+ }
+
+ $userline = $this->createUserLine(
+ $newuser,
+ $userinfo['pass'],
+ $userinfo['name'],
+ $userinfo['mail'],
+ $userinfo['grps']
+ );
+
+ if (!io_replaceInFile($config_cascade['plainauth.users']['default'], '/^'.$user.':/', $userline, true)) {
+ msg('There was an error modifying your user data. You may need to register again.', -1);
+ // FIXME, io functions should be fail-safe so existing data isn't lost
+ $ACT = 'register';
+ return false;
+ }
+
+ $this->users[$newuser] = $userinfo;
+ return true;
+ }
+
+ /**
+ * Remove one or more users from the list of registered users
+ *
+ * @author Christopher Smith <chris@jalakai.co.uk>
+ * @param array $users array of users to be deleted
+ * @return int the number of users deleted
+ */
+ public function deleteUsers($users)
+ {
+ global $config_cascade;
+
+ if (!is_array($users) || empty($users)) return 0;
+
+ if ($this->users === null) $this->loadUserData();
+
+ $deleted = array();
+ foreach ($users as $user) {
+ // don't delete protected users
+ if (!empty($this->users[$user]['protected'])) {
+ msg(sprintf($this->getLang('protected'), hsc($user)), -1);
+ continue;
+ }
+ if (isset($this->users[$user])) $deleted[] = preg_quote($user, '/');
+ }
+
+ if (empty($deleted)) return 0;
+
+ $pattern = '/^('.join('|', $deleted).'):/';
+ if (!io_deleteFromFile($config_cascade['plainauth.users']['default'], $pattern, true)) {
+ msg($this->getLang('writefail'), -1);
+ return 0;
+ }
+
+ // reload the user list and count the difference
+ $count = count($this->users);
+ $this->loadUserData();
+ $count -= count($this->users);
+ return $count;
+ }
+
+ /**
+ * Return a count of the number of user which meet $filter criteria
+ *
+ * @author Chris Smith <chris@jalakai.co.uk>
+ *
+ * @param array $filter
+ * @return int
+ */
+ public function getUserCount($filter = array())
+ {
+
+ if ($this->users === null) $this->loadUserData();
+
+ if (!count($filter)) return count($this->users);
+
+ $count = 0;
+ $this->constructPattern($filter);
+
+ foreach ($this->users as $user => $info) {
+ $count += $this->filter($user, $info);
+ }
+
+ return $count;
+ }
+
+ /**
+ * Bulk retrieval of user data
+ *
+ * @author Chris Smith <chris@jalakai.co.uk>
+ *
+ * @param int $start index of first user to be returned
+ * @param int $limit max number of users to be returned
+ * @param array $filter array of field/pattern pairs
+ * @return array userinfo (refer getUserData for internal userinfo details)
+ */
+ public function retrieveUsers($start = 0, $limit = 0, $filter = array())
+ {
+
+ if ($this->users === null) $this->loadUserData();
+
+ ksort($this->users);
+
+ $i = 0;
+ $count = 0;
+ $out = array();
+ $this->constructPattern($filter);
+
+ foreach ($this->users as $user => $info) {
+ if ($this->filter($user, $info)) {
+ if ($i >= $start) {
+ $out[$user] = $info;
+ $count++;
+ if (($limit > 0) && ($count >= $limit)) break;
+ }
+ $i++;
+ }
+ }
+
+ return $out;
+ }
+
+ /**
+ * Retrieves groups.
+ * Loads complete user data into memory before searching for groups.
+ *
+ * @param int $start index of first group to be returned
+ * @param int $limit max number of groups to be returned
+ * @return array
+ */
+ public function retrieveGroups($start = 0, $limit = 0)
+ {
+ $groups = [];
+
+ if ($this->users === null) $this->loadUserData();
+ foreach($this->users as $user => $info) {
+ $groups = array_merge($groups, array_diff($info['grps'], $groups));
+ }
+
+ if($limit > 0) {
+ return array_splice($groups, $start, $limit);
+ }
+ return array_splice($groups, $start);
+ }
+
+ /**
+ * Only valid pageid's (no namespaces) for usernames
+ *
+ * @param string $user
+ * @return string
+ */
+ public function cleanUser($user)
+ {
+ global $conf;
+ return cleanID(str_replace(':', $conf['sepchar'], $user));
+ }
+
+ /**
+ * Only valid pageid's (no namespaces) for groupnames
+ *
+ * @param string $group
+ * @return string
+ */
+ public function cleanGroup($group)
+ {
+ global $conf;
+ return cleanID(str_replace(':', $conf['sepchar'], $group));
+ }
+
+ /**
+ * Load all user data
+ *
+ * loads the user file into a datastructure
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+ protected function loadUserData()
+ {
+ global $config_cascade;
+
+ $this->users = $this->readUserFile($config_cascade['plainauth.users']['default']);
+
+ // support protected users
+ if (!empty($config_cascade['plainauth.users']['protected'])) {
+ $protected = $this->readUserFile($config_cascade['plainauth.users']['protected']);
+ foreach (array_keys($protected) as $key) {
+ $protected[$key]['protected'] = true;
+ }
+ $this->users = array_merge($this->users, $protected);
+ }
+ }
+
+ /**
+ * Read user data from given file
+ *
+ * ignores non existing files
+ *
+ * @param string $file the file to load data from
+ * @return array
+ */
+ protected function readUserFile($file)
+ {
+ $users = array();
+ if (!file_exists($file)) return $users;
+
+ $lines = file($file);
+ foreach ($lines as $line) {
+ $line = preg_replace('/#.*$/', '', $line); //ignore comments
+ $line = trim($line);
+ if (empty($line)) continue;
+
+ $row = $this->splitUserData($line);
+ $row = str_replace('\\:', ':', $row);
+ $row = str_replace('\\\\', '\\', $row);
+
+ $groups = array_values(array_filter(explode(",", $row[4])));
+
+ $users[$row[0]]['pass'] = $row[1];
+ $users[$row[0]]['name'] = urldecode($row[2]);
+ $users[$row[0]]['mail'] = $row[3];
+ $users[$row[0]]['grps'] = $groups;
+ }
+ return $users;
+ }
+
+ /**
+ * Get the user line split into it's parts
+ *
+ * @param string $line
+ * @return string[]
+ */
+ protected function splitUserData($line)
+ {
+ // due to a bug in PCRE 6.6, preg_split will fail with the regex we use here
+ // refer github issues 877 & 885
+ if ($this->pregsplit_safe) {
+ return preg_split('/(?<![^\\\\]\\\\)\:/', $line, 5); // allow for : escaped as \:
+ }
+
+ $row = array();
+ $piece = '';
+ $len = strlen($line);
+ for ($i=0; $i<$len; $i++) {
+ if ($line[$i]=='\\') {
+ $piece .= $line[$i];
+ $i++;
+ if ($i>=$len) break;
+ } elseif ($line[$i]==':') {
+ $row[] = $piece;
+ $piece = '';
+ continue;
+ }
+ $piece .= $line[$i];
+ }
+ $row[] = $piece;
+
+ return $row;
+ }
+
+ /**
+ * return true if $user + $info match $filter criteria, false otherwise
+ *
+ * @author Chris Smith <chris@jalakai.co.uk>
+ *
+ * @param string $user User login
+ * @param array $info User's userinfo array
+ * @return bool
+ */
+ protected function filter($user, $info)
+ {
+ foreach ($this->pattern as $item => $pattern) {
+ if ($item == 'user') {
+ if (!preg_match($pattern, $user)) return false;
+ } elseif ($item == 'grps') {
+ if (!count(preg_grep($pattern, $info['grps']))) return false;
+ } else {
+ if (!preg_match($pattern, $info[$item])) return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * construct a filter pattern
+ *
+ * @param array $filter
+ */
+ protected function constructPattern($filter)
+ {
+ $this->pattern = array();
+ foreach ($filter as $item => $pattern) {
+ $this->pattern[$item] = '/'.str_replace('/', '\/', $pattern).'/i'; // allow regex characters
+ }
+ }
+}
diff --git a/platform/www/lib/plugins/authplain/lang/en/lang.php b/platform/www/lib/plugins/authplain/lang/en/lang.php
new file mode 100644
index 0000000..7108f38
--- /dev/null
+++ b/platform/www/lib/plugins/authplain/lang/en/lang.php
@@ -0,0 +1,9 @@
+<?php
+/**
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ */
+$lang['userexists'] = 'Sorry, a user with this login already exists.';
+$lang['usernotexists'] = 'Sorry, that user doesn\'t exist.';
+$lang['writefail'] = 'Unable to modify user data. Please inform the Wiki-Admin';
+$lang['protected'] = 'Data for user %s is protected and can not be modified or deleted.';
diff --git a/platform/www/lib/plugins/authplain/plugin.info.txt b/platform/www/lib/plugins/authplain/plugin.info.txt
new file mode 100644
index 0000000..c09dbcb
--- /dev/null
+++ b/platform/www/lib/plugins/authplain/plugin.info.txt
@@ -0,0 +1,7 @@
+base authplain
+author Andreas Gohr
+email andi@splitbrain.org
+date 2015-07-18
+name Plain Auth Plugin
+desc Provides user authentication against DokuWiki's local password storage
+url http://www.dokuwiki.org/plugin:authplain
diff --git a/platform/www/lib/plugins/cli.php b/platform/www/lib/plugins/cli.php
new file mode 100644
index 0000000..a3cbec7
--- /dev/null
+++ b/platform/www/lib/plugins/cli.php
@@ -0,0 +1,2 @@
+<?php
+dbg_deprecated('Autoloading. Do not require() files yourself.');
diff --git a/platform/www/lib/plugins/config/admin.php b/platform/www/lib/plugins/config/admin.php
new file mode 100644
index 0000000..219612c
--- /dev/null
+++ b/platform/www/lib/plugins/config/admin.php
@@ -0,0 +1,282 @@
+<?php
+/**
+ * Configuration Manager admin plugin
+ *
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ * @author Christopher Smith <chris@jalakai.co.uk>
+ * @author Ben Coburn <btcoburn@silicodon.net>
+ */
+
+use dokuwiki\plugin\config\core\Configuration;
+use dokuwiki\plugin\config\core\Setting\Setting;
+use dokuwiki\plugin\config\core\Setting\SettingFieldset;
+use dokuwiki\plugin\config\core\Setting\SettingHidden;
+
+/**
+ * All DokuWiki plugins to extend the admin function
+ * need to inherit from this class
+ */
+class admin_plugin_config extends DokuWiki_Admin_Plugin {
+
+ const IMGDIR = DOKU_BASE . 'lib/plugins/config/images/';
+
+ /** @var Configuration */
+ protected $configuration;
+
+ /** @var bool were there any errors in the submitted data? */
+ protected $hasErrors = false;
+
+ /** @var bool have the settings translations been loaded? */
+ protected $promptsLocalized = false;
+
+
+ /**
+ * handle user request
+ */
+ public function handle() {
+ global $ID, $INPUT;
+
+ // always initialize the configuration
+ $this->configuration = new Configuration();
+
+ if(!$INPUT->bool('save') || !checkSecurityToken()) {
+ return;
+ }
+
+ // don't go any further if the configuration is locked
+ if($this->configuration->isLocked()) return;
+
+ // update settings and redirect of successful
+ $ok = $this->configuration->updateSettings($INPUT->arr('config'));
+ if($ok) { // no errors
+ try {
+ if($this->configuration->hasChanged()) {
+ $this->configuration->save();
+ } else {
+ $this->configuration->touch();
+ }
+ msg($this->getLang('updated'), 1);
+ } catch(Exception $e) {
+ msg($this->getLang('error'), -1);
+ }
+ send_redirect(wl($ID, array('do' => 'admin', 'page' => 'config'), true, '&'));
+ } else {
+ $this->hasErrors = true;
+ }
+ }
+
+ /**
+ * output appropriate html
+ */
+ public function html() {
+ $allow_debug = $GLOBALS['conf']['allowdebug']; // avoid global $conf; here.
+ global $lang;
+ global $ID;
+
+ $this->setupLocale(true);
+
+ echo $this->locale_xhtml('intro');
+
+ echo '<div id="config__manager">';
+
+ if($this->configuration->isLocked()) {
+ echo '<div class="info">' . $this->getLang('locked') . '</div>';
+ }
+
+ // POST to script() instead of wl($ID) so config manager still works if
+ // rewrite config is broken. Add $ID as hidden field to remember
+ // current ID in most cases.
+ echo '<form id="dw__configform" action="' . script() . '" method="post">';
+ echo '<div class="no"><input type="hidden" name="id" value="' . $ID . '" /></div>';
+ formSecurityToken();
+ $this->printH1('dokuwiki_settings', $this->getLang('_header_dokuwiki'));
+
+ $in_fieldset = false;
+ $first_plugin_fieldset = true;
+ $first_template_fieldset = true;
+ foreach($this->configuration->getSettings() as $setting) {
+ if(is_a($setting, SettingHidden::class)) {
+ continue;
+ } else if(is_a($setting, settingFieldset::class)) {
+ // config setting group
+ if($in_fieldset) {
+ echo '</table>';
+ echo '</div>';
+ echo '</fieldset>';
+ } else {
+ $in_fieldset = true;
+ }
+ if($first_plugin_fieldset && $setting->getType() == 'plugin') {
+ $this->printH1('plugin_settings', $this->getLang('_header_plugin'));
+ $first_plugin_fieldset = false;
+ } else if($first_template_fieldset && $setting->getType() == 'template') {
+ $this->printH1('template_settings', $this->getLang('_header_template'));
+ $first_template_fieldset = false;
+ }
+ echo '<fieldset id="' . $setting->getKey() . '">';
+ echo '<legend>' . $setting->prompt($this) . '</legend>';
+ echo '<div class="table">';
+ echo '<table class="inline">';
+ } else {
+ // config settings
+ list($label, $input) = $setting->html($this, $this->hasErrors);
+
+ $class = $setting->isDefault()
+ ? ' class="default"'
+ : ($setting->isProtected() ? ' class="protected"' : '');
+ $error = $setting->hasError()
+ ? ' class="value error"'
+ : ' class="value"';
+ $icon = $setting->caution()
+ ? '<img src="' . self::IMGDIR . $setting->caution() . '.png" ' .
+ 'alt="' . $setting->caution() . '" title="' . $this->getLang($setting->caution()) . '" />'
+ : '';
+
+ echo '<tr' . $class . '>';
+ echo '<td class="label">';
+ echo '<span class="outkey">' . $setting->getPrettyKey() . '</span>';
+ echo $icon . $label;
+ echo '</td>';
+ echo '<td' . $error . '>' . $input . '</td>';
+ echo '</tr>';
+ }
+ }
+
+ echo '</table>';
+ echo '</div>';
+ if($in_fieldset) {
+ echo '</fieldset>';
+ }
+
+ // show undefined settings list
+ $undefined_settings = $this->configuration->getUndefined();
+ if($allow_debug && !empty($undefined_settings)) {
+ /**
+ * Callback for sorting settings
+ *
+ * @param Setting $a
+ * @param Setting $b
+ * @return int if $a is lower/equal/higher than $b
+ */
+ function settingNaturalComparison($a, $b) {
+ return strnatcmp($a->getKey(), $b->getKey());
+ }
+
+ usort($undefined_settings, 'settingNaturalComparison');
+ $this->printH1('undefined_settings', $this->getLang('_header_undefined'));
+ echo '<fieldset>';
+ echo '<div class="table">';
+ echo '<table class="inline">';
+ foreach($undefined_settings as $setting) {
+ list($label, $input) = $setting->html($this);
+ echo '<tr>';
+ echo '<td class="label">' . $label . '</td>';
+ echo '<td>' . $input . '</td>';
+ echo '</tr>';
+ }
+ echo '</table>';
+ echo '</div>';
+ echo '</fieldset>';
+ }
+
+ // finish up form
+ echo '<p>';
+ echo '<input type="hidden" name="do" value="admin" />';
+ echo '<input type="hidden" name="page" value="config" />';
+
+ if(!$this->configuration->isLocked()) {
+ echo '<input type="hidden" name="save" value="1" />';
+ echo '<button type="submit" name="submit" accesskey="s">' . $lang['btn_save'] . '</button>';
+ echo '<button type="reset">' . $lang['btn_reset'] . '</button>';
+ }
+
+ echo '</p>';
+
+ echo '</form>';
+ echo '</div>';
+ }
+
+ /**
+ * @param bool $prompts
+ */
+ public function setupLocale($prompts = false) {
+ parent::setupLocale();
+ if(!$prompts || $this->promptsLocalized) return;
+ $this->lang = array_merge($this->lang, $this->configuration->getLangs());
+ $this->promptsLocalized = true;
+ }
+
+ /**
+ * Generates a two-level table of contents for the config plugin.
+ *
+ * @author Ben Coburn <btcoburn@silicodon.net>
+ *
+ * @return array
+ */
+ public function getTOC() {
+ $this->setupLocale(true);
+
+ $allow_debug = $GLOBALS['conf']['allowdebug']; // avoid global $conf; here.
+ $toc = array();
+ $check = false;
+
+ // gather settings data into three sub arrays
+ $labels = ['dokuwiki' => [], 'plugin' => [], 'template' => []];
+ foreach($this->configuration->getSettings() as $setting) {
+ if(is_a($setting, SettingFieldset::class)) {
+ $labels[$setting->getType()][] = $setting;
+ }
+ }
+
+ // top header
+ $title = $this->getLang('_configuration_manager');
+ $toc[] = html_mktocitem(sectionID($title, $check), $title, 1);
+
+ // main entries
+ foreach(['dokuwiki', 'plugin', 'template'] as $section) {
+ if(empty($labels[$section])) continue; // no entries, skip
+
+ // create main header
+ $toc[] = html_mktocitem(
+ $section . '_settings',
+ $this->getLang('_header_' . $section),
+ 1
+ );
+
+ // create sub headers
+ foreach($labels[$section] as $setting) {
+ /** @var SettingFieldset $setting */
+ $name = $setting->prompt($this);
+ $toc[] = html_mktocitem($setting->getKey(), $name, 2);
+ }
+ }
+
+ // undefined settings if allowed
+ if(count($this->configuration->getUndefined()) && $allow_debug) {
+ $toc[] = html_mktocitem('undefined_settings', $this->getLang('_header_undefined'), 1);
+ }
+
+ return $toc;
+ }
+
+ /**
+ * @param string $id
+ * @param string $text
+ */
+ protected function printH1($id, $text) {
+ echo '<h1 id="' . $id . '">' . $text . '</h1>';
+ }
+
+ /**
+ * Adds a translation to this plugin's language array
+ *
+ * Used by some settings to set up dynamic translations
+ *
+ * @param string $key
+ * @param string $value
+ */
+ public function addLang($key, $value) {
+ if(!$this->localised) $this->setupLocale();
+ $this->lang[$key] = $value;
+ }
+}
diff --git a/platform/www/lib/plugins/config/admin.svg b/platform/www/lib/plugins/config/admin.svg
new file mode 100644
index 0000000..ced9871
--- /dev/null
+++ b/platform/www/lib/plugins/config/admin.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M12 15.5A3.5 3.5 0 0 1 8.5 12 3.5 3.5 0 0 1 12 8.5a3.5 3.5 0 0 1 3.5 3.5 3.5 3.5 0 0 1-3.5 3.5m7.43-2.53c.04-.32.07-.64.07-.97 0-.33-.03-.66-.07-1l2.11-1.63c.19-.15.24-.42.12-.64l-2-3.46c-.12-.22-.39-.31-.61-.22l-2.49 1c-.52-.39-1.06-.73-1.69-.98l-.37-2.65A.506.506 0 0 0 14 2h-4c-.25 0-.46.18-.5.42l-.37 2.65c-.63.25-1.17.59-1.69.98l-2.49-1c-.22-.09-.49 0-.61.22l-2 3.46c-.13.22-.07.49.12.64L4.57 11c-.04.34-.07.67-.07 1 0 .33.03.65.07.97l-2.11 1.66c-.19.15-.25.42-.12.64l2 3.46c.12.22.39.3.61.22l2.49-1.01c.52.4 1.06.74 1.69.99l.37 2.65c.04.24.25.42.5.42h4c.25 0 .46-.18.5-.42l.37-2.65c.63-.26 1.17-.59 1.69-.99l2.49 1.01c.22.08.49 0 .61-.22l2-3.46c.12-.22.07-.49-.12-.64l-2.11-1.66z"/></svg> \ No newline at end of file
diff --git a/platform/www/lib/plugins/config/core/ConfigParser.php b/platform/www/lib/plugins/config/core/ConfigParser.php
new file mode 100644
index 0000000..9e79b96
--- /dev/null
+++ b/platform/www/lib/plugins/config/core/ConfigParser.php
@@ -0,0 +1,90 @@
+<?php
+
+namespace dokuwiki\plugin\config\core;
+
+/**
+ * A naive PHP file parser
+ *
+ * This parses our very simple config file in PHP format. We use this instead of simply including
+ * the file, because we want to keep expressions such as 24*60*60 as is.
+ *
+ * @author Chris Smith <chris@jalakai.co.uk>
+ */
+class ConfigParser {
+ /** @var string variable to parse from the file */
+ protected $varname = 'conf';
+ /** @var string the key to mark sub arrays */
+ protected $keymarker = Configuration::KEYMARKER;
+
+ /**
+ * Parse the given PHP file into an array
+ *
+ * When the given files does not exist, this returns an empty array
+ *
+ * @param string $file
+ * @return array
+ */
+ public function parse($file) {
+ if(!file_exists($file)) return array();
+
+ $config = array();
+ $contents = @php_strip_whitespace($file);
+ $pattern = '/\$' . $this->varname . '\[[\'"]([^=]+)[\'"]\] ?= ?(.*?);(?=[^;]*(?:\$' . $this->varname . '|$))/s';
+ $matches = array();
+ preg_match_all($pattern, $contents, $matches, PREG_SET_ORDER);
+
+ for($i = 0; $i < count($matches); $i++) {
+ $value = $matches[$i][2];
+
+ // merge multi-dimensional array indices using the keymarker
+ $key = preg_replace('/.\]\[./', $this->keymarker, $matches[$i][1]);
+
+ // handle arrays
+ if(preg_match('/^array ?\((.*)\)/', $value, $match)) {
+ $arr = explode(',', $match[1]);
+
+ // remove quotes from quoted strings & unescape escaped data
+ $len = count($arr);
+ for($j = 0; $j < $len; $j++) {
+ $arr[$j] = trim($arr[$j]);
+ $arr[$j] = $this->readValue($arr[$j]);
+ }
+
+ $value = $arr;
+ } else {
+ $value = $this->readValue($value);
+ }
+
+ $config[$key] = $value;
+ }
+
+ return $config;
+ }
+
+ /**
+ * Convert php string into value
+ *
+ * @param string $value
+ * @return bool|string
+ */
+ protected function readValue($value) {
+ $removequotes_pattern = '/^(\'|")(.*)(?<!\\\\)\1$/s';
+ $unescape_pairs = array(
+ '\\\\' => '\\',
+ '\\\'' => '\'',
+ '\\"' => '"'
+ );
+
+ if($value == 'true') {
+ $value = true;
+ } elseif($value == 'false') {
+ $value = false;
+ } else {
+ // remove quotes from quoted strings & unescape escaped data
+ $value = preg_replace($removequotes_pattern, '$2', $value);
+ $value = strtr($value, $unescape_pairs);
+ }
+ return $value;
+ }
+
+}
diff --git a/platform/www/lib/plugins/config/core/Configuration.php b/platform/www/lib/plugins/config/core/Configuration.php
new file mode 100644
index 0000000..c58645c
--- /dev/null
+++ b/platform/www/lib/plugins/config/core/Configuration.php
@@ -0,0 +1,219 @@
+<?php
+
+namespace dokuwiki\plugin\config\core;
+
+use dokuwiki\plugin\config\core\Setting\Setting;
+use dokuwiki\plugin\config\core\Setting\SettingNoClass;
+use dokuwiki\plugin\config\core\Setting\SettingNoDefault;
+use dokuwiki\plugin\config\core\Setting\SettingNoKnownClass;
+use dokuwiki\plugin\config\core\Setting\SettingUndefined;
+
+/**
+ * Holds all the current settings and proxies the Loader and Writer
+ *
+ * @author Chris Smith <chris@jalakai.co.uk>
+ * @author Ben Coburn <btcoburn@silicodon.net>
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+class Configuration {
+
+ const KEYMARKER = '____';
+
+ /** @var Setting[] metadata as array of Settings objects */
+ protected $settings = array();
+ /** @var Setting[] undefined and problematic settings */
+ protected $undefined = array();
+
+ /** @var array all metadata */
+ protected $metadata;
+ /** @var array all default settings */
+ protected $default;
+ /** @var array all local settings */
+ protected $local;
+ /** @var array all protected settings */
+ protected $protected;
+
+ /** @var bool have the settings been changed since loading from disk? */
+ protected $changed = false;
+
+ /** @var Loader */
+ protected $loader;
+ /** @var Writer */
+ protected $writer;
+
+ /**
+ * ConfigSettings constructor.
+ */
+ public function __construct() {
+ $this->loader = new Loader(new ConfigParser());
+ $this->writer = new Writer();
+
+ $this->metadata = $this->loader->loadMeta();
+ $this->default = $this->loader->loadDefaults();
+ $this->local = $this->loader->loadLocal();
+ $this->protected = $this->loader->loadProtected();
+
+ $this->initSettings();
+ }
+
+ /**
+ * Get all settings
+ *
+ * @return Setting[]
+ */
+ public function getSettings() {
+ return $this->settings;
+ }
+
+ /**
+ * Get all unknown or problematic settings
+ *
+ * @return Setting[]
+ */
+ public function getUndefined() {
+ return $this->undefined;
+ }
+
+ /**
+ * Have the settings been changed since loading from disk?
+ *
+ * @return bool
+ */
+ public function hasChanged() {
+ return $this->changed;
+ }
+
+ /**
+ * Check if the config can be written
+ *
+ * @return bool
+ */
+ public function isLocked() {
+ return $this->writer->isLocked();
+ }
+
+ /**
+ * Update the settings using the data provided
+ *
+ * @param array $input as posted
+ * @return bool true if all updates went through, false on errors
+ */
+ public function updateSettings($input) {
+ $ok = true;
+
+ foreach($this->settings as $key => $obj) {
+ $value = isset($input[$key]) ? $input[$key] : null;
+ if($obj->update($value)) {
+ $this->changed = true;
+ }
+ if($obj->hasError()) $ok = false;
+ }
+
+ return $ok;
+ }
+
+ /**
+ * Save the settings
+ *
+ * This save the current state as defined in this object, including the
+ * undefined settings
+ *
+ * @throws \Exception
+ */
+ public function save() {
+ // only save the undefined settings that have not been handled in settings
+ $undefined = array_diff_key($this->undefined, $this->settings);
+ $this->writer->save(array_merge($this->settings, $undefined));
+ }
+
+ /**
+ * Touch the settings
+ *
+ * @throws \Exception
+ */
+ public function touch() {
+ $this->writer->touch();
+ }
+
+ /**
+ * Load the extension language strings
+ *
+ * @return array
+ */
+ public function getLangs() {
+ return $this->loader->loadLangs();
+ }
+
+ /**
+ * Initalizes the $settings and $undefined properties
+ */
+ protected function initSettings() {
+ $keys = array_merge(
+ array_keys($this->metadata),
+ array_keys($this->default),
+ array_keys($this->local),
+ array_keys($this->protected)
+ );
+ $keys = array_unique($keys);
+
+ foreach($keys as $key) {
+ $obj = $this->instantiateClass($key);
+
+ if($obj->shouldHaveDefault() && !isset($this->default[$key])) {
+ $this->undefined[$key] = new SettingNoDefault($key);
+ }
+
+ $d = isset($this->default[$key]) ? $this->default[$key] : null;
+ $l = isset($this->local[$key]) ? $this->local[$key] : null;
+ $p = isset($this->protected[$key]) ? $this->protected[$key] : null;
+
+ $obj->initialize($d, $l, $p);
+ }
+ }
+
+ /**
+ * Instantiates the proper class for the given config key
+ *
+ * The class is added to the $settings or $undefined arrays and returned
+ *
+ * @param string $key
+ * @return Setting
+ */
+ protected function instantiateClass($key) {
+ if(isset($this->metadata[$key])) {
+ $param = $this->metadata[$key];
+ $class = $this->determineClassName(array_shift($param), $key); // first param is class
+ $obj = new $class($key, $param);
+ $this->settings[$key] = $obj;
+ } else {
+ $obj = new SettingUndefined($key);
+ $this->undefined[$key] = $obj;
+ }
+ return $obj;
+ }
+
+ /**
+ * Return the class to load
+ *
+ * @param string $class the class name as given in the meta file
+ * @param string $key the settings key
+ * @return string
+ */
+ protected function determineClassName($class, $key) {
+ // try namespaced class first
+ if(is_string($class)) {
+ $modern = str_replace('_', '', ucwords($class, '_'));
+ $modern = '\\dokuwiki\\plugin\\config\\core\\Setting\\Setting' . $modern;
+ if($modern && class_exists($modern)) return $modern;
+ // try class as given
+ if(class_exists($class)) return $class;
+ // class wasn't found add to errors
+ $this->undefined[$key] = new SettingNoKnownClass($key);
+ } else {
+ // no class given, add to errors
+ $this->undefined[$key] = new SettingNoClass($key);
+ }
+ return '\\dokuwiki\\plugin\\config\\core\\Setting\\Setting';
+ }
+
+}
diff --git a/platform/www/lib/plugins/config/core/Loader.php b/platform/www/lib/plugins/config/core/Loader.php
new file mode 100644
index 0000000..90ad0f5
--- /dev/null
+++ b/platform/www/lib/plugins/config/core/Loader.php
@@ -0,0 +1,269 @@
+<?php
+
+namespace dokuwiki\plugin\config\core;
+
+use dokuwiki\Extension\Event;
+
+/**
+ * Configuration loader
+ *
+ * Loads configuration meta data and settings from the various files. Honors the
+ * configuration cascade and installed plugins.
+ */
+class Loader {
+ /** @var ConfigParser */
+ protected $parser;
+
+ /** @var string[] list of enabled plugins */
+ protected $plugins;
+ /** @var string current template */
+ protected $template;
+
+ /**
+ * Loader constructor.
+ * @param ConfigParser $parser
+ * @triggers PLUGIN_CONFIG_PLUGINLIST
+ */
+ public function __construct(ConfigParser $parser) {
+ global $conf;
+ $this->parser = $parser;
+ $this->plugins = plugin_list();
+ $this->template = $conf['template'];
+ // allow plugins to remove configurable plugins
+ Event::createAndTrigger('PLUGIN_CONFIG_PLUGINLIST', $this->plugins);
+ }
+
+ /**
+ * Read the settings meta data
+ *
+ * Reads the main file, plugins and template settings meta data
+ *
+ * @return array
+ */
+ public function loadMeta() {
+ // load main file
+ $meta = array();
+ include DOKU_PLUGIN . 'config/settings/config.metadata.php';
+
+ // plugins
+ foreach($this->plugins as $plugin) {
+ $meta = array_merge(
+ $meta,
+ $this->loadExtensionMeta(
+ DOKU_PLUGIN . $plugin . '/conf/metadata.php',
+ 'plugin',
+ $plugin
+ )
+ );
+ }
+
+ // current template
+ $meta = array_merge(
+ $meta,
+ $this->loadExtensionMeta(
+ tpl_incdir() . '/conf/metadata.php',
+ 'tpl',
+ $this->template
+ )
+ );
+
+ return $meta;
+ }
+
+ /**
+ * Read the default values
+ *
+ * Reads the main file, plugins and template defaults
+ *
+ * @return array
+ */
+ public function loadDefaults() {
+ // load main files
+ global $config_cascade;
+ $conf = $this->loadConfigs($config_cascade['main']['default']);
+
+ // plugins
+ foreach($this->plugins as $plugin) {
+ $conf = array_merge(
+ $conf,
+ $this->loadExtensionConf(
+ DOKU_PLUGIN . $plugin . '/conf/default.php',
+ 'plugin',
+ $plugin
+ )
+ );
+ }
+
+ // current template
+ $conf = array_merge(
+ $conf,
+ $this->loadExtensionConf(
+ tpl_incdir() . '/conf/default.php',
+ 'tpl',
+ $this->template
+ )
+ );
+
+ return $conf;
+ }
+
+ /**
+ * Reads the language strings
+ *
+ * Only reads extensions, main one is loaded the usual way
+ *
+ * @return array
+ */
+ public function loadLangs() {
+ $lang = array();
+
+ // plugins
+ foreach($this->plugins as $plugin) {
+ $lang = array_merge(
+ $lang,
+ $this->loadExtensionLang(
+ DOKU_PLUGIN . $plugin . '/',
+ 'plugin',
+ $plugin
+ )
+ );
+ }
+
+ // current template
+ $lang = array_merge(
+ $lang,
+ $this->loadExtensionLang(
+ tpl_incdir() . '/',
+ 'tpl',
+ $this->template
+ )
+ );
+
+ return $lang;
+ }
+
+ /**
+ * Read the local settings
+ *
+ * @return array
+ */
+ public function loadLocal() {
+ global $config_cascade;
+ return $this->loadConfigs($config_cascade['main']['local']);
+ }
+
+ /**
+ * Read the protected settings
+ *
+ * @return array
+ */
+ public function loadProtected() {
+ global $config_cascade;
+ return $this->loadConfigs($config_cascade['main']['protected']);
+ }
+
+ /**
+ * Read the config values from the given files
+ *
+ * @param string[] $files paths to config php's
+ * @return array
+ */
+ protected function loadConfigs($files) {
+ $conf = array();
+ foreach($files as $file) {
+ $conf = array_merge($conf, $this->parser->parse($file));
+ }
+ return $conf;
+ }
+
+ /**
+ * Read settings file from an extension
+ *
+ * This is used to read the settings.php files of plugins and templates
+ *
+ * @param string $file php file to read
+ * @param string $type should be 'plugin' or 'tpl'
+ * @param string $extname name of the extension
+ * @return array
+ */
+ protected function loadExtensionMeta($file, $type, $extname) {
+ if(!file_exists($file)) return array();
+ $prefix = $type . Configuration::KEYMARKER . $extname . Configuration::KEYMARKER;
+
+ // include file
+ $meta = array();
+ include $file;
+ if(empty($meta)) return array();
+
+ // read data
+ $data = array();
+ $data[$prefix . $type . '_settings_name'] = ['fieldset'];
+ foreach($meta as $key => $value) {
+ if($value[0] == 'fieldset') continue; //plugins only get one fieldset
+ $data[$prefix . $key] = $value;
+ }
+
+ return $data;
+ }
+
+ /**
+ * Read a default file from an extension
+ *
+ * This is used to read the default.php files of plugins and templates
+ *
+ * @param string $file php file to read
+ * @param string $type should be 'plugin' or 'tpl'
+ * @param string $extname name of the extension
+ * @return array
+ */
+ protected function loadExtensionConf($file, $type, $extname) {
+ if(!file_exists($file)) return array();
+ $prefix = $type . Configuration::KEYMARKER . $extname . Configuration::KEYMARKER;
+
+ // parse file
+ $conf = $this->parser->parse($file);
+ if(empty($conf)) return array();
+
+ // read data
+ $data = array();
+ foreach($conf as $key => $value) {
+ $data[$prefix . $key] = $value;
+ }
+
+ return $data;
+ }
+
+ /**
+ * Read the language file of an extension
+ *
+ * @param string $dir directory of the extension
+ * @param string $type should be 'plugin' or 'tpl'
+ * @param string $extname name of the extension
+ * @return array
+ */
+ protected function loadExtensionLang($dir, $type, $extname) {
+ global $conf;
+ $ll = $conf['lang'];
+ $prefix = $type . Configuration::KEYMARKER . $extname . Configuration::KEYMARKER;
+
+ // include files
+ $lang = array();
+ if(file_exists($dir . 'lang/en/settings.php')) {
+ include $dir . 'lang/en/settings.php';
+ }
+ if($ll != 'en' && file_exists($dir . 'lang/' . $ll . '/settings.php')) {
+ include $dir . 'lang/' . $ll . '/settings.php';
+ }
+
+ // set up correct keys
+ $strings = array();
+ foreach($lang as $key => $val) {
+ $strings[$prefix . $key] = $val;
+ }
+
+ // add fieldset key
+ $strings[$prefix . $type . '_settings_name'] = ucwords(str_replace('_', ' ', $extname));
+
+ return $strings;
+ }
+}
diff --git a/platform/www/lib/plugins/config/core/Setting/Setting.php b/platform/www/lib/plugins/config/core/Setting/Setting.php
new file mode 100644
index 0000000..d64f684
--- /dev/null
+++ b/platform/www/lib/plugins/config/core/Setting/Setting.php
@@ -0,0 +1,294 @@
+<?php
+
+namespace dokuwiki\plugin\config\core\Setting;
+
+use dokuwiki\plugin\config\core\Configuration;
+
+/**
+ * Class Setting
+ */
+class Setting {
+ /** @var string unique identifier of this setting */
+ protected $key = '';
+
+ /** @var mixed the default value of this setting */
+ protected $default = null;
+ /** @var mixed the local value of this setting */
+ protected $local = null;
+ /** @var mixed the protected value of this setting */
+ protected $protected = null;
+
+ /** @var array valid alerts, images matching the alerts are in the plugin's images directory */
+ static protected $validCautions = array('warning', 'danger', 'security');
+
+ protected $pattern = '';
+ protected $error = false; // only used by those classes which error check
+ protected $input = null; // only used by those classes which error check
+ protected $caution = null; // used by any setting to provide an alert along with the setting
+
+ /**
+ * Constructor.
+ *
+ * The given parameters will be set up as class properties
+ *
+ * @see initialize() to set the actual value of the setting
+ *
+ * @param string $key
+ * @param array|null $params array with metadata of setting
+ */
+ public function __construct($key, $params = null) {
+ $this->key = $key;
+
+ if(is_array($params)) {
+ foreach($params as $property => $value) {
+ $property = trim($property, '_'); // we don't use underscores anymore
+ $this->$property = $value;
+ }
+ }
+ }
+
+ /**
+ * Set the current values for the setting $key
+ *
+ * This is used to initialize the setting with the data read form the config files.
+ *
+ * @see update() to set a new value
+ * @param mixed $default default setting value
+ * @param mixed $local local setting value
+ * @param mixed $protected protected setting value
+ */
+ public function initialize($default = null, $local = null, $protected = null) {
+ $this->default = $this->cleanValue($default);
+ $this->local = $this->cleanValue($local);
+ $this->protected = $this->cleanValue($protected);
+ }
+
+ /**
+ * update changed setting with validated user provided value $input
+ * - if changed value fails validation check, save it to $this->input (to allow echoing later)
+ * - if changed value passes validation check, set $this->local to the new value
+ *
+ * @param mixed $input the new value
+ * @return boolean true if changed, false otherwise
+ */
+ public function update($input) {
+ if(is_null($input)) return false;
+ if($this->isProtected()) return false;
+ $input = $this->cleanValue($input);
+
+ $value = is_null($this->local) ? $this->default : $this->local;
+ if($value == $input) return false;
+
+ // validate new value
+ if($this->pattern && !preg_match($this->pattern, $input)) {
+ $this->error = true;
+ $this->input = $input;
+ return false;
+ }
+
+ // update local copy of this setting with new value
+ $this->local = $input;
+
+ // setting ready for update
+ return true;
+ }
+
+ /**
+ * Clean a value read from a config before using it internally
+ *
+ * Default implementation returns $value as is. Subclasses can override.
+ * Note: null should always be returned as null!
+ *
+ * This is applied in initialize() and update()
+ *
+ * @param mixed $value
+ * @return mixed
+ */
+ protected function cleanValue($value) {
+ return $value;
+ }
+
+ /**
+ * Should this type of config have a default?
+ *
+ * @return bool
+ */
+ public function shouldHaveDefault() {
+ return true;
+ }
+
+ /**
+ * Get this setting's unique key
+ *
+ * @return string
+ */
+ public function getKey() {
+ return $this->key;
+ }
+
+ /**
+ * Get the key of this setting marked up human readable
+ *
+ * @param bool $url link to dokuwiki.org manual?
+ * @return string
+ */
+ public function getPrettyKey($url = true) {
+ $out = str_replace(Configuration::KEYMARKER, "»", $this->key);
+ if($url && !strstr($out, '»')) {//provide no urls for plugins, etc.
+ if($out == 'start') {
+ // exception, because this config name is clashing with our actual start page
+ return '<a href="http://www.dokuwiki.org/config:startpage">' . $out . '</a>';
+ } else {
+ return '<a href="http://www.dokuwiki.org/config:' . $out . '">' . $out . '</a>';
+ }
+ }
+ return $out;
+ }
+
+ /**
+ * Returns setting key as an array key separator
+ *
+ * This is used to create form output
+ *
+ * @return string key
+ */
+ public function getArrayKey() {
+ return str_replace(Configuration::KEYMARKER, "']['", $this->key);
+ }
+
+ /**
+ * What type of configuration is this
+ *
+ * Returns one of
+ *
+ * 'plugin' for plugin configuration
+ * 'template' for template configuration
+ * 'dokuwiki' for core configuration
+ *
+ * @return string
+ */
+ public function getType() {
+ if(substr($this->getKey(), 0, 10) == 'plugin' . Configuration::KEYMARKER) {
+ return 'plugin';
+ } else if(substr($this->getKey(), 0, 7) == 'tpl' . Configuration::KEYMARKER) {
+ return 'template';
+ } else {
+ return 'dokuwiki';
+ }
+ }
+
+ /**
+ * Build html for label and input of setting
+ *
+ * @param \admin_plugin_config $plugin object of config plugin
+ * @param bool $echo true: show inputted value, when error occurred, otherwise the stored setting
+ * @return string[] with content array(string $label_html, string $input_html)
+ */
+ public function html(\admin_plugin_config $plugin, $echo = false) {
+ $disable = '';
+
+ if($this->isProtected()) {
+ $value = $this->protected;
+ $disable = 'disabled="disabled"';
+ } else {
+ if($echo && $this->error) {
+ $value = $this->input;
+ } else {
+ $value = is_null($this->local) ? $this->default : $this->local;
+ }
+ }
+
+ $key = htmlspecialchars($this->key);
+ $value = formText($value);
+
+ $label = '<label for="config___' . $key . '">' . $this->prompt($plugin) . '</label>';
+ $input = '<textarea rows="3" cols="40" id="config___' . $key .
+ '" name="config[' . $key . ']" class="edit" ' . $disable . '>' . $value . '</textarea>';
+ return array($label, $input);
+ }
+
+ /**
+ * Should the current local value be saved?
+ *
+ * @see out() to run when this returns true
+ * @return bool
+ */
+ public function shouldBeSaved() {
+ if($this->isProtected()) return false;
+ if($this->local === null) return false;
+ if($this->default == $this->local) return false;
+ return true;
+ }
+
+ /**
+ * Generate string to save local setting value to file according to $fmt
+ *
+ * @see shouldBeSaved() to check if this should be called
+ * @param string $var name of variable
+ * @param string $fmt save format
+ * @return string
+ */
+ public function out($var, $fmt = 'php') {
+ if($fmt != 'php') return '';
+
+ $tr = array("\\" => '\\\\', "'" => '\\\''); // escape the value
+ $out = '$' . $var . "['" . $this->getArrayKey() . "'] = '" . strtr(cleanText($this->local), $tr) . "';\n";
+
+ return $out;
+ }
+
+ /**
+ * Returns the localized prompt
+ *
+ * @param \admin_plugin_config $plugin object of config plugin
+ * @return string text
+ */
+ public function prompt(\admin_plugin_config $plugin) {
+ $prompt = $plugin->getLang($this->key);
+ if(!$prompt) $prompt = htmlspecialchars(str_replace(array('____', '_'), ' ', $this->key));
+ return $prompt;
+ }
+
+ /**
+ * Is setting protected
+ *
+ * @return bool
+ */
+ public function isProtected() {
+ return !is_null($this->protected);
+ }
+
+ /**
+ * Is setting the default?
+ *
+ * @return bool
+ */
+ public function isDefault() {
+ return !$this->isProtected() && is_null($this->local);
+ }
+
+ /**
+ * Has an error?
+ *
+ * @return bool
+ */
+ public function hasError() {
+ return $this->error;
+ }
+
+ /**
+ * Returns caution
+ *
+ * @return false|string caution string, otherwise false for invalid caution
+ */
+ public function caution() {
+ if(empty($this->caution)) return false;
+ if(!in_array($this->caution, Setting::$validCautions)) {
+ throw new \RuntimeException(
+ 'Invalid caution string (' . $this->caution . ') in metadata for setting "' . $this->key . '"'
+ );
+ }
+ return $this->caution;
+ }
+
+}
diff --git a/platform/www/lib/plugins/config/core/Setting/SettingArray.php b/platform/www/lib/plugins/config/core/Setting/SettingArray.php
new file mode 100644
index 0000000..c48dc76
--- /dev/null
+++ b/platform/www/lib/plugins/config/core/Setting/SettingArray.php
@@ -0,0 +1,105 @@
+<?php
+
+namespace dokuwiki\plugin\config\core\Setting;
+
+/**
+ * Class setting_array
+ */
+class SettingArray extends Setting {
+
+ /**
+ * Create an array from a string
+ *
+ * @param string $string
+ * @return array
+ */
+ protected function fromString($string) {
+ $array = explode(',', $string);
+ $array = array_map('trim', $array);
+ $array = array_filter($array);
+ $array = array_unique($array);
+ return $array;
+ }
+
+ /**
+ * Create a string from an array
+ *
+ * @param array $array
+ * @return string
+ */
+ protected function fromArray($array) {
+ return join(', ', (array) $array);
+ }
+
+ /**
+ * update setting with user provided value $input
+ * if value fails error check, save it
+ *
+ * @param string $input
+ * @return bool true if changed, false otherwise (incl. on error)
+ */
+ public function update($input) {
+ if(is_null($input)) return false;
+ if($this->isProtected()) return false;
+
+ $input = $this->fromString($input);
+
+ $value = is_null($this->local) ? $this->default : $this->local;
+ if($value == $input) return false;
+
+ foreach($input as $item) {
+ if($this->pattern && !preg_match($this->pattern, $item)) {
+ $this->error = true;
+ $this->input = $input;
+ return false;
+ }
+ }
+
+ $this->local = $input;
+ return true;
+ }
+
+ /**
+ * Escaping
+ *
+ * @param string $string
+ * @return string
+ */
+ protected function escape($string) {
+ $tr = array("\\" => '\\\\', "'" => '\\\'');
+ return "'" . strtr(cleanText($string), $tr) . "'";
+ }
+
+ /** @inheritdoc */
+ public function out($var, $fmt = 'php') {
+ if($fmt != 'php') return '';
+
+ $vals = array_map(array($this, 'escape'), $this->local);
+ $out = '$' . $var . "['" . $this->getArrayKey() . "'] = array(" . join(', ', $vals) . ");\n";
+ return $out;
+ }
+
+ /** @inheritdoc */
+ public function html(\admin_plugin_config $plugin, $echo = false) {
+ $disable = '';
+
+ if($this->isProtected()) {
+ $value = $this->protected;
+ $disable = 'disabled="disabled"';
+ } else {
+ if($echo && $this->error) {
+ $value = $this->input;
+ } else {
+ $value = is_null($this->local) ? $this->default : $this->local;
+ }
+ }
+
+ $key = htmlspecialchars($this->key);
+ $value = htmlspecialchars($this->fromArray($value));
+
+ $label = '<label for="config___' . $key . '">' . $this->prompt($plugin) . '</label>';
+ $input = '<input id="config___' . $key . '" name="config[' . $key .
+ ']" type="text" class="edit" value="' . $value . '" ' . $disable . '/>';
+ return array($label, $input);
+ }
+}
diff --git a/platform/www/lib/plugins/config/core/Setting/SettingAuthtype.php b/platform/www/lib/plugins/config/core/Setting/SettingAuthtype.php
new file mode 100644
index 0000000..3a6df6f
--- /dev/null
+++ b/platform/www/lib/plugins/config/core/Setting/SettingAuthtype.php
@@ -0,0 +1,60 @@
+<?php
+
+namespace dokuwiki\plugin\config\core\Setting;
+
+/**
+ * Class setting_authtype
+ */
+class SettingAuthtype extends SettingMultichoice {
+
+ /** @inheritdoc */
+ public function initialize($default = null, $local = null, $protected = null) {
+ /** @var $plugin_controller \dokuwiki\Extension\PluginController */
+ global $plugin_controller;
+
+ // retrieve auth types provided by plugins
+ foreach($plugin_controller->getList('auth') as $plugin) {
+ $this->choices[] = $plugin;
+ }
+
+ parent::initialize($default, $local, $protected);
+ }
+
+ /** @inheritdoc */
+ public function update($input) {
+ /** @var $plugin_controller \dokuwiki\Extension\PluginController */
+ global $plugin_controller;
+
+ // is an update possible/requested?
+ $local = $this->local; // save this, parent::update() may change it
+ if(!parent::update($input)) return false; // nothing changed or an error caught by parent
+ $this->local = $local; // restore original, more error checking to come
+
+ // attempt to load the plugin
+ $auth_plugin = $plugin_controller->load('auth', $input);
+
+ // @TODO: throw an error in plugin controller instead of returning null
+ if(is_null($auth_plugin)) {
+ $this->error = true;
+ msg('Cannot load Auth Plugin "' . $input . '"', -1);
+ return false;
+ }
+
+ // verify proper instantiation (is this really a plugin?) @TODO use instanceof? implement interface?
+ if(is_object($auth_plugin) && !method_exists($auth_plugin, 'getPluginName')) {
+ $this->error = true;
+ msg('Cannot create Auth Plugin "' . $input . '"', -1);
+ return false;
+ }
+
+ // did we change the auth type? logout
+ global $conf;
+ if($conf['authtype'] != $input) {
+ msg('Authentication system changed. Please re-login.');
+ auth_logoff();
+ }
+
+ $this->local = $input;
+ return true;
+ }
+}
diff --git a/platform/www/lib/plugins/config/core/Setting/SettingCompression.php b/platform/www/lib/plugins/config/core/Setting/SettingCompression.php
new file mode 100644
index 0000000..f97d828
--- /dev/null
+++ b/platform/www/lib/plugins/config/core/Setting/SettingCompression.php
@@ -0,0 +1,21 @@
+<?php
+
+namespace dokuwiki\plugin\config\core\Setting;
+
+/**
+ * Class setting_compression
+ */
+class SettingCompression extends SettingMultichoice {
+
+ protected $choices = array('0'); // 0 = no compression, always supported
+
+ /** @inheritdoc */
+ public function initialize($default = null, $local = null, $protected = null) {
+
+ // populate _choices with the compression methods supported by this php installation
+ if(function_exists('gzopen')) $this->choices[] = 'gz';
+ if(function_exists('bzopen')) $this->choices[] = 'bz2';
+
+ parent::initialize($default, $local, $protected);
+ }
+}
diff --git a/platform/www/lib/plugins/config/core/Setting/SettingDirchoice.php b/platform/www/lib/plugins/config/core/Setting/SettingDirchoice.php
new file mode 100644
index 0000000..dfb27f5
--- /dev/null
+++ b/platform/www/lib/plugins/config/core/Setting/SettingDirchoice.php
@@ -0,0 +1,33 @@
+<?php
+
+namespace dokuwiki\plugin\config\core\Setting;
+
+/**
+ * Class setting_dirchoice
+ */
+class SettingDirchoice extends SettingMultichoice {
+
+ protected $dir = '';
+
+ /** @inheritdoc */
+ public function initialize($default = null, $local = null, $protected = null) {
+
+ // populate $this->_choices with a list of directories
+ $list = array();
+
+ if($dh = @opendir($this->dir)) {
+ while(false !== ($entry = readdir($dh))) {
+ if($entry == '.' || $entry == '..') continue;
+ if($this->pattern && !preg_match($this->pattern, $entry)) continue;
+
+ $file = (is_link($this->dir . $entry)) ? readlink($this->dir . $entry) : $this->dir . $entry;
+ if(is_dir($file)) $list[] = $entry;
+ }
+ closedir($dh);
+ }
+ sort($list);
+ $this->choices = $list;
+
+ parent::initialize($default, $local, $protected);
+ }
+}
diff --git a/platform/www/lib/plugins/config/core/Setting/SettingDisableactions.php b/platform/www/lib/plugins/config/core/Setting/SettingDisableactions.php
new file mode 100644
index 0000000..2553175
--- /dev/null
+++ b/platform/www/lib/plugins/config/core/Setting/SettingDisableactions.php
@@ -0,0 +1,23 @@
+<?php
+
+namespace dokuwiki\plugin\config\core\Setting;
+
+/**
+ * Class setting_disableactions
+ */
+class SettingDisableactions extends SettingMulticheckbox {
+
+ /** @inheritdoc */
+ public function html(\admin_plugin_config $plugin, $echo = false) {
+ global $lang;
+
+ // make some language adjustments (there must be a better way)
+ // transfer some DokuWiki language strings to the plugin
+ $plugin->addLang($this->key . '_revisions', $lang['btn_revs']);
+ foreach($this->choices as $choice) {
+ if(isset($lang['btn_' . $choice])) $plugin->addLang($this->key . '_' . $choice, $lang['btn_' . $choice]);
+ }
+
+ return parent::html($plugin, $echo);
+ }
+}
diff --git a/platform/www/lib/plugins/config/core/Setting/SettingEmail.php b/platform/www/lib/plugins/config/core/Setting/SettingEmail.php
new file mode 100644
index 0000000..25a0c0e
--- /dev/null
+++ b/platform/www/lib/plugins/config/core/Setting/SettingEmail.php
@@ -0,0 +1,58 @@
+<?php
+
+namespace dokuwiki\plugin\config\core\Setting;
+
+/**
+ * Class setting_email
+ */
+class SettingEmail extends SettingString {
+ protected $multiple = false;
+ protected $placeholders = false;
+
+ /** @inheritdoc */
+ public function update($input) {
+ if(is_null($input)) return false;
+ if($this->isProtected()) return false;
+
+ $value = is_null($this->local) ? $this->default : $this->local;
+ if($value == $input) return false;
+ if($input === '') {
+ $this->local = $input;
+ return true;
+ }
+ $mail = $input;
+
+ if($this->placeholders) {
+ // replace variables with pseudo values
+ $mail = str_replace('@USER@', 'joe', $mail);
+ $mail = str_replace('@NAME@', 'Joe Schmoe', $mail);
+ $mail = str_replace('@MAIL@', 'joe@example.com', $mail);
+ }
+
+ // multiple mail addresses?
+ if($this->multiple) {
+ $mails = array_filter(array_map('trim', explode(',', $mail)));
+ } else {
+ $mails = array($mail);
+ }
+
+ // check them all
+ foreach($mails as $mail) {
+ // only check the address part
+ if(preg_match('#(.*?)<(.*?)>#', $mail, $matches)) {
+ $addr = $matches[2];
+ } else {
+ $addr = $mail;
+ }
+
+ if(!mail_isvalid($addr)) {
+ $this->error = true;
+ $this->input = $input;
+ return false;
+ }
+ }
+
+ $this->local = $input;
+ return true;
+ }
+}
diff --git a/platform/www/lib/plugins/config/core/Setting/SettingFieldset.php b/platform/www/lib/plugins/config/core/Setting/SettingFieldset.php
new file mode 100644
index 0000000..4e86189
--- /dev/null
+++ b/platform/www/lib/plugins/config/core/Setting/SettingFieldset.php
@@ -0,0 +1,17 @@
+<?php
+
+namespace dokuwiki\plugin\config\core\Setting;
+
+/**
+ * A do-nothing class used to detect the 'fieldset' type.
+ *
+ * Used to start a new settings "display-group".
+ */
+class SettingFieldset extends Setting {
+
+ /** @inheritdoc */
+ public function shouldHaveDefault() {
+ return false;
+ }
+
+}
diff --git a/platform/www/lib/plugins/config/core/Setting/SettingHidden.php b/platform/www/lib/plugins/config/core/Setting/SettingHidden.php
new file mode 100644
index 0000000..ca8a03e
--- /dev/null
+++ b/platform/www/lib/plugins/config/core/Setting/SettingHidden.php
@@ -0,0 +1,10 @@
+<?php
+
+namespace dokuwiki\plugin\config\core\Setting;
+
+/**
+ * Class setting_hidden
+ */
+class SettingHidden extends Setting {
+ // Used to explicitly ignore a setting in the configuration manager.
+}
diff --git a/platform/www/lib/plugins/config/core/Setting/SettingImConvert.php b/platform/www/lib/plugins/config/core/Setting/SettingImConvert.php
new file mode 100644
index 0000000..8740d94
--- /dev/null
+++ b/platform/www/lib/plugins/config/core/Setting/SettingImConvert.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace dokuwiki\plugin\config\core\Setting;
+
+/**
+ * Class setting_im_convert
+ */
+class SettingImConvert extends SettingString {
+
+ /** @inheritdoc */
+ public function update($input) {
+ if($this->isProtected()) return false;
+
+ $input = trim($input);
+
+ $value = is_null($this->local) ? $this->default : $this->local;
+ if($value == $input) return false;
+
+ if($input && !file_exists($input)) {
+ $this->error = true;
+ $this->input = $input;
+ return false;
+ }
+
+ $this->local = $input;
+ return true;
+ }
+}
diff --git a/platform/www/lib/plugins/config/core/Setting/SettingLicense.php b/platform/www/lib/plugins/config/core/Setting/SettingLicense.php
new file mode 100644
index 0000000..8dacf8e
--- /dev/null
+++ b/platform/www/lib/plugins/config/core/Setting/SettingLicense.php
@@ -0,0 +1,23 @@
+<?php
+
+namespace dokuwiki\plugin\config\core\Setting;
+
+/**
+ * Class setting_license
+ */
+class SettingLicense extends SettingMultichoice {
+
+ protected $choices = array(''); // none choosen
+
+ /** @inheritdoc */
+ public function initialize($default = null, $local = null, $protected = null) {
+ global $license;
+
+ foreach($license as $key => $data) {
+ $this->choices[] = $key;
+ $this->lang[$this->key . '_o_' . $key] = $data['name']; // stored in setting
+ }
+
+ parent::initialize($default, $local, $protected);
+ }
+}
diff --git a/platform/www/lib/plugins/config/core/Setting/SettingMulticheckbox.php b/platform/www/lib/plugins/config/core/Setting/SettingMulticheckbox.php
new file mode 100644
index 0000000..df212cc
--- /dev/null
+++ b/platform/www/lib/plugins/config/core/Setting/SettingMulticheckbox.php
@@ -0,0 +1,163 @@
+<?php
+
+namespace dokuwiki\plugin\config\core\Setting;
+
+/**
+ * Class setting_multicheckbox
+ */
+class SettingMulticheckbox extends SettingString {
+
+ protected $choices = array();
+ protected $combine = array();
+ protected $other = 'always';
+
+ /** @inheritdoc */
+ public function update($input) {
+ if($this->isProtected()) return false;
+
+ // split any combined values + convert from array to comma separated string
+ $input = ($input) ? $input : array();
+ $input = $this->array2str($input);
+
+ $value = is_null($this->local) ? $this->default : $this->local;
+ if($value == $input) return false;
+
+ if($this->pattern && !preg_match($this->pattern, $input)) {
+ $this->error = true;
+ $this->input = $input;
+ return false;
+ }
+
+ $this->local = $input;
+ return true;
+ }
+
+ /** @inheritdoc */
+ public function html(\admin_plugin_config $plugin, $echo = false) {
+
+ $disable = '';
+
+ if($this->isProtected()) {
+ $value = $this->protected;
+ $disable = 'disabled="disabled"';
+ } else {
+ if($echo && $this->error) {
+ $value = $this->input;
+ } else {
+ $value = is_null($this->local) ? $this->default : $this->local;
+ }
+ }
+
+ $key = htmlspecialchars($this->key);
+
+ // convert from comma separated list into array + combine complimentary actions
+ $value = $this->str2array($value);
+ $default = $this->str2array($this->default);
+
+ $input = '';
+ foreach($this->choices as $choice) {
+ $idx = array_search($choice, $value);
+ $idx_default = array_search($choice, $default);
+
+ $checked = ($idx !== false) ? 'checked="checked"' : '';
+
+ // @todo ideally this would be handled using a second class of "default"
+ $class = (($idx !== false) == (false !== $idx_default)) ? " selectiondefault" : "";
+
+ $prompt = ($plugin->getLang($this->key . '_' . $choice) ?
+ $plugin->getLang($this->key . '_' . $choice) : htmlspecialchars($choice));
+
+ $input .= '<div class="selection' . $class . '">' . "\n";
+ $input .= '<label for="config___' . $key . '_' . $choice . '">' . $prompt . "</label>\n";
+ $input .= '<input id="config___' . $key . '_' . $choice . '" name="config[' . $key .
+ '][]" type="checkbox" class="checkbox" value="' . $choice . '" ' . $disable . ' ' . $checked . "/>\n";
+ $input .= "</div>\n";
+
+ // remove this action from the disabledactions array
+ if($idx !== false) unset($value[$idx]);
+ if($idx_default !== false) unset($default[$idx_default]);
+ }
+
+ // handle any remaining values
+ if($this->other != 'never') {
+ $other = join(',', $value);
+ // test equivalent to ($this->_other == 'always' || ($other && $this->_other == 'exists')
+ // use != 'exists' rather than == 'always' to ensure invalid values default to 'always'
+ if($this->other != 'exists' || $other) {
+
+ $class = (
+ (count($default) == count($value)) &&
+ (count($value) == count(array_intersect($value, $default)))
+ ) ?
+ " selectiondefault" : "";
+
+ $input .= '<div class="other' . $class . '">' . "\n";
+ $input .= '<label for="config___' . $key . '_other">' .
+ $plugin->getLang($key . '_other') .
+ "</label>\n";
+ $input .= '<input id="config___' . $key . '_other" name="config[' . $key .
+ '][other]" type="text" class="edit" value="' . htmlspecialchars($other) .
+ '" ' . $disable . " />\n";
+ $input .= "</div>\n";
+ }
+ }
+ $label = '<label>' . $this->prompt($plugin) . '</label>';
+ return array($label, $input);
+ }
+
+ /**
+ * convert comma separated list to an array and combine any complimentary values
+ *
+ * @param string $str
+ * @return array
+ */
+ protected function str2array($str) {
+ $array = explode(',', $str);
+
+ if(!empty($this->combine)) {
+ foreach($this->combine as $key => $combinators) {
+ $idx = array();
+ foreach($combinators as $val) {
+ if(($idx[] = array_search($val, $array)) === false) break;
+ }
+
+ if(count($idx) && $idx[count($idx) - 1] !== false) {
+ foreach($idx as $i) unset($array[$i]);
+ $array[] = $key;
+ }
+ }
+ }
+
+ return $array;
+ }
+
+ /**
+ * convert array of values + other back to a comma separated list, incl. splitting any combined values
+ *
+ * @param array $input
+ * @return string
+ */
+ protected function array2str($input) {
+
+ // handle other
+ $other = trim($input['other']);
+ $other = !empty($other) ? explode(',', str_replace(' ', '', $input['other'])) : array();
+ unset($input['other']);
+
+ $array = array_unique(array_merge($input, $other));
+
+ // deconstruct any combinations
+ if(!empty($this->combine)) {
+ foreach($this->combine as $key => $combinators) {
+
+ $idx = array_search($key, $array);
+ if($idx !== false) {
+ unset($array[$idx]);
+ $array = array_merge($array, $combinators);
+ }
+ }
+ }
+
+ return join(',', array_unique($array));
+ }
+}
diff --git a/platform/www/lib/plugins/config/core/Setting/SettingMultichoice.php b/platform/www/lib/plugins/config/core/Setting/SettingMultichoice.php
new file mode 100644
index 0000000..3a50857
--- /dev/null
+++ b/platform/www/lib/plugins/config/core/Setting/SettingMultichoice.php
@@ -0,0 +1,71 @@
+<?php
+
+namespace dokuwiki\plugin\config\core\Setting;
+
+/**
+ * Class setting_multichoice
+ */
+class SettingMultichoice extends SettingString {
+ protected $choices = array();
+ public $lang; //some custom language strings are stored in setting
+
+ /** @inheritdoc */
+ public function html(\admin_plugin_config $plugin, $echo = false) {
+ $disable = '';
+ $nochoice = '';
+
+ if($this->isProtected()) {
+ $value = $this->protected;
+ $disable = ' disabled="disabled"';
+ } else {
+ $value = is_null($this->local) ? $this->default : $this->local;
+ }
+
+ // ensure current value is included
+ if(!in_array($value, $this->choices)) {
+ $this->choices[] = $value;
+ }
+ // disable if no other choices
+ if(!$this->isProtected() && count($this->choices) <= 1) {
+ $disable = ' disabled="disabled"';
+ $nochoice = $plugin->getLang('nochoice');
+ }
+
+ $key = htmlspecialchars($this->key);
+
+ $label = '<label for="config___' . $key . '">' . $this->prompt($plugin) . '</label>';
+
+ $input = "<div class=\"input\">\n";
+ $input .= '<select class="edit" id="config___' . $key . '" name="config[' . $key . ']"' . $disable . '>' . "\n";
+ foreach($this->choices as $choice) {
+ $selected = ($value == $choice) ? ' selected="selected"' : '';
+ $option = $plugin->getLang($this->key . '_o_' . $choice);
+ if(!$option && isset($this->lang[$this->key . '_o_' . $choice])) {
+ $option = $this->lang[$this->key . '_o_' . $choice];
+ }
+ if(!$option) $option = $choice;
+
+ $choice = htmlspecialchars($choice);
+ $option = htmlspecialchars($option);
+ $input .= ' <option value="' . $choice . '"' . $selected . ' >' . $option . '</option>' . "\n";
+ }
+ $input .= "</select> $nochoice \n";
+ $input .= "</div>\n";
+
+ return array($label, $input);
+ }
+
+ /** @inheritdoc */
+ public function update($input) {
+ if(is_null($input)) return false;
+ if($this->isProtected()) return false;
+
+ $value = is_null($this->local) ? $this->default : $this->local;
+ if($value == $input) return false;
+
+ if(!in_array($input, $this->choices)) return false;
+
+ $this->local = $input;
+ return true;
+ }
+}
diff --git a/platform/www/lib/plugins/config/core/Setting/SettingNoClass.php b/platform/www/lib/plugins/config/core/Setting/SettingNoClass.php
new file mode 100644
index 0000000..8efff21
--- /dev/null
+++ b/platform/www/lib/plugins/config/core/Setting/SettingNoClass.php
@@ -0,0 +1,12 @@
+<?php
+
+namespace dokuwiki\plugin\config\core\Setting;
+
+/**
+ * Class setting_no_class
+ * A do-nothing class used to detect settings with a missing setting class.
+ * Used internaly to hide undefined settings, and generate the undefined settings list.
+ */
+class SettingNoClass extends SettingUndefined {
+ protected $errorMessage = '_msg_setting_no_class';
+}
diff --git a/platform/www/lib/plugins/config/core/Setting/SettingNoDefault.php b/platform/www/lib/plugins/config/core/Setting/SettingNoDefault.php
new file mode 100644
index 0000000..07b8412
--- /dev/null
+++ b/platform/www/lib/plugins/config/core/Setting/SettingNoDefault.php
@@ -0,0 +1,13 @@
+<?php
+
+namespace dokuwiki\plugin\config\core\Setting;
+
+/**
+ * Class setting_no_default
+ *
+ * A do-nothing class used to detect settings with no default value.
+ * Used internaly to hide undefined settings, and generate the undefined settings list.
+ */
+class SettingNoDefault extends SettingUndefined {
+ protected $errorMessage = '_msg_setting_no_default';
+}
diff --git a/platform/www/lib/plugins/config/core/Setting/SettingNoKnownClass.php b/platform/www/lib/plugins/config/core/Setting/SettingNoKnownClass.php
new file mode 100644
index 0000000..3c527e1
--- /dev/null
+++ b/platform/www/lib/plugins/config/core/Setting/SettingNoKnownClass.php
@@ -0,0 +1,11 @@
+<?php
+
+namespace dokuwiki\plugin\config\core\Setting;
+
+/**
+ * A do-nothing class used to detect settings with a missing setting class.
+ * Used internaly to hide undefined settings, and generate the undefined settings list.
+ */
+class SettingNoKnownClass extends SettingUndefined {
+ protected $errorMessage = '_msg_setting_no_known_class';
+}
diff --git a/platform/www/lib/plugins/config/core/Setting/SettingNumeric.php b/platform/www/lib/plugins/config/core/Setting/SettingNumeric.php
new file mode 100644
index 0000000..8a6b179
--- /dev/null
+++ b/platform/www/lib/plugins/config/core/Setting/SettingNumeric.php
@@ -0,0 +1,42 @@
+<?php
+
+namespace dokuwiki\plugin\config\core\Setting;
+
+/**
+ * Class setting_numeric
+ */
+class SettingNumeric extends SettingString {
+ // This allows for many PHP syntax errors...
+ // var $_pattern = '/^[-+\/*0-9 ]*$/';
+ // much more restrictive, but should eliminate syntax errors.
+ protected $pattern = '/^[-+]? *[0-9]+ *(?:[-+*] *[0-9]+ *)*$/';
+ protected $min = null;
+ protected $max = null;
+
+ /** @inheritdoc */
+ public function update($input) {
+ $local = $this->local;
+ $valid = parent::update($input);
+ if($valid && !(is_null($this->min) && is_null($this->max))) {
+ $numeric_local = (int) eval('return ' . $this->local . ';');
+ if((!is_null($this->min) && $numeric_local < $this->min) ||
+ (!is_null($this->max) && $numeric_local > $this->max)) {
+ $this->error = true;
+ $this->input = $input;
+ $this->local = $local;
+ $valid = false;
+ }
+ }
+ return $valid;
+ }
+
+ /** @inheritdoc */
+ public function out($var, $fmt = 'php') {
+ if($fmt != 'php') return '';
+
+ $local = $this->local === '' ? "''" : $this->local;
+ $out = '$' . $var . "['" . $this->getArrayKey() . "'] = " . $local . ";\n";
+
+ return $out;
+ }
+}
diff --git a/platform/www/lib/plugins/config/core/Setting/SettingNumericopt.php b/platform/www/lib/plugins/config/core/Setting/SettingNumericopt.php
new file mode 100644
index 0000000..a486e18
--- /dev/null
+++ b/platform/www/lib/plugins/config/core/Setting/SettingNumericopt.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace dokuwiki\plugin\config\core\Setting;
+
+/**
+ * Class setting_numericopt
+ */
+class SettingNumericopt extends SettingNumeric {
+ // just allow an empty config
+ protected $pattern = '/^(|[-]?[0-9]+(?:[-+*][0-9]+)*)$/';
+
+ /**
+ * @inheritdoc
+ * Empty string is valid for numericopt
+ */
+ public function update($input) {
+ if($input === '') {
+ if($input == $this->local) return false;
+ $this->local = $input;
+ return true;
+ }
+
+ return parent::update($input);
+ }
+}
diff --git a/platform/www/lib/plugins/config/core/Setting/SettingOnoff.php b/platform/www/lib/plugins/config/core/Setting/SettingOnoff.php
new file mode 100644
index 0000000..780778b
--- /dev/null
+++ b/platform/www/lib/plugins/config/core/Setting/SettingOnoff.php
@@ -0,0 +1,57 @@
+<?php
+
+namespace dokuwiki\plugin\config\core\Setting;
+
+/**
+ * Class setting_onoff
+ */
+class SettingOnoff extends SettingNumeric {
+
+ /**
+ * We treat the strings 'false' and 'off' as false
+ * @inheritdoc
+ */
+ protected function cleanValue($value) {
+ if($value === null) return null;
+
+ if(is_string($value)) {
+ if(strtolower($value) === 'false') return 0;
+ if(strtolower($value) === 'off') return 0;
+ if(trim($value) === '') return 0;
+ }
+
+ return (int) (bool) $value;
+ }
+
+ /** @inheritdoc */
+ public function html(\admin_plugin_config $plugin, $echo = false) {
+ $disable = '';
+
+ if($this->isProtected()) {
+ $value = $this->protected;
+ $disable = ' disabled="disabled"';
+ } else {
+ $value = is_null($this->local) ? $this->default : $this->local;
+ }
+
+ $key = htmlspecialchars($this->key);
+ $checked = ($value) ? ' checked="checked"' : '';
+
+ $label = '<label for="config___' . $key . '">' . $this->prompt($plugin) . '</label>';
+ $input = '<div class="input"><input id="config___' . $key . '" name="config[' . $key .
+ ']" type="checkbox" class="checkbox" value="1"' . $checked . $disable . '/></div>';
+ return array($label, $input);
+ }
+
+ /** @inheritdoc */
+ public function update($input) {
+ if($this->isProtected()) return false;
+
+ $input = ($input) ? 1 : 0;
+ $value = is_null($this->local) ? $this->default : $this->local;
+ if($value == $input) return false;
+
+ $this->local = $input;
+ return true;
+ }
+}
diff --git a/platform/www/lib/plugins/config/core/Setting/SettingPassword.php b/platform/www/lib/plugins/config/core/Setting/SettingPassword.php
new file mode 100644
index 0000000..9d9c533
--- /dev/null
+++ b/platform/www/lib/plugins/config/core/Setting/SettingPassword.php
@@ -0,0 +1,39 @@
+<?php
+
+namespace dokuwiki\plugin\config\core\Setting;
+
+/**
+ * Class setting_password
+ */
+class SettingPassword extends SettingString {
+
+ protected $code = 'plain'; // mechanism to be used to obscure passwords
+
+ /** @inheritdoc */
+ public function update($input) {
+ if($this->isProtected()) return false;
+ if(!$input) return false;
+
+ if($this->pattern && !preg_match($this->pattern, $input)) {
+ $this->error = true;
+ $this->input = $input;
+ return false;
+ }
+
+ $this->local = conf_encodeString($input, $this->code);
+ return true;
+ }
+
+ /** @inheritdoc */
+ public function html(\admin_plugin_config $plugin, $echo = false) {
+
+ $disable = $this->isProtected() ? 'disabled="disabled"' : '';
+
+ $key = htmlspecialchars($this->key);
+
+ $label = '<label for="config___' . $key . '">' . $this->prompt($plugin) . '</label>';
+ $input = '<input id="config___' . $key . '" name="config[' . $key .
+ ']" autocomplete="off" type="password" class="edit" value="" ' . $disable . ' />';
+ return array($label, $input);
+ }
+}
diff --git a/platform/www/lib/plugins/config/core/Setting/SettingRegex.php b/platform/www/lib/plugins/config/core/Setting/SettingRegex.php
new file mode 100644
index 0000000..b38f0a5
--- /dev/null
+++ b/platform/www/lib/plugins/config/core/Setting/SettingRegex.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace dokuwiki\plugin\config\core\Setting;
+
+/**
+ * Class setting_regex
+ */
+class SettingRegex extends SettingString {
+
+ protected $delimiter = '/'; // regex delimiter to be used in testing input
+ protected $pregflags = 'ui'; // regex pattern modifiers to be used in testing input
+
+ /** @inheritdoc */
+ public function update($input) {
+
+ // let parent do basic checks, value, not changed, etc.
+ $local = $this->local;
+ if(!parent::update($input)) return false;
+ $this->local = $local;
+
+ // see if the regex compiles and runs (we don't check for effectiveness)
+ $regex = $this->delimiter . $input . $this->delimiter . $this->pregflags;
+ $lastError = error_get_last();
+ @preg_match($regex, 'testdata');
+ if(preg_last_error() != PREG_NO_ERROR || error_get_last() != $lastError) {
+ $this->input = $input;
+ $this->error = true;
+ return false;
+ }
+
+ $this->local = $input;
+ return true;
+ }
+}
diff --git a/platform/www/lib/plugins/config/core/Setting/SettingRenderer.php b/platform/www/lib/plugins/config/core/Setting/SettingRenderer.php
new file mode 100644
index 0000000..37ba9c7
--- /dev/null
+++ b/platform/www/lib/plugins/config/core/Setting/SettingRenderer.php
@@ -0,0 +1,56 @@
+<?php
+/**
+ * additional setting classes specific to these settings
+ *
+ * @author Chris Smith <chris@jalakai.co.uk>
+ */
+
+namespace dokuwiki\plugin\config\core\Setting;
+
+/**
+ * Class setting_renderer
+ */
+class SettingRenderer extends SettingMultichoice {
+ protected $prompts = array();
+ protected $format = null;
+
+ /** @inheritdoc */
+ public function initialize($default = null, $local = null, $protected = null) {
+ $format = $this->format;
+
+ foreach(plugin_list('renderer') as $plugin) {
+ $renderer = plugin_load('renderer', $plugin);
+ if(method_exists($renderer, 'canRender') && $renderer->canRender($format)) {
+ $this->choices[] = $plugin;
+
+ $info = $renderer->getInfo();
+ $this->prompts[$plugin] = $info['name'];
+ }
+ }
+
+ parent::initialize($default, $local, $protected);
+ }
+
+ /** @inheritdoc */
+ public function html(\admin_plugin_config $plugin, $echo = false) {
+
+ // make some language adjustments (there must be a better way)
+ // transfer some plugin names to the config plugin
+ foreach($this->choices as $choice) {
+ if(!$plugin->getLang($this->key . '_o_' . $choice)) {
+ if(!isset($this->prompts[$choice])) {
+ $plugin->addLang(
+ $this->key . '_o_' . $choice,
+ sprintf($plugin->getLang('renderer__core'), $choice)
+ );
+ } else {
+ $plugin->addLang(
+ $this->key . '_o_' . $choice,
+ sprintf($plugin->getLang('renderer__plugin'), $this->prompts[$choice])
+ );
+ }
+ }
+ }
+ return parent::html($plugin, $echo);
+ }
+}
diff --git a/platform/www/lib/plugins/config/core/Setting/SettingSavedir.php b/platform/www/lib/plugins/config/core/Setting/SettingSavedir.php
new file mode 100644
index 0000000..43e428d
--- /dev/null
+++ b/platform/www/lib/plugins/config/core/Setting/SettingSavedir.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace dokuwiki\plugin\config\core\Setting;
+
+/**
+ * Class setting_savedir
+ */
+class SettingSavedir extends SettingString {
+
+ /** @inheritdoc */
+ public function update($input) {
+ if($this->isProtected()) return false;
+
+ $value = is_null($this->local) ? $this->default : $this->local;
+ if($value == $input) return false;
+
+ if(!init_path($input)) {
+ $this->error = true;
+ $this->input = $input;
+ return false;
+ }
+
+ $this->local = $input;
+ return true;
+ }
+}
diff --git a/platform/www/lib/plugins/config/core/Setting/SettingSepchar.php b/platform/www/lib/plugins/config/core/Setting/SettingSepchar.php
new file mode 100644
index 0000000..57cd0ae
--- /dev/null
+++ b/platform/www/lib/plugins/config/core/Setting/SettingSepchar.php
@@ -0,0 +1,18 @@
+<?php
+
+namespace dokuwiki\plugin\config\core\Setting;
+
+/**
+ * Class setting_sepchar
+ */
+class SettingSepchar extends SettingMultichoice {
+
+ /** @inheritdoc */
+ public function __construct($key, $param = null) {
+ $str = '_-.';
+ for($i = 0; $i < strlen($str); $i++) $this->choices[] = $str[$i];
+
+ // call foundation class constructor
+ parent::__construct($key, $param);
+ }
+}
diff --git a/platform/www/lib/plugins/config/core/Setting/SettingString.php b/platform/www/lib/plugins/config/core/Setting/SettingString.php
new file mode 100644
index 0000000..b819407
--- /dev/null
+++ b/platform/www/lib/plugins/config/core/Setting/SettingString.php
@@ -0,0 +1,32 @@
+<?php
+
+namespace dokuwiki\plugin\config\core\Setting;
+
+/**
+ * Class setting_string
+ */
+class SettingString extends Setting {
+ /** @inheritdoc */
+ public function html(\admin_plugin_config $plugin, $echo = false) {
+ $disable = '';
+
+ if($this->isProtected()) {
+ $value = $this->protected;
+ $disable = 'disabled="disabled"';
+ } else {
+ if($echo && $this->error) {
+ $value = $this->input;
+ } else {
+ $value = is_null($this->local) ? $this->default : $this->local;
+ }
+ }
+
+ $key = htmlspecialchars($this->key);
+ $value = htmlspecialchars($value);
+
+ $label = '<label for="config___' . $key . '">' . $this->prompt($plugin) . '</label>';
+ $input = '<input id="config___' . $key . '" name="config[' . $key .
+ ']" type="text" class="edit" value="' . $value . '" ' . $disable . '/>';
+ return array($label, $input);
+ }
+}
diff --git a/platform/www/lib/plugins/config/core/Setting/SettingUndefined.php b/platform/www/lib/plugins/config/core/Setting/SettingUndefined.php
new file mode 100644
index 0000000..fa46a9f
--- /dev/null
+++ b/platform/www/lib/plugins/config/core/Setting/SettingUndefined.php
@@ -0,0 +1,40 @@
+<?php
+
+namespace dokuwiki\plugin\config\core\Setting;
+
+use dokuwiki\plugin\config\core\Configuration;
+
+/**
+ * A do-nothing class used to detect settings with no metadata entry.
+ * Used internaly to hide undefined settings, and generate the undefined settings list.
+ */
+class SettingUndefined extends SettingHidden {
+
+ protected $errorMessage = '_msg_setting_undefined';
+
+ /** @inheritdoc */
+ public function shouldHaveDefault() {
+ return false;
+ }
+
+ /** @inheritdoc */
+ public function html(\admin_plugin_config $plugin, $echo = false) {
+ // determine the name the meta key would be called
+ if(preg_match(
+ '/^(?:plugin|tpl)' . Configuration::KEYMARKER . '.*?' . Configuration::KEYMARKER . '(.*)$/',
+ $this->getKey(),
+ $undefined_setting_match
+ )) {
+ $undefined_setting_key = $undefined_setting_match[1];
+ } else {
+ $undefined_setting_key = $this->getKey();
+ }
+
+ $label = '<span title="$meta[\'' . $undefined_setting_key . '\']">$' .
+ 'conf' . '[\'' . $this->getArrayKey() . '\']</span>';
+ $input = $plugin->getLang($this->errorMessage);
+
+ return array($label, $input);
+ }
+
+}
diff --git a/platform/www/lib/plugins/config/core/Writer.php b/platform/www/lib/plugins/config/core/Writer.php
new file mode 100644
index 0000000..56de621
--- /dev/null
+++ b/platform/www/lib/plugins/config/core/Writer.php
@@ -0,0 +1,116 @@
+<?php
+
+namespace dokuwiki\plugin\config\core;
+use dokuwiki\plugin\config\core\Setting\Setting;
+
+/**
+ * Writes the settings to the correct local file
+ */
+class Writer {
+ /** @var string header info */
+ protected $header = 'Dokuwiki\'s Main Configuration File - Local Settings';
+
+ /** @var string the file where the config will be saved to */
+ protected $savefile;
+
+ /**
+ * Writer constructor.
+ */
+ public function __construct() {
+ global $config_cascade;
+ $this->savefile = end($config_cascade['main']['local']);
+ }
+
+ /**
+ * Save the given settings
+ *
+ * @param Setting[] $settings
+ * @throws \Exception
+ */
+ public function save($settings) {
+ global $conf;
+ if($this->isLocked()) throw new \Exception('no save');
+
+ // backup current file (remove any existing backup)
+ if(file_exists($this->savefile)) {
+ if(file_exists($this->savefile . '.bak.php')) @unlink($this->savefile . '.bak.php');
+ if(!io_rename($this->savefile, $this->savefile . '.bak.php')) throw new \Exception('no backup');
+ }
+
+ if(!$fh = @fopen($this->savefile, 'wb')) {
+ io_rename($this->savefile . '.bak.php', $this->savefile); // problem opening, restore the backup
+ throw new \Exception('no save');
+ }
+
+ $out = $this->getHeader();
+ foreach($settings as $setting) {
+ if($setting->shouldBeSaved()) {
+ $out .= $setting->out('conf', 'php');
+ }
+ }
+
+ fwrite($fh, $out);
+ fclose($fh);
+ if($conf['fperm']) chmod($this->savefile, $conf['fperm']);
+ $this->opcacheUpdate($this->savefile);
+ }
+
+ /**
+ * Update last modified time stamp of the config file
+ *
+ * Will invalidate all DokuWiki caches
+ *
+ * @throws \Exception when the config isn't writable
+ */
+ public function touch() {
+ if($this->isLocked()) throw new \Exception('no save');
+ @touch($this->savefile);
+ $this->opcacheUpdate($this->savefile);
+ }
+
+ /**
+ * Invalidate the opcache of the given file
+ *
+ * @todo this should probably be moved to core
+ * @param string $file
+ */
+ protected function opcacheUpdate($file) {
+ if(!function_exists('opcache_invalidate')) return;
+ opcache_invalidate($file);
+ }
+
+ /**
+ * Configuration is considered locked if there is no local settings filename
+ * or the directory its in is not writable or the file exists and is not writable
+ *
+ * @return bool true: locked, false: writable
+ */
+ public function isLocked() {
+ if(!$this->savefile) return true;
+ if(!is_writable(dirname($this->savefile))) return true;
+ if(file_exists($this->savefile) && !is_writable($this->savefile)) return true;
+ return false;
+ }
+
+ /**
+ * Returns the PHP intro header for the config file
+ *
+ * @return string
+ */
+ protected function getHeader() {
+ return join(
+ "\n",
+ array(
+ '<?php',
+ '/*',
+ ' * ' . $this->header,
+ ' * Auto-generated by config plugin',
+ ' * Run for user: ' . $_SERVER['REMOTE_USER'],
+ ' * Date: ' . date('r'),
+ ' */',
+ '',
+ ''
+ )
+ );
+ }
+}
diff --git a/platform/www/lib/plugins/config/images/danger.png b/platform/www/lib/plugins/config/images/danger.png
new file mode 100644
index 0000000..da06924
--- /dev/null
+++ b/platform/www/lib/plugins/config/images/danger.png
Binary files differ
diff --git a/platform/www/lib/plugins/config/images/security.png b/platform/www/lib/plugins/config/images/security.png
new file mode 100644
index 0000000..3ee8476
--- /dev/null
+++ b/platform/www/lib/plugins/config/images/security.png
Binary files differ
diff --git a/platform/www/lib/plugins/config/images/warning.png b/platform/www/lib/plugins/config/images/warning.png
new file mode 100644
index 0000000..c1af79f
--- /dev/null
+++ b/platform/www/lib/plugins/config/images/warning.png
Binary files differ
diff --git a/platform/www/lib/plugins/config/lang/en/intro.txt b/platform/www/lib/plugins/config/lang/en/intro.txt
new file mode 100644
index 0000000..0108987
--- /dev/null
+++ b/platform/www/lib/plugins/config/lang/en/intro.txt
@@ -0,0 +1,7 @@
+====== Configuration Manager ======
+
+Use this page to control the settings of your DokuWiki installation. For help on individual settings refer to [[doku>config]]. For more details about this plugin see [[doku>plugin:config]].
+
+Settings shown with a light red background are protected and can not be altered with this plugin. Settings shown with a blue background are the default values and settings shown with a white background have been set locally for this particular installation. Both blue and white settings can be altered.
+
+Remember to press the **Save** button before leaving this page otherwise your changes will be lost.
diff --git a/platform/www/lib/plugins/config/lang/en/lang.php b/platform/www/lib/plugins/config/lang/en/lang.php
new file mode 100644
index 0000000..fb8186c
--- /dev/null
+++ b/platform/www/lib/plugins/config/lang/en/lang.php
@@ -0,0 +1,277 @@
+<?php
+/**
+ * english language file
+ *
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ * @author Christopher Smith <chris@jalakai.co.uk>
+ * @author Matthias Schulte <dokuwiki@lupo49.de>
+ * @author Schplurtz le Déboulonné <Schplurtz@laposte.net>
+ */
+
+// for admin plugins, the menu prompt to be displayed in the admin menu
+// if set here, the plugin doesn't need to override the getMenuText() method
+$lang['menu'] = 'Configuration Settings';
+
+$lang['error'] = 'Settings not updated due to an invalid value, please review your changes and resubmit.
+ <br />The incorrect value(s) will be shown surrounded by a red border.';
+$lang['updated'] = 'Settings updated successfully.';
+$lang['nochoice'] = '(no other choices available)';
+$lang['locked'] = 'The settings file can not be updated, if this is unintentional, <br />
+ ensure the local settings file name and permissions are correct.';
+
+$lang['danger'] = 'Danger: Changing this option could make your wiki and the configuration menu inaccessible.';
+$lang['warning'] = 'Warning: Changing this option could cause unintended behaviour.';
+$lang['security'] = 'Security Warning: Changing this option could present a security risk.';
+
+/* --- Config Setting Headers --- */
+$lang['_configuration_manager'] = 'Configuration Manager'; //same as heading in intro.txt
+$lang['_header_dokuwiki'] = 'DokuWiki';
+$lang['_header_plugin'] = 'Plugin';
+$lang['_header_template'] = 'Template';
+$lang['_header_undefined'] = 'Undefined Settings';
+
+/* --- Config Setting Groups --- */
+$lang['_basic'] = 'Basic';
+$lang['_display'] = 'Display';
+$lang['_authentication'] = 'Authentication';
+$lang['_anti_spam'] = 'Anti-Spam';
+$lang['_editing'] = 'Editing';
+$lang['_links'] = 'Links';
+$lang['_media'] = 'Media';
+$lang['_notifications'] = 'Notification';
+$lang['_syndication'] = 'Syndication (RSS)';
+$lang['_advanced'] = 'Advanced';
+$lang['_network'] = 'Network';
+
+/* --- Undefined Setting Messages --- */
+$lang['_msg_setting_undefined'] = 'No setting metadata.';
+$lang['_msg_setting_no_class'] = 'No setting class.';
+$lang['_msg_setting_no_known_class'] = 'Setting class not available.';
+$lang['_msg_setting_no_default'] = 'No default value.';
+
+/* -------------------- Config Options --------------------------- */
+
+/* Basic Settings */
+$lang['title'] = 'Wiki title aka. your wiki\'s name';
+$lang['start'] = 'Page name to use as the starting point for each namespace';
+$lang['lang'] = 'Interface language';
+$lang['template'] = 'Template aka. the design of the wiki.';
+$lang['tagline'] = 'Tagline (if template supports it)';
+$lang['sidebar'] = 'Sidebar page name (if template supports it), empty field disables the sidebar';
+$lang['license'] = 'Under which license should your content be released?';
+$lang['savedir'] = 'Directory for saving data';
+$lang['basedir'] = 'Server path (eg. <code>/dokuwiki/</code>). Leave blank for autodetection.';
+$lang['baseurl'] = 'Server URL (eg. <code>http://www.yourserver.com</code>). Leave blank for autodetection.';
+$lang['cookiedir'] = 'Cookie path. Leave blank for using baseurl.';
+$lang['dmode'] = 'Directory creation mode';
+$lang['fmode'] = 'File creation mode';
+$lang['allowdebug'] = 'Allow debug. <b>Disable if not needed!</b>';
+
+/* Display Settings */
+$lang['recent'] = 'Number of entries per page in the recent changes';
+$lang['recent_days'] = 'How many recent changes to keep (days)';
+$lang['breadcrumbs'] = 'Number of "trace" breadcrumbs. Set to 0 to disable.';
+$lang['youarehere'] = 'Use hierarchical breadcrumbs (you probably want to disable the above option then)';
+$lang['fullpath'] = 'Reveal full path of pages in the footer';
+$lang['typography'] = 'Do typographical replacements';
+$lang['dformat'] = 'Date format (see PHP\'s <a href="http://php.net/strftime">strftime</a> function)';
+$lang['signature'] = 'What to insert with the signature button in the editor';
+$lang['showuseras'] = 'What to display when showing the user that last edited a page';
+$lang['toptoclevel'] = 'Top level for table of contents';
+$lang['tocminheads'] = 'Minimum amount of headlines that determines whether the TOC is built';
+$lang['maxtoclevel'] = 'Maximum level for table of contents';
+$lang['maxseclevel'] = 'Maximum section edit level';
+$lang['camelcase'] = 'Use CamelCase for links';
+$lang['deaccent'] = 'How to clean pagenames';
+$lang['useheading'] = 'Use first heading for pagenames';
+$lang['sneaky_index'] = 'By default, DokuWiki will show all namespaces in the sitemap. Enabling this option will hide those where the user doesn\'t have read permissions. This might result in hiding of accessable subnamespaces which may make the index unusable with certain ACL setups.';
+$lang['hidepages'] = 'Hide pages matching this regular expression from search, the sitemap and other automatic indexes';
+
+/* Authentication Settings */
+$lang['useacl'] = 'Use access control lists';
+$lang['autopasswd'] = 'Autogenerate passwords';
+$lang['authtype'] = 'Authentication backend';
+$lang['passcrypt'] = 'Password encryption method';
+$lang['defaultgroup']= 'Default group, all new users will be placed in this group';
+$lang['superuser'] = 'Superuser - group, user or comma separated list user1,@group1,user2 with full access to all pages and functions regardless of the ACL settings';
+$lang['manager'] = 'Manager - group, user or comma separated list user1,@group1,user2 with access to certain management functions';
+$lang['profileconfirm'] = 'Confirm profile changes with password';
+$lang['rememberme'] = 'Allow permanent login cookies (remember me)';
+$lang['disableactions'] = 'Disable DokuWiki actions';
+$lang['disableactions_check'] = 'Check';
+$lang['disableactions_subscription'] = 'Subscribe/Unsubscribe';
+$lang['disableactions_wikicode'] = 'View source/Export Raw';
+$lang['disableactions_profile_delete'] = 'Delete Own Account';
+$lang['disableactions_other'] = 'Other actions (comma separated)';
+$lang['disableactions_rss'] = 'XML Syndication (RSS)';
+$lang['auth_security_timeout'] = 'Authentication Security Timeout (seconds)';
+$lang['securecookie'] = 'Should cookies set via HTTPS only be sent via HTTPS by the browser? Disable this option when only the login of your wiki is secured with SSL but browsing the wiki is done unsecured.';
+$lang['remote'] = 'Enable the remote API system. This allows other applications to access the wiki via XML-RPC or other mechanisms.';
+$lang['remoteuser'] = 'Restrict remote API access to the comma separated groups or users given here. Leave empty to give access to everyone.';
+
+/* Anti-Spam Settings */
+$lang['usewordblock']= 'Block spam based on wordlist';
+$lang['relnofollow'] = 'Use rel="ugc nofollow" on external links';
+$lang['indexdelay'] = 'Time delay before indexing (sec)';
+$lang['mailguard'] = 'Obfuscate email addresses';
+$lang['iexssprotect']= 'Check uploaded files for possibly malicious JavaScript or HTML code';
+
+/* Editing Settings */
+$lang['usedraft'] = 'Automatically save a draft while editing';
+$lang['htmlok'] = 'Allow embedded HTML';
+$lang['phpok'] = 'Allow embedded PHP';
+$lang['locktime'] = 'Maximum age for lock files (sec)';
+$lang['cachetime'] = 'Maximum age for cache (sec)';
+
+/* Link settings */
+$lang['target____wiki'] = 'Target window for internal links';
+$lang['target____interwiki'] = 'Target window for interwiki links';
+$lang['target____extern'] = 'Target window for external links';
+$lang['target____media'] = 'Target window for media links';
+$lang['target____windows'] = 'Target window for windows links';
+
+/* Media Settings */
+$lang['mediarevisions'] = 'Enable Mediarevisions?';
+$lang['refcheck'] = 'Check if a media file is still in use before deleting it';
+$lang['gdlib'] = 'GD Lib version';
+$lang['im_convert'] = 'Path to ImageMagick\'s convert tool';
+$lang['jpg_quality'] = 'JPG compression quality (0-100)';
+$lang['fetchsize'] = 'Maximum size (bytes) fetch.php may download from external URLs, eg. to cache and resize external images.';
+
+/* Notification Settings */
+$lang['subscribers'] = 'Allow users to subscribe to page changes by email';
+$lang['subscribe_time'] = 'Time after which subscription lists and digests are sent (sec); This should be smaller than the time specified in recent_days.';
+$lang['notify'] = 'Always send change notifications to this email address';
+$lang['registernotify'] = 'Always send info on newly registered users to this email address';
+$lang['mailfrom'] = 'Sender email address to use for automatic mails';
+$lang['mailreturnpath'] = 'Recipient email address for non delivery notifications';
+$lang['mailprefix'] = 'Email subject prefix to use for automatic mails. Leave blank to use the wiki title';
+$lang['htmlmail'] = 'Send better looking, but larger in size HTML multipart emails. Disable for plain text only mails.';
+
+/* Syndication Settings */
+$lang['sitemap'] = 'Generate Google sitemap this often (in days). 0 to disable';
+$lang['rss_type'] = 'XML feed type';
+$lang['rss_linkto'] = 'XML feed links to';
+$lang['rss_content'] = 'What to display in the XML feed items?';
+$lang['rss_update'] = 'XML feed update interval (sec)';
+$lang['rss_show_summary'] = 'XML feed show summary in title';
+$lang['rss_show_deleted'] = 'XML feed Show deleted feeds';
+$lang['rss_media'] = 'What kind of changes should be listed in the XML feed?';
+$lang['rss_media_o_both'] = 'both';
+$lang['rss_media_o_pages'] = 'pages';
+$lang['rss_media_o_media'] = 'media';
+
+
+/* Advanced Options */
+$lang['updatecheck'] = 'Check for updates and security warnings? DokuWiki needs to contact update.dokuwiki.org for this feature.';
+$lang['userewrite'] = 'Use nice URLs';
+$lang['useslash'] = 'Use slash as namespace separator in URLs';
+$lang['sepchar'] = 'Page name word separator';
+$lang['canonical'] = 'Use fully canonical URLs';
+$lang['fnencode'] = 'Method for encoding non-ASCII filenames.';
+$lang['autoplural'] = 'Check for plural forms in links';
+$lang['compression'] = 'Compression method for attic files';
+$lang['gzip_output'] = 'Use gzip Content-Encoding for xhtml';
+$lang['compress'] = 'Compact CSS and javascript output';
+$lang['cssdatauri'] = 'Size in bytes up to which images referenced in CSS files should be embedded right into the stylesheet to reduce HTTP request header overhead. <code>400</code> to <code>600</code> bytes is a good value. Set <code>0</code> to disable.';
+$lang['send404'] = 'Send "HTTP 404/Page Not Found" for non existing pages';
+$lang['broken_iua'] = 'Is the ignore_user_abort function broken on your system? This could cause a non working search index. IIS+PHP/CGI is known to be broken. See <a href="http://bugs.dokuwiki.org/?do=details&amp;task_id=852">Bug 852</a> for more info.';
+$lang['xsendfile'] = 'Use the X-Sendfile header to let the webserver deliver static files? Your webserver needs to support this.';
+$lang['renderer_xhtml'] = 'Renderer to use for main (xhtml) wiki output';
+$lang['renderer__core'] = '%s (dokuwiki core)';
+$lang['renderer__plugin'] = '%s (plugin)';
+$lang['search_nslimit'] = 'Limit the search to the current X namespaces. When a search is executed from a page within a deeper namespace, the first X namespaces will be added as filter';
+$lang['search_fragment'] = 'Specify the default fragment search behavior';
+$lang['search_fragment_o_exact'] = 'exact';
+$lang['search_fragment_o_starts_with'] = 'starts with';
+$lang['search_fragment_o_ends_with'] = 'ends with';
+$lang['search_fragment_o_contains'] = 'contains';
+$lang['trustedproxy'] = 'Trust forwarding proxies matching this regular expression about the true client IP they report. The default matches local networks. Leave empty to trust no proxy.';
+
+$lang['_feature_flags'] = 'Feature Flags';
+$lang['defer_js'] = 'Defer javascript to be execute after the page\'s HTML has been parsed. Improves perceived page speed but could break a small number of plugins.';
+
+/* Network Options */
+$lang['dnslookups'] = 'DokuWiki will lookup hostnames for remote IP addresses of users editing pages. If you have a slow or non working DNS server or don\'t want this feature, disable this option';
+$lang['jquerycdn'] = 'Should the jQuery and jQuery UI script files be loaded from a CDN? This adds additional HTTP requests, but files may load faster and users may have them cached already.';
+
+/* jQuery CDN options */
+$lang['jquerycdn_o_0'] = 'No CDN, local delivery only';
+$lang['jquerycdn_o_jquery'] = 'CDN at code.jquery.com';
+$lang['jquerycdn_o_cdnjs'] = 'CDN at cdnjs.com';
+
+/* Proxy Options */
+$lang['proxy____host'] = 'Proxy servername';
+$lang['proxy____port'] = 'Proxy port';
+$lang['proxy____user'] = 'Proxy user name';
+$lang['proxy____pass'] = 'Proxy password';
+$lang['proxy____ssl'] = 'Use SSL to connect to proxy';
+$lang['proxy____except'] = 'Regular expression to match URLs for which the proxy should be skipped.';
+
+/* License Options */
+$lang['license_o_'] = 'None chosen';
+
+/* typography options */
+$lang['typography_o_0'] = 'none';
+$lang['typography_o_1'] = 'excluding single quotes';
+$lang['typography_o_2'] = 'including single quotes (might not always work)';
+
+/* userewrite options */
+$lang['userewrite_o_0'] = 'none';
+$lang['userewrite_o_1'] = '.htaccess';
+$lang['userewrite_o_2'] = 'DokuWiki internal';
+
+/* deaccent options */
+$lang['deaccent_o_0'] = 'off';
+$lang['deaccent_o_1'] = 'remove accents';
+$lang['deaccent_o_2'] = 'romanize';
+
+/* gdlib options */
+$lang['gdlib_o_0'] = 'GD Lib not available';
+$lang['gdlib_o_1'] = 'Version 1.x';
+$lang['gdlib_o_2'] = 'Autodetection';
+
+/* rss_type options */
+$lang['rss_type_o_rss'] = 'RSS 0.91';
+$lang['rss_type_o_rss1'] = 'RSS 1.0';
+$lang['rss_type_o_rss2'] = 'RSS 2.0';
+$lang['rss_type_o_atom'] = 'Atom 0.3';
+$lang['rss_type_o_atom1'] = 'Atom 1.0';
+
+/* rss_content options */
+$lang['rss_content_o_abstract'] = 'Abstract';
+$lang['rss_content_o_diff'] = 'Unified Diff';
+$lang['rss_content_o_htmldiff'] = 'HTML formatted diff table';
+$lang['rss_content_o_html'] = 'Full HTML page content';
+
+/* rss_linkto options */
+$lang['rss_linkto_o_diff'] = 'difference view';
+$lang['rss_linkto_o_page'] = 'the revised page';
+$lang['rss_linkto_o_rev'] = 'list of revisions';
+$lang['rss_linkto_o_current'] = 'the current page';
+
+/* compression options */
+$lang['compression_o_0'] = 'none';
+$lang['compression_o_gz'] = 'gzip';
+$lang['compression_o_bz2'] = 'bz2';
+
+/* xsendfile header */
+$lang['xsendfile_o_0'] = "don't use";
+$lang['xsendfile_o_1'] = 'Proprietary lighttpd header (before release 1.5)';
+$lang['xsendfile_o_2'] = 'Standard X-Sendfile header';
+$lang['xsendfile_o_3'] = 'Proprietary Nginx X-Accel-Redirect header';
+
+/* Display user info */
+$lang['showuseras_o_loginname'] = 'Login name';
+$lang['showuseras_o_username'] = "User's full name";
+$lang['showuseras_o_username_link'] = "User's full name as interwiki user link";
+$lang['showuseras_o_email'] = "User's e-mail addresss (obfuscated according to mailguard setting)";
+$lang['showuseras_o_email_link'] = "User's e-mail addresss as a mailto: link";
+
+/* useheading options */
+$lang['useheading_o_0'] = 'Never';
+$lang['useheading_o_navigation'] = 'Navigation Only';
+$lang['useheading_o_content'] = 'Wiki Content Only';
+$lang['useheading_o_1'] = 'Always';
+
+$lang['readdircache'] = 'Maximum age for readdir cache (sec)';
diff --git a/platform/www/lib/plugins/config/plugin.info.txt b/platform/www/lib/plugins/config/plugin.info.txt
new file mode 100644
index 0000000..ddd7265
--- /dev/null
+++ b/platform/www/lib/plugins/config/plugin.info.txt
@@ -0,0 +1,7 @@
+base config
+author Christopher Smith
+email chris@jalakai.co.uk
+date 2015-07-18
+name Configuration Manager
+desc Manage Dokuwiki's Configuration Settings
+url http://dokuwiki.org/plugin:config
diff --git a/platform/www/lib/plugins/config/settings/config.metadata.php b/platform/www/lib/plugins/config/settings/config.metadata.php
new file mode 100644
index 0000000..6fdd64d
--- /dev/null
+++ b/platform/www/lib/plugins/config/settings/config.metadata.php
@@ -0,0 +1,245 @@
+<?php
+/**
+ * Metadata for configuration manager plugin
+ *
+ * Note: This file is loaded in Loader::loadMeta().
+ *
+ * Format:
+ * $meta[<setting name>] = array(<handler class id>,<param name> => <param value>);
+ *
+ * <handler class id> is the handler class name without the "setting_" prefix
+ *
+ * Defined classes (see core/Setting/*):
+ * Generic
+ * -------------------------------------------
+ * '' - default class ('setting'), textarea, minimal input validation, setting output in quotes
+ * 'string' - single line text input, minimal input validation, setting output in quotes
+ * 'numeric' - text input, accepts numbers and arithmetic operators, setting output without quotes
+ * if given the '_min' and '_max' parameters are used for validation
+ * 'numericopt' - like above, but accepts empty values
+ * 'onoff' - checkbox input, setting output 0|1
+ * 'multichoice' - select input (single choice), setting output with quotes, required _choices parameter
+ * 'email' - text input, input must conform to email address format, supports optional '_multiple'
+ * parameter for multiple comma separated email addresses
+ * 'password' - password input, minimal input validation, setting output text in quotes, maybe encoded
+ * according to the _code parameter
+ * 'dirchoice' - as multichoice, selection choices based on folders found at location specified in _dir
+ * parameter (required). A pattern can be used to restrict the folders to only those which
+ * match the pattern.
+ * 'multicheckbox'- a checkbox for each choice plus an "other" string input, config file setting is a comma
+ * separated list of checked choices
+ * 'fieldset' - used to group configuration settings, but is not itself a setting. To make this clear in
+ * the language files the keys for this type should start with '_'.
+ * 'array' - a simple (one dimensional) array of string values, shown as comma separated list in the
+ * config manager but saved as PHP array(). Values may not contain commas themselves.
+ * _pattern matching on the array values supported.
+ * 'regex' - regular expression string, normally without delimiters; as for string, in addition tested
+ * to see if will compile & run as a regex. in addition to _pattern, also accepts _delimiter
+ * (default '/') and _pregflags (default 'ui')
+ *
+ * Single Setting
+ * -------------------------------------------------
+ * 'savedir' - as 'setting', input tested against initpath() (inc/init.php)
+ * 'sepchar' - as multichoice, selection constructed from string of valid values
+ * 'authtype' - as 'setting', input validated against a valid php file at expected location for auth files
+ * 'im_convert' - as 'setting', input must exist and be an im_convert module
+ * 'disableactions' - as 'setting'
+ * 'compression' - no additional parameters. checks php installation supports possible compression alternatives
+ * 'licence' - as multichoice, selection constructed from licence strings in language files
+ * 'renderer' - as multichoice, selection constructed from enabled renderer plugins which canRender()
+ * 'authtype' - as multichoice, selection constructed from the enabled auth plugins
+ *
+ * Any setting commented or missing will use 'setting' class - text input, minimal validation, quoted output
+ *
+ * Defined parameters:
+ * '_caution' - no value (default) or 'warning', 'danger', 'security'. display an alert along with the setting
+ * '_pattern' - string, a preg pattern. input is tested against this pattern before being accepted
+ * optional all classes, except onoff & multichoice which ignore it
+ * '_choices' - array of choices. used to populate a selection box. choice will be replaced by a localised
+ * language string, indexed by <setting name>_o_<choice>, if one exists
+ * required by 'multichoice' & 'multicheckbox' classes, ignored by others
+ * '_dir' - location of directory to be used to populate choice list
+ * required by 'dirchoice' class, ignored by other classes
+ * '_combine' - complimentary output setting values which can be combined into a single display checkbox
+ * optional for 'multicheckbox', ignored by other classes
+ * '_code' - encoding method to use, accepted values: 'base64','uuencode','plain'. defaults to plain.
+ * '_min' - minimum numeric value, optional for 'numeric' and 'numericopt', ignored by others
+ * '_max' - maximum numeric value, optional for 'numeric' and 'numericopt', ignored by others
+ * '_delimiter' - string, default '/', a single character used as a delimiter for testing regex input values
+ * '_pregflags' - string, default 'ui', valid preg pattern modifiers used when testing regex input values, for more
+ * information see http://php.net/manual/en/reference.pcre.pattern.modifiers.php
+ * '_multiple' - bool, allow multiple comma separated email values; optional for 'email', ignored by others
+ * '_other' - how to handle other values (not listed in _choices). accepted values: 'always','exists','never'
+ * default value 'always'. 'exists' only shows 'other' input field when the setting contains value(s)
+ * not listed in choices (e.g. due to manual editing or update changing _choices). This is safer than
+ * 'never' as it will not discard unknown/other values.
+ * optional for 'multicheckbox', ignored by others
+ *
+ * The order of the settings influences the order in which they apppear in the config manager
+ *
+ * @author Chris Smith <chris@jalakai.co.uk>
+ */
+
+$meta['_basic'] = array('fieldset');
+$meta['title'] = array('string');
+$meta['start'] = array('string','_caution' => 'warning','_pattern' => '!^[^:;/]+$!'); // don't accept namespaces
+$meta['lang'] = array('dirchoice','_dir' => DOKU_INC.'inc/lang/');
+$meta['template'] = array('dirchoice','_dir' => DOKU_INC.'lib/tpl/','_pattern' => '/^[\w-]+$/');
+$meta['tagline'] = array('string');
+$meta['sidebar'] = array('string');
+$meta['license'] = array('license');
+$meta['savedir'] = array('savedir','_caution' => 'danger');
+$meta['basedir'] = array('string','_caution' => 'danger');
+$meta['baseurl'] = array('string','_caution' => 'danger');
+$meta['cookiedir'] = array('string','_caution' => 'danger');
+$meta['dmode'] = array('numeric','_pattern' => '/0[0-7]{3,4}/'); // only accept octal representation
+$meta['fmode'] = array('numeric','_pattern' => '/0[0-7]{3,4}/'); // only accept octal representation
+$meta['allowdebug'] = array('onoff','_caution' => 'security');
+
+$meta['_display'] = array('fieldset');
+$meta['recent'] = array('numeric');
+$meta['recent_days'] = array('numeric');
+$meta['breadcrumbs'] = array('numeric','_min' => 0);
+$meta['youarehere'] = array('onoff');
+$meta['fullpath'] = array('onoff','_caution' => 'security');
+$meta['typography'] = array('multichoice','_choices' => array(0,1,2));
+$meta['dformat'] = array('string');
+$meta['signature'] = array('string');
+$meta['showuseras'] = array(
+ 'multichoice',
+ '_choices' => array('loginname', 'username', 'username_link', 'email', 'email_link')
+);
+$meta['toptoclevel'] = array('multichoice','_choices' => array(1,2,3,4,5)); // 5 toc levels
+$meta['tocminheads'] = array('multichoice','_choices' => array(0,1,2,3,4,5,10,15,20));
+$meta['maxtoclevel'] = array('multichoice','_choices' => array(0,1,2,3,4,5));
+$meta['maxseclevel'] = array('multichoice','_choices' => array(0,1,2,3,4,5)); // 0 for no sec edit buttons
+$meta['camelcase'] = array('onoff','_caution' => 'warning');
+$meta['deaccent'] = array('multichoice','_choices' => array(0,1,2),'_caution' => 'warning');
+$meta['useheading'] = array('multichoice','_choices' => array(0,'navigation','content',1));
+$meta['sneaky_index'] = array('onoff');
+$meta['hidepages'] = array('regex');
+
+$meta['_authentication'] = array('fieldset');
+$meta['useacl'] = array('onoff','_caution' => 'danger');
+$meta['autopasswd'] = array('onoff');
+$meta['authtype'] = array('authtype','_caution' => 'danger');
+$meta['passcrypt'] = array('multichoice','_choices' => array(
+ 'smd5','md5','apr1','sha1','ssha','lsmd5','crypt','mysql','my411','kmd5','pmd5','hmd5',
+ 'mediawiki','bcrypt','djangomd5','djangosha1','djangopbkdf2_sha1','djangopbkdf2_sha256',
+ 'sha512','argon2i','argon2id'
+));
+$meta['defaultgroup']= array('string');
+$meta['superuser'] = array('string','_caution' => 'danger');
+$meta['manager'] = array('string');
+$meta['profileconfirm'] = array('onoff');
+$meta['rememberme'] = array('onoff');
+$meta['disableactions'] = array(
+ 'disableactions',
+ '_choices' => array(
+ 'backlink',
+ 'index',
+ 'recent',
+ 'revisions',
+ 'search',
+ 'subscription',
+ 'register',
+ 'resendpwd',
+ 'profile',
+ 'profile_delete',
+ 'edit',
+ 'wikicode',
+ 'check',
+ 'rss'
+ ),
+ '_combine' => array(
+ 'subscription' => array('subscribe', 'unsubscribe'),
+ 'wikicode' => array('source', 'export_raw')
+ )
+);
+$meta['auth_security_timeout'] = array('numeric');
+$meta['securecookie'] = array('onoff');
+$meta['remote'] = array('onoff','_caution' => 'security');
+$meta['remoteuser'] = array('string');
+
+$meta['_anti_spam'] = array('fieldset');
+$meta['usewordblock']= array('onoff');
+$meta['relnofollow'] = array('onoff');
+$meta['indexdelay'] = array('numeric');
+$meta['mailguard'] = array('multichoice','_choices' => array('visible','hex','none'));
+$meta['iexssprotect']= array('onoff','_caution' => 'security');
+
+$meta['_editing'] = array('fieldset');
+$meta['usedraft'] = array('onoff');
+$meta['htmlok'] = array('onoff','_caution' => 'security');
+$meta['phpok'] = array('onoff','_caution' => 'security');
+$meta['locktime'] = array('numeric');
+$meta['cachetime'] = array('numeric');
+
+$meta['_links'] = array('fieldset');
+$meta['target____wiki'] = array('string');
+$meta['target____interwiki'] = array('string');
+$meta['target____extern'] = array('string');
+$meta['target____media'] = array('string');
+$meta['target____windows'] = array('string');
+
+$meta['_media'] = array('fieldset');
+$meta['mediarevisions'] = array('onoff');
+$meta['gdlib'] = array('multichoice','_choices' => array(0,1,2));
+$meta['im_convert'] = array('im_convert');
+$meta['jpg_quality'] = array('numeric','_pattern' => '/^100$|^[1-9]?[0-9]$/'); //(0-100)
+$meta['fetchsize'] = array('numeric');
+$meta['refcheck'] = array('onoff');
+
+$meta['_notifications'] = array('fieldset');
+$meta['subscribers'] = array('onoff');
+$meta['subscribe_time'] = array('numeric');
+$meta['notify'] = array('email', '_multiple' => true);
+$meta['registernotify'] = array('email', '_multiple' => true);
+$meta['mailfrom'] = array('email', '_placeholders' => true);
+$meta['mailreturnpath'] = array('email', '_placeholders' => true);
+$meta['mailprefix'] = array('string');
+$meta['htmlmail'] = array('onoff');
+
+$meta['_syndication'] = array('fieldset');
+$meta['sitemap'] = array('numeric');
+$meta['rss_type'] = array('multichoice','_choices' => array('rss','rss1','rss2','atom','atom1'));
+$meta['rss_linkto'] = array('multichoice','_choices' => array('diff','page','rev','current'));
+$meta['rss_content'] = array('multichoice','_choices' => array('abstract','diff','htmldiff','html'));
+$meta['rss_media'] = array('multichoice','_choices' => array('both','pages','media'));
+$meta['rss_update'] = array('numeric');
+$meta['rss_show_summary'] = array('onoff');
+$meta['rss_show_deleted'] = array('onoff');
+
+$meta['_advanced'] = array('fieldset');
+$meta['updatecheck'] = array('onoff');
+$meta['userewrite'] = array('multichoice','_choices' => array(0,1,2),'_caution' => 'danger');
+$meta['useslash'] = array('onoff');
+$meta['sepchar'] = array('sepchar','_caution' => 'warning');
+$meta['canonical'] = array('onoff');
+$meta['fnencode'] = array('multichoice','_choices' => array('url','safe','utf-8'),'_caution' => 'warning');
+$meta['autoplural'] = array('onoff');
+$meta['compress'] = array('onoff');
+$meta['cssdatauri'] = array('numeric','_pattern' => '/^\d+$/');
+$meta['gzip_output'] = array('onoff');
+$meta['send404'] = array('onoff');
+$meta['compression'] = array('compression','_caution' => 'warning');
+$meta['broken_iua'] = array('onoff');
+$meta['xsendfile'] = array('multichoice','_choices' => array(0,1,2,3),'_caution' => 'warning');
+$meta['renderer_xhtml'] = array('renderer','_format' => 'xhtml','_choices' => array('xhtml'),'_caution' => 'warning');
+$meta['readdircache'] = array('numeric');
+$meta['search_nslimit'] = array('numeric', '_min' => 0);
+$meta['search_fragment'] = array('multichoice','_choices' => array('exact', 'starts_with', 'ends_with', 'contains'),);
+$meta['trustedproxy'] = array('regex');
+
+$meta['_feature_flags'] = ['fieldset'];
+$meta['defer_js'] = ['onoff'];
+
+$meta['_network'] = array('fieldset');
+$meta['dnslookups'] = array('onoff');
+$meta['jquerycdn'] = array('multichoice', '_choices' => array(0,'jquery', 'cdnjs'));
+$meta['proxy____host'] = array('string','_pattern' => '#^(|[a-z0-9\-\.+]+)$#i');
+$meta['proxy____port'] = array('numericopt');
+$meta['proxy____user'] = array('string');
+$meta['proxy____pass'] = array('password','_code' => 'base64');
+$meta['proxy____ssl'] = array('onoff');
+$meta['proxy____except'] = array('string');
diff --git a/platform/www/lib/plugins/config/style.css b/platform/www/lib/plugins/config/style.css
new file mode 100644
index 0000000..054021e
--- /dev/null
+++ b/platform/www/lib/plugins/config/style.css
@@ -0,0 +1,167 @@
+/* plugin:configmanager */
+#config__manager div.success,
+#config__manager div.error,
+#config__manager div.info {
+ background-position: 0.5em;
+ padding: 0.5em;
+ text-align: center;
+}
+
+#config__manager fieldset {
+ margin: 1em;
+ width: auto;
+ margin-bottom: 2em;
+ background-color: __background_alt__;
+ color: __text__;
+ padding: 0 1em;
+}
+[dir=rtl] #config__manager fieldset {
+ clear: both;
+}
+#config__manager legend {
+ font-size: 1.25em;
+}
+
+#config__manager form { }
+#config__manager table {
+ margin: 1em 0;
+ width: 100%;
+}
+
+#config__manager fieldset td {
+ text-align: left;
+}
+[dir=rtl] #config__manager fieldset td {
+ text-align: right;
+}
+#config__manager fieldset td.value {
+ /* fixed data column width */
+ width: 31em;
+}
+
+[dir=rtl] #config__manager label {
+ text-align: right;
+}
+[dir=rtl] #config__manager td.value input.checkbox {
+ float: right;
+ padding-left: 0;
+ padding-right: 0.7em;
+}
+[dir=rtl] #config__manager td.value label {
+ float: left;
+}
+
+#config__manager td.label {
+ padding: 0.8em 0 0.6em 1em;
+ vertical-align: top;
+}
+[dir=rtl] #config__manager td.label {
+ padding: 0.8em 1em 0.6em 0;
+}
+
+#config__manager td.label label {
+ clear: left;
+ display: block;
+}
+[dir=rtl] #config__manager td.label label {
+ clear: right;
+}
+#config__manager td.label img {
+ padding: 0 10px;
+ vertical-align: middle;
+ float: right;
+}
+[dir=rtl] #config__manager td.label img {
+ float: left;
+}
+
+#config__manager td.label span.outkey {
+ font-size: 70%;
+ margin-top: -1.7em;
+ margin-left: -1em;
+ display: block;
+ background-color: __background__;
+ color: __text_neu__;
+ float: left;
+ padding: 0 0.1em;
+ position: relative;
+ z-index: 1;
+}
+[dir=rtl] #config__manager td.label span.outkey {
+ float: right;
+ margin-right: 1em;
+}
+
+#config__manager td input.edit {
+ width: 30em;
+}
+#config__manager td .input {
+ width: 30.8em;
+}
+#config__manager td select.edit { }
+#config__manager td textarea.edit {
+ width: 27.5em;
+ height: 4em;
+}
+
+#config__manager td textarea.edit:focus {
+ height: 10em;
+}
+
+#config__manager tr .input,
+#config__manager tr input,
+#config__manager tr textarea,
+#config__manager tr select {
+ background-color: #fff;
+ color: #000;
+}
+
+#config__manager tr.default .input,
+#config__manager tr.default input,
+#config__manager tr.default textarea,
+#config__manager tr.default select,
+#config__manager .selectiondefault {
+ background-color: #ccddff;
+ color: #000;
+}
+
+#config__manager tr.protected .input,
+#config__manager tr.protected input,
+#config__manager tr.protected textarea,
+#config__manager tr.protected select,
+#config__manager tr.protected .selection {
+ background-color: #ffcccc!important;
+ color: #000 !important;
+}
+
+#config__manager td.error { background-color: red; color: #000; }
+
+#config__manager .selection {
+ width: 14.8em;
+ float: left;
+ margin: 0 0.3em 2px 0;
+}
+[dir=rtl] #config__manager .selection {
+ width: 14.8em;
+ float: right;
+ margin: 0 0 2px 0.3em;
+}
+
+#config__manager .selection label {
+ float: right;
+ width: 14em;
+ font-size: 90%;
+}
+
+
+#config__manager .other {
+ clear: both;
+ padding-top: 0.5em;
+}
+
+#config__manager .other label {
+ padding-left: 2px;
+ font-size: 90%;
+}
+
+/* end plugin:configmanager */
diff --git a/platform/www/lib/plugins/extension/action.php b/platform/www/lib/plugins/extension/action.php
new file mode 100644
index 0000000..3bb0448
--- /dev/null
+++ b/platform/www/lib/plugins/extension/action.php
@@ -0,0 +1,82 @@
+<?php
+/** DokuWiki Plugin extension (Action Component)
+ *
+ * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+
+class action_plugin_extension extends DokuWiki_Action_Plugin
+{
+
+ /**
+ * Registers a callback function for a given event
+ *
+ * @param Doku_Event_Handler $controller DokuWiki's event controller object
+ * @return void
+ */
+ public function register(Doku_Event_Handler $controller)
+ {
+ $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, 'info');
+ }
+
+ /**
+ * Create the detail info for a single plugin
+ *
+ * @param Doku_Event $event
+ * @param $param
+ */
+ public function info(Doku_Event $event, $param)
+ {
+ global $USERINFO;
+ global $INPUT;
+
+ if ($event->data != 'plugin_extension') return;
+ $event->preventDefault();
+ $event->stopPropagation();
+
+ /** @var admin_plugin_extension $admin */
+ $admin = plugin_load('admin', 'extension');
+ if (!$admin->isAccessibleByCurrentUser()) {
+ http_status(403);
+ echo 'Forbidden';
+ exit;
+ }
+
+ $ext = $INPUT->str('ext');
+ if (!$ext) {
+ http_status(400);
+ echo 'no extension given';
+ return;
+ }
+
+ /** @var helper_plugin_extension_extension $extension */
+ $extension = plugin_load('helper', 'extension_extension');
+ $extension->setExtension($ext);
+
+ $act = $INPUT->str('act');
+ switch ($act) {
+ case 'enable':
+ case 'disable':
+ $extension->$act(); //enables/disables
+
+ $reverse = ($act == 'disable') ? 'enable' : 'disable';
+
+ $return = array(
+ 'state' => $act.'d', // isn't English wonderful? :-)
+ 'reverse' => $reverse,
+ 'label' => $extension->getLang('btn_'.$reverse)
+ );
+
+ header('Content-Type: application/json');
+ echo json_encode($return);
+ break;
+
+ case 'info':
+ default:
+ /** @var helper_plugin_extension_list $list */
+ $list = plugin_load('helper', 'extension_list');
+ header('Content-Type: text/html; charset=utf-8');
+ echo $list->makeInfo($extension);
+ }
+ }
+}
diff --git a/platform/www/lib/plugins/extension/admin.php b/platform/www/lib/plugins/extension/admin.php
new file mode 100644
index 0000000..7e7eb60
--- /dev/null
+++ b/platform/www/lib/plugins/extension/admin.php
@@ -0,0 +1,185 @@
+<?php
+/**
+ * DokuWiki Plugin extension (Admin Component)
+ *
+ * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
+ * @author Michael Hamann <michael@content-space.de>
+ */
+
+/**
+ * Admin part of the extension manager
+ */
+class admin_plugin_extension extends DokuWiki_Admin_Plugin
+{
+ protected $infoFor = null;
+ /** @var helper_plugin_extension_gui */
+ protected $gui;
+
+ /**
+ * Constructor
+ *
+ * loads additional helpers
+ */
+ public function __construct()
+ {
+ $this->gui = plugin_load('helper', 'extension_gui');
+ }
+
+ /**
+ * @return int sort number in admin menu
+ */
+ public function getMenuSort()
+ {
+ return 0;
+ }
+
+ /**
+ * @return bool true if only access for superuser, false is for superusers and moderators
+ */
+ public function forAdminOnly()
+ {
+ return true;
+ }
+
+ /**
+ * Execute the requested action(s) and initialize the plugin repository
+ */
+ public function handle()
+ {
+ global $INPUT;
+ // initialize the remote repository
+ /* @var helper_plugin_extension_repository $repository */
+ $repository = $this->loadHelper('extension_repository');
+
+ if (!$repository->hasAccess(!$INPUT->bool('purge'))) {
+ $url = $this->gui->tabURL('', ['purge' => 1], '&');
+ msg($this->getLang('repo_error').
+ ' [<a href="'.$url.'">'.$this->getLang('repo_retry').'</a>]', -1
+ );
+ }
+
+ if (!in_array('ssl', stream_get_transports())) {
+ msg($this->getLang('nossl'), -1);
+ }
+
+ /* @var helper_plugin_extension_extension $extension */
+ $extension = $this->loadHelper('extension_extension');
+
+ try {
+ if ($INPUT->post->has('fn') && checkSecurityToken()) {
+ $actions = $INPUT->post->arr('fn');
+ foreach ($actions as $action => $extensions) {
+ foreach ($extensions as $extname => $label) {
+ switch ($action) {
+ case 'install':
+ case 'reinstall':
+ case 'update':
+ $extension->setExtension($extname);
+ $installed = $extension->installOrUpdate();
+ foreach ($installed as $ext => $info) {
+ msg(sprintf(
+ $this->getLang('msg_'.$info['type'].'_'.$info['action'].'_success'),
+ $info['base']), 1
+ );
+ }
+ break;
+ case 'uninstall':
+ $extension->setExtension($extname);
+ $status = $extension->uninstall();
+ if ($status) {
+ msg(sprintf(
+ $this->getLang('msg_delete_success'),
+ hsc($extension->getDisplayName())), 1
+ );
+ } else {
+ msg(sprintf(
+ $this->getLang('msg_delete_failed'),
+ hsc($extension->getDisplayName())), -1
+ );
+ }
+ break;
+ case 'enable':
+ $extension->setExtension($extname);
+ $status = $extension->enable();
+ if ($status !== true) {
+ msg($status, -1);
+ } else {
+ msg(sprintf(
+ $this->getLang('msg_enabled'),
+ hsc($extension->getDisplayName())), 1
+ );
+ }
+ break;
+ case 'disable':
+ $extension->setExtension($extname);
+ $status = $extension->disable();
+ if ($status !== true) {
+ msg($status, -1);
+ } else {
+ msg(sprintf(
+ $this->getLang('msg_disabled'),
+ hsc($extension->getDisplayName())), 1
+ );
+ }
+ break;
+ }
+ }
+ }
+ send_redirect($this->gui->tabURL('', [], '&', true));
+ } elseif ($INPUT->post->str('installurl') && checkSecurityToken()) {
+ $installed = $extension->installFromURL(
+ $INPUT->post->str('installurl'),
+ $INPUT->post->bool('overwrite'));
+ foreach ($installed as $ext => $info) {
+ msg(sprintf(
+ $this->getLang('msg_'.$info['type'].'_'.$info['action'].'_success'),
+ $info['base']), 1
+ );
+ }
+ send_redirect($this->gui->tabURL('', [], '&', true));
+ } elseif (isset($_FILES['installfile']) && checkSecurityToken()) {
+ $installed = $extension->installFromUpload('installfile', $INPUT->post->bool('overwrite'));
+ foreach ($installed as $ext => $info) {
+ msg(sprintf(
+ $this->getLang('msg_'.$info['type'].'_'.$info['action'].'_success'),
+ $info['base']), 1
+ );
+ }
+ send_redirect($this->gui->tabURL('', [], '&', true));
+ }
+ } catch (Exception $e) {
+ msg($e->getMessage(), -1);
+ send_redirect($this->gui->tabURL('', [], '&', true));
+ }
+ }
+
+ /**
+ * Render HTML output
+ */
+ public function html()
+ {
+ echo '<h1>'.$this->getLang('menu').'</h1>'.DOKU_LF;
+ echo '<div id="extension__manager">'.DOKU_LF;
+
+ $this->gui->tabNavigation();
+
+ switch ($this->gui->currentTab()) {
+ case 'search':
+ $this->gui->tabSearch();
+ break;
+ case 'templates':
+ $this->gui->tabTemplates();
+ break;
+ case 'install':
+ $this->gui->tabInstall();
+ break;
+ case 'plugins':
+ default:
+ $this->gui->tabPlugins();
+ }
+
+ echo '</div>'.DOKU_LF;
+ }
+}
+
+// vim:ts=4:sw=4:et:
diff --git a/platform/www/lib/plugins/extension/admin.svg b/platform/www/lib/plugins/extension/admin.svg
new file mode 100644
index 0000000..6bd7c0d
--- /dev/null
+++ b/platform/www/lib/plugins/extension/admin.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M20.5 11H19V7a2 2 0 0 0-2-2h-4V3.5A2.5 2.5 0 0 0 10.5 1 2.5 2.5 0 0 0 8 3.5V5H4a2 2 0 0 0-2 2v3.8h1.5c1.5 0 2.7 1.2 2.7 2.7 0 1.5-1.2 2.7-2.7 2.7H2V20a2 2 0 0 0 2 2h3.8v-1.5c0-1.5 1.2-2.7 2.7-2.7 1.5 0 2.7 1.2 2.7 2.7V22H17a2 2 0 0 0 2-2v-4h1.5a2.5 2.5 0 0 0 2.5-2.5 2.5 2.5 0 0 0-2.5-2.5z"/></svg> \ No newline at end of file
diff --git a/platform/www/lib/plugins/extension/all.less b/platform/www/lib/plugins/extension/all.less
new file mode 100644
index 0000000..3d9688e
--- /dev/null
+++ b/platform/www/lib/plugins/extension/all.less
@@ -0,0 +1,37 @@
+
+@media only screen and (max-width: 600px) {
+
+#extension__list .legend {
+ > div {
+ padding-left: 0;
+ }
+
+ div.screenshot {
+ margin: 0 .5em .5em 0;
+ }
+
+ h2 {
+ width: auto;
+ float: none;
+ }
+
+ div.linkbar {
+ clear: left;
+ }
+}
+
+[dir=rtl] #extension__list .legend {
+ > div {
+ padding-right: 0;
+ }
+
+ div.screenshot {
+ margin: 0 0 .5em .5em;
+ }
+
+ div.linkbar {
+ clear: right;
+ }
+}
+
+} /* /@media */
diff --git a/platform/www/lib/plugins/extension/cli.php b/platform/www/lib/plugins/extension/cli.php
new file mode 100644
index 0000000..a293f87
--- /dev/null
+++ b/platform/www/lib/plugins/extension/cli.php
@@ -0,0 +1,372 @@
+<?php
+
+use splitbrain\phpcli\Colors;
+
+/**
+ * Class cli_plugin_extension
+ *
+ * Command Line component for the extension manager
+ *
+ * @license GPL2
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+class cli_plugin_extension extends DokuWiki_CLI_Plugin
+{
+ /** @inheritdoc */
+ protected function setup(\splitbrain\phpcli\Options $options)
+ {
+ // general setup
+ $options->setHelp(
+ "Manage plugins and templates for this DokuWiki instance\n\n" .
+ "Status codes:\n" .
+ " i - installed\n" .
+ " b - bundled with DokuWiki\n" .
+ " g - installed via git\n" .
+ " d - disabled\n" .
+ " u - update available\n"
+ );
+
+ // search
+ $options->registerCommand('search', 'Search for an extension');
+ $options->registerOption('max', 'Maximum number of results (default 10)', 'm', 'number', 'search');
+ $options->registerOption('verbose', 'Show detailed extension information', 'v', false, 'search');
+ $options->registerArgument('query', 'The keyword(s) to search for', true, 'search');
+
+ // list
+ $options->registerCommand('list', 'List installed extensions');
+ $options->registerOption('verbose', 'Show detailed extension information', 'v', false, 'list');
+ $options->registerOption('filter', 'Filter by this status', 'f', 'status', 'list');
+
+ // upgrade
+ $options->registerCommand('upgrade', 'Update all installed extensions to their latest versions');
+
+ // install
+ $options->registerCommand('install', 'Install or upgrade extensions');
+ $options->registerArgument('extensions...', 'One or more extensions to install', true, 'install');
+
+ // uninstall
+ $options->registerCommand('uninstall', 'Uninstall a new extension');
+ $options->registerArgument('extensions...', 'One or more extensions to install', true, 'uninstall');
+
+ // enable
+ $options->registerCommand('enable', 'Enable installed extensions');
+ $options->registerArgument('extensions...', 'One or more extensions to enable', true, 'enable');
+
+ // disable
+ $options->registerCommand('disable', 'Disable installed extensions');
+ $options->registerArgument('extensions...', 'One or more extensions to disable', true, 'disable');
+
+
+ }
+
+ /** @inheritdoc */
+ protected function main(\splitbrain\phpcli\Options $options)
+ {
+ /** @var helper_plugin_extension_repository $repo */
+ $repo = plugin_load('helper', 'extension_repository');
+ if (!$repo->hasAccess(false)) {
+ $this->warning('Extension Repository API is not accessible, no remote info available!');
+ }
+
+ switch ($options->getCmd()) {
+ case 'list':
+ $ret = $this->cmdList($options->getOpt('verbose'), $options->getOpt('filter', ''));
+ break;
+ case 'search':
+ $ret = $this->cmdSearch(
+ implode(' ', $options->getArgs()),
+ $options->getOpt('verbose'),
+ (int)$options->getOpt('max', 10)
+ );
+ break;
+ case 'install':
+ $ret = $this->cmdInstall($options->getArgs());
+ break;
+ case 'uninstall':
+ $ret = $this->cmdUnInstall($options->getArgs());
+ break;
+ case 'enable':
+ $ret = $this->cmdEnable(true, $options->getArgs());
+ break;
+ case 'disable':
+ $ret = $this->cmdEnable(false, $options->getArgs());
+ break;
+ case 'upgrade':
+ $ret = $this->cmdUpgrade();
+ break;
+ default:
+ echo $options->help();
+ $ret = 0;
+ }
+
+ exit($ret);
+ }
+
+ /**
+ * Upgrade all extensions
+ *
+ * @return int
+ */
+ protected function cmdUpgrade()
+ {
+ /* @var helper_plugin_extension_extension $ext */
+ $ext = $this->loadHelper('extension_extension');
+ $list = $this->getInstalledExtensions();
+
+ $ok = 0;
+ foreach ($list as $extname) {
+ $ext->setExtension($extname);
+ $date = $ext->getInstalledVersion();
+ $avail = $ext->getLastUpdate();
+ if ($avail && $avail > $date) {
+ $ok += $this->cmdInstall([$extname]);
+ }
+ }
+
+ return $ok;
+ }
+
+ /**
+ * Enable or disable one or more extensions
+ *
+ * @param bool $set
+ * @param string[] $extensions
+ * @return int
+ */
+ protected function cmdEnable($set, $extensions)
+ {
+ /* @var helper_plugin_extension_extension $ext */
+ $ext = $this->loadHelper('extension_extension');
+
+ $ok = 0;
+ foreach ($extensions as $extname) {
+ $ext->setExtension($extname);
+ if (!$ext->isInstalled()) {
+ $this->error(sprintf('Extension %s is not installed', $ext->getID()));
+ $ok += 1;
+ continue;
+ }
+
+ if ($set) {
+ $status = $ext->enable();
+ $msg = 'msg_enabled';
+ } else {
+ $status = $ext->disable();
+ $msg = 'msg_disabled';
+ }
+
+ if ($status !== true) {
+ $this->error($status);
+ $ok += 1;
+ continue;
+ } else {
+ $this->success(sprintf($this->getLang($msg), $ext->getID()));
+ }
+ }
+
+ return $ok;
+ }
+
+ /**
+ * Uninstall one or more extensions
+ *
+ * @param string[] $extensions
+ * @return int
+ */
+ protected function cmdUnInstall($extensions)
+ {
+ /* @var helper_plugin_extension_extension $ext */
+ $ext = $this->loadHelper('extension_extension');
+
+ $ok = 0;
+ foreach ($extensions as $extname) {
+ $ext->setExtension($extname);
+ if (!$ext->isInstalled()) {
+ $this->error(sprintf('Extension %s is not installed', $ext->getID()));
+ $ok += 1;
+ continue;
+ }
+
+ $status = $ext->uninstall();
+ if ($status) {
+ $this->success(sprintf($this->getLang('msg_delete_success'), $ext->getID()));
+ } else {
+ $this->error(sprintf($this->getLang('msg_delete_failed'), hsc($ext->getID())));
+ $ok = 1;
+ }
+ }
+
+ return $ok;
+ }
+
+ /**
+ * Install one or more extensions
+ *
+ * @param string[] $extensions
+ * @return int
+ */
+ protected function cmdInstall($extensions)
+ {
+ /* @var helper_plugin_extension_extension $ext */
+ $ext = $this->loadHelper('extension_extension');
+
+ $ok = 0;
+ foreach ($extensions as $extname) {
+ $ext->setExtension($extname);
+
+ if (!$ext->getDownloadURL()) {
+ $ok += 1;
+ $this->error(
+ sprintf('Could not find download for %s', $ext->getID())
+ );
+ continue;
+ }
+
+ try {
+ $installed = $ext->installOrUpdate();
+ foreach ($installed as $name => $info) {
+ $this->success(sprintf(
+ $this->getLang('msg_' . $info['type'] . '_' . $info['action'] . '_success'),
+ $info['base'])
+ );
+ }
+ } catch (Exception $e) {
+ $this->error($e->getMessage());
+ $ok += 1;
+ }
+ }
+ return $ok;
+ }
+
+ /**
+ * Search for an extension
+ *
+ * @param string $query
+ * @param bool $showdetails
+ * @param int $max
+ * @return int
+ * @throws \splitbrain\phpcli\Exception
+ */
+ protected function cmdSearch($query, $showdetails, $max)
+ {
+ /** @var helper_plugin_extension_repository $repository */
+ $repository = $this->loadHelper('extension_repository');
+ $result = $repository->search($query);
+ if ($max) {
+ $result = array_slice($result, 0, $max);
+ }
+
+ $this->listExtensions($result, $showdetails);
+ return 0;
+ }
+
+ /**
+ * @param bool $showdetails
+ * @param string $filter
+ * @return int
+ * @throws \splitbrain\phpcli\Exception
+ */
+ protected function cmdList($showdetails, $filter)
+ {
+ $list = $this->getInstalledExtensions();
+ $this->listExtensions($list, $showdetails, $filter);
+
+ return 0;
+ }
+
+ /**
+ * Get all installed extensions
+ *
+ * @return array
+ */
+ protected function getInstalledExtensions()
+ {
+ /** @var Doku_Plugin_Controller $plugin_controller */
+ global $plugin_controller;
+ $pluginlist = $plugin_controller->getList('', true);
+ $tpllist = glob(DOKU_INC . 'lib/tpl/*', GLOB_ONLYDIR);
+ $tpllist = array_map(function ($path) {
+ return 'template:' . basename($path);
+ }, $tpllist);
+ $list = array_merge($pluginlist, $tpllist);
+ sort($list);
+ return $list;
+ }
+
+ /**
+ * List the given extensions
+ *
+ * @param string[] $list
+ * @param bool $details display details
+ * @param string $filter filter for this status
+ * @throws \splitbrain\phpcli\Exception
+ */
+ protected function listExtensions($list, $details, $filter = '')
+ {
+ /** @var helper_plugin_extension_extension $ext */
+ $ext = $this->loadHelper('extension_extension');
+ $tr = new \splitbrain\phpcli\TableFormatter($this->colors);
+
+
+ foreach ($list as $name) {
+ $ext->setExtension($name);
+
+ $status = '';
+ if ($ext->isInstalled()) {
+ $date = $ext->getInstalledVersion();
+ $avail = $ext->getLastUpdate();
+ $status = 'i';
+ if ($avail && $avail > $date) {
+ $vcolor = Colors::C_RED;
+ $status .= 'u';
+ } else {
+ $vcolor = Colors::C_GREEN;
+ }
+ if ($ext->isGitControlled()) $status = 'g';
+ if ($ext->isBundled()) $status = 'b';
+ if ($ext->isEnabled()) {
+ $ecolor = Colors::C_BROWN;
+ } else {
+ $ecolor = Colors::C_DARKGRAY;
+ $status .= 'd';
+ }
+ } else {
+ $ecolor = null;
+ $date = $ext->getLastUpdate();
+ $vcolor = null;
+ }
+
+ if ($filter && strpos($status, $filter) === false) {
+ continue;
+ }
+
+ echo $tr->format(
+ [20, 3, 12, '*'],
+ [
+ $ext->getID(),
+ $status,
+ $date,
+ strip_tags(sprintf(
+ $this->getLang('extensionby'),
+ $ext->getDisplayName(),
+ $this->colors->wrap($ext->getAuthor(), Colors::C_PURPLE))
+ )
+ ],
+ [
+ $ecolor,
+ Colors::C_YELLOW,
+ $vcolor,
+ null,
+ ]
+ );
+
+ if (!$details) continue;
+
+ echo $tr->format(
+ [5, '*'],
+ ['', $ext->getDescription()],
+ [null, Colors::C_CYAN]
+ );
+ }
+ }
+}
diff --git a/platform/www/lib/plugins/extension/helper/extension.php b/platform/www/lib/plugins/extension/helper/extension.php
new file mode 100644
index 0000000..5ddf332
--- /dev/null
+++ b/platform/www/lib/plugins/extension/helper/extension.php
@@ -0,0 +1,1298 @@
+<?php
+/**
+ * DokuWiki Plugin extension (Helper Component)
+ *
+ * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
+ * @author Michael Hamann <michael@content-space.de>
+ */
+
+use dokuwiki\HTTP\DokuHTTPClient;
+use dokuwiki\Extension\PluginController;
+
+/**
+ * Class helper_plugin_extension_extension represents a single extension (plugin or template)
+ */
+class helper_plugin_extension_extension extends DokuWiki_Plugin
+{
+ private $id;
+ private $base;
+ private $is_template = false;
+ private $localInfo;
+ private $remoteInfo;
+ private $managerData;
+ /** @var helper_plugin_extension_repository $repository */
+ private $repository = null;
+
+ /** @var array list of temporary directories */
+ private $temporary = array();
+
+ /** @var string where templates are installed to */
+ private $tpllib = '';
+
+ /**
+ * helper_plugin_extension_extension constructor.
+ */
+ public function __construct()
+ {
+ $this->tpllib = dirname(tpl_incdir()).'/';
+ }
+
+ /**
+ * Destructor
+ *
+ * deletes any dangling temporary directories
+ */
+ public function __destruct()
+ {
+ foreach ($this->temporary as $dir) {
+ io_rmdir($dir, true);
+ }
+ }
+
+ /**
+ * @return bool false, this component is not a singleton
+ */
+ public function isSingleton()
+ {
+ return false;
+ }
+
+ /**
+ * Set the name of the extension this instance shall represents, triggers loading the local and remote data
+ *
+ * @param string $id The id of the extension (prefixed with template: for templates)
+ * @return bool If some (local or remote) data was found
+ */
+ public function setExtension($id)
+ {
+ $id = cleanID($id);
+ $this->id = $id;
+ $this->base = $id;
+
+ if (substr($id, 0, 9) == 'template:') {
+ $this->base = substr($id, 9);
+ $this->is_template = true;
+ } else {
+ $this->is_template = false;
+ }
+
+ $this->localInfo = array();
+ $this->managerData = array();
+ $this->remoteInfo = array();
+
+ if ($this->isInstalled()) {
+ $this->readLocalData();
+ $this->readManagerData();
+ }
+
+ if ($this->repository == null) {
+ $this->repository = $this->loadHelper('extension_repository');
+ }
+
+ $this->remoteInfo = $this->repository->getData($this->getID());
+
+ return ($this->localInfo || $this->remoteInfo);
+ }
+
+ /**
+ * If the extension is installed locally
+ *
+ * @return bool If the extension is installed locally
+ */
+ public function isInstalled()
+ {
+ return is_dir($this->getInstallDir());
+ }
+
+ /**
+ * If the extension is under git control
+ *
+ * @return bool
+ */
+ public function isGitControlled()
+ {
+ if (!$this->isInstalled()) return false;
+ return is_dir($this->getInstallDir().'/.git');
+ }
+
+ /**
+ * If the extension is bundled
+ *
+ * @return bool If the extension is bundled
+ */
+ public function isBundled()
+ {
+ if (!empty($this->remoteInfo['bundled'])) return $this->remoteInfo['bundled'];
+ return in_array(
+ $this->id,
+ array(
+ 'authad', 'authldap', 'authpdo', 'authplain',
+ 'acl', 'config', 'extension', 'info', 'popularity', 'revert',
+ 'safefnrecode', 'styling', 'testing', 'usermanager',
+ 'template:dokuwiki',
+ )
+ );
+ }
+
+ /**
+ * If the extension is protected against any modification (disable/uninstall)
+ *
+ * @return bool if the extension is protected
+ */
+ public function isProtected()
+ {
+ // never allow deinstalling the current auth plugin:
+ global $conf;
+ if ($this->id == $conf['authtype']) return true;
+
+ /** @var PluginController $plugin_controller */
+ global $plugin_controller;
+ $cascade = $plugin_controller->getCascade();
+ return (isset($cascade['protected'][$this->id]) && $cascade['protected'][$this->id]);
+ }
+
+ /**
+ * If the extension is installed in the correct directory
+ *
+ * @return bool If the extension is installed in the correct directory
+ */
+ public function isInWrongFolder()
+ {
+ return $this->base != $this->getBase();
+ }
+
+ /**
+ * If the extension is enabled
+ *
+ * @return bool If the extension is enabled
+ */
+ public function isEnabled()
+ {
+ global $conf;
+ if ($this->isTemplate()) {
+ return ($conf['template'] == $this->getBase());
+ }
+
+ /* @var PluginController $plugin_controller */
+ global $plugin_controller;
+ return $plugin_controller->isEnabled($this->base);
+ }
+
+ /**
+ * If the extension should be updated, i.e. if an updated version is available
+ *
+ * @return bool If an update is available
+ */
+ public function updateAvailable()
+ {
+ if (!$this->isInstalled()) return false;
+ if ($this->isBundled()) return false;
+ $lastupdate = $this->getLastUpdate();
+ if ($lastupdate === false) return false;
+ $installed = $this->getInstalledVersion();
+ if ($installed === false || $installed === $this->getLang('unknownversion')) return true;
+ return $this->getInstalledVersion() < $this->getLastUpdate();
+ }
+
+ /**
+ * If the extension is a template
+ *
+ * @return bool If this extension is a template
+ */
+ public function isTemplate()
+ {
+ return $this->is_template;
+ }
+
+ /**
+ * Get the ID of the extension
+ *
+ * This is the same as getName() for plugins, for templates it's getName() prefixed with 'template:'
+ *
+ * @return string
+ */
+ public function getID()
+ {
+ return $this->id;
+ }
+
+ /**
+ * Get the name of the installation directory
+ *
+ * @return string The name of the installation directory
+ */
+ public function getInstallName()
+ {
+ return $this->base;
+ }
+
+ // Data from plugin.info.txt/template.info.txt or the repo when not available locally
+ /**
+ * Get the basename of the extension
+ *
+ * @return string The basename
+ */
+ public function getBase()
+ {
+ if (!empty($this->localInfo['base'])) return $this->localInfo['base'];
+ return $this->base;
+ }
+
+ /**
+ * Get the display name of the extension
+ *
+ * @return string The display name
+ */
+ public function getDisplayName()
+ {
+ if (!empty($this->localInfo['name'])) return $this->localInfo['name'];
+ if (!empty($this->remoteInfo['name'])) return $this->remoteInfo['name'];
+ return $this->base;
+ }
+
+ /**
+ * Get the author name of the extension
+ *
+ * @return string|bool The name of the author or false if there is none
+ */
+ public function getAuthor()
+ {
+ if (!empty($this->localInfo['author'])) return $this->localInfo['author'];
+ if (!empty($this->remoteInfo['author'])) return $this->remoteInfo['author'];
+ return false;
+ }
+
+ /**
+ * Get the email of the author of the extension if there is any
+ *
+ * @return string|bool The email address or false if there is none
+ */
+ public function getEmail()
+ {
+ // email is only in the local data
+ if (!empty($this->localInfo['email'])) return $this->localInfo['email'];
+ return false;
+ }
+
+ /**
+ * Get the email id, i.e. the md5sum of the email
+ *
+ * @return string|bool The md5sum of the email if there is any, false otherwise
+ */
+ public function getEmailID()
+ {
+ if (!empty($this->remoteInfo['emailid'])) return $this->remoteInfo['emailid'];
+ if (!empty($this->localInfo['email'])) return md5($this->localInfo['email']);
+ return false;
+ }
+
+ /**
+ * Get the description of the extension
+ *
+ * @return string The description
+ */
+ public function getDescription()
+ {
+ if (!empty($this->localInfo['desc'])) return $this->localInfo['desc'];
+ if (!empty($this->remoteInfo['description'])) return $this->remoteInfo['description'];
+ return '';
+ }
+
+ /**
+ * Get the URL of the extension, usually a page on dokuwiki.org
+ *
+ * @return string The URL
+ */
+ public function getURL()
+ {
+ if (!empty($this->localInfo['url'])) return $this->localInfo['url'];
+ return 'https://www.dokuwiki.org/'.
+ ($this->isTemplate() ? 'template' : 'plugin').':'.$this->getBase();
+ }
+
+ /**
+ * Get the installed version of the extension
+ *
+ * @return string|bool The version, usually in the form yyyy-mm-dd if there is any
+ */
+ public function getInstalledVersion()
+ {
+ if (!empty($this->localInfo['date'])) return $this->localInfo['date'];
+ if ($this->isInstalled()) return $this->getLang('unknownversion');
+ return false;
+ }
+
+ /**
+ * Get the install date of the current version
+ *
+ * @return string|bool The date of the last update or false if not available
+ */
+ public function getUpdateDate()
+ {
+ if (!empty($this->managerData['updated'])) return $this->managerData['updated'];
+ return $this->getInstallDate();
+ }
+
+ /**
+ * Get the date of the installation of the plugin
+ *
+ * @return string|bool The date of the installation or false if not available
+ */
+ public function getInstallDate()
+ {
+ if (!empty($this->managerData['installed'])) return $this->managerData['installed'];
+ return false;
+ }
+
+ /**
+ * Get the names of the dependencies of this extension
+ *
+ * @return array The base names of the dependencies
+ */
+ public function getDependencies()
+ {
+ if (!empty($this->remoteInfo['dependencies'])) return $this->remoteInfo['dependencies'];
+ return array();
+ }
+
+ /**
+ * Get the names of the missing dependencies
+ *
+ * @return array The base names of the missing dependencies
+ */
+ public function getMissingDependencies()
+ {
+ /* @var PluginController $plugin_controller */
+ global $plugin_controller;
+ $dependencies = $this->getDependencies();
+ $missing_dependencies = array();
+ foreach ($dependencies as $dependency) {
+ if (!$plugin_controller->isEnabled($dependency)) {
+ $missing_dependencies[] = $dependency;
+ }
+ }
+ return $missing_dependencies;
+ }
+
+ /**
+ * Get the names of all conflicting extensions
+ *
+ * @return array The names of the conflicting extensions
+ */
+ public function getConflicts()
+ {
+ if (!empty($this->remoteInfo['conflicts'])) return $this->remoteInfo['conflicts'];
+ return array();
+ }
+
+ /**
+ * Get the names of similar extensions
+ *
+ * @return array The names of similar extensions
+ */
+ public function getSimilarExtensions()
+ {
+ if (!empty($this->remoteInfo['similar'])) return $this->remoteInfo['similar'];
+ return array();
+ }
+
+ /**
+ * Get the names of the tags of the extension
+ *
+ * @return array The names of the tags of the extension
+ */
+ public function getTags()
+ {
+ if (!empty($this->remoteInfo['tags'])) return $this->remoteInfo['tags'];
+ return array();
+ }
+
+ /**
+ * Get the popularity information as floating point number [0,1]
+ *
+ * @return float|bool The popularity information or false if it isn't available
+ */
+ public function getPopularity()
+ {
+ if (!empty($this->remoteInfo['popularity'])) return $this->remoteInfo['popularity'];
+ return false;
+ }
+
+
+ /**
+ * Get the text of the security warning if there is any
+ *
+ * @return string|bool The security warning if there is any, false otherwise
+ */
+ public function getSecurityWarning()
+ {
+ if (!empty($this->remoteInfo['securitywarning'])) return $this->remoteInfo['securitywarning'];
+ return false;
+ }
+
+ /**
+ * Get the text of the security issue if there is any
+ *
+ * @return string|bool The security issue if there is any, false otherwise
+ */
+ public function getSecurityIssue()
+ {
+ if (!empty($this->remoteInfo['securityissue'])) return $this->remoteInfo['securityissue'];
+ return false;
+ }
+
+ /**
+ * Get the URL of the screenshot of the extension if there is any
+ *
+ * @return string|bool The screenshot URL if there is any, false otherwise
+ */
+ public function getScreenshotURL()
+ {
+ if (!empty($this->remoteInfo['screenshoturl'])) return $this->remoteInfo['screenshoturl'];
+ return false;
+ }
+
+ /**
+ * Get the URL of the thumbnail of the extension if there is any
+ *
+ * @return string|bool The thumbnail URL if there is any, false otherwise
+ */
+ public function getThumbnailURL()
+ {
+ if (!empty($this->remoteInfo['thumbnailurl'])) return $this->remoteInfo['thumbnailurl'];
+ return false;
+ }
+ /**
+ * Get the last used download URL of the extension if there is any
+ *
+ * @return string|bool The previously used download URL, false if the extension has been installed manually
+ */
+ public function getLastDownloadURL()
+ {
+ if (!empty($this->managerData['downloadurl'])) return $this->managerData['downloadurl'];
+ return false;
+ }
+
+ /**
+ * Get the download URL of the extension if there is any
+ *
+ * @return string|bool The download URL if there is any, false otherwise
+ */
+ public function getDownloadURL()
+ {
+ if (!empty($this->remoteInfo['downloadurl'])) return $this->remoteInfo['downloadurl'];
+ return false;
+ }
+
+ /**
+ * If the download URL has changed since the last download
+ *
+ * @return bool If the download URL has changed
+ */
+ public function hasDownloadURLChanged()
+ {
+ $lasturl = $this->getLastDownloadURL();
+ $currenturl = $this->getDownloadURL();
+ return ($lasturl && $currenturl && $lasturl != $currenturl);
+ }
+
+ /**
+ * Get the bug tracker URL of the extension if there is any
+ *
+ * @return string|bool The bug tracker URL if there is any, false otherwise
+ */
+ public function getBugtrackerURL()
+ {
+ if (!empty($this->remoteInfo['bugtracker'])) return $this->remoteInfo['bugtracker'];
+ return false;
+ }
+
+ /**
+ * Get the URL of the source repository if there is any
+ *
+ * @return string|bool The URL of the source repository if there is any, false otherwise
+ */
+ public function getSourcerepoURL()
+ {
+ if (!empty($this->remoteInfo['sourcerepo'])) return $this->remoteInfo['sourcerepo'];
+ return false;
+ }
+
+ /**
+ * Get the donation URL of the extension if there is any
+ *
+ * @return string|bool The donation URL if there is any, false otherwise
+ */
+ public function getDonationURL()
+ {
+ if (!empty($this->remoteInfo['donationurl'])) return $this->remoteInfo['donationurl'];
+ return false;
+ }
+
+ /**
+ * Get the extension type(s)
+ *
+ * @return array The type(s) as array of strings
+ */
+ public function getTypes()
+ {
+ if (!empty($this->remoteInfo['types'])) return $this->remoteInfo['types'];
+ if ($this->isTemplate()) return array(32 => 'template');
+ return array();
+ }
+
+ /**
+ * Get a list of all DokuWiki versions this extension is compatible with
+ *
+ * @return array The versions in the form yyyy-mm-dd => ('label' => label, 'implicit' => implicit)
+ */
+ public function getCompatibleVersions()
+ {
+ if (!empty($this->remoteInfo['compatible'])) return $this->remoteInfo['compatible'];
+ return array();
+ }
+
+ /**
+ * Get the date of the last available update
+ *
+ * @return string|bool The last available update in the form yyyy-mm-dd if there is any, false otherwise
+ */
+ public function getLastUpdate()
+ {
+ if (!empty($this->remoteInfo['lastupdate'])) return $this->remoteInfo['lastupdate'];
+ return false;
+ }
+
+ /**
+ * Get the base path of the extension
+ *
+ * @return string The base path of the extension
+ */
+ public function getInstallDir()
+ {
+ if ($this->isTemplate()) {
+ return $this->tpllib.$this->base;
+ } else {
+ return DOKU_PLUGIN.$this->base;
+ }
+ }
+
+ /**
+ * The type of extension installation
+ *
+ * @return string One of "none", "manual", "git" or "automatic"
+ */
+ public function getInstallType()
+ {
+ if (!$this->isInstalled()) return 'none';
+ if (!empty($this->managerData)) return 'automatic';
+ if (is_dir($this->getInstallDir().'/.git')) return 'git';
+ return 'manual';
+ }
+
+ /**
+ * If the extension can probably be installed/updated or uninstalled
+ *
+ * @return bool|string True or error string
+ */
+ public function canModify()
+ {
+ if ($this->isInstalled()) {
+ if (!is_writable($this->getInstallDir())) {
+ return 'noperms';
+ }
+ }
+
+ if ($this->isTemplate() && !is_writable($this->tpllib)) {
+ return 'notplperms';
+ } elseif (!is_writable(DOKU_PLUGIN)) {
+ return 'nopluginperms';
+ }
+ return true;
+ }
+
+ /**
+ * Install an extension from a user upload
+ *
+ * @param string $field name of the upload file
+ * @param boolean $overwrite overwrite folder if the extension name is the same
+ * @throws Exception when something goes wrong
+ * @return array The list of installed extensions
+ */
+ public function installFromUpload($field, $overwrite = true)
+ {
+ if ($_FILES[$field]['error']) {
+ throw new Exception($this->getLang('msg_upload_failed').' ('.$_FILES[$field]['error'].')');
+ }
+
+ $tmp = $this->mkTmpDir();
+ if (!$tmp) throw new Exception($this->getLang('error_dircreate'));
+
+ // filename may contain the plugin name for old style plugins...
+ $basename = basename($_FILES[$field]['name']);
+ $basename = preg_replace('/\.(tar\.gz|tar\.bz|tar\.bz2|tar|tgz|tbz|zip)$/', '', $basename);
+ $basename = preg_replace('/[\W]+/', '', $basename);
+
+ if (!move_uploaded_file($_FILES[$field]['tmp_name'], "$tmp/upload.archive")) {
+ throw new Exception($this->getLang('msg_upload_failed'));
+ }
+
+ try {
+ $installed = $this->installArchive("$tmp/upload.archive", $overwrite, $basename);
+ $this->updateManagerData('', $installed);
+ $this->removeDeletedfiles($installed);
+ // purge cache
+ $this->purgeCache();
+ } catch (Exception $e) {
+ throw $e;
+ }
+ return $installed;
+ }
+
+ /**
+ * Install an extension from a remote URL
+ *
+ * @param string $url
+ * @param boolean $overwrite overwrite folder if the extension name is the same
+ * @throws Exception when something goes wrong
+ * @return array The list of installed extensions
+ */
+ public function installFromURL($url, $overwrite = true)
+ {
+ try {
+ $path = $this->download($url);
+ $installed = $this->installArchive($path, $overwrite);
+ $this->updateManagerData($url, $installed);
+ $this->removeDeletedfiles($installed);
+
+ // purge cache
+ $this->purgeCache();
+ } catch (Exception $e) {
+ throw $e;
+ }
+ return $installed;
+ }
+
+ /**
+ * Install or update the extension
+ *
+ * @throws \Exception when something goes wrong
+ * @return array The list of installed extensions
+ */
+ public function installOrUpdate()
+ {
+ $url = $this->getDownloadURL();
+ $path = $this->download($url);
+ $installed = $this->installArchive($path, $this->isInstalled(), $this->getBase());
+ $this->updateManagerData($url, $installed);
+
+ // refresh extension information
+ if (!isset($installed[$this->getID()])) {
+ throw new Exception('Error, the requested extension hasn\'t been installed or updated');
+ }
+ $this->removeDeletedfiles($installed);
+ $this->setExtension($this->getID());
+ $this->purgeCache();
+ return $installed;
+ }
+
+ /**
+ * Uninstall the extension
+ *
+ * @return bool If the plugin was sucessfully uninstalled
+ */
+ public function uninstall()
+ {
+ $this->purgeCache();
+ return io_rmdir($this->getInstallDir(), true);
+ }
+
+ /**
+ * Enable the extension
+ *
+ * @return bool|string True or an error message
+ */
+ public function enable()
+ {
+ if ($this->isTemplate()) return $this->getLang('notimplemented');
+ if (!$this->isInstalled()) return $this->getLang('notinstalled');
+ if ($this->isEnabled()) return $this->getLang('alreadyenabled');
+
+ /* @var PluginController $plugin_controller */
+ global $plugin_controller;
+ if ($plugin_controller->enable($this->base)) {
+ $this->purgeCache();
+ return true;
+ } else {
+ return $this->getLang('pluginlistsaveerror');
+ }
+ }
+
+ /**
+ * Disable the extension
+ *
+ * @return bool|string True or an error message
+ */
+ public function disable()
+ {
+ if ($this->isTemplate()) return $this->getLang('notimplemented');
+
+ /* @var PluginController $plugin_controller */
+ global $plugin_controller;
+ if (!$this->isInstalled()) return $this->getLang('notinstalled');
+ if (!$this->isEnabled()) return $this->getLang('alreadydisabled');
+ if ($plugin_controller->disable($this->base)) {
+ $this->purgeCache();
+ return true;
+ } else {
+ return $this->getLang('pluginlistsaveerror');
+ }
+ }
+
+ /**
+ * Purge the cache by touching the main configuration file
+ */
+ protected function purgeCache()
+ {
+ global $config_cascade;
+
+ // expire dokuwiki caches
+ // touching local.php expires wiki page, JS and CSS caches
+ @touch(reset($config_cascade['main']['local']));
+ }
+
+ /**
+ * Read local extension data either from info.txt or getInfo()
+ */
+ protected function readLocalData()
+ {
+ if ($this->isTemplate()) {
+ $infopath = $this->getInstallDir().'/template.info.txt';
+ } else {
+ $infopath = $this->getInstallDir().'/plugin.info.txt';
+ }
+
+ if (is_readable($infopath)) {
+ $this->localInfo = confToHash($infopath);
+ } elseif (!$this->isTemplate() && $this->isEnabled()) {
+ $path = $this->getInstallDir().'/';
+ $plugin = null;
+
+ foreach (PluginController::PLUGIN_TYPES as $type) {
+ if (file_exists($path.$type.'.php')) {
+ $plugin = plugin_load($type, $this->base);
+ if ($plugin) break;
+ }
+
+ if ($dh = @opendir($path.$type.'/')) {
+ while (false !== ($cp = readdir($dh))) {
+ if ($cp == '.' || $cp == '..' || strtolower(substr($cp, -4)) != '.php') continue;
+
+ $plugin = plugin_load($type, $this->base.'_'.substr($cp, 0, -4));
+ if ($plugin) break;
+ }
+ if ($plugin) break;
+ closedir($dh);
+ }
+ }
+
+ if ($plugin) {
+ /* @var DokuWiki_Plugin $plugin */
+ $this->localInfo = $plugin->getInfo();
+ }
+ }
+ }
+
+ /**
+ * Save the given URL and current datetime in the manager.dat file of all installed extensions
+ *
+ * @param string $url Where the extension was downloaded from. (empty for manual installs via upload)
+ * @param array $installed Optional list of installed plugins
+ */
+ protected function updateManagerData($url = '', $installed = null)
+ {
+ $origID = $this->getID();
+
+ if (is_null($installed)) {
+ $installed = array($origID);
+ }
+
+ foreach ($installed as $ext => $info) {
+ if ($this->getID() != $ext) $this->setExtension($ext);
+ if ($url) {
+ $this->managerData['downloadurl'] = $url;
+ } elseif (isset($this->managerData['downloadurl'])) {
+ unset($this->managerData['downloadurl']);
+ }
+ if (isset($this->managerData['installed'])) {
+ $this->managerData['updated'] = date('r');
+ } else {
+ $this->managerData['installed'] = date('r');
+ }
+ $this->writeManagerData();
+ }
+
+ if ($this->getID() != $origID) $this->setExtension($origID);
+ }
+
+ /**
+ * Read the manager.dat file
+ */
+ protected function readManagerData()
+ {
+ $managerpath = $this->getInstallDir().'/manager.dat';
+ if (is_readable($managerpath)) {
+ $file = @file($managerpath);
+ if (!empty($file)) {
+ foreach ($file as $line) {
+ list($key, $value) = explode('=', trim($line, DOKU_LF), 2);
+ $key = trim($key);
+ $value = trim($value);
+ // backwards compatible with old plugin manager
+ if ($key == 'url') $key = 'downloadurl';
+ $this->managerData[$key] = $value;
+ }
+ }
+ }
+ }
+
+ /**
+ * Write the manager.data file
+ */
+ protected function writeManagerData()
+ {
+ $managerpath = $this->getInstallDir().'/manager.dat';
+ $data = '';
+ foreach ($this->managerData as $k => $v) {
+ $data .= $k.'='.$v.DOKU_LF;
+ }
+ io_saveFile($managerpath, $data);
+ }
+
+ /**
+ * Returns a temporary directory
+ *
+ * The directory is registered for cleanup when the class is destroyed
+ *
+ * @return false|string
+ */
+ protected function mkTmpDir()
+ {
+ $dir = io_mktmpdir();
+ if (!$dir) return false;
+ $this->temporary[] = $dir;
+ return $dir;
+ }
+
+ /**
+ * downloads a file from the net and saves it
+ *
+ * - $file is the directory where the file should be saved
+ * - if successful will return the name used for the saved file, false otherwise
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @author Chris Smith <chris@jalakai.co.uk>
+ *
+ * @param string $url url to download
+ * @param string $file path to file or directory where to save
+ * @param string $defaultName fallback for name of download
+ * @return bool|string if failed false, otherwise true or the name of the file in the given dir
+ */
+ protected function downloadToFile($url, $file, $defaultName = '')
+ {
+ global $conf;
+ $http = new DokuHTTPClient();
+ $http->max_bodysize = 0;
+ $http->timeout = 25; //max. 25 sec
+ $http->keep_alive = false; // we do single ops here, no need for keep-alive
+ $http->agent = 'DokuWiki HTTP Client (Extension Manager)';
+
+ $data = $http->get($url);
+ if ($data === false) return false;
+
+ $name = '';
+ if (isset($http->resp_headers['content-disposition'])) {
+ $content_disposition = $http->resp_headers['content-disposition'];
+ $match = array();
+ if (is_string($content_disposition) &&
+ preg_match('/attachment;\s*filename\s*=\s*"([^"]*)"/i', $content_disposition, $match)
+ ) {
+ $name = \dokuwiki\Utf8\PhpString::basename($match[1]);
+ }
+
+ }
+
+ if (!$name) {
+ if (!$defaultName) return false;
+ $name = $defaultName;
+ }
+
+ $file = $file.$name;
+
+ $fileexists = file_exists($file);
+ $fp = @fopen($file,"w");
+ if (!$fp) return false;
+ fwrite($fp, $data);
+ fclose($fp);
+ if (!$fileexists and $conf['fperm']) chmod($file, $conf['fperm']);
+ return $name;
+ }
+
+ /**
+ * Download an archive to a protected path
+ *
+ * @param string $url The url to get the archive from
+ * @throws Exception when something goes wrong
+ * @return string The path where the archive was saved
+ */
+ public function download($url)
+ {
+ // check the url
+ if (!preg_match('/https?:\/\//i', $url)) {
+ throw new Exception($this->getLang('error_badurl'));
+ }
+
+ // try to get the file from the path (used as plugin name fallback)
+ $file = parse_url($url, PHP_URL_PATH);
+ if (is_null($file)) {
+ $file = md5($url);
+ } else {
+ $file = \dokuwiki\Utf8\PhpString::basename($file);
+ }
+
+ // create tmp directory for download
+ if (!($tmp = $this->mkTmpDir())) {
+ throw new Exception($this->getLang('error_dircreate'));
+ }
+
+ // download
+ if (!$file = $this->downloadToFile($url, $tmp.'/', $file)) {
+ io_rmdir($tmp, true);
+ throw new Exception(sprintf($this->getLang('error_download'),
+ '<bdi>'.hsc($url).'</bdi>')
+ );
+ }
+
+ return $tmp.'/'.$file;
+ }
+
+ /**
+ * @param string $file The path to the archive that shall be installed
+ * @param bool $overwrite If an already installed plugin should be overwritten
+ * @param string $base The basename of the plugin if it's known
+ * @throws Exception when something went wrong
+ * @return array list of installed extensions
+ */
+ public function installArchive($file, $overwrite = false, $base = '')
+ {
+ $installed_extensions = array();
+
+ // create tmp directory for decompression
+ if (!($tmp = $this->mkTmpDir())) {
+ throw new Exception($this->getLang('error_dircreate'));
+ }
+
+ // add default base folder if specified to handle case where zip doesn't contain this
+ if ($base && !@mkdir($tmp.'/'.$base)) {
+ throw new Exception($this->getLang('error_dircreate'));
+ }
+
+ // decompress
+ $this->decompress($file, "$tmp/".$base);
+
+ // search $tmp/$base for the folder(s) that has been created
+ // move the folder(s) to lib/..
+ $result = array('old'=>array(), 'new'=>array());
+ $default = ($this->isTemplate() ? 'template' : 'plugin');
+ if (!$this->findFolders($result, $tmp.'/'.$base, $default)) {
+ throw new Exception($this->getLang('error_findfolder'));
+ }
+
+ // choose correct result array
+ if (count($result['new'])) {
+ $install = $result['new'];
+ } else {
+ $install = $result['old'];
+ }
+
+ if (!count($install)) {
+ throw new Exception($this->getLang('error_findfolder'));
+ }
+
+ // now install all found items
+ foreach ($install as $item) {
+ // where to install?
+ if ($item['type'] == 'template') {
+ $target_base_dir = $this->tpllib;
+ } else {
+ $target_base_dir = DOKU_PLUGIN;
+ }
+
+ if (!empty($item['base'])) {
+ // use base set in info.txt
+ } elseif ($base && count($install) == 1) {
+ $item['base'] = $base;
+ } else {
+ // default - use directory as found in zip
+ // plugins from github/master without *.info.txt will install in wrong folder
+ // but using $info->id will make 'code3' fail (which should install in lib/code/..)
+ $item['base'] = basename($item['tmp']);
+ }
+
+ // check to make sure we aren't overwriting anything
+ $target = $target_base_dir.$item['base'];
+ if (!$overwrite && file_exists($target)) {
+ // this info message is not being exposed via exception,
+ // so that it's not interrupting the installation
+ msg(sprintf($this->getLang('msg_nooverwrite'), $item['base']));
+ continue;
+ }
+
+ $action = file_exists($target) ? 'update' : 'install';
+
+ // copy action
+ if ($this->dircopy($item['tmp'], $target)) {
+ // return info
+ $id = $item['base'];
+ if ($item['type'] == 'template') {
+ $id = 'template:'.$id;
+ }
+ $installed_extensions[$id] = array(
+ 'base' => $item['base'],
+ 'type' => $item['type'],
+ 'action' => $action
+ );
+ } else {
+ throw new Exception(sprintf($this->getLang('error_copy').DOKU_LF,
+ '<bdi>'.$item['base'].'</bdi>')
+ );
+ }
+ }
+
+ // cleanup
+ if ($tmp) io_rmdir($tmp, true);
+
+ return $installed_extensions;
+ }
+
+ /**
+ * Find out what was in the extracted directory
+ *
+ * Correct folders are searched recursively using the "*.info.txt" configs
+ * as indicator for a root folder. When such a file is found, it's base
+ * setting is used (when set). All folders found by this method are stored
+ * in the 'new' key of the $result array.
+ *
+ * For backwards compatibility all found top level folders are stored as
+ * in the 'old' key of the $result array.
+ *
+ * When no items are found in 'new' the copy mechanism should fall back
+ * the 'old' list.
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @param array $result - results are stored here
+ * @param string $directory - the temp directory where the package was unpacked to
+ * @param string $default_type - type used if no info.txt available
+ * @param string $subdir - a subdirectory. do not set. used by recursion
+ * @return bool - false on error
+ */
+ protected function findFolders(&$result, $directory, $default_type = 'plugin', $subdir = '')
+ {
+ $this_dir = "$directory$subdir";
+ $dh = @opendir($this_dir);
+ if (!$dh) return false;
+
+ $found_dirs = array();
+ $found_files = 0;
+ $found_template_parts = 0;
+ while (false !== ($f = readdir($dh))) {
+ if ($f == '.' || $f == '..') continue;
+
+ if (is_dir("$this_dir/$f")) {
+ $found_dirs[] = "$subdir/$f";
+ } else {
+ // it's a file -> check for config
+ $found_files++;
+ switch ($f) {
+ case 'plugin.info.txt':
+ case 'template.info.txt':
+ // we have found a clear marker, save and return
+ $info = array();
+ $type = explode('.', $f, 2);
+ $info['type'] = $type[0];
+ $info['tmp'] = $this_dir;
+ $conf = confToHash("$this_dir/$f");
+ $info['base'] = basename($conf['base']);
+ $result['new'][] = $info;
+ return true;
+
+ case 'main.php':
+ case 'details.php':
+ case 'mediamanager.php':
+ case 'style.ini':
+ $found_template_parts++;
+ break;
+ }
+ }
+ }
+ closedir($dh);
+
+ // files where found but no info.txt - use old method
+ if ($found_files) {
+ $info = array();
+ $info['tmp'] = $this_dir;
+ // does this look like a template or should we use the default type?
+ if ($found_template_parts >= 2) {
+ $info['type'] = 'template';
+ } else {
+ $info['type'] = $default_type;
+ }
+
+ $result['old'][] = $info;
+ return true;
+ }
+
+ // we have no files yet -> recurse
+ foreach ($found_dirs as $found_dir) {
+ $this->findFolders($result, $directory, $default_type, "$found_dir");
+ }
+ return true;
+ }
+
+ /**
+ * Decompress a given file to the given target directory
+ *
+ * Determines the compression type from the file extension
+ *
+ * @param string $file archive to extract
+ * @param string $target directory to extract to
+ * @throws Exception
+ * @return bool
+ */
+ private function decompress($file, $target)
+ {
+ // decompression library doesn't like target folders ending in "/"
+ if (substr($target, -1) == "/") $target = substr($target, 0, -1);
+
+ $ext = $this->guessArchiveType($file);
+ if (in_array($ext, array('tar', 'bz', 'gz'))) {
+ try {
+ $tar = new \splitbrain\PHPArchive\Tar();
+ $tar->open($file);
+ $tar->extract($target);
+ } catch (\splitbrain\PHPArchive\ArchiveIOException $e) {
+ throw new Exception($this->getLang('error_decompress').' '.$e->getMessage());
+ }
+
+ return true;
+ } elseif ($ext == 'zip') {
+ try {
+ $zip = new \splitbrain\PHPArchive\Zip();
+ $zip->open($file);
+ $zip->extract($target);
+ } catch (\splitbrain\PHPArchive\ArchiveIOException $e) {
+ throw new Exception($this->getLang('error_decompress').' '.$e->getMessage());
+ }
+
+ return true;
+ }
+
+ // the only case when we don't get one of the recognized archive types is
+ // when the archive file can't be read
+ throw new Exception($this->getLang('error_decompress').' Couldn\'t read archive file');
+ }
+
+ /**
+ * Determine the archive type of the given file
+ *
+ * Reads the first magic bytes of the given file for content type guessing,
+ * if neither bz, gz or zip are recognized, tar is assumed.
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @param string $file The file to analyze
+ * @return string|false false if the file can't be read, otherwise an "extension"
+ */
+ private function guessArchiveType($file)
+ {
+ $fh = fopen($file, 'rb');
+ if (!$fh) return false;
+ $magic = fread($fh, 5);
+ fclose($fh);
+
+ if (strpos($magic, "\x42\x5a") === 0) return 'bz';
+ if (strpos($magic, "\x1f\x8b") === 0) return 'gz';
+ if (strpos($magic, "\x50\x4b\x03\x04") === 0) return 'zip';
+ return 'tar';
+ }
+
+ /**
+ * Copy with recursive sub-directory support
+ *
+ * @param string $src filename path to file
+ * @param string $dst filename path to file
+ * @return bool|int|string
+ */
+ private function dircopy($src, $dst)
+ {
+ global $conf;
+
+ if (is_dir($src)) {
+ if (!$dh = @opendir($src)) return false;
+
+ if ($ok = io_mkdir_p($dst)) {
+ while ($ok && (false !== ($f = readdir($dh)))) {
+ if ($f == '..' || $f == '.') continue;
+ $ok = $this->dircopy("$src/$f", "$dst/$f");
+ }
+ }
+
+ closedir($dh);
+ return $ok;
+ } else {
+ $existed = file_exists($dst);
+
+ if (!@copy($src, $dst)) return false;
+ if (!$existed && $conf['fperm']) chmod($dst, $conf['fperm']);
+ @touch($dst, filemtime($src));
+ }
+
+ return true;
+ }
+
+ /**
+ * Delete outdated files from updated plugins
+ *
+ * @param array $installed
+ */
+ private function removeDeletedfiles($installed)
+ {
+ foreach ($installed as $id => $extension) {
+ // only on update
+ if ($extension['action'] == 'install') continue;
+
+ // get definition file
+ if ($extension['type'] == 'template') {
+ $extensiondir = $this->tpllib;
+ } else {
+ $extensiondir = DOKU_PLUGIN;
+ }
+ $extensiondir = $extensiondir . $extension['base'] .'/';
+ $definitionfile = $extensiondir . 'deleted.files';
+ if (!file_exists($definitionfile)) continue;
+
+ // delete the old files
+ $list = file($definitionfile);
+
+ foreach ($list as $line) {
+ $line = trim(preg_replace('/#.*$/', '', $line));
+ if (!$line) continue;
+ $file = $extensiondir . $line;
+ if (!file_exists($file)) continue;
+
+ io_rmdir($file, true);
+ }
+ }
+ }
+}
+
+// vim:ts=4:sw=4:et:
diff --git a/platform/www/lib/plugins/extension/helper/gui.php b/platform/www/lib/plugins/extension/helper/gui.php
new file mode 100644
index 0000000..919eb2c
--- /dev/null
+++ b/platform/www/lib/plugins/extension/helper/gui.php
@@ -0,0 +1,237 @@
+<?php
+/**
+ * DokuWiki Plugin extension (Helper Component)
+ *
+ * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+
+use dokuwiki\Form\Form;
+
+/**
+ * Class helper_plugin_extension_list takes care of the overall GUI
+ */
+class helper_plugin_extension_gui extends DokuWiki_Plugin
+{
+ protected $tabs = array('plugins', 'templates', 'search', 'install');
+
+ /** @var string the extension that should have an open info window FIXME currently broken */
+ protected $infoFor = '';
+
+ /**
+ * Constructor
+ *
+ * initializes requested info window
+ */
+ public function __construct()
+ {
+ global $INPUT;
+ $this->infoFor = $INPUT->str('info');
+ }
+
+ /**
+ * display the plugin tab
+ */
+ public function tabPlugins()
+ {
+ echo '<div class="panelHeader">';
+ echo $this->locale_xhtml('intro_plugins');
+ echo '</div>';
+
+ $pluginlist = plugin_list('', true);
+ /* @var helper_plugin_extension_extension $extension */
+ $extension = $this->loadHelper('extension_extension');
+ /* @var helper_plugin_extension_list $list */
+ $list = $this->loadHelper('extension_list');
+
+ $form = new Form([
+ 'action' => $this->tabURL('', [], '&'),
+ 'id' => 'extension__list',
+ ]);
+ $list->startForm();
+ foreach ($pluginlist as $name) {
+ $extension->setExtension($name);
+ $list->addRow($extension, $extension->getID() == $this->infoFor);
+ }
+ $list->endForm();
+ $form->addHTML($list->render(true));
+ echo $form->toHTML();
+ }
+
+ /**
+ * Display the template tab
+ */
+ public function tabTemplates()
+ {
+ echo '<div class="panelHeader">';
+ echo $this->locale_xhtml('intro_templates');
+ echo '</div>';
+
+ // FIXME do we have a real way?
+ $tpllist = glob(DOKU_INC.'lib/tpl/*', GLOB_ONLYDIR);
+ $tpllist = array_map('basename', $tpllist);
+ sort($tpllist);
+
+ /* @var helper_plugin_extension_extension $extension */
+ $extension = $this->loadHelper('extension_extension');
+ /* @var helper_plugin_extension_list $list */
+ $list = $this->loadHelper('extension_list');
+
+ $form = new Form([
+ 'action' => $this->tabURL('', [], '&'),
+ 'id' => 'extension__list',
+ ]);
+ $list->startForm();
+ foreach ($tpllist as $name) {
+ $extension->setExtension("template:$name");
+ $list->addRow($extension, $extension->getID() == $this->infoFor);
+ }
+ $list->endForm();
+ $form->addHTML($list->render(true));
+ echo $form->toHTML();
+ }
+
+ /**
+ * Display the search tab
+ */
+ public function tabSearch()
+ {
+ global $INPUT;
+ echo '<div class="panelHeader">';
+ echo $this->locale_xhtml('intro_search');
+ echo '</div>';
+
+ $form = new Form([
+ 'action' => $this->tabURL('', [], '&'),
+ 'class' => 'search',
+ ]);
+ $form->addTagOpen('div')->addClass('no');
+ $form->addTextInput('q', $this->getLang('search_for'))
+ ->addClass('edit')
+ ->val($INPUT->str('q'));
+ $form->addButton('submit', $this->getLang('search'))
+ ->attrs(['type' => 'submit', 'title' => $this->getLang('search')]);
+ $form->addTagClose('div');
+ echo $form->toHTML();
+
+ if (!$INPUT->bool('q')) return;
+
+ /* @var helper_plugin_extension_repository $repository FIXME should we use some gloabl instance? */
+ $repository = $this->loadHelper('extension_repository');
+ $result = $repository->search($INPUT->str('q'));
+
+ /* @var helper_plugin_extension_extension $extension */
+ $extension = $this->loadHelper('extension_extension');
+ /* @var helper_plugin_extension_list $list */
+ $list = $this->loadHelper('extension_list');
+
+ $form = new Form([
+ 'action' => $this->tabURL('', [], '&'),
+ 'id' => 'extension__list',
+ ]);
+ $list->startForm();
+ if ($result) {
+ foreach ($result as $name) {
+ $extension->setExtension($name);
+ $list->addRow($extension, $extension->getID() == $this->infoFor);
+ }
+ } else {
+ $list->nothingFound();
+ }
+ $list->endForm();
+ $form->addHTML($list->render(true));
+ echo $form->toHTML();
+ }
+
+ /**
+ * Display the template tab
+ */
+ public function tabInstall()
+ {
+ global $lang;
+ echo '<div class="panelHeader">';
+ echo $this->locale_xhtml('intro_install');
+ echo '</div>';
+
+ $form = new Form([
+ 'action' => $this->tabURL('', [], '&'),
+ 'enctype' => 'multipart/form-data',
+ 'class' => 'install',
+ ]);
+ $form->addTagOpen('div')->addClass('no');
+ $form->addTextInput('installurl', $this->getLang('install_url'))
+ ->addClass('block')
+ ->attrs(['type' => 'url']);
+ $form->addTag('br');
+ $form->addTextInput('installfile', $this->getLang('install_upload'))
+ ->addClass('block')
+ ->attrs(['type' => 'file']);
+ $form->addTag('br');
+ $form->addCheckbox('overwrite', $lang['js']['media_overwrt'])
+ ->addClass('block');
+ $form->addTag('br');
+ $form->addButton('', $this->getLang('btn_install'))
+ ->attrs(['type' => 'submit', 'title' => $this->getLang('btn_install')]);
+ $form->addTagClose('div');
+ echo $form->toHTML();
+ }
+
+ /**
+ * Print the tab navigation
+ *
+ * @fixme style active one
+ */
+ public function tabNavigation()
+ {
+ echo '<ul class="tabs">';
+ foreach ($this->tabs as $tab) {
+ $url = $this->tabURL($tab);
+ if ($this->currentTab() == $tab) {
+ $class = ' active';
+ } else {
+ $class = '';
+ }
+ echo '<li class="'.$tab.$class.'"><a href="'.$url.'">'.$this->getLang('tab_'.$tab).'</a></li>';
+ }
+ echo '</ul>';
+ }
+
+ /**
+ * Return the currently selected tab
+ *
+ * @return string
+ */
+ public function currentTab()
+ {
+ global $INPUT;
+
+ $tab = $INPUT->str('tab', 'plugins', true);
+ if (!in_array($tab, $this->tabs)) $tab = 'plugins';
+ return $tab;
+ }
+
+ /**
+ * Create an URL inside the extension manager
+ *
+ * @param string $tab tab to load, empty for current tab
+ * @param array $params associative array of parameter to set
+ * @param string $sep seperator to build the URL
+ * @param bool $absolute create absolute URLs?
+ * @return string
+ */
+ public function tabURL($tab = '', $params = [], $sep = '&', $absolute = false)
+ {
+ global $ID;
+ global $INPUT;
+
+ if (!$tab) $tab = $this->currentTab();
+ $defaults = array(
+ 'do' => 'admin',
+ 'page' => 'extension',
+ 'tab' => $tab,
+ );
+ if ($tab == 'search') $defaults['q'] = $INPUT->str('q');
+
+ return wl($ID, array_merge($defaults, $params), $absolute, $sep);
+ }
+}
diff --git a/platform/www/lib/plugins/extension/helper/list.php b/platform/www/lib/plugins/extension/helper/list.php
new file mode 100644
index 0000000..647575b
--- /dev/null
+++ b/platform/www/lib/plugins/extension/helper/list.php
@@ -0,0 +1,674 @@
+<?php
+/**
+ * DokuWiki Plugin extension (Helper Component)
+ *
+ * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
+ * @author Michael Hamann <michael@content-space.de>
+ */
+
+/**
+ * Class helper_plugin_extension_list takes care of creating a HTML list of extensions
+ */
+class helper_plugin_extension_list extends DokuWiki_Plugin
+{
+ protected $form = '';
+ /** @var helper_plugin_extension_gui */
+ protected $gui;
+
+ /**
+ * Constructor
+ *
+ * loads additional helpers
+ */
+ public function __construct()
+ {
+ $this->gui = plugin_load('helper', 'extension_gui');
+ }
+
+ /**
+ * Initialize the extension table form
+ */
+ public function startForm()
+ {
+ $this->form .= '<ul class="extensionList">';
+ }
+
+ /**
+ * Build single row of extension table
+ *
+ * @param helper_plugin_extension_extension $extension The extension that shall be added
+ * @param bool $showinfo Show the info area
+ */
+ public function addRow(helper_plugin_extension_extension $extension, $showinfo = false)
+ {
+ $this->startRow($extension);
+ $this->populateColumn('legend', $this->makeLegend($extension, $showinfo));
+ $this->populateColumn('actions', $this->makeActions($extension));
+ $this->endRow();
+ }
+
+ /**
+ * Adds a header to the form
+ *
+ * @param string $id The id of the header
+ * @param string $header The content of the header
+ * @param int $level The level of the header
+ */
+ public function addHeader($id, $header, $level = 2)
+ {
+ $this->form .='<h'.$level.' id="'.$id.'">'.hsc($header).'</h'.$level.'>'.DOKU_LF;
+ }
+
+ /**
+ * Adds a paragraph to the form
+ *
+ * @param string $data The content
+ */
+ public function addParagraph($data)
+ {
+ $this->form .= '<p>'.hsc($data).'</p>'.DOKU_LF;
+ }
+
+ /**
+ * Add hidden fields to the form with the given data
+ *
+ * @param array $data key-value list of fields and their values to add
+ */
+ public function addHidden(array $data)
+ {
+ $this->form .= '<div class="no">';
+ foreach ($data as $key => $value) {
+ $this->form .= '<input type="hidden" name="'.hsc($key).'" value="'.hsc($value).'" />';
+ }
+ $this->form .= '</div>'.DOKU_LF;
+ }
+
+ /**
+ * Add closing tags
+ */
+ public function endForm()
+ {
+ $this->form .= '</ul>';
+ }
+
+ /**
+ * Show message when no results are found
+ */
+ public function nothingFound()
+ {
+ global $lang;
+ $this->form .= '<li class="notfound">'.$lang['nothingfound'].'</li>';
+ }
+
+ /**
+ * Print the form
+ *
+ * @param bool $returnonly whether to return html or print
+ */
+ public function render($returnonly = false)
+ {
+ if ($returnonly) return $this->form;
+ echo $this->form;
+ }
+
+ /**
+ * Start the HTML for the row for the extension
+ *
+ * @param helper_plugin_extension_extension $extension The extension
+ */
+ private function startRow(helper_plugin_extension_extension $extension)
+ {
+ $this->form .= '<li id="extensionplugin__'.hsc($extension->getID()).
+ '" class="'.$this->makeClass($extension).'">';
+ }
+
+ /**
+ * Add a column with the given class and content
+ * @param string $class The class name
+ * @param string $html The content
+ */
+ private function populateColumn($class, $html)
+ {
+ $this->form .= '<div class="'.$class.' col">'.$html.'</div>'.DOKU_LF;
+ }
+
+ /**
+ * End the row
+ */
+ private function endRow()
+ {
+ $this->form .= '</li>'.DOKU_LF;
+ }
+
+ /**
+ * Generate the link to the plugin homepage
+ *
+ * @param helper_plugin_extension_extension $extension The extension
+ * @return string The HTML code
+ */
+ public function makeHomepageLink(helper_plugin_extension_extension $extension)
+ {
+ global $conf;
+ $url = $extension->getURL();
+ if (strtolower(parse_url($url, PHP_URL_HOST)) == 'www.dokuwiki.org') {
+ $linktype = 'interwiki';
+ } else {
+ $linktype = 'extern';
+ }
+ $param = array(
+ 'href' => $url,
+ 'title' => $url,
+ 'class' => ($linktype == 'extern') ? 'urlextern' : 'interwiki iw_doku',
+ 'target' => $conf['target'][$linktype],
+ 'rel' => ($linktype == 'extern') ? 'noopener' : '',
+ );
+ if ($linktype == 'extern' && $conf['relnofollow']) {
+ $param['rel'] = implode(' ', [$param['rel'], 'ugc nofollow']);
+ }
+ $html = ' <a '. buildAttributes($param, true).'>'.
+ $this->getLang('homepage_link').'</a>';
+ return $html;
+ }
+
+ /**
+ * Generate the class name for the row of the extension
+ *
+ * @param helper_plugin_extension_extension $extension The extension object
+ * @return string The class name
+ */
+ public function makeClass(helper_plugin_extension_extension $extension)
+ {
+ $class = ($extension->isTemplate()) ? 'template' : 'plugin';
+ if ($extension->isInstalled()) {
+ $class.=' installed';
+ $class.= ($extension->isEnabled()) ? ' enabled':' disabled';
+ if ($extension->updateAvailable()) $class .= ' updatable';
+ }
+ if (!$extension->canModify()) $class.= ' notselect';
+ if ($extension->isProtected()) $class.= ' protected';
+ //if($this->showinfo) $class.= ' showinfo';
+ return $class;
+ }
+
+ /**
+ * Generate a link to the author of the extension
+ *
+ * @param helper_plugin_extension_extension $extension The extension object
+ * @return string The HTML code of the link
+ */
+ public function makeAuthor(helper_plugin_extension_extension $extension)
+ {
+ if ($extension->getAuthor()) {
+ $mailid = $extension->getEmailID();
+ if ($mailid) {
+ $url = $this->gui->tabURL('search', array('q' => 'authorid:'.$mailid));
+ $html = '<a href="'.$url.'" class="author" title="'.$this->getLang('author_hint').'" >'.
+ '<img src="//www.gravatar.com/avatar/'.$mailid.
+ '?s=20&amp;d=mm" width="20" height="20" alt="" /> '.
+ hsc($extension->getAuthor()).'</a>';
+ } else {
+ $html = '<span class="author">'.hsc($extension->getAuthor()).'</span>';
+ }
+ $html = '<bdi>'.$html.'</bdi>';
+ } else {
+ $html = '<em class="author">'.$this->getLang('unknown_author').'</em>'.DOKU_LF;
+ }
+ return $html;
+ }
+
+ /**
+ * Get the link and image tag for the screenshot/thumbnail
+ *
+ * @param helper_plugin_extension_extension $extension The extension object
+ * @return string The HTML code
+ */
+ public function makeScreenshot(helper_plugin_extension_extension $extension)
+ {
+ $screen = $extension->getScreenshotURL();
+ $thumb = $extension->getThumbnailURL();
+
+ if ($screen) {
+ // use protocol independent URLs for images coming from us #595
+ $screen = str_replace('http://www.dokuwiki.org', '//www.dokuwiki.org', $screen);
+ $thumb = str_replace('http://www.dokuwiki.org', '//www.dokuwiki.org', $thumb);
+
+ $title = sprintf($this->getLang('screenshot'), hsc($extension->getDisplayName()));
+ $img = '<a href="'.hsc($screen).'" target="_blank" class="extension_screenshot">'.
+ '<img alt="'.$title.'" width="120" height="70" src="'.hsc($thumb).'" />'.
+ '</a>';
+ } elseif ($extension->isTemplate()) {
+ $img = '<img alt="" width="120" height="70" src="'.DOKU_BASE.
+ 'lib/plugins/extension/images/template.png" />';
+ } else {
+ $img = '<img alt="" width="120" height="70" src="'.DOKU_BASE.
+ 'lib/plugins/extension/images/plugin.png" />';
+ }
+ $html = '<div class="screenshot" >'.$img.'<span></span></div>'.DOKU_LF;
+ return $html;
+ }
+
+ /**
+ * Extension main description
+ *
+ * @param helper_plugin_extension_extension $extension The extension object
+ * @param bool $showinfo Show the info section
+ * @return string The HTML code
+ */
+ public function makeLegend(helper_plugin_extension_extension $extension, $showinfo = false)
+ {
+ $html = '<div>';
+ $html .= '<h2>';
+ $html .= sprintf(
+ $this->getLang('extensionby'),
+ '<bdi>'.hsc($extension->getDisplayName()).'</bdi>',
+ $this->makeAuthor($extension)
+ );
+ $html .= '</h2>'.DOKU_LF;
+
+ $html .= $this->makeScreenshot($extension);
+
+ $popularity = $extension->getPopularity();
+ if ($popularity !== false && !$extension->isBundled()) {
+ $popularityText = sprintf($this->getLang('popularity'), round($popularity*100, 2));
+ $html .= '<div class="popularity" title="'.$popularityText.'">'.
+ '<div style="width: '.($popularity * 100).'%;">'.
+ '<span class="a11y">'.$popularityText.'</span>'.
+ '</div></div>'.DOKU_LF;
+ }
+
+ if ($extension->getDescription()) {
+ $html .= '<p><bdi>';
+ $html .= hsc($extension->getDescription()).' ';
+ $html .= '</bdi></p>'.DOKU_LF;
+ }
+
+ $html .= $this->makeLinkbar($extension);
+
+ if ($showinfo) {
+ $url = $this->gui->tabURL('');
+ $class = 'close';
+ } else {
+ $url = $this->gui->tabURL('', array('info' => $extension->getID()));
+ $class = '';
+ }
+ $html .= ' <a href="'.$url.'#extensionplugin__'.$extension->getID().
+ '" class="info '.$class.'" title="'.$this->getLang('btn_info').
+ '" data-extid="'.$extension->getID().'">'.$this->getLang('btn_info').'</a>';
+
+ if ($showinfo) {
+ $html .= $this->makeInfo($extension);
+ }
+ $html .= $this->makeNoticeArea($extension);
+ $html .= '</div>'.DOKU_LF;
+ return $html;
+ }
+
+ /**
+ * Generate the link bar HTML code
+ *
+ * @param helper_plugin_extension_extension $extension The extension instance
+ * @return string The HTML code
+ */
+ public function makeLinkbar(helper_plugin_extension_extension $extension)
+ {
+ global $conf;
+ $html = '<div class="linkbar">';
+ $html .= $this->makeHomepageLink($extension);
+
+ $bugtrackerURL = $extension->getBugtrackerURL();
+ if ($bugtrackerURL) {
+ if (strtolower(parse_url($bugtrackerURL, PHP_URL_HOST)) == 'www.dokuwiki.org') {
+ $linktype = 'interwiki';
+ } else {
+ $linktype = 'extern';
+ }
+ $param = array(
+ 'href' => $bugtrackerURL,
+ 'title' => $bugtrackerURL,
+ 'class' => 'bugs',
+ 'target' => $conf['target'][$linktype],
+ 'rel' => ($linktype == 'extern') ? 'noopener' : '',
+ );
+ if ($conf['relnofollow']) {
+ $param['rel'] = implode(' ', [$param['rel'], 'ugc nofollow']);
+ }
+ $html .= ' <a '.buildAttributes($param, true).'>'.
+ $this->getLang('bugs_features').'</a>';
+ }
+ if ($extension->getTags()) {
+ $first = true;
+ $html .= ' <span class="tags">'.$this->getLang('tags').' ';
+ foreach ($extension->getTags() as $tag) {
+ if (!$first) {
+ $html .= ', ';
+ } else {
+ $first = false;
+ }
+ $url = $this->gui->tabURL('search', ['q' => 'tag:'.$tag]);
+ $html .= '<bdi><a href="'.$url.'">'.hsc($tag).'</a></bdi>';
+ }
+ $html .= '</span>';
+ }
+ $html .= '</div>'.DOKU_LF;
+ return $html;
+ }
+
+ /**
+ * Notice area
+ *
+ * @param helper_plugin_extension_extension $extension The extension
+ * @return string The HTML code
+ */
+ public function makeNoticeArea(helper_plugin_extension_extension $extension)
+ {
+ $html = '';
+ $missing_dependencies = $extension->getMissingDependencies();
+ if (!empty($missing_dependencies)) {
+ $html .= '<div class="msg error">' .
+ sprintf(
+ $this->getLang('missing_dependency'),
+ '<bdi>' . implode(', ', $missing_dependencies) . '</bdi>'
+ ) .
+ '</div>';
+ }
+ if ($extension->isInWrongFolder()) {
+ $html .= '<div class="msg error">' .
+ sprintf(
+ $this->getLang('wrong_folder'),
+ '<bdi>' . hsc($extension->getInstallName()) . '</bdi>',
+ '<bdi>' . hsc($extension->getBase()) . '</bdi>'
+ ) .
+ '</div>';
+ }
+ if (($securityissue = $extension->getSecurityIssue()) !== false) {
+ $html .= '<div class="msg error">'.
+ sprintf($this->getLang('security_issue'), '<bdi>'.hsc($securityissue).'</bdi>').
+ '</div>';
+ }
+ if (($securitywarning = $extension->getSecurityWarning()) !== false) {
+ $html .= '<div class="msg notify">'.
+ sprintf($this->getLang('security_warning'), '<bdi>'.hsc($securitywarning).'</bdi>').
+ '</div>';
+ }
+ if ($extension->updateAvailable()) {
+ $html .= '<div class="msg notify">'.
+ sprintf($this->getLang('update_available'), hsc($extension->getLastUpdate())).
+ '</div>';
+ }
+ if ($extension->hasDownloadURLChanged()) {
+ $html .= '<div class="msg notify">' .
+ sprintf(
+ $this->getLang('url_change'),
+ '<bdi>' . hsc($extension->getDownloadURL()) . '</bdi>',
+ '<bdi>' . hsc($extension->getLastDownloadURL()) . '</bdi>'
+ ) .
+ '</div>';
+ }
+ return $html.DOKU_LF;
+ }
+
+ /**
+ * Create a link from the given URL
+ *
+ * Shortens the URL for display
+ *
+ * @param string $url
+ * @return string HTML link
+ */
+ public function shortlink($url)
+ {
+ $link = parse_url($url);
+
+ $base = $link['host'];
+ if (!empty($link['port'])) $base .= $base.':'.$link['port'];
+ $long = $link['path'];
+ if (!empty($link['query'])) $long .= $link['query'];
+
+ $name = shorten($base, $long, 55);
+
+ $html = '<a href="'.hsc($url).'" class="urlextern">'.hsc($name).'</a>';
+ return $html;
+ }
+
+ /**
+ * Plugin/template details
+ *
+ * @param helper_plugin_extension_extension $extension The extension
+ * @return string The HTML code
+ */
+ public function makeInfo(helper_plugin_extension_extension $extension)
+ {
+ $default = $this->getLang('unknown');
+ $html = '<dl class="details">';
+
+ $html .= '<dt>'.$this->getLang('status').'</dt>';
+ $html .= '<dd>'.$this->makeStatus($extension).'</dd>';
+
+ if ($extension->getDonationURL()) {
+ $html .= '<dt>'.$this->getLang('donate').'</dt>';
+ $html .= '<dd>';
+ $html .= '<a href="'.$extension->getDonationURL().'" class="donate">'.
+ $this->getLang('donate_action').'</a>';
+ $html .= '</dd>';
+ }
+
+ if (!$extension->isBundled()) {
+ $html .= '<dt>'.$this->getLang('downloadurl').'</dt>';
+ $html .= '<dd><bdi>';
+ $html .= ($extension->getDownloadURL()
+ ? $this->shortlink($extension->getDownloadURL())
+ : $default);
+ $html .= '</bdi></dd>';
+
+ $html .= '<dt>'.$this->getLang('repository').'</dt>';
+ $html .= '<dd><bdi>';
+ $html .= ($extension->getSourcerepoURL()
+ ? $this->shortlink($extension->getSourcerepoURL())
+ : $default);
+ $html .= '</bdi></dd>';
+ }
+
+ if ($extension->isInstalled()) {
+ if ($extension->getInstalledVersion()) {
+ $html .= '<dt>'.$this->getLang('installed_version').'</dt>';
+ $html .= '<dd>';
+ $html .= hsc($extension->getInstalledVersion());
+ $html .= '</dd>';
+ }
+ if (!$extension->isBundled()) {
+ $html .= '<dt>'.$this->getLang('install_date').'</dt>';
+ $html .= '<dd>';
+ $html .= ($extension->getUpdateDate()
+ ? hsc($extension->getUpdateDate())
+ : $this->getLang('unknown'));
+ $html .= '</dd>';
+ }
+ }
+ if (!$extension->isInstalled() || $extension->updateAvailable()) {
+ $html .= '<dt>'.$this->getLang('available_version').'</dt>';
+ $html .= '<dd>';
+ $html .= ($extension->getLastUpdate()
+ ? hsc($extension->getLastUpdate())
+ : $this->getLang('unknown'));
+ $html .= '</dd>';
+ }
+
+ $html .= '<dt>'.$this->getLang('provides').'</dt>';
+ $html .= '<dd><bdi>';
+ $html .= ($extension->getTypes()
+ ? hsc(implode(', ', $extension->getTypes()))
+ : $default);
+ $html .= '</bdi></dd>';
+
+ if (!$extension->isBundled() && $extension->getCompatibleVersions()) {
+ $html .= '<dt>'.$this->getLang('compatible').'</dt>';
+ $html .= '<dd>';
+ foreach ($extension->getCompatibleVersions() as $date => $version) {
+ $html .= '<bdi>'.$version['label'].' ('.$date.')</bdi>, ';
+ }
+ $html = rtrim($html, ', ');
+ $html .= '</dd>';
+ }
+ if ($extension->getDependencies()) {
+ $html .= '<dt>'.$this->getLang('depends').'</dt>';
+ $html .= '<dd>';
+ $html .= $this->makeLinkList($extension->getDependencies());
+ $html .= '</dd>';
+ }
+
+ if ($extension->getSimilarExtensions()) {
+ $html .= '<dt>'.$this->getLang('similar').'</dt>';
+ $html .= '<dd>';
+ $html .= $this->makeLinkList($extension->getSimilarExtensions());
+ $html .= '</dd>';
+ }
+
+ if ($extension->getConflicts()) {
+ $html .= '<dt>'.$this->getLang('conflicts').'</dt>';
+ $html .= '<dd>';
+ $html .= $this->makeLinkList($extension->getConflicts());
+ $html .= '</dd>';
+ }
+ $html .= '</dl>'.DOKU_LF;
+ return $html;
+ }
+
+ /**
+ * Generate a list of links for extensions
+ *
+ * @param array $ext The extensions
+ * @return string The HTML code
+ */
+ public function makeLinkList($ext)
+ {
+ $html = '';
+ foreach ($ext as $link) {
+ $html .= '<bdi><a href="'.
+ $this->gui->tabURL('search', array('q'=>'ext:'.$link)).'">'.
+ hsc($link).'</a></bdi>, ';
+ }
+ return rtrim($html, ', ');
+ }
+
+ /**
+ * Display the action buttons if they are possible
+ *
+ * @param helper_plugin_extension_extension $extension The extension
+ * @return string The HTML code
+ */
+ public function makeActions(helper_plugin_extension_extension $extension)
+ {
+ global $conf;
+ $html = '';
+ $errors = '';
+
+ if ($extension->isInstalled()) {
+ if (($canmod = $extension->canModify()) === true) {
+ if (!$extension->isProtected()) {
+ $html .= $this->makeAction('uninstall', $extension);
+ }
+ if ($extension->getDownloadURL()) {
+ if ($extension->updateAvailable()) {
+ $html .= $this->makeAction('update', $extension);
+ } else {
+ $html .= $this->makeAction('reinstall', $extension);
+ }
+ }
+ } else {
+ $errors .= '<p class="permerror">'.$this->getLang($canmod).'</p>';
+ }
+
+ if (!$extension->isProtected() && !$extension->isTemplate()) { // no enable/disable for templates
+ if ($extension->isEnabled()) {
+ $html .= $this->makeAction('disable', $extension);
+ } else {
+ $html .= $this->makeAction('enable', $extension);
+ }
+ }
+
+ if ($extension->isGitControlled()) {
+ $errors .= '<p class="permerror">'.$this->getLang('git').'</p>';
+ }
+
+ if ($extension->isEnabled() &&
+ in_array('Auth', $extension->getTypes()) &&
+ $conf['authtype'] != $extension->getID()
+ ) {
+ $errors .= '<p class="permerror">'.$this->getLang('auth').'</p>';
+ }
+ } else {
+ if (($canmod = $extension->canModify()) === true) {
+ if ($extension->getDownloadURL()) {
+ $html .= $this->makeAction('install', $extension);
+ }
+ } else {
+ $errors .= '<div class="permerror">'.$this->getLang($canmod).'</div>';
+ }
+ }
+
+ if (!$extension->isInstalled() && $extension->getDownloadURL()) {
+ $html .= ' <span class="version">'.$this->getLang('available_version').' ';
+ $html .= ($extension->getLastUpdate()
+ ? hsc($extension->getLastUpdate())
+ : $this->getLang('unknown')).'</span>';
+ }
+
+ return $html.' '.$errors.DOKU_LF;
+ }
+
+ /**
+ * Display an action button for an extension
+ *
+ * @param string $action The action
+ * @param helper_plugin_extension_extension $extension The extension
+ * @return string The HTML code
+ */
+ public function makeAction($action, $extension)
+ {
+ $title = '';
+
+ switch ($action) {
+ case 'install':
+ case 'reinstall':
+ $title = 'title="'.hsc($extension->getDownloadURL()).'"';
+ break;
+ }
+
+ $classes = 'button '.$action;
+ $name = 'fn['.$action.']['.hsc($extension->getID()).']';
+
+ $html = '<button class="'.$classes.'" name="'.$name.'" type="submit" '.$title.'>'.
+ $this->getLang('btn_'.$action).'</button> ';
+ return $html;
+ }
+
+ /**
+ * Plugin/template status
+ *
+ * @param helper_plugin_extension_extension $extension The extension
+ * @return string The description of all relevant statusses
+ */
+ public function makeStatus(helper_plugin_extension_extension $extension)
+ {
+ $status = array();
+
+ if ($extension->isInstalled()) {
+ $status[] = $this->getLang('status_installed');
+ if ($extension->isProtected()) {
+ $status[] = $this->getLang('status_protected');
+ } else {
+ $status[] = $extension->isEnabled()
+ ? $this->getLang('status_enabled')
+ : $this->getLang('status_disabled');
+ }
+ } else {
+ $status[] = $this->getLang('status_not_installed');
+ }
+ if (!$extension->canModify()) $status[] = $this->getLang('status_unmodifiable');
+ if ($extension->isBundled()) $status[] = $this->getLang('status_bundled');
+ $status[] = $extension->isTemplate()
+ ? $this->getLang('status_template')
+ : $this->getLang('status_plugin');
+ return implode(', ', $status);
+ }
+}
diff --git a/platform/www/lib/plugins/extension/helper/repository.php b/platform/www/lib/plugins/extension/helper/repository.php
new file mode 100644
index 0000000..712baa0
--- /dev/null
+++ b/platform/www/lib/plugins/extension/helper/repository.php
@@ -0,0 +1,203 @@
+<?php
+/**
+ * DokuWiki Plugin extension (Helper Component)
+ *
+ * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
+ * @author Michael Hamann <michael@content-space.de>
+ */
+
+use dokuwiki\Cache\Cache;
+use dokuwiki\HTTP\DokuHTTPClient;
+use dokuwiki\Extension\PluginController;
+
+/**
+ * Class helper_plugin_extension_repository provides access to the extension repository on dokuwiki.org
+ */
+class helper_plugin_extension_repository extends DokuWiki_Plugin
+{
+
+ const EXTENSION_REPOSITORY_API = 'http://www.dokuwiki.org/lib/plugins/pluginrepo/api.php';
+
+ private $loaded_extensions = array();
+ private $has_access = null;
+
+ /**
+ * Initialize the repository (cache), fetches data for all installed plugins
+ */
+ public function init()
+ {
+ /* @var PluginController $plugin_controller */
+ global $plugin_controller;
+ if ($this->hasAccess()) {
+ $list = $plugin_controller->getList('', true);
+ $request_data = array('fmt' => 'php');
+ $request_needed = false;
+ foreach ($list as $name) {
+ $cache = new Cache('##extension_manager##'.$name, '.repo');
+
+ if (!isset($this->loaded_extensions[$name]) &&
+ $this->hasAccess() &&
+ !$cache->useCache(array('age' => 3600 * 24))
+ ) {
+ $this->loaded_extensions[$name] = true;
+ $request_data['ext'][] = $name;
+ $request_needed = true;
+ }
+ }
+
+ if ($request_needed) {
+ $httpclient = new DokuHTTPClient();
+ $data = $httpclient->post(self::EXTENSION_REPOSITORY_API, $request_data);
+ if ($data !== false) {
+ $extensions = unserialize($data);
+ foreach ($extensions as $extension) {
+ $cache = new Cache('##extension_manager##'.$extension['plugin'], '.repo');
+ $cache->storeCache(serialize($extension));
+ }
+ } else {
+ $this->has_access = false;
+ }
+ }
+ }
+ }
+
+ /**
+ * If repository access is available
+ *
+ * @param bool $usecache use cached result if still valid
+ * @return bool If repository access is available
+ */
+ public function hasAccess($usecache = true) {
+ if ($this->has_access === null) {
+ $cache = new Cache('##extension_manager###hasAccess', '.repo');
+
+ if (!$cache->useCache(array('age' => 60*10, 'purge' => !$usecache))) {
+ $httpclient = new DokuHTTPClient();
+ $httpclient->timeout = 5;
+ $data = $httpclient->get(self::EXTENSION_REPOSITORY_API.'?cmd=ping');
+ if ($data !== false) {
+ $this->has_access = true;
+ $cache->storeCache(1);
+ } else {
+ $this->has_access = false;
+ $cache->storeCache(0);
+ }
+ } else {
+ $this->has_access = ($cache->retrieveCache(false) == 1);
+ }
+ }
+ return $this->has_access;
+ }
+
+ /**
+ * Get the remote data of an individual plugin or template
+ *
+ * @param string $name The plugin name to get the data for, template names need to be prefix by 'template:'
+ * @return array The data or null if nothing was found (possibly no repository access)
+ */
+ public function getData($name)
+ {
+ $cache = new Cache('##extension_manager##'.$name, '.repo');
+
+ if (!isset($this->loaded_extensions[$name]) &&
+ $this->hasAccess() &&
+ !$cache->useCache(array('age' => 3600 * 24))
+ ) {
+ $this->loaded_extensions[$name] = true;
+ $httpclient = new DokuHTTPClient();
+ $data = $httpclient->get(self::EXTENSION_REPOSITORY_API.'?fmt=php&ext[]='.urlencode($name));
+ if ($data !== false) {
+ $result = unserialize($data);
+ $cache->storeCache(serialize($result[0]));
+ return $result[0];
+ } else {
+ $this->has_access = false;
+ }
+ }
+ if (file_exists($cache->cache)) {
+ return unserialize($cache->retrieveCache(false));
+ }
+ return array();
+ }
+
+ /**
+ * Search for plugins or templates using the given query string
+ *
+ * @param string $q the query string
+ * @return array a list of matching extensions
+ */
+ public function search($q)
+ {
+ $query = $this->parseQuery($q);
+ $query['fmt'] = 'php';
+
+ $httpclient = new DokuHTTPClient();
+ $data = $httpclient->post(self::EXTENSION_REPOSITORY_API, $query);
+ if ($data === false) return array();
+ $result = unserialize($data);
+
+ $ids = array();
+
+ // store cache info for each extension
+ foreach ($result as $ext) {
+ $name = $ext['plugin'];
+ $cache = new Cache('##extension_manager##'.$name, '.repo');
+ $cache->storeCache(serialize($ext));
+ $ids[] = $name;
+ }
+
+ return $ids;
+ }
+
+ /**
+ * Parses special queries from the query string
+ *
+ * @param string $q
+ * @return array
+ */
+ protected function parseQuery($q)
+ {
+ $parameters = array(
+ 'tag' => array(),
+ 'mail' => array(),
+ 'type' => array(),
+ 'ext' => array()
+ );
+
+ // extract tags
+ if (preg_match_all('/(^|\s)(tag:([\S]+))/', $q, $matches, PREG_SET_ORDER)) {
+ foreach ($matches as $m) {
+ $q = str_replace($m[2], '', $q);
+ $parameters['tag'][] = $m[3];
+ }
+ }
+ // extract author ids
+ if (preg_match_all('/(^|\s)(authorid:([\S]+))/', $q, $matches, PREG_SET_ORDER)) {
+ foreach ($matches as $m) {
+ $q = str_replace($m[2], '', $q);
+ $parameters['mail'][] = $m[3];
+ }
+ }
+ // extract extensions
+ if (preg_match_all('/(^|\s)(ext:([\S]+))/', $q, $matches, PREG_SET_ORDER)) {
+ foreach ($matches as $m) {
+ $q = str_replace($m[2], '', $q);
+ $parameters['ext'][] = $m[3];
+ }
+ }
+ // extract types
+ if (preg_match_all('/(^|\s)(type:([\S]+))/', $q, $matches, PREG_SET_ORDER)) {
+ foreach ($matches as $m) {
+ $q = str_replace($m[2], '', $q);
+ $parameters['type'][] = $m[3];
+ }
+ }
+
+ // FIXME make integer from type value
+
+ $parameters['q'] = trim($q);
+ return $parameters;
+ }
+}
+
+// vim:ts=4:sw=4:et:
diff --git a/platform/www/lib/plugins/extension/images/bug.gif b/platform/www/lib/plugins/extension/images/bug.gif
new file mode 100644
index 0000000..08c1ca1
--- /dev/null
+++ b/platform/www/lib/plugins/extension/images/bug.gif
Binary files differ
diff --git a/platform/www/lib/plugins/extension/images/disabled.png b/platform/www/lib/plugins/extension/images/disabled.png
new file mode 100644
index 0000000..9c18b04
--- /dev/null
+++ b/platform/www/lib/plugins/extension/images/disabled.png
Binary files differ
diff --git a/platform/www/lib/plugins/extension/images/donate.png b/platform/www/lib/plugins/extension/images/donate.png
new file mode 100644
index 0000000..a76dfaa
--- /dev/null
+++ b/platform/www/lib/plugins/extension/images/donate.png
Binary files differ
diff --git a/platform/www/lib/plugins/extension/images/down.png b/platform/www/lib/plugins/extension/images/down.png
new file mode 100644
index 0000000..8e399a9
--- /dev/null
+++ b/platform/www/lib/plugins/extension/images/down.png
Binary files differ
diff --git a/platform/www/lib/plugins/extension/images/enabled.png b/platform/www/lib/plugins/extension/images/enabled.png
new file mode 100644
index 0000000..edbbb5b
--- /dev/null
+++ b/platform/www/lib/plugins/extension/images/enabled.png
Binary files differ
diff --git a/platform/www/lib/plugins/extension/images/icons.xcf b/platform/www/lib/plugins/extension/images/icons.xcf
new file mode 100644
index 0000000..ab69b30
--- /dev/null
+++ b/platform/www/lib/plugins/extension/images/icons.xcf
Binary files differ
diff --git a/platform/www/lib/plugins/extension/images/license.txt b/platform/www/lib/plugins/extension/images/license.txt
new file mode 100644
index 0000000..44e176a
--- /dev/null
+++ b/platform/www/lib/plugins/extension/images/license.txt
@@ -0,0 +1,4 @@
+enabled.png - CC0, (c) Tanguy Ortolo
+disabled.png - public domain, (c) Tango Desktop Project http://commons.wikimedia.org/wiki/File:Dialog-information.svg
+plugin.png - public domain, (c) nicubunu, http://openclipart.org/detail/15093/blue-jigsaw-piece-07-by-nicubunu
+template.png - public domain, (c) mathec, http://openclipart.org/detail/166596/palette-by-mathec
diff --git a/platform/www/lib/plugins/extension/images/overlay.png b/platform/www/lib/plugins/extension/images/overlay.png
new file mode 100644
index 0000000..5414206
--- /dev/null
+++ b/platform/www/lib/plugins/extension/images/overlay.png
Binary files differ
diff --git a/platform/www/lib/plugins/extension/images/plugin.png b/platform/www/lib/plugins/extension/images/plugin.png
new file mode 100644
index 0000000..62424b2
--- /dev/null
+++ b/platform/www/lib/plugins/extension/images/plugin.png
Binary files differ
diff --git a/platform/www/lib/plugins/extension/images/tag.png b/platform/www/lib/plugins/extension/images/tag.png
new file mode 100644
index 0000000..1b1dd75
--- /dev/null
+++ b/platform/www/lib/plugins/extension/images/tag.png
Binary files differ
diff --git a/platform/www/lib/plugins/extension/images/template.png b/platform/www/lib/plugins/extension/images/template.png
new file mode 100644
index 0000000..67240d1
--- /dev/null
+++ b/platform/www/lib/plugins/extension/images/template.png
Binary files differ
diff --git a/platform/www/lib/plugins/extension/images/up.png b/platform/www/lib/plugins/extension/images/up.png
new file mode 100644
index 0000000..531b2dd
--- /dev/null
+++ b/platform/www/lib/plugins/extension/images/up.png
Binary files differ
diff --git a/platform/www/lib/plugins/extension/images/warning.png b/platform/www/lib/plugins/extension/images/warning.png
new file mode 100644
index 0000000..c1af79f
--- /dev/null
+++ b/platform/www/lib/plugins/extension/images/warning.png
Binary files differ
diff --git a/platform/www/lib/plugins/extension/lang/en/intro_install.txt b/platform/www/lib/plugins/extension/lang/en/intro_install.txt
new file mode 100644
index 0000000..a5d5ab0
--- /dev/null
+++ b/platform/www/lib/plugins/extension/lang/en/intro_install.txt
@@ -0,0 +1 @@
+Here you can manually install plugins and templates by either uploading them or providing a direct download URL.
diff --git a/platform/www/lib/plugins/extension/lang/en/intro_plugins.txt b/platform/www/lib/plugins/extension/lang/en/intro_plugins.txt
new file mode 100644
index 0000000..4e42efe
--- /dev/null
+++ b/platform/www/lib/plugins/extension/lang/en/intro_plugins.txt
@@ -0,0 +1 @@
+These are the plugins currently installed in your DokuWiki. You can enable or disable or even completely uninstall them here. Plugin updates are shown here as well, be sure to read the plugin's documentation before updating. \ No newline at end of file
diff --git a/platform/www/lib/plugins/extension/lang/en/intro_search.txt b/platform/www/lib/plugins/extension/lang/en/intro_search.txt
new file mode 100644
index 0000000..81aa431
--- /dev/null
+++ b/platform/www/lib/plugins/extension/lang/en/intro_search.txt
@@ -0,0 +1 @@
+This tab gives you access to all available 3rd party [[doku>plugins|plugins]] and [[doku>template|templates]] for DokuWiki. Please be aware that installing 3rd party code may pose a **security risk**, you may want to read about [[doku>security#plugin_security|plugin security]] first.
diff --git a/platform/www/lib/plugins/extension/lang/en/intro_templates.txt b/platform/www/lib/plugins/extension/lang/en/intro_templates.txt
new file mode 100644
index 0000000..012a749
--- /dev/null
+++ b/platform/www/lib/plugins/extension/lang/en/intro_templates.txt
@@ -0,0 +1 @@
+These are the templates currently installed in your DokuWiki. You can select the template to be used in the [[?do=admin&page=config|Configuration Manager]].
diff --git a/platform/www/lib/plugins/extension/lang/en/lang.php b/platform/www/lib/plugins/extension/lang/en/lang.php
new file mode 100644
index 0000000..f9753ae
--- /dev/null
+++ b/platform/www/lib/plugins/extension/lang/en/lang.php
@@ -0,0 +1,110 @@
+<?php
+/**
+ * English language file for extension plugin
+ *
+ * @author Michael Hamann <michael@content-space.de>
+ * @author Christopher Smith <chris@jalakai.co.uk>
+ */
+
+$lang['menu'] = 'Extension Manager';
+
+$lang['tab_plugins'] = 'Installed Plugins';
+$lang['tab_templates'] = 'Installed Templates';
+$lang['tab_search'] = 'Search and Install';
+$lang['tab_install'] = 'Manual Install';
+
+$lang['notimplemented'] = 'This feature hasn\'t been implemented yet';
+$lang['notinstalled'] = 'This extension is not installed';
+$lang['alreadyenabled'] = 'This extension has already been enabled';
+$lang['alreadydisabled'] = 'This extension has already been disabled';
+$lang['pluginlistsaveerror'] = 'There was an error saving the plugin list';
+$lang['unknownauthor'] = 'Unknown author';
+$lang['unknownversion'] = 'Unknown version';
+
+$lang['btn_info'] = 'Show more info';
+$lang['btn_update'] = 'Update';
+$lang['btn_uninstall'] = 'Uninstall';
+$lang['btn_enable'] = 'Enable';
+$lang['btn_disable'] = 'Disable';
+$lang['btn_install'] = 'Install';
+$lang['btn_reinstall'] = 'Re-install';
+
+$lang['js']['reallydel'] = 'Really uninstall this extension?';
+
+$lang['search_for'] = 'Search Extension:';
+$lang['search'] = 'Search';
+
+$lang['extensionby'] = '<strong>%s</strong> by %s';
+$lang['screenshot'] = 'Screenshot of %s';
+$lang['popularity'] = 'Popularity: %s%%';
+$lang['homepage_link'] = 'Docs';
+$lang['bugs_features'] = 'Bugs';
+$lang['tags'] = 'Tags:';
+$lang['author_hint'] = 'Search extensions by this author';
+$lang['installed'] = 'Installed:';
+$lang['downloadurl'] = 'Download URL:';
+$lang['repository'] = 'Repository:';
+$lang['unknown'] = '<em>unknown</em>';
+$lang['installed_version'] = 'Installed version:';
+$lang['install_date'] = 'Your last update:';
+$lang['available_version'] = 'Available version:';
+$lang['compatible'] = 'Compatible with:';
+$lang['depends'] = 'Depends on:';
+$lang['similar'] = 'Similar to:';
+$lang['conflicts'] = 'Conflicts with:';
+$lang['donate'] = 'Like this?';
+$lang['donate_action'] = 'Buy the author a coffee!';
+$lang['repo_retry'] = 'Retry';
+$lang['provides'] = 'Provides:';
+$lang['status'] = 'Status:';
+$lang['status_installed'] = 'installed';
+$lang['status_not_installed'] = 'not installed';
+$lang['status_protected'] = 'protected';
+$lang['status_enabled'] = 'enabled';
+$lang['status_disabled'] = 'disabled';
+$lang['status_unmodifiable'] = 'unmodifiable';
+$lang['status_plugin'] = 'plugin';
+$lang['status_template'] = 'template';
+$lang['status_bundled'] = 'bundled';
+
+$lang['msg_enabled'] = 'Plugin %s enabled';
+$lang['msg_disabled'] = 'Plugin %s disabled';
+$lang['msg_delete_success'] = 'Extension %s uninstalled';
+$lang['msg_delete_failed'] = 'Uninstalling Extension %s failed';
+$lang['msg_template_install_success'] = 'Template %s installed successfully';
+$lang['msg_template_update_success'] = 'Template %s updated successfully';
+$lang['msg_plugin_install_success'] = 'Plugin %s installed successfully';
+$lang['msg_plugin_update_success'] = 'Plugin %s updated successfully';
+$lang['msg_upload_failed'] = 'Uploading the file failed';
+$lang['msg_nooverwrite'] = 'Extension %s already exists so it is not being overwritten; to overwrite, tick the overwrite option';
+
+$lang['missing_dependency'] = '<strong>Missing or disabled dependency:</strong> %s';
+$lang['security_issue'] = '<strong>Security Issue:</strong> %s';
+$lang['security_warning'] = '<strong>Security Warning:</strong> %s';
+$lang['update_available'] = '<strong>Update:</strong> New version %s is available.';
+$lang['wrong_folder'] = '<strong>Plugin installed incorrectly:</strong> Rename plugin directory "%s" to "%s".';
+$lang['url_change'] = '<strong>URL changed:</strong> Download URL has changed since last download. Check if the new URL is valid before updating the extension.<br />New: %s<br />Old: %s';
+
+$lang['error_badurl'] = 'URLs should start with http or https';
+$lang['error_dircreate'] = 'Unable to create temporary folder to receive download';
+$lang['error_download'] = 'Unable to download the file: %s';
+$lang['error_decompress'] = 'Unable to decompress the downloaded file. This maybe as a result of a bad download, in which case you should try again; or the compression format may be unknown, in which case you will need to download and install manually.';
+$lang['error_findfolder'] = 'Unable to identify extension directory, you need to download and install manually';
+$lang['error_copy'] = 'There was a file copy error while attempting to install files for directory <em>%s</em>: the disk could be full or file access permissions may be incorrect. This may have resulted in a partially installed plugin and leave your wiki installation unstable';
+
+$lang['noperms'] = 'Extension directory is not writable';
+$lang['notplperms'] = 'Template directory is not writable';
+$lang['nopluginperms'] = 'Plugin directory is not writable';
+$lang['git'] = 'This extension was installed via git, you may not want to update it here.';
+$lang['auth'] = 'This auth plugin is not enabled in configuration, consider disabling it.';
+
+$lang['install_url'] = 'Install from URL:';
+$lang['install_upload'] = 'Upload Extension:';
+
+$lang['repo_error'] = 'The plugin repository could not be contacted. Make sure your server is allowed to contact www.dokuwiki.org and check your proxy settings.';
+$lang['nossl'] = 'Your PHP seems to miss SSL support. Downloading will not work for many DokuWiki extensions.';
+
+$lang['js']['display_viewoptions'] = 'View Options:';
+$lang['js']['display_enabled'] = 'enabled';
+$lang['js']['display_disabled'] = 'disabled';
+$lang['js']['display_updatable'] = 'updatable';
diff --git a/platform/www/lib/plugins/extension/plugin.info.txt b/platform/www/lib/plugins/extension/plugin.info.txt
new file mode 100644
index 0000000..7ee84dc
--- /dev/null
+++ b/platform/www/lib/plugins/extension/plugin.info.txt
@@ -0,0 +1,7 @@
+base extension
+author Michael Hamann
+email michael@content-space.de
+date 2015-07-26
+name Extension Manager
+desc Allows managing and installing plugins and templates
+url https://www.dokuwiki.org/plugin:extension
diff --git a/platform/www/lib/plugins/extension/script.js b/platform/www/lib/plugins/extension/script.js
new file mode 100644
index 0000000..7c91580
--- /dev/null
+++ b/platform/www/lib/plugins/extension/script.js
@@ -0,0 +1,145 @@
+jQuery(function(){
+
+ var $extmgr = jQuery('#extension__manager');
+
+ /**
+ * Confirm uninstalling
+ */
+ $extmgr.find('button.uninstall').on('click', function(e){
+ if(!window.confirm(LANG.plugins.extension.reallydel)){
+ e.preventDefault();
+ return false;
+ }
+ return true;
+ });
+
+ /**
+ * very simple lightbox
+ * @link http://webdesign.tutsplus.com/tutorials/htmlcss-tutorials/super-simple-lightbox-with-css-and-jquery/
+ */
+ $extmgr.find('a.extension_screenshot').on('click', function(e) {
+ e.preventDefault();
+
+ //Get clicked link href
+ var image_href = jQuery(this).attr("href");
+
+ // create lightbox if needed
+ var $lightbox = jQuery('#plugin__extensionlightbox');
+ if(!$lightbox.length){
+ $lightbox = jQuery('<div id="plugin__extensionlightbox"><p>Click to close</p><div></div></div>')
+ .appendTo(jQuery('body'))
+ .hide()
+ .on('click', function(){
+ $lightbox.hide();
+ });
+ }
+
+ // fill and show it
+ $lightbox
+ .show()
+ .find('div').html('<img src="' + image_href + '" />');
+
+
+ return false;
+ });
+
+ /**
+ * Enable/Disable extension via AJAX
+ */
+ $extmgr.find('button.disable, button.enable').on('click', function (e) {
+ e.preventDefault();
+ var $btn = jQuery(this);
+
+ // get current state
+ var extension = $btn.attr('name').split('[')[2];
+ extension = extension.substr(0, extension.length - 1);
+ var act = ($btn.hasClass('disable')) ? 'disable' : 'enable';
+
+ // disable while we wait
+ $btn.attr('disabled', 'disabled');
+ $btn.css('cursor', 'wait');
+
+ // execute
+ jQuery.get(
+ DOKU_BASE + 'lib/exe/ajax.php',
+ {
+ call: 'plugin_extension',
+ ext: extension,
+ act: act
+ },
+ function (data) {
+ $btn.css('cursor', '')
+ .removeAttr('disabled')
+ .removeClass('disable')
+ .removeClass('enable')
+ .text(data.label)
+ .addClass(data.reverse)
+ .parents('li')
+ .removeClass('disabled')
+ .removeClass('enabled')
+ .addClass(data.state);
+ }
+ );
+ });
+
+ /**
+ * AJAX detail infos
+ */
+ $extmgr.find('a.info').on('click', function(e){
+ e.preventDefault();
+
+ var $link = jQuery(this);
+ var $details = $link.parent().find('dl.details');
+ if($details.length){
+ $link.toggleClass('close');
+ $details.toggle();
+ return;
+ }
+
+ $link.addClass('close');
+ jQuery.get(
+ DOKU_BASE + 'lib/exe/ajax.php',
+ {
+ call: 'plugin_extension',
+ ext: $link.data('extid'),
+ act: 'info'
+ },
+ function(data){
+ $link.parent().append(data);
+ }
+ );
+ });
+
+ /**
+ Create section for enabling/disabling viewing options
+ */
+ if ( $extmgr.find('.plugins, .templates').hasClass('active') ) {
+ var $extlist = jQuery('#extension__list');
+ $extlist.addClass('hasDisplayOptions');
+
+ var $displayOpts = jQuery('<p>', { id: 'extension__viewoptions'} ).appendTo($extmgr.find( '.panelHeader' ));
+ $displayOpts.append(LANG.plugins.extension.display_viewoptions);
+
+ var displayOptionsHandler = function(){
+ $extlist.toggleClass( this.name );
+ DokuCookie.setValue('ext_'+this.name, $extlist.hasClass(this.name) ? '1' : '0');
+ };
+
+ jQuery(['enabled', 'disabled', 'updatable']).each(function(index, chkName){
+ var $label = jQuery( '<label></label>' )
+ .appendTo($displayOpts);
+ var $input = jQuery( '<input />', { type: 'checkbox', name: chkName })
+ .on('change', displayOptionsHandler)
+ .appendTo($label);
+
+ var previous = DokuCookie.getValue('ext_'+chkName);
+ if(typeof previous === "undefined" || previous == '1') {
+ $input.trigger('click');
+ }
+
+ jQuery( '<span/>' )
+ .append(' '+LANG.plugins.extension['display_'+chkName])
+ .appendTo($label);
+ });
+ }
+});
diff --git a/platform/www/lib/plugins/extension/style.less b/platform/www/lib/plugins/extension/style.less
new file mode 100644
index 0000000..261fa1c
--- /dev/null
+++ b/platform/www/lib/plugins/extension/style.less
@@ -0,0 +1,386 @@
+/*
+ * Extension plugin styles
+ *
+ * @author Christopher Smith <chris@jalakai.co.uk>
+ * @author Piyush Mishra <me@piyushmishra.com>
+ * @author Håkan Sandell <sandell.hakan@gmail.com>
+ * @author Anika Henke <anika@selfthinker.org>
+ */
+
+/**
+ * very simple lightbox
+ * @link http://webdesign.tutsplus.com/tutorials/htmlcss-tutorials/super-simple-lightbox-with-css-and-jquery/
+ */
+#plugin__extensionlightbox {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: url(images/overlay.png) repeat;
+ text-align: center;
+ cursor: pointer;
+ z-index: 9999;
+
+ p {
+ text-align: right;
+ color: #fff;
+ margin-right: 20px;
+ font-size: 12px;
+ }
+
+ img {
+ box-shadow: 0 0 25px #111;
+ max-width: 90%;
+ max-height: 90%;
+ }
+}
+
+/**
+ * general styles
+ */
+#extension__manager {
+ // tab layout - most of it is in the main template
+ ul.tabs li.active a {
+ background-color: @ini_background_alt;
+ border-bottom: solid 1px @ini_background_alt;
+ z-index: 2;
+ }
+ .panelHeader {
+ background-color: @ini_background_alt;
+ margin: 0 0 10px 0;
+ padding: 10px 10px 8px;
+ overflow: hidden;
+ }
+
+ // message spacing
+ div.msg {
+ margin: 0.4em 0 0 0;
+ }
+}
+
+/*
+ * extensions table
+ */
+#extension__list {
+ ul.extensionList {
+ margin-left: 0;
+ margin-right: 0;
+ padding: 0;
+ list-style: none;
+ }
+
+ ul.extensionList li {
+ margin: 0 0 .5em;
+ padding: 0 0 .5em;
+ color: @ini_text;
+ border-bottom: 1px solid @ini_border;
+ overflow: hidden;
+ }
+
+ button {
+ margin-bottom: .3em;
+ }
+}
+
+/**
+ * extension table left column
+ */
+#extension__list .legend {
+ position: relative;
+ width: 75%;
+ float: left;
+
+ // padding
+ > div {
+ padding: 0 .5em 0 132px;
+ border-right: 1px solid @ini_background_alt;
+ overflow: hidden;
+ }
+
+ // screenshot
+ div.screenshot {
+ margin-top: 4px;
+ margin-left: -132px;
+ max-width: 120px;
+ float: left;
+ position: relative;
+
+ img {
+ width: 120px;
+ height: 70px;
+ border-radius: 5px;
+ box-shadow: 2px 2px 2px #666;
+ }
+
+ span {
+ min-height: 24px;
+ min-width: 24px;
+ position: absolute;
+ left: 0;
+ top: 0;
+ }
+ }
+
+ // plugin headline
+ h2 {
+ width: 100%;
+ float: right;
+ margin: 0.2em 0 0.5em;
+ font-size: 100%;
+ font-weight: normal;
+ border: none;
+
+ strong {
+ font-size: 120%;
+ font-weight: bold;
+ vertical-align: baseline;
+ }
+ }
+
+ // description
+ p {
+ margin: 0 0 0.6em 0;
+ }
+
+ // popularity bar
+ div.popularity {
+ background-color: @ini_background;
+ border: 1px solid silver;
+ height: .4em;
+ margin: 0 auto;
+ padding: 1px;
+ width: 5.5em;
+ position: absolute;
+ right: .5em;
+ top: 0.2em;
+
+ div {
+ background-color: @ini_border;
+ height: 100%;
+ }
+ }
+
+ // Docs, Bugs, Tags
+ div.linkbar {
+ font-size: 85%;
+
+ span.tags {
+ padding-left: 18px;
+ background: transparent url(images/tag.png) no-repeat 0 0;
+ }
+
+ a.bugs {
+ padding-left: 18px;
+ background: transparent url(images/bug.gif) no-repeat 0 0;
+ }
+ }
+
+ // more info button
+ a.info {
+ background: transparent url(images/down.png) no-repeat 0 0;
+ border-width: 0;
+ height: 13px;
+ width: 13px;
+ text-indent: -9999px;
+ float: right;
+ margin: .5em 0 0;
+ overflow: hidden;
+
+ &.close {
+ background: transparent url(images/up.png) no-repeat 0 0;
+ }
+ }
+
+ // detailed info box
+ dl.details {
+ margin: 0.4em 0 0 0;
+ font-size: 85%;
+ border-top: 1px solid @ini_background_alt;
+ clear: both;
+
+ dt {
+ clear: left;
+ float: left;
+ width: 25%;
+ margin: 0;
+ text-align: right;
+ font-weight: normal;
+ padding: 0.2em 5px 0 0;
+ font-weight: bold;
+ }
+
+ dd {
+ margin-left: 25%;
+ padding: 0.2em 0 0 5px;
+
+ a.donate {
+ padding-left: 18px;
+ background: transparent url(images/donate.png) left center no-repeat;
+ }
+ }
+ }
+}
+
+[dir=rtl] #extension__list .legend {
+ float: right;
+
+ > div {
+ padding: 0 132px 0 .5em;
+ border-left: 1px solid @ini_background_alt;
+ border-right-width: 0;
+ }
+
+ div.screenshot {
+ margin-left: 0;
+ margin-right: -132px;
+ float: right;
+
+ span {
+ left: auto;
+ right: 0;
+ }
+ }
+
+ h2 {
+ float: left;
+ }
+
+ div.popularity {
+ right: auto;
+ left: .5em;
+ }
+
+ div.linkbar span.tags,
+ dl.details dd a.donate {
+ padding-left: 0;
+ padding-right: 18px;
+ background-position: top right;
+ }
+
+ a.info {
+ float: left;
+ }
+
+ dl.details {
+ dt {
+ clear: right;
+ float: right;
+ text-align: left;
+ padding-left: 5px;
+ padding-right: 0;
+ }
+
+ dd {
+ margin-left: 0;
+ margin-right: 25%;
+ padding-left: 0;
+ padding-right: 5px;
+ }
+ }
+}
+
+/*
+ * Enabled/Disabled overrides
+ */
+#extension__list {
+
+ &.hasDisplayOptions {
+ .enabled,
+ .disabled,
+ .updatable {
+ display: none;
+ }
+
+ &.enabled .enabled,
+ &.disabled .disabled,
+ &.updatable .updatable {
+ display: block;
+ }
+ }
+
+ .enabled div.screenshot span {
+ background: transparent url(images/enabled.png) no-repeat 2px 2px;
+ }
+
+ .disabled div.screenshot span {
+ background: transparent url(images/disabled.png) no-repeat 2px 2px;
+ }
+
+ .disabled .legend {
+ opacity: 0.7;
+ }
+}
+
+/**
+ * extension table right column
+ */
+#extension__manager .actions {
+ padding: 0;
+ font-size: 95%;
+ width: 25%;
+ float: right;
+ text-align: right;
+
+ .version {
+ display: block;
+ }
+
+ p {
+ margin: 0.2em 0;
+ text-align: center;
+ }
+
+ p.permerror {
+ margin-left: 0.4em;
+ text-align: left;
+ padding-left: 19px;
+ background: transparent url(images/warning.png) center left no-repeat;
+ line-height: 18px;
+ font-size: 12px;
+ }
+}
+
+[dir=rtl] #extension__manager .actions {
+ float: left;
+ text-align: left;
+
+ p.permerror {
+ margin-left: 0;
+ margin-right: 0.4em;
+ text-align: right;
+ padding-left: 0;
+ padding-right: 19px;
+ background-position: center right;
+ }
+}
+
+/**
+ * Search form
+ */
+#extension__manager form.search {
+ display: block;
+ margin-bottom: 2em;
+
+ span {
+ font-weight: bold;
+ }
+
+ input.edit {
+ width: 25em;
+ }
+}
+
+/**
+ * Install form
+ */
+#extension__manager form.install {
+ text-align: center;
+ display: block;
+ width: 60%;
+}
+
+#extension__viewoptions label {
+ margin-left: 1em;
+ vertical-align: baseline;
+}
diff --git a/platform/www/lib/plugins/farmer/.github/auto-comment.yml b/platform/www/lib/plugins/farmer/.github/auto-comment.yml
new file mode 100644
index 0000000..f6a72e3
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/.github/auto-comment.yml
@@ -0,0 +1,9 @@
+# auto replies used by probot/auto-comment
+
+issuesOpened: >
+ Thank you for opening this issue.
+
+ [CosmoCode](https://www.cosmocode.de) is a software company in Berlin providing services for wiki, app and web development. As such we can't guarantee quick responses for issues opened on our Open Source projects.
+
+ If you require certain features or bugs fixed, you can always hire us. Feel free to contact us at dokuwiki@cosmocode.de for an offer.
+
diff --git a/platform/www/lib/plugins/farmer/.travis.yml b/platform/www/lib/plugins/farmer/.travis.yml
new file mode 100644
index 0000000..6814beb
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/.travis.yml
@@ -0,0 +1,15 @@
+# Config file for travis-ci.org
+
+language: php
+php:
+ - "7.3"
+ - "7.2"
+ - "7.1"
+ - "7.0"
+ - "5.6"
+env:
+ - DOKUWIKI=master
+ - DOKUWIKI=stable
+before_install: wget https://raw.github.com/splitbrain/dokuwiki-travis/master/travis.sh
+install: sh travis.sh
+script: cd _test && ./phpunit.phar --stderr --group plugin_farmer
diff --git a/platform/www/lib/plugins/farmer/3rdparty/PHPIco.php b/platform/www/lib/plugins/farmer/3rdparty/PHPIco.php
new file mode 100644
index 0000000..17b3b55
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/3rdparty/PHPIco.php
@@ -0,0 +1,248 @@
+<?php
+/*
+Copyright 2011-2013 Chris Jean & iThemes
+Licensed under GPLv2 or above
+
+Version 1.0.2
+
+Adjusted for DokuWiki Farmer Plugin
+*/
+
+namespace chrisbliss18\phpico;
+
+class PHPIco {
+ /**
+ * Images in the BMP format.
+ *
+ * @var array
+ * @access private
+ */
+ var $_images = array();
+
+ /**
+ * Constructor - Create a new ICO generator.
+ *
+ * If the constructor is not passed a file, a file will need to be supplied using the {@link PHP_ICO::add_image}
+ * function in order to generate an ICO file.
+ *
+ * @param bool|string $file Optional. Path to the source image file.
+ * @param array $sizes Optional. An array of sizes (each size is an array with a width and height) that the source image should be rendered at in the generated ICO file. If sizes are not supplied, the size of the source image will be used.
+ * @throws \Exception
+ */
+ function __construct( $file = false, $sizes = array() ) {
+ $required_functions = array(
+ 'getimagesize',
+ 'imagecreatefromstring',
+ 'imagecreatetruecolor',
+ 'imagecolortransparent',
+ 'imagecolorallocatealpha',
+ 'imagealphablending',
+ 'imagesavealpha',
+ 'imagesx',
+ 'imagesy',
+ 'imagecopyresampled',
+ );
+
+ foreach ( $required_functions as $function ) {
+ if ( ! function_exists( $function ) ) {
+ throw new \Exception( "The PHP_ICO class was unable to find the $function function, which is part of the GD library. Ensure that the system has the GD library installed and that PHP has access to it through a PHP interface, such as PHP's GD module. Since this function was not found, the library will be unable to create ICO files." );
+ }
+ }
+
+ if ( false != $file )
+ $this->add_image( $file, $sizes );
+ }
+
+ /**
+ * Add an image to the generator.
+ *
+ * This function adds a source image to the generator. It serves two main purposes: add a source image if one was
+ * not supplied to the constructor and to add additional source images so that different images can be supplied for
+ * different sized images in the resulting ICO file. For instance, a small source image can be used for the small
+ * resolutions while a larger source image can be used for large resolutions.
+ *
+ * @param string $file Path to the source image file.
+ * @param array $sizes Optional. An array of sizes (each size is an array with a width and height) that the source image should be rendered at in the generated ICO file. If sizes are not supplied, the size of the source image will be used.
+ * @return boolean true on success and false on failure.
+ */
+ function add_image( $file, $sizes = array() ) {
+ if ( false === ( $im = $this->_load_image_file( $file ) ) )
+ return false;
+
+
+ if ( empty( $sizes ) )
+ $sizes = array( imagesx( $im ), imagesy( $im ) );
+
+ // If just a single size was passed, put it in array.
+ if ( ! is_array( $sizes[0] ) )
+ $sizes = array( $sizes );
+
+ foreach ( (array) $sizes as $size ) {
+ list( $width, $height ) = $size;
+
+ $new_im = imagecreatetruecolor( $width, $height );
+
+ imagecolortransparent( $new_im, imagecolorallocatealpha( $new_im, 0, 0, 0, 127 ) );
+ imagealphablending( $new_im, false );
+ imagesavealpha( $new_im, true );
+
+ $source_width = imagesx( $im );
+ $source_height = imagesy( $im );
+
+ if ( false === imagecopyresampled( $new_im, $im, 0, 0, 0, 0, $width, $height, $source_width, $source_height ) )
+ continue;
+
+ $this->_add_image_data( $new_im );
+ }
+
+ return true;
+ }
+
+ /**
+ * Write the ICO file data to a file path.
+ *
+ * @param string $file Path to save the ICO file data into.
+ * @return boolean true on success and false on failure.
+ */
+ function save_ico( $file ) {
+ if ( false === ( $data = $this->_get_ico_data() ) )
+ return false;
+
+ if ( false === ( $fh = fopen( $file, 'w' ) ) )
+ return false;
+
+ if ( false === ( fwrite( $fh, $data ) ) ) {
+ fclose( $fh );
+ return false;
+ }
+
+ fclose( $fh );
+
+ return true;
+ }
+
+ /**
+ * Generate the final ICO data by creating a file header and adding the image data.
+ */
+ protected function _get_ico_data() {
+ if ( ! is_array( $this->_images ) || empty( $this->_images ) )
+ return false;
+
+
+ $data = pack( 'vvv', 0, 1, count( $this->_images ) );
+ $pixel_data = '';
+
+ $icon_dir_entry_size = 16;
+
+ $offset = 6 + ( $icon_dir_entry_size * count( $this->_images ) );
+
+ foreach ( $this->_images as $image ) {
+ $data .= pack( 'CCCCvvVV', $image['width'], $image['height'], $image['color_palette_colors'], 0, 1, $image['bits_per_pixel'], $image['size'], $offset );
+ $pixel_data .= $image['data'];
+
+ $offset += $image['size'];
+ }
+
+ $data .= $pixel_data;
+ unset( $pixel_data );
+
+
+ return $data;
+ }
+
+ /**
+ * Take a GD image resource and change it into a raw BMP format.
+ *
+ * @param resource $im
+ */
+ protected function _add_image_data( $im ) {
+ $width = imagesx( $im );
+ $height = imagesy( $im );
+
+
+ $pixel_data = array();
+
+ $opacity_data = array();
+ $current_opacity_val = 0;
+
+ for ( $y = $height - 1; $y >= 0; $y-- ) {
+ for ( $x = 0; $x < $width; $x++ ) {
+ $color = imagecolorat( $im, $x, $y );
+
+ $alpha = ( $color & 0x7F000000 ) >> 24;
+ $alpha = ( 1 - ( $alpha / 127 ) ) * 255;
+
+ $color &= 0xFFFFFF;
+ $color |= 0xFF000000 & ( $alpha << 24 );
+
+ $pixel_data[] = $color;
+
+
+ $opacity = ( $alpha <= 127 ) ? 1 : 0;
+
+ $current_opacity_val = ( $current_opacity_val << 1 ) | $opacity;
+
+ if ( ( ( $x + 1 ) % 32 ) == 0 ) {
+ $opacity_data[] = $current_opacity_val;
+ $current_opacity_val = 0;
+ }
+ }
+
+ if ( ( $x % 32 ) > 0 ) {
+ while ( ( $x++ % 32 ) > 0 )
+ $current_opacity_val = $current_opacity_val << 1;
+
+ $opacity_data[] = $current_opacity_val;
+ $current_opacity_val = 0;
+ }
+ }
+
+ $image_header_size = 40;
+ $color_mask_size = $width * $height * 4;
+ $opacity_mask_size = ( ceil( $width / 32 ) * 4 ) * $height;
+
+
+ $data = pack( 'VVVvvVVVVVV', 40, $width, ( $height * 2 ), 1, 32, 0, 0, 0, 0, 0, 0 );
+
+ foreach ( $pixel_data as $color )
+ $data .= pack( 'V', $color );
+
+ foreach ( $opacity_data as $opacity )
+ $data .= pack( 'N', $opacity );
+
+
+ $image = array(
+ 'width' => $width,
+ 'height' => $height,
+ 'color_palette_colors' => 0,
+ 'bits_per_pixel' => 32,
+ 'size' => $image_header_size + $color_mask_size + $opacity_mask_size,
+ 'data' => $data,
+ );
+
+ $this->_images[] = $image;
+ }
+
+ /**
+ * Read in the source image file and convert it into a GD image resource.
+ *
+ * @param string $file
+ * @return bool|resource
+ */
+ protected function _load_image_file( $file ) {
+ // Run a cheap check to verify that it is an image file.
+ if ( false === ( $size = getimagesize( $file ) ) )
+ return false;
+
+ if ( false === ( $file_data = file_get_contents( $file ) ) )
+ return false;
+
+ if ( false === ( $im = imagecreatefromstring( $file_data ) ) )
+ return false;
+
+ unset( $file_data );
+
+
+ return $im;
+ }
+}
diff --git a/platform/www/lib/plugins/farmer/3rdparty/RingIcon.php b/platform/www/lib/plugins/farmer/3rdparty/RingIcon.php
new file mode 100644
index 0000000..c8dfdbc
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/3rdparty/RingIcon.php
@@ -0,0 +1,186 @@
+<?php
+
+namespace splitbrain\RingIcon;
+
+/**
+ * Class RingIcon
+ *
+ * Generates a identicon/visiglyph like image based on concentric rings
+ *
+ * @todo add a mono color version
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @license MIT
+ * @package splitbrain\RingIcon
+ */
+class RingIcon
+{
+
+ protected $size;
+ protected $fullsize;
+ protected $rings;
+ protected $center;
+ protected $ringwidth;
+ protected $seed;
+ protected $ismono = false;
+ protected $monocolor = null;
+
+ /**
+ * RingIcon constructor.
+ * @param int $size width and height of the resulting image
+ * @param int $rings number of rings
+ */
+ public function __construct($size, $rings = 3)
+ {
+ $this->size = $size;
+ $this->fullsize = $this->size * 5;
+ $this->rings = 4;
+
+ $this->center = floor($this->fullsize / 2);
+ $this->ringwidth = floor($this->fullsize / $rings);
+
+ $this->seed = mt_rand() . time();
+ }
+
+ /**
+ * Generates an ring image
+ *
+ * If a seed is given, the image will be based on that seed
+ *
+ * @param string $seed initialize the genrator with this string
+ * @param string $file if given, the image is saved at that path, otherwise is printed to browser
+ */
+ public function createImage($seed = '', $file = '')
+ {
+ if (!$seed) {
+ $seed = mt_rand() . time();
+ }
+ $this->seed = $seed;
+
+ // monochrome wanted?
+ if($this->ismono) {
+ $this->monocolor = array(
+ $this->rand(20,255),
+ $this->rand(20,255),
+ $this->rand(20,255)
+ );
+ } else {
+ $this->monocolor = null;
+ }
+
+ // create
+ $image = $this->createTransparentImage($this->fullsize, $this->fullsize);
+ $arcwidth = $this->fullsize;
+ for ($i = $this->rings; $i > 0; $i--) {
+ $this->drawRing($image, $arcwidth);
+ $arcwidth -= $this->ringwidth;
+ }
+
+ // resample for antialiasing
+ $out = $this->createTransparentImage($this->size, $this->size);
+ imagecopyresampled($out, $image, 0, 0, 0, 0, $this->size, $this->size, $this->fullsize, $this->fullsize);
+ if ($file) {
+ imagepng($out, $file);
+ } else {
+ header("Content-type: image/png");
+ imagepng($out);
+ }
+ imagedestroy($out);
+ imagedestroy($image);
+ }
+
+ /**
+ * When set to true a monochrome version is returned
+ *
+ * @param bool $ismono
+ */
+ public function setMono($ismono) {
+ $this->ismono = $ismono;
+ }
+
+ /**
+ * Generate number from seed
+ *
+ * Each call runs MD5 on the seed again
+ *
+ * @param int $min
+ * @param int $max
+ * @return int
+ */
+ protected function rand($min, $max)
+ {
+ $this->seed = md5($this->seed);
+ $rand = hexdec(substr($this->seed, 0, 8));
+ return ($rand % ($max - $min + 1)) + $min;
+ }
+
+ /**
+ * Drawas a single ring
+ *
+ * @param resource $image
+ * @param int $arcwidth outer width of the ring
+ */
+ protected function drawRing($image, $arcwidth)
+ {
+ $color = $this->randomColor($image);
+ $transparency = $this->transparentColor($image);
+
+ $start = $this->rand(20, 360);
+ $stop = $this->rand(20, 360);
+ if($stop < $start) list($start, $stop) = array($stop, $start);
+
+ imagefilledarc($image, $this->center, $this->center, $arcwidth, $arcwidth, $stop, $start, $color, IMG_ARC_PIE);
+ imagefilledellipse($image, $this->center, $this->center, $arcwidth - $this->ringwidth,
+ $arcwidth - $this->ringwidth, $transparency);
+
+ imagecolordeallocate($image, $color);
+ imagecolordeallocate($image, $transparency);
+ }
+
+ /**
+ * Allocate a transparent color
+ *
+ * @param resource $image
+ * @return int
+ */
+ protected function transparentColor($image)
+ {
+ return imagecolorallocatealpha($image, 0, 0, 0, 127);
+ }
+
+ /**
+ * Allocate a random color
+ *
+ * @param $image
+ * @return int
+ */
+ protected function randomColor($image)
+ {
+ if($this->ismono) {
+ return imagecolorallocatealpha($image, $this->monocolor[0], $this->monocolor[1], $this->monocolor[2], $this->rand(0, 96));
+ }
+ return imagecolorallocate($image, $this->rand(0, 255), $this->rand(0, 255), $this->rand(0, 255));
+ }
+
+ /**
+ * Create a transparent image
+ *
+ * @param int $width
+ * @param int $height
+ * @return resource
+ * @throws \Exception
+ */
+ protected function createTransparentImage($width, $height)
+ {
+ $image = @imagecreatetruecolor($width, $height);
+ if (!$image) {
+ throw new \Exception('Missing libgd support');
+ }
+ imagealphablending($image, false);
+ $transparency = $this->transparentColor($image);
+ imagefill($image, 0, 0, $transparency);
+ imagecolordeallocate($image, $transparency);
+ imagesavealpha($image, true);
+ return $image;
+ }
+
+}
diff --git a/platform/www/lib/plugins/farmer/DokuWikiFarmCore.php b/platform/www/lib/plugins/farmer/DokuWikiFarmCore.php
new file mode 100644
index 0000000..6c8547a
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/DokuWikiFarmCore.php
@@ -0,0 +1,375 @@
+<?php
+
+/**
+ * Core Manager for the Farm functionality
+ *
+ * This class is initialized before any other DokuWiki code runs. Therefore it is
+ * completely selfcontained and does not use any of DokuWiki's utility functions.
+ *
+ * It's registered as a global $FARMCORE variable but you should not interact with
+ * it directly. Instead use the Farmer plugin's helper component.
+ *
+ * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
+ * @author Andreas Gohr <gohr@cosmocode.de>
+ */
+class DokuWikiFarmCore {
+ /**
+ * @var array The default config - changed by loadConfig
+ */
+ protected $config = array(
+ 'base' => array(
+ 'farmdir' => '',
+ 'farmhost' => '',
+ 'basedomain' => '',
+ ),
+ 'notfound' => array(
+ 'show' => 'farmer',
+ 'url' => ''
+ ),
+ 'inherit' => array(
+ 'main' => 1,
+ 'acronyms' => 1,
+ 'entities' => 1,
+ 'interwiki' => 1,
+ 'license' => 1,
+ 'mime' => 1,
+ 'scheme' => 1,
+ 'smileys' => 1,
+ 'wordblock' => 1,
+ 'users' => 0,
+ 'plugins' => 0,
+ 'userstyle' => 0,
+ 'userscript' => 0,
+ 'styleini' => 0
+ )
+ );
+
+ /** @var string|false The current animal, false for farmer */
+ protected $animal = false;
+ /** @var bool true if an animal was requested but was not found */
+ protected $notfound = false;
+ /** @var bool true if the current animal was requested by host */
+ protected $hostbased = false;
+
+ /**
+ * DokuWikiFarmCore constructor.
+ *
+ * This initializes the whole farm by loading the configuration and setting
+ * DOKU_CONF depending on the requested animal
+ */
+ public function __construct() {
+ $this->loadConfig();
+ if($this->config['base']['farmdir'] === '') return; // farm setup not complete
+ $this->config['base']['farmdir'] = rtrim($this->config['base']['farmdir'], '/').'/'; // trailing slash always
+ define('DOKU_FARMDIR', $this->config['base']['farmdir']);
+
+ // animal?
+ $this->detectAnimal();
+
+ // setup defines
+ define('DOKU_FARM_ANIMAL', $this->animal);
+ if($this->animal) {
+ define('DOKU_CONF', DOKU_FARMDIR . $this->animal . '/conf/');
+ } else {
+ define('DOKU_CONF', DOKU_INC . '/conf/');
+ }
+
+ $this->setupCascade();
+ $this->adjustCascade();
+ }
+
+ /**
+ * @return array the current farm configuration
+ */
+ public function getConfig() {
+ return $this->config;
+ }
+
+ /**
+ * @return false|string
+ */
+ public function getAnimal() {
+ return $this->animal;
+ }
+
+ /**
+ * @return boolean
+ */
+ public function isHostbased() {
+ return $this->hostbased;
+ }
+
+ /**
+ * @return boolean
+ */
+ public function wasNotfound() {
+ return $this->notfound;
+ }
+
+ /**
+ * @return string
+ */
+ public function getAnimalDataDir() {
+ return DOKU_FARMDIR . $this->getAnimal() . '/data/';
+ }
+
+ /**
+ * @return string
+ */
+ public function getAnimalBaseDir() {
+ if($this->isHostbased()) return '/';
+ return getBaseURL() . '!' . $this->getAnimal();
+ }
+
+ /**
+ * Detect the current animal
+ *
+ * Sets internal members $animal, $notfound and $hostbased
+ *
+ * This borrows form DokuWiki's inc/farm.php but does not support a default conf dir
+ */
+ protected function detectAnimal() {
+ $farmdir = $this->config['base']['farmdir'];
+ $farmhost = $this->config['base']['farmhost'];
+
+ // check if animal was set via parameter (rewrite or CLI)
+ $animal = '';
+ if(isset($_REQUEST['animal'])) $animal = $_REQUEST['animal'];
+ if('cli' == php_sapi_name() && isset($_SERVER['animal'])) $animal = $_SERVER['animal'];
+ if($animal) {
+ // check that $animal is a string and just a directory name and not a path
+ if(!is_string($animal) || strpbrk($animal, '\\/') !== false) {
+ $this->notfound = true;
+ return;
+ };
+ $animal = strtolower($animal);
+
+ // check if animal exists
+ if(is_dir("$farmdir/$animal/conf")) {
+ $this->animal = $animal;
+ return;
+ } else {
+ $this->notfound = true;
+ return;
+ }
+ }
+
+ // no host - no host based setup. if we're still here then it's the farmer
+ if(!isset($_SERVER['HTTP_HOST'])) return;
+
+ // is this the farmer?
+ if(strtolower($_SERVER['HTTP_HOST']) == $farmhost) {
+ return;
+ }
+
+ // still here? check for host based
+ $this->hostbased = true;
+ $possible = $this->getAnimalNamesForHost($_SERVER['HTTP_HOST']);
+ foreach($possible as $animal) {
+ if(is_dir("$farmdir/$animal/conf/")) {
+ $this->animal = $animal;
+ return;
+ }
+ }
+
+ // no hit
+ $this->notfound = true;
+ return;
+ }
+
+ /**
+ * Return a list of possible animal names for the given host
+ *
+ * @param string $host the HTTP_HOST header
+ * @return array
+ */
+ protected function getAnimalNamesForHost($host) {
+ $animals = array();
+ $parts = explode('.', implode('.', explode(':', rtrim($host, '.'))));
+ for($j = count($parts); $j > 0; $j--) {
+ // strip from the end
+ $animals[] = implode('.', array_slice($parts, 0, $j));
+ // strip from the end without host part
+ $animals[] = implode('.', array_slice($parts, 1, $j));
+ }
+ $animals = array_unique($animals);
+ $animals = array_filter($animals);
+ usort(
+ $animals,
+ // compare by length, then alphabet
+ function ($a, $b) {
+ $ret = strlen($b) - strlen($a);
+ if($ret != 0) return $ret;
+ return $a > $b;
+ }
+ );
+ return $animals;
+ }
+
+ /**
+ * This sets up the default farming config cascade
+ */
+ protected function setupCascade() {
+ global $config_cascade;
+ $config_cascade = array(
+ 'main' => array(
+ 'default' => array(DOKU_INC . 'conf/dokuwiki.php',),
+ 'local' => array(DOKU_CONF . 'local.php',),
+ 'protected' => array(DOKU_CONF . 'local.protected.php',),
+ ),
+ 'acronyms' => array(
+ 'default' => array(DOKU_INC . 'conf/acronyms.conf',),
+ 'local' => array(DOKU_CONF . 'acronyms.local.conf',),
+ ),
+ 'entities' => array(
+ 'default' => array(DOKU_INC . 'conf/entities.conf',),
+ 'local' => array(DOKU_CONF . 'entities.local.conf',),
+ ),
+ 'interwiki' => array(
+ 'default' => array(DOKU_INC . 'conf/interwiki.conf',),
+ 'local' => array(DOKU_CONF . 'interwiki.local.conf',),
+ ),
+ 'license' => array(
+ 'default' => array(DOKU_INC . 'conf/license.php',),
+ 'local' => array(DOKU_CONF . 'license.local.php',),
+ ),
+ 'manifest' => array(
+ 'default' => array(DOKU_INC . 'conf/manifest.json',),
+ 'local' => array(DOKU_CONF . 'manifest.local.json',),
+ ),
+ 'mediameta' => array(
+ 'default' => array(DOKU_INC . 'conf/mediameta.php',),
+ 'local' => array(DOKU_CONF . 'mediameta.local.php',),
+ ),
+ 'mime' => array(
+ 'default' => array(DOKU_INC . 'conf/mime.conf',),
+ 'local' => array(DOKU_CONF . 'mime.local.conf',),
+ ),
+ 'scheme' => array(
+ 'default' => array(DOKU_INC . 'conf/scheme.conf',),
+ 'local' => array(DOKU_CONF . 'scheme.local.conf',),
+ ),
+ 'smileys' => array(
+ 'default' => array(DOKU_INC . 'conf/smileys.conf',),
+ 'local' => array(DOKU_CONF . 'smileys.local.conf',),
+ ),
+ 'wordblock' => array(
+ 'default' => array(DOKU_INC . 'conf/wordblock.conf',),
+ 'local' => array(DOKU_CONF . 'wordblock.local.conf',),
+ ),
+ 'acl' => array(
+ 'default' => DOKU_CONF . 'acl.auth.php',
+ ),
+ 'plainauth.users' => array(
+ 'default' => DOKU_CONF . 'users.auth.php',
+ ),
+ 'plugins' => array(
+ 'default' => array(DOKU_INC . 'conf/plugins.php',),
+ 'local' => array(DOKU_CONF . 'plugins.local.php',),
+ 'protected' => array(
+ DOKU_INC . 'conf/plugins.required.php',
+ DOKU_CONF . 'plugins.protected.php',
+ ),
+ ),
+ 'userstyle' => array(
+ 'screen' => array(DOKU_CONF . 'userstyle.css', DOKU_CONF . 'userstyle.less',),
+ 'print' => array(DOKU_CONF . 'userprint.css', DOKU_CONF . 'userprint.less',),
+ 'feed' => array(DOKU_CONF . 'userfeed.css', DOKU_CONF . 'userfeed.less',),
+ 'all' => array(DOKU_CONF . 'userall.css', DOKU_CONF . 'userall.less',),
+ ),
+ 'userscript' => array(
+ 'default' => array(DOKU_CONF . 'userscript.js',),
+ ),
+ 'styleini' => array(
+ 'default' => array(DOKU_INC . 'lib/tpl/%TEMPLATE%/' . 'style.ini'),
+ 'local' => array(DOKU_CONF . 'tpl/%TEMPLATE%/' . 'style.ini')
+ ),
+ );
+ }
+
+ /**
+ * This adds additional files to the config cascade based on the inheritence settings
+ *
+ * These are only added for animals, not the farmer
+ */
+ protected function adjustCascade() {
+ // nothing to do when on the farmer:
+ if(!$this->animal) return;
+
+ global $config_cascade;
+ foreach($this->config['inherit'] as $key => $val) {
+ if(!$val) continue;
+
+ // prepare what is to append or prepend
+ $append = array();
+ $prepend = array();
+ if($key == 'main') {
+ $append = array(
+ 'default' => array(DOKU_INC . 'conf/local.php'),
+ 'protected' => array(DOKU_INC . 'lib/plugins/farmer/includes/config.php')
+ );
+ } elseif($key == 'license') {
+ $append = array('default' => array(DOKU_INC . 'conf/' . $key . '.local.php'));
+ } elseif($key == 'userscript') {
+ $prepend = array('default' => array(DOKU_INC . 'conf/userscript.js'));
+ } elseif($key == 'userstyle') {
+ $prepend = array(
+ 'screen' => array(DOKU_INC . 'conf/userstyle.css', DOKU_INC . 'conf/userstyle.less',),
+ 'print' => array(DOKU_INC . 'conf/userprint.css', DOKU_INC . 'conf/userprint.less',),
+ 'feed' => array(DOKU_INC . 'conf/userfeed.css', DOKU_INC . 'conf/userfeed.less',),
+ 'all' => array(DOKU_INC . 'conf/userall.css', DOKU_INC . 'conf/userall.less',),
+ );
+ } elseif ($key == 'styleini') {
+ $append = array(
+ 'local' => array(
+ DOKU_INC . 'conf/tpl/%TEMPLATE%/style.ini'
+ )
+ );
+ } elseif($key == 'users') {
+ $config_cascade['plainauth.users']['protected'] = DOKU_INC . 'conf/users.auth.php';
+ } elseif($key == 'plugins') {
+ $append = array('default' => array(DOKU_INC . 'conf/plugins.local.php'));
+ } else {
+ $append = array('default' => array(DOKU_INC . 'conf/' . $key . '.local.conf'));
+ }
+
+ // add to cascade
+ foreach($prepend as $section => $data) {
+ $config_cascade[$key][$section] = array_merge($data, $config_cascade[$key][$section]);
+ }
+ foreach($append as $section => $data) {
+ $config_cascade[$key][$section] = array_merge($config_cascade[$key][$section], $data);
+ }
+ }
+
+ // add plugin overrides
+ $config_cascade['plugins']['protected'][] = DOKU_INC . 'lib/plugins/farmer/includes/plugins.php';
+ }
+
+ /**
+ * Loads the farm config
+ */
+ protected function loadConfig() {
+ $ini = DOKU_INC . 'conf/farm.ini';
+ if(!file_exists($ini)) return;
+ $config = parse_ini_file($ini, true);
+ foreach(array_keys($this->config) as $section) {
+ if(isset($config[$section])) {
+ $this->config[$section] = array_merge(
+ $this->config[$section],
+ $config[$section]
+ );
+ }
+ }
+
+ $this->config['base']['farmdir'] = trim($this->config['base']['farmdir']);
+ $this->config['base']['farmhost'] = strtolower(trim($this->config['base']['farmhost']));
+ }
+
+}
+
+// initialize it globally
+if(!defined('DOKU_UNITTEST')) {
+ global $FARMCORE;
+ $FARMCORE = new DokuWikiFarmCore();
+}
diff --git a/platform/www/lib/plugins/farmer/README b/platform/www/lib/plugins/farmer/README
new file mode 100644
index 0000000..0515a92
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/README
@@ -0,0 +1,27 @@
+farmer Plugin for DokuWiki
+
+A plugin to help with creating and administring wiki farm animals
+
+All documentation for this plugin can be found at
+https://dokuwiki.org/plugin:farmer
+
+If you install this plugin manually, make sure it is installed in
+lib/plugins/farmer/ - if the folder is called different it
+will not work!
+
+Please refer to http://www.dokuwiki.org/plugins for additional info
+on how to install plugins in DokuWiki.
+
+----
+Copyright (C) Michael Große, Andreas Gohr <dokuwiki@cosmocode.de>
+
+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; version 2 of the License
+
+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.
+
+See the COPYING file in your DokuWiki folder for details
diff --git a/platform/www/lib/plugins/farmer/_animal/conf/acl.auth.php b/platform/www/lib/plugins/farmer/_animal/conf/acl.auth.php
new file mode 100644
index 0000000..14344d7
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/_animal/conf/acl.auth.php
@@ -0,0 +1,21 @@
+# acl.auth.php
+# <?php exit()?>
+# Don't modify the lines above
+#
+# Access Control Lists
+#
+# Editing this file by hand shouldn't be necessary. Use the ACL
+# Manager interface instead.
+#
+# If your auth backend allows special char like spaces in groups
+# or user names you need to urlencode them (only chars <128, leave
+# UTF-8 multibyte chars as is)
+#
+# none 0
+# read 1
+# edit 2
+# create 4
+# upload 8
+# delete 16
+
+* @ALL 8
diff --git a/platform/www/lib/plugins/farmer/_animal/conf/local.php b/platform/www/lib/plugins/farmer/_animal/conf/local.php
new file mode 100644
index 0000000..109a33a
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/_animal/conf/local.php
@@ -0,0 +1,6 @@
+<?php
+/**
+ * Minimal local config
+ */
+$conf['useacl'] = 1;
+$conf['superuser'] = '@admin';
diff --git a/platform/www/lib/plugins/farmer/_animal/data/_dummy b/platform/www/lib/plugins/farmer/_animal/data/_dummy
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/_animal/data/_dummy
diff --git a/platform/www/lib/plugins/farmer/_animal/data/attic/_dummy b/platform/www/lib/plugins/farmer/_animal/data/attic/_dummy
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/_animal/data/attic/_dummy
diff --git a/platform/www/lib/plugins/farmer/_animal/data/cache/_dummy b/platform/www/lib/plugins/farmer/_animal/data/cache/_dummy
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/_animal/data/cache/_dummy
diff --git a/platform/www/lib/plugins/farmer/_animal/data/index/_dummy b/platform/www/lib/plugins/farmer/_animal/data/index/_dummy
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/_animal/data/index/_dummy
diff --git a/platform/www/lib/plugins/farmer/_animal/data/locks/_dummy b/platform/www/lib/plugins/farmer/_animal/data/locks/_dummy
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/_animal/data/locks/_dummy
diff --git a/platform/www/lib/plugins/farmer/_animal/data/log/_dummy b/platform/www/lib/plugins/farmer/_animal/data/log/_dummy
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/_animal/data/log/_dummy
diff --git a/platform/www/lib/plugins/farmer/_animal/data/media/wiki/dokuwiki-128.png b/platform/www/lib/plugins/farmer/_animal/data/media/wiki/dokuwiki-128.png
new file mode 100644
index 0000000..b2306ac
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/_animal/data/media/wiki/dokuwiki-128.png
Binary files differ
diff --git a/platform/www/lib/plugins/farmer/_animal/data/media_attic/_dummy b/platform/www/lib/plugins/farmer/_animal/data/media_attic/_dummy
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/_animal/data/media_attic/_dummy
diff --git a/platform/www/lib/plugins/farmer/_animal/data/media_meta/_dummy b/platform/www/lib/plugins/farmer/_animal/data/media_meta/_dummy
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/_animal/data/media_meta/_dummy
diff --git a/platform/www/lib/plugins/farmer/_animal/data/meta/_dummy b/platform/www/lib/plugins/farmer/_animal/data/meta/_dummy
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/_animal/data/meta/_dummy
diff --git a/platform/www/lib/plugins/farmer/_animal/data/pages/wiki/dokuwiki.txt b/platform/www/lib/plugins/farmer/_animal/data/pages/wiki/dokuwiki.txt
new file mode 100644
index 0000000..e6fac5b
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/_animal/data/pages/wiki/dokuwiki.txt
@@ -0,0 +1,64 @@
+====== DokuWiki ======
+
+[[doku>wiki:dokuwiki|{{wiki:dokuwiki-128.png }}]] DokuWiki is a standards compliant, simple to use [[wp>Wiki]], mainly aimed at creating documentation of any kind. It is targeted at developer teams, workgroups and small companies. It has a simple but powerful [[wiki:syntax]] which makes sure the datafiles remain readable outside the Wiki and eases the creation of structured texts. All data is stored in plain text files -- no database is required.
+
+Read the [[doku>manual|DokuWiki Manual]] to unleash the full power of DokuWiki.
+
+===== Download =====
+
+DokuWiki is available at http://www.splitbrain.org/go/dokuwiki
+
+
+===== Read More =====
+
+All documentation and additional information besides the [[syntax|syntax description]] is maintained in the DokuWiki at [[doku>|www.dokuwiki.org]].
+
+**About DokuWiki**
+
+ * [[doku>features|A feature list]] :!:
+ * [[doku>users|Happy Users]]
+ * [[doku>press|Who wrote about it]]
+ * [[doku>blogroll|What Bloggers think]]
+ * [[http://www.wikimatrix.org/show/DokuWiki|Compare it with other wiki software]]
+
+**Installing DokuWiki**
+
+ * [[doku>requirements|System Requirements]]
+ * [[http://www.splitbrain.org/go/dokuwiki|Download DokuWiki]] :!:
+ * [[doku>changes|Change Log]]
+ * [[doku>Install|How to install or upgrade]] :!:
+ * [[doku>config|Configuration]]
+
+**Using DokuWiki**
+
+ * [[doku>syntax|Wiki Syntax]]
+ * [[doku>manual|The manual]] :!:
+ * [[doku>FAQ|Frequently Asked Questions (FAQ)]]
+ * [[doku>glossary|Glossary]]
+ * [[http://search.dokuwiki.org|Search for DokuWiki help and documentation]]
+
+**Customizing DokuWiki**
+
+ * [[doku>tips|Tips and Tricks]]
+ * [[doku>Template|How to create and use templates]]
+ * [[doku>plugins|Installing plugins]]
+ * [[doku>development|Development Resources]]
+
+**DokuWiki Feedback and Community**
+
+ * [[doku>newsletter|Subscribe to the newsletter]] :!:
+ * [[doku>mailinglist|Join the mailing list]]
+ * [[http://forum.dokuwiki.org|Check out the user forum]]
+ * [[doku>irc|Talk to other users in the IRC channel]]
+ * [[http://bugs.splitbrain.org/index.php?project=1|Submit bugs and feature wishes]]
+ * [[http://www.wikimatrix.org/forum/viewforum.php?id=10|Share your experiences in the WikiMatrix forum]]
+ * [[doku>thanks|Some humble thanks]]
+
+
+===== Copyright =====
+
+2004-2010 (c) Andreas Gohr <andi@splitbrain.org>((Please do not contact me for help and support -- use the [[doku>mailinglist]] or [[http://forum.dokuwiki.org|forum]] instead)) and the DokuWiki Community
+
+The DokuWiki engine is licensed under [[http://www.gnu.org/licenses/gpl.html|GNU General Public License]] Version 2. If you use DokuWiki in your company, consider [[doku>donate|donating]] a few bucks ;-).
+
+Not sure what this means? See the [[doku>faq:license|FAQ on the Licenses]].
diff --git a/platform/www/lib/plugins/farmer/_animal/data/pages/wiki/syntax.txt b/platform/www/lib/plugins/farmer/_animal/data/pages/wiki/syntax.txt
new file mode 100644
index 0000000..dd3154e
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/_animal/data/pages/wiki/syntax.txt
@@ -0,0 +1,486 @@
+====== Formatting Syntax ======
+
+[[doku>DokuWiki]] supports some simple markup language, which tries to make the datafiles to be as readable as possible. This page contains all possible syntax you may use when editing the pages. Simply have a look at the source of this page by pressing the //Edit this page// button at the top or bottom of the page. If you want to try something, just use the [[playground:playground|playground]] page. The simpler markup is easily accessible via [[doku>toolbar|quickbuttons]], too.
+
+===== Basic Text Formatting =====
+
+DokuWiki supports **bold**, //italic//, __underlined__ and ''monospaced'' texts. Of course you can **__//''combine''//__** all these.
+
+ DokuWiki supports **bold**, //italic//, __underlined__ and ''monospaced'' texts.
+ Of course you can **__//''combine''//__** all these.
+
+You can use <sub>subscript</sub> and <sup>superscript</sup>, too.
+
+ You can use <sub>subscript</sub> and <sup>superscript</sup>, too.
+
+You can mark something as <del>deleted</del> as well.
+
+ You can mark something as <del>deleted</del> as well.
+
+**Paragraphs** are created from blank lines. If you want to **force a newline** without a paragraph, you can use two backslashes followed by a whitespace or the end of line.
+
+This is some text with some linebreaks\\ Note that the
+two backslashes are only recognized at the end of a line\\
+or followed by\\ a whitespace \\this happens without it.
+
+ This is some text with some linebreaks\\ Note that the
+ two backslashes are only recognized at the end of a line\\
+ or followed by\\ a whitespace \\this happens without it.
+
+You should use forced newlines only if really needed.
+
+===== Links =====
+
+DokuWiki supports multiple ways of creating links.
+
+==== External ====
+
+External links are recognized automagically: http://www.google.com or simply www.google.com - You can set the link text as well: [[http://www.google.com|This Link points to google]]. Email addresses like this one: <andi@splitbrain.org> are recognized, too.
+
+ DokuWiki supports multiple ways of creating links. External links are recognized
+ automagically: http://www.google.com or simply www.google.com - You can set
+ link text as well: [[http://www.google.com|This Link points to google]]. Email
+ addresses like this one: <andi@splitbrain.org> are recognized, too.
+
+==== Internal ====
+
+Internal links are created by using square brackets. You can either just give a [[pagename]] or use an additional [[pagename|link text]].
+
+ Internal links are created by using square brackets. You can either just give
+ a [[pagename]] or use an additional [[pagename|link text]].
+
+[[doku>pagename|Wiki pagenames]] are converted to lowercase automatically, special characters are not allowed.
+
+You can use [[some:namespaces]] by using a colon in the pagename.
+
+ You can use [[some:namespaces]] by using a colon in the pagename.
+
+For details about namespaces see [[doku>namespaces]].
+
+Linking to a specific section is possible, too. Just add the section name behind a hash character as known from HTML. This links to [[syntax#internal|this Section]].
+
+ This links to [[syntax#internal|this Section]].
+
+Notes:
+
+ * Links to [[syntax|existing pages]] are shown in a different style from [[nonexisting]] ones.
+ * DokuWiki does not use [[wp>CamelCase]] to automatically create links by default, but this behavior can be enabled in the [[doku>config]] file. Hint: If DokuWiki is a link, then it's enabled.
+ * When a section's heading is changed, its bookmark changes, too. So don't rely on section linking too much.
+
+==== Interwiki ====
+
+DokuWiki supports [[doku>Interwiki]] links. These are quick links to other Wikis. For example this is a link to Wikipedia's page about Wikis: [[wp>Wiki]].
+
+ DokuWiki supports [[doku>Interwiki]] links. These are quick links to other Wikis.
+ For example this is a link to Wikipedia's page about Wikis: [[wp>Wiki]].
+
+==== Windows Shares ====
+
+Windows shares like [[\\server\share|this]] are recognized, too. Please note that these only make sense in a homogeneous user group like a corporate [[wp>Intranet]].
+
+ Windows Shares like [[\\server\share|this]] are recognized, too.
+
+Notes:
+
+ * For security reasons direct browsing of windows shares only works in Microsoft Internet Explorer per default (and only in the "local zone").
+ * For Mozilla and Firefox it can be enabled through different workaround mentioned in the [[http://kb.mozillazine.org/Links_to_local_pages_do_not_work|Mozilla Knowledge Base]]. However, there will still be a JavaScript warning about trying to open a Windows Share. To remove this warning (for all users), put the following line in ''conf/local.protected.php'':
+
+ $lang['js']['nosmblinks'] = '';
+
+==== Image Links ====
+
+You can also use an image to link to another internal or external page by combining the syntax for links and [[#images_and_other_files|images]] (see below) like this:
+
+ [[http://www.php.net|{{wiki:dokuwiki-128.png}}]]
+
+[[http://www.php.net|{{wiki:dokuwiki-128.png}}]]
+
+Please note: The image formatting is the only formatting syntax accepted in link names.
+
+The whole [[#images_and_other_files|image]] and [[#links|link]] syntax is supported (including image resizing, internal and external images and URLs and interwiki links).
+
+===== Footnotes =====
+
+You can add footnotes ((This is a footnote)) by using double parentheses.
+
+ You can add footnotes ((This is a footnote)) by using double parentheses.
+
+===== Sectioning =====
+
+You can use up to five different levels of headlines to structure your content. If you have more than three headlines, a table of contents is generated automatically -- this can be disabled by including the string ''<nowiki>~~NOTOC~~</nowiki>'' in the document.
+
+==== Headline Level 3 ====
+=== Headline Level 4 ===
+== Headline Level 5 ==
+
+ ==== Headline Level 3 ====
+ === Headline Level 4 ===
+ == Headline Level 5 ==
+
+By using four or more dashes, you can make a horizontal line:
+
+----
+
+===== Images and Other Files =====
+
+You can include external and internal [[doku>images]] with curly brackets. Optionally you can specify the size of them.
+
+Real size: {{wiki:dokuwiki-128.png}}
+
+Resize to given width: {{wiki:dokuwiki-128.png?50}}
+
+Resize to given width and height((when the aspect ratio of the given width and height doesn't match that of the image, it will be cropped to the new ratio before resizing)): {{wiki:dokuwiki-128.png?200x50}}
+
+Resized external image: {{http://de3.php.net/images/php.gif?200x50}}
+
+ Real size: {{wiki:dokuwiki-128.png}}
+ Resize to given width: {{wiki:dokuwiki-128.png?50}}
+ Resize to given width and height: {{wiki:dokuwiki-128.png?200x50}}
+ Resized external image: {{http://de3.php.net/images/php.gif?200x50}}
+
+
+By using left or right whitespaces you can choose the alignment.
+
+{{ wiki:dokuwiki-128.png}}
+
+{{wiki:dokuwiki-128.png }}
+
+{{ wiki:dokuwiki-128.png }}
+
+ {{ wiki:dokuwiki-128.png}}
+ {{wiki:dokuwiki-128.png }}
+ {{ wiki:dokuwiki-128.png }}
+
+Of course, you can add a title (displayed as a tooltip by most browsers), too.
+
+{{ wiki:dokuwiki-128.png |This is the caption}}
+
+ {{ wiki:dokuwiki-128.png |This is the caption}}
+
+If you specify a filename (external or internal) that is not an image (''gif, jpeg, png''), then it will be displayed as a link instead.
+
+For linking an image to another page see [[#Image Links]] above.
+
+===== Lists =====
+
+Dokuwiki supports ordered and unordered lists. To create a list item, indent your text by two spaces and use a ''*'' for unordered lists or a ''-'' for ordered ones.
+
+ * This is a list
+ * The second item
+ * You may have different levels
+ * Another item
+
+ - The same list but ordered
+ - Another item
+ - Just use indention for deeper levels
+ - That's it
+
+<code>
+ * This is a list
+ * The second item
+ * You may have different levels
+ * Another item
+
+ - The same list but ordered
+ - Another item
+ - Just use indention for deeper levels
+ - That's it
+</code>
+
+Also take a look at the [[doku>faq:lists|FAQ on list items]].
+
+===== Text Conversions =====
+
+DokuWiki can convert certain pre-defined characters or strings into images or other text or HTML.
+
+The text to image conversion is mainly done for smileys. And the text to HTML conversion is used for typography replacements, but can be configured to use other HTML as well.
+
+==== Text to Image Conversions ====
+
+DokuWiki converts commonly used [[wp>emoticon]]s to their graphical equivalents. Those [[doku>Smileys]] and other images can be configured and extended. Here is an overview of Smileys included in DokuWiki:
+
+ * 8-) %% 8-) %%
+ * 8-O %% 8-O %%
+ * :-( %% :-( %%
+ * :-) %% :-) %%
+ * =) %% =) %%
+ * :-/ %% :-/ %%
+ * :-\ %% :-\ %%
+ * :-? %% :-? %%
+ * :-D %% :-D %%
+ * :-P %% :-P %%
+ * :-O %% :-O %%
+ * :-X %% :-X %%
+ * :-| %% :-| %%
+ * ;-) %% ;-) %%
+ * ^_^ %% ^_^ %%
+ * :?: %% :?: %%
+ * :!: %% :!: %%
+ * LOL %% LOL %%
+ * FIXME %% FIXME %%
+ * DELETEME %% DELETEME %%
+
+==== Text to HTML Conversions ====
+
+Typography: [[DokuWiki]] can convert simple text characters to their typographically correct entities. Here is an example of recognized characters.
+
+-> <- <-> => <= <=> >> << -- --- 640x480 (c) (tm) (r)
+"He thought 'It's a man's world'..."
+
+<code>
+-> <- <-> => <= <=> >> << -- --- 640x480 (c) (tm) (r)
+"He thought 'It's a man's world'..."
+</code>
+
+The same can be done to produce any kind of HTML, it just needs to be added to the [[doku>entities|pattern file]].
+
+There are three exceptions which do not come from that pattern file: multiplication entity (640x480), 'single' and "double quotes". They can be turned off through a [[doku>config:typography|config option]].
+
+===== Quoting =====
+
+Some times you want to mark some text to show it's a reply or comment. You can use the following syntax:
+
+ I think we should do it
+
+ > No we shouldn't
+
+ >> Well, I say we should
+
+ > Really?
+
+ >> Yes!
+
+ >>> Then lets do it!
+
+I think we should do it
+
+> No we shouldn't
+
+>> Well, I say we should
+
+> Really?
+
+>> Yes!
+
+>>> Then lets do it!
+
+===== Tables =====
+
+DokuWiki supports a simple syntax to create tables.
+
+^ Heading 1 ^ Heading 2 ^ Heading 3 ^
+| Row 1 Col 1 | Row 1 Col 2 | Row 1 Col 3 |
+| Row 2 Col 1 | some colspan (note the double pipe) ||
+| Row 3 Col 1 | Row 3 Col 2 | Row 3 Col 3 |
+
+Table rows have to start and end with a ''|'' for normal rows or a ''^'' for headers.
+
+ ^ Heading 1 ^ Heading 2 ^ Heading 3 ^
+ | Row 1 Col 1 | Row 1 Col 2 | Row 1 Col 3 |
+ | Row 2 Col 1 | some colspan (note the double pipe) ||
+ | Row 3 Col 1 | Row 3 Col 2 | Row 3 Col 3 |
+
+To connect cells horizontally, just make the next cell completely empty as shown above. Be sure to have always the same amount of cell separators!
+
+Vertical tableheaders are possible, too.
+
+| ^ Heading 1 ^ Heading 2 ^
+^ Heading 3 | Row 1 Col 2 | Row 1 Col 3 |
+^ Heading 4 | no colspan this time | |
+^ Heading 5 | Row 2 Col 2 | Row 2 Col 3 |
+
+As you can see, it's the cell separator before a cell which decides about the formatting:
+
+ | ^ Heading 1 ^ Heading 2 ^
+ ^ Heading 3 | Row 1 Col 2 | Row 1 Col 3 |
+ ^ Heading 4 | no colspan this time | |
+ ^ Heading 5 | Row 2 Col 2 | Row 2 Col 3 |
+
+You can have rowspans (vertically connected cells) by adding '':::'' into the cells below the one to which they should connect.
+
+^ Heading 1 ^ Heading 2 ^ Heading 3 ^
+| Row 1 Col 1 | this cell spans vertically | Row 1 Col 3 |
+| Row 2 Col 1 | ::: | Row 2 Col 3 |
+| Row 3 Col 1 | ::: | Row 2 Col 3 |
+
+Apart from the rowspan syntax those cells should not contain anything else.
+
+ ^ Heading 1 ^ Heading 2 ^ Heading 3 ^
+ | Row 1 Col 1 | this cell spans vertically | Row 1 Col 3 |
+ | Row 2 Col 1 | ::: | Row 2 Col 3 |
+ | Row 3 Col 1 | ::: | Row 2 Col 3 |
+
+You can align the table contents, too. Just add at least two whitespaces at the opposite end of your text: Add two spaces on the left to align right, two spaces on the right to align left and two spaces at least at both ends for centered text.
+
+^ Table with alignment ^^^
+| right| center |left |
+|left | right| center |
+| xxxxxxxxxxxx | xxxxxxxxxxxx | xxxxxxxxxxxx |
+
+This is how it looks in the source:
+
+ ^ Table with alignment ^^^
+ | right| center |left |
+ |left | right| center |
+ | xxxxxxxxxxxx | xxxxxxxxxxxx | xxxxxxxxxxxx |
+
+Note: Vertical alignment is not supported.
+
+===== No Formatting =====
+
+If you need to display text exactly like it is typed (without any formatting), enclose the area either with ''%%<nowiki>%%'' tags or even simpler, with double percent signs ''<nowiki>%%</nowiki>''.
+
+<nowiki>
+This is some text which contains addresses like this: http://www.splitbrain.org and **formatting**, but nothing is done with it.
+</nowiki>
+The same is true for %%//__this__ text// with a smiley ;-)%%.
+
+ <nowiki>
+ This is some text which contains addresses like this: http://www.splitbrain.org and **formatting**, but nothing is done with it.
+ </nowiki>
+ The same is true for %%//__this__ text// with a smiley ;-)%%.
+
+===== Code Blocks =====
+
+You can include code blocks into your documents by either indenting them by at least two spaces (like used for the previous examples) or by using the tags ''%%<code>%%'' or ''%%<file>%%''.
+
+ This is text is indented by two spaces.
+
+<code>
+This is preformatted code all spaces are preserved: like <-this
+</code>
+
+<file>
+This is pretty much the same, but you could use it to show that you quoted a file.
+</file>
+
+Those blocks were created by this source:
+
+ This is text is indented by two spaces.
+
+ <code>
+ This is preformatted code all spaces are preserved: like <-this
+ </code>
+
+ <file>
+ This is pretty much the same, but you could use it to show that you quoted a file.
+ </file>
+
+==== Syntax Highlighting ====
+
+[[wiki:DokuWiki]] can highlight sourcecode, which makes it easier to read. It uses the [[http://qbnz.com/highlighter/|GeSHi]] Generic Syntax Highlighter -- so any language supported by GeSHi is supported. The syntax uses the same code and file blocks described in the previous section, but this time the name of the language syntax to be highlighted is included inside the tag, e.g. ''<nowiki><code java></nowiki>'' or ''<nowiki><file java></nowiki>''.
+
+<code java>
+/**
+ * The HelloWorldApp class implements an application that
+ * simply displays "Hello World!" to the standard output.
+ */
+class HelloWorldApp {
+ public static void main(String[] args) {
+ System.out.println("Hello World!"); //Display the string.
+ }
+}
+</code>
+
+The following language strings are currently recognized: //4cs, 6502acme, 6502kickass, 6502tasm, 68000devpac, abap, actionscript-french, actionscript, actionscript3, ada, algol68, apache, applescript, asm, asp, autoconf, autohotkey, autoit, avisynth, awk, bascomavr, bash, basic4gl, bf, bibtex, blitzbasic, bnf, boo, c, c_loadrunner, c_mac, caddcl, cadlisp, cfdg, cfm, chaiscript, cil, clojure, cmake, cobol, coffeescript, cpp, cpp-qt, csharp, css, cuesheet, d, dcs, delphi, diff, div, dos, dot, e, epc, ecmascript, eiffel, email, erlang, euphoria, f1, falcon, fo, fortran, freebasic, fsharp, gambas, genero, genie, gdb, glsl, gml, gnuplot, go, groovy, gettext, gwbasic, haskell, hicest, hq9plus, html, html5, icon, idl, ini, inno, intercal, io, j, java5, java, javascript, jquery, kixtart, klonec, klonecpp, latex, lb, lisp, llvm, locobasic, logtalk, lolcode, lotusformulas, lotusscript, lscript, lsl2, lua, m68k, magiksf, make, mapbasic, matlab, mirc, modula2, modula3, mmix, mpasm, mxml, mysql, newlisp, nsis, oberon2, objc, objeck, ocaml-brief, ocaml, oobas, oracle8, oracle11, oxygene, oz, pascal, pcre, perl, perl6, per, pf, php-brief, php, pike, pic16, pixelbender, pli, plsql, postgresql, povray, powerbuilder, powershell, proftpd, progress, prolog, properties, providex, purebasic, pycon, python, q, qbasic, rails, rebol, reg, robots, rpmspec, rsplus, ruby, sas, scala, scheme, scilab, sdlbasic, smalltalk, smarty, sql, systemverilog, tcl, teraterm, text, thinbasic, tsql, typoscript, unicon, uscript, vala, vbnet, vb, verilog, vhdl, vim, visualfoxpro, visualprolog, whitespace, winbatch, whois, xbasic, xml, xorg_conf, xpp, yaml, z80, zxbasic//
+
+==== Downloadable Code Blocks ====
+
+When you use the ''%%<code>%%'' or ''%%<file>%%'' syntax as above, you might want to make the shown code available for download as well. You can do this by specifying a file name after language code like this:
+
+<code>
+<file php myexample.php>
+<?php echo "hello world!"; ?>
+</file>
+</code>
+
+<file php myexample.php>
+<?php echo "hello world!"; ?>
+</file>
+
+If you don't want any highlighting but want a downloadable file, specify a dash (''-'') as the language code: ''%%<code - myfile.foo>%%''.
+
+
+===== Embedding HTML and PHP =====
+
+You can embed raw HTML or PHP code into your documents by using the ''%%<html>%%'' or ''%%<php>%%'' tags. (Use uppercase tags if you need to enclose block level elements.)
+
+HTML example:
+
+<code>
+<html>
+This is some <span style="color:red;font-size:150%;">inline HTML</span>
+</html>
+<HTML>
+<p style="border:2px dashed red;">And this is some block HTML</p>
+</HTML>
+</code>
+
+<html>
+This is some <span style="color:red;font-size:150%;">inline HTML</span>
+</html>
+<HTML>
+<p style="border:2px dashed red;">And this is some block HTML</p>
+</HTML>
+
+PHP example:
+
+<code>
+<php>
+echo 'A logo generated by PHP:';
+echo '<img src="' . $_SERVER['PHP_SELF'] . '?=' . php_logo_guid() . '" alt="PHP Logo !" />';
+echo '(generated inline HTML)';
+</php>
+<PHP>
+echo '<table class="inline"><tr><td>The same, but inside a block level element:</td>';
+echo '<td><img src="' . $_SERVER['PHP_SELF'] . '?=' . php_logo_guid() . '" alt="PHP Logo !" /></td>';
+echo '</tr></table>';
+</PHP>
+</code>
+
+<php>
+echo 'A logo generated by PHP:';
+echo '<img src="' . $_SERVER['PHP_SELF'] . '?=' . php_logo_guid() . '" alt="PHP Logo !" />';
+echo '(inline HTML)';
+</php>
+<PHP>
+echo '<table class="inline"><tr><td>The same, but inside a block level element:</td>';
+echo '<td><img src="' . $_SERVER['PHP_SELF'] . '?=' . php_logo_guid() . '" alt="PHP Logo !" /></td>';
+echo '</tr></table>';
+</PHP>
+
+**Please Note**: HTML and PHP embedding is disabled by default in the configuration. If disabled, the code is displayed instead of executed.
+
+===== RSS/ATOM Feed Aggregation =====
+[[DokuWiki]] can integrate data from external XML feeds. For parsing the XML feeds, [[http://simplepie.org/|SimplePie]] is used. All formats understood by SimplePie can be used in DokuWiki as well. You can influence the rendering by multiple additional space separated parameters:
+
+^ Parameter ^ Description ^
+| any number | will be used as maximum number items to show, defaults to 8 |
+| reverse | display the last items in the feed first |
+| author | show item authors names |
+| date | show item dates |
+| description| show the item description. If [[doku>config:htmlok|HTML]] is disabled all tags will be stripped |
+| //n//[dhm] | refresh period, where d=days, h=hours, m=minutes. (e.g. 12h = 12 hours). |
+
+The refresh period defaults to 4 hours. Any value below 10 minutes will be treated as 10 minutes. [[wiki:DokuWiki]] will generally try to supply a cached version of a page, obviously this is inappropriate when the page contains dynamic external content. The parameter tells [[wiki:DokuWiki]] to re-render the page if it is more than //refresh period// since the page was last rendered.
+
+**Example:**
+
+ {{rss>http://slashdot.org/index.rss 5 author date 1h }}
+
+{{rss>http://slashdot.org/index.rss 5 author date 1h }}
+
+
+===== Control Macros =====
+
+Some syntax influences how DokuWiki renders a page without creating any output it self. The following control macros are availble:
+
+^ Macro ^ Description |
+| %%~~NOTOC~~%% | If this macro is found on the page, no table of contents will be created |
+| %%~~NOCACHE~~%% | DokuWiki caches all output by default. Sometimes this might not be wanted (eg. when the %%<php>%% syntax above is used), adding this macro will force DokuWiki to rerender a page on every call |
+
+===== Syntax Plugins =====
+
+DokuWiki's syntax can be extended by [[doku>plugins|Plugins]]. How the installed plugins are used is described on their appropriate description pages. The following syntax plugins are available in this particular DokuWiki installation:
+
+~~INFO:syntaxplugins~~
diff --git a/platform/www/lib/plugins/farmer/_animal/data/tmp/_dummy b/platform/www/lib/plugins/farmer/_animal/data/tmp/_dummy
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/_animal/data/tmp/_dummy
diff --git a/platform/www/lib/plugins/farmer/_test/core.test.php b/platform/www/lib/plugins/farmer/_test/core.test.php
new file mode 100644
index 0000000..d6a1dab
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/_test/core.test.php
@@ -0,0 +1,40 @@
+<?php
+namespace plugin\struct\test;
+
+require_once(__DIR__ . '/../DokuWikiFarmCore.php');
+
+class DokuWikiFarmCore extends \DokuWikiFarmCore {
+ public function getAnimalNamesForHost($host) {
+ return parent::getAnimalNamesForHost($host);
+ }
+}
+
+
+/**
+ * @group plugin_farmer
+ * @group plugins
+ */
+class core_plugin_farmer_test extends \DokuWikiTest {
+
+ protected $pluginsEnabled = array('farmer');
+
+
+ public function test_hostsAnimals() {
+ $core = new DokuWikiFarmCore();
+
+ $input = 'www.foobar.example.com:8000';
+ $expect = array(
+ 'www.foobar.example.com.8000',
+ 'foobar.example.com.8000',
+ 'www.foobar.example.com',
+ 'foobar.example.com',
+ 'www.foobar.example',
+ 'foobar.example',
+ 'www.foobar',
+ 'foobar',
+ 'www',
+ );
+
+ $this->assertEquals($expect, $core->getAnimalNamesForHost($input));
+ }
+}
diff --git a/platform/www/lib/plugins/farmer/_test/general.test.php b/platform/www/lib/plugins/farmer/_test/general.test.php
new file mode 100644
index 0000000..af45139
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/_test/general.test.php
@@ -0,0 +1,36 @@
+<?php
+
+/**
+ * General tests for the farmer plugin
+ *
+ * @group plugin_farmer
+ * @group plugins
+ */
+class general_plugin_farmer_test extends DokuWikiTest {
+
+ protected $pluginsEnabled = array('farmer');
+
+ /**
+ * Simple test to make sure the plugin.info.txt is in correct format
+ */
+ public function test_plugininfo() {
+ $file = __DIR__ . '/../plugin.info.txt';
+ $this->assertFileExists($file);
+
+ $info = confToHash($file);
+
+ $this->assertArrayHasKey('base', $info);
+ $this->assertArrayHasKey('author', $info);
+ $this->assertArrayHasKey('email', $info);
+ $this->assertArrayHasKey('date', $info);
+ $this->assertArrayHasKey('name', $info);
+ $this->assertArrayHasKey('desc', $info);
+ $this->assertArrayHasKey('url', $info);
+
+ $this->assertEquals('farmer', $info['base']);
+ $this->assertRegExp('/^https?:\/\//', $info['url']);
+ $this->assertTrue(mail_isvalid($info['email']));
+ $this->assertRegExp('/^\d\d\d\d-\d\d-\d\d$/', $info['date']);
+ $this->assertTrue(false !== strtotime($info['date']));
+ }
+}
diff --git a/platform/www/lib/plugins/farmer/_test/getUserLine.test.php b/platform/www/lib/plugins/farmer/_test/getUserLine.test.php
new file mode 100644
index 0000000..8f228cb
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/_test/getUserLine.test.php
@@ -0,0 +1,86 @@
+<?php
+
+namespace plugin\struct\test;
+
+class admin_plugin_farmer_new extends \admin_plugin_farmer_new {
+ public function getAdminLine() {
+ return parent::getAdminLine();
+ }
+
+}
+
+
+/**
+ * Tests for the validation functionality of the farmer plugin
+ *
+ * @group plugin_farmer
+ * @group plugins
+ */
+class getUserLine_plugin_farmer_test extends \DokuWikiTest {
+
+ protected $pluginsEnabled = array('farmer',);
+ private $usersfile;
+
+ public function setUp() {
+ parent::setUp();
+ $this->usersfile = DOKU_CONF . 'users.auth.php';
+ copy($this->usersfile, $this->usersfile . "org");
+ unlink($this->usersfile);
+ }
+
+ public function tearDown() {
+ parent::tearDown();
+ unlink($this->usersfile);
+ copy($this->usersfile . "org", $this->usersfile);
+ unlink($this->usersfile . "org");
+ }
+
+
+ public function test_getUserLine_oneUser () {
+ $helper = new admin_plugin_farmer_new();
+ $usersfileData = "# users.auth.php
+# <?php exit()?>
+# Don't modify the lines above
+#
+# Userfile
+#
+# Format:
+#
+# user:MD5password:Real Name:email:groups,comma,seperated
+#
+# testuser : testpass
+testuser:179ad45c6ce2cb97cf1029e212046e81:Arthur Dent:arthur@example.com:\n";
+ file_put_contents($this->usersfile,$usersfileData);
+
+ $_SERVER['REMOTE_USER'] = 'testuser';
+ $expected_result = 'testuser:179ad45c6ce2cb97cf1029e212046e81:Arthur Dent:arthur@example.com:' . "\n";
+ $actual_result = $helper->getAdminLine();
+
+ $this->assertSame($expected_result, $actual_result);
+ }
+
+ public function test_getUserLine_manyUser () {
+ $helper = new admin_plugin_farmer_new();
+ $usersfileData = "# users.auth.php
+# <?php exit()?>
+# Don't modify the lines above
+#
+# Userfile
+#
+# Format:
+#
+# user:MD5password:Real Name:email:groups,comma,seperated
+#
+# testuser : testpass
+1testuser:179ad45c6ce43897cf1029e212046e81:Arthur Dent:brthur@example.com:admin
+testuser:179ad45c6ce2cb97cf1029e212046e81:Arthur Dent:arthur@example.com:
+2testuser:179ad45c6ce2cb97cf10214712046e81:Arthur inDent:crthur@example.com:admin\n";
+ file_put_contents($this->usersfile,$usersfileData);
+
+ $_SERVER['REMOTE_USER'] = 'testuser';
+ $expected_result = 'testuser:179ad45c6ce2cb97cf1029e212046e81:Arthur Dent:arthur@example.com:' . "\n";
+ $actual_result = $helper->getAdminLine();
+
+ $this->assertSame($expected_result, $actual_result);
+ }
+}
diff --git a/platform/www/lib/plugins/farmer/_test/helper.test.php b/platform/www/lib/plugins/farmer/_test/helper.test.php
new file mode 100644
index 0000000..3fc689b
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/_test/helper.test.php
@@ -0,0 +1,64 @@
+<?php
+
+/**
+ * Tests for the validation functionality of the farmer plugin
+ *
+ * @group plugin_farmer
+ * @group plugins
+ */
+class helper_plugin_farmer_test extends DokuWikiTest {
+
+ protected $pluginsEnabled = array('farmer',);
+
+ public function validationProvider() {
+ return array(
+ array('ant', true),
+ array('ant.lion', true),
+ array('ant.lion.cow', true),
+ array('ant-lion', true),
+ array('ant-lion.cow', true),
+ array('4ant', true),
+ array('ant4', true),
+ array('ant44lion', true),
+ array('44', true),
+
+ array('ant.', false),
+ array('.ant', false),
+ array('ant-', false),
+ array('-ant', false),
+ array('ant--lion', false),
+ array('ant..lion', false),
+ array('ant.-lion', false),
+ array('ant/lion', false),
+ array('!ant', false),
+ array('ant lion', false),
+ );
+ }
+
+ /**
+ * @dataProvider validationProvider
+ * @param $input
+ * @param $expect
+ */
+ public function test_validateAnimalName($input, $expect) {
+ /** @var helper_plugin_farmer $helper */
+ $helper = plugin_load('helper', 'farmer');
+ $this->assertEquals($expect, $helper->validateAnimalName($input));
+ }
+
+ public function test_isInPath() {
+ /** @var helper_plugin_farmer $helper */
+ $helper = plugin_load('helper', 'farmer');
+
+ $this->assertTrue($helper->isInPath('/var/www/foo', '/var/www'));
+ $this->assertFalse($helper->isInPath('/var/www/../foo', '/var/www'));
+
+ // same dir should return false, too
+ $this->assertFalse($helper->isInPath('/var/www/foo', '/var/www/foo'));
+ $this->assertFalse($helper->isInPath('/var/www/foo/', '/var/www/foo'));
+ $this->assertFalse($helper->isInPath('/var/www/foo/bar/../', '/var/www/foo'));
+
+ // https://github.com/cosmocode/dokuwiki-plugin-farmer/issues/30
+ $this->assertFalse($helper->isInPath('/var/lib/dokuwiki.animals', '/var/lib/dokuwiki'));
+ }
+}
diff --git a/platform/www/lib/plugins/farmer/action/ajax.php b/platform/www/lib/plugins/farmer/action/ajax.php
new file mode 100644
index 0000000..ac20eae
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/action/ajax.php
@@ -0,0 +1,267 @@
+<?php
+/**
+ * DokuWiki Plugin farmer (Action Component)
+ *
+ * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
+ * @author Michael Große <grosse@cosmocode.de>
+ * @author Andreas Gohr <gohr@cosmocode.de>
+ */
+
+if(!defined('DOKU_INC')) die();
+
+/**
+ * Manage AJAX features
+ */
+class action_plugin_farmer_ajax extends DokuWiki_Action_Plugin {
+
+ /**
+ * plugin should use this method to register its handlers with the DokuWiki's event controller
+ *
+ * @param Doku_Event_Handler $controller DokuWiki's event controller object. Also available as global $EVENT_HANDLER
+ *
+ */
+ public function register(Doku_Event_Handler $controller) {
+ $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, '_ajax_call');
+ }
+
+ /**
+ * handle ajax requests
+ *
+ * @param Doku_Event $event
+ * @param $param
+ */
+ public function _ajax_call(Doku_Event $event, $param) {
+ if(substr($event->data, 0, 13) !== 'plugin_farmer') {
+ return;
+ }
+ //no other ajax call handlers needed
+ $event->stopPropagation();
+ $event->preventDefault();
+
+ if(!auth_isadmin()) die('Only admins allowed');
+
+ if(substr($event->data, 14) === 'getPluginMatrix') {
+ $this->get_plugin_matrix($event, $param);
+ return;
+ }
+ if(substr($event->data, 14) === 'modPlugin') {
+ $this->plugin_mod($event, $param);
+ return;
+ }
+ if(substr($event->data, 14, 10) === 'getPlugins') {
+ $this->get_animal_plugins($event, $param);
+ return;
+ }
+ if(substr($event->data, 14, 10) === 'checkSetup') {
+ $this->check_setup($event, $param);
+ }
+ }
+
+ /**
+ * This function exists in order to provide a positive (i.e. 200) response to an ajax request to a non-existing animal.
+ *
+ * @param Doku_Event $event
+ * @param $param
+ */
+ public function check_setup(Doku_Event $event, $param) {
+ $data = '';
+ $json = new JSON();
+ header('Content-Type: application/json');
+ echo $json->encode($data);
+ }
+
+ public function plugin_mod(Doku_Event $event, $param) {
+ global $INPUT;
+
+ /** @var helper_plugin_farmer $helper */
+ $helper = plugin_load('helper', 'farmer');
+
+ $pname = $INPUT->str('plugin');
+ $animal = $INPUT->str('ani');
+
+
+ $plugins = $helper->getAnimalPluginRealState($animal);
+ if(!isset($plugins[$pname])) die('no such plugin');
+ $plugin = $plugins[$pname];
+
+ // figure out what to toggle to
+ if($plugin['isdefault']) {
+ $new = (int) !$plugin['actual'];
+ } else {
+ $new = -1;
+ }
+ $helper->setPluginState($pname, $animal, $new);
+
+ // show new state
+ $plugins = $helper->getAnimalPluginRealState($animal);
+ $plugin = $plugins[$pname];
+ header('Content-Type: text/html; charset=utf-8');
+ echo $this->plugin_matrix_cell($plugin, $animal);
+ }
+
+ /**
+ * Create a matrix of all animals and plugin states
+ *
+ * @param Doku_Event $event
+ * @param $param
+ */
+ public function get_plugin_matrix(Doku_Event $event, $param) {
+ /** @var helper_plugin_farmer $helper */
+ $helper = plugin_load('helper', 'farmer');
+
+ $animals = $helper->getAllAnimals();
+ $plugins = $helper->getAnimalPluginRealState($animals[0]);
+
+ header('Content-Type: text/html; charset=utf-8');
+
+ echo '<div class="table pluginmatrix">';
+ echo '<table>';
+ echo '<thead>';
+ echo '<tr>';
+ echo '<th></th>';
+ foreach($plugins as $plugin) {
+ echo '<th><div>' . hsc($plugin['name']) . '</div></th>';
+ }
+ echo '</tr>';
+ echo '</thead>';
+
+ echo '<tbody>';
+
+ echo '<tr>';
+ echo '<th>Default</th>';
+ foreach($plugins as $plugin) {
+ echo $this->plugin_matrix_cell($plugin, $this->getLang('plugin_default'), true);
+ }
+ echo '</tr>';
+
+ foreach($animals as $animal) {
+ $plugins = $helper->getAnimalPluginRealState($animal);
+ echo '<tr>';
+ echo '<th>' . hsc($animal) . '</th>';
+ foreach($plugins as $plugin) {
+ echo $this->plugin_matrix_cell($plugin, $animal);
+ }
+ echo '</tr>';
+ }
+ echo '</tbody>';
+ echo '</table>';
+ echo '</div>';
+ }
+
+ /**
+ * create a single cell in the matrix
+ *
+ * @param array $plugin
+ * @param string $animal
+ * @param bool $defaults show the defaults
+ * @return string
+ */
+ protected function plugin_matrix_cell($plugin, $animal, $defaults=false) {
+ if($defaults) {
+ $current = $plugin['default'];
+ $isdefault = true;
+ $td = 'th';
+ } else {
+ $current = $plugin['actual'];
+ $isdefault = $plugin['isdefault'];
+ $td = 'td';
+ }
+
+ if($current) {
+ $class = 'on';
+ $lbl = '✓';
+ } else {
+ $class = 'off';
+ $lbl = '✗';
+ }
+ if($isdefault) $class .= ' default';
+
+
+ $attrs = array(
+ 'class' => $class,
+ 'title' => $animal . ': ' . $plugin['name'],
+ 'data-animal' => $animal,
+ 'data-plugin' => $plugin['name']
+ );
+ $attr = buildAttributes($attrs);
+
+ return "<$td $attr>$lbl</$td>";
+ }
+
+ /**
+ * @param Doku_Event $event
+ * @param $param
+ */
+ public function get_animal_plugins(Doku_Event $event, $param) {
+ $animal = substr($event->data, 25);
+ /** @var helper_plugin_farmer $helper */
+ $helper = plugin_load('helper', 'farmer');
+
+ $plugins = $helper->getAnimalPluginRealState($animal);
+
+ header('Content-Type: text/html; charset=utf-8');
+
+ echo '<table>';
+ echo '<tr>';
+ echo '<th>' . $this->getLang('plugin') . '</th>';
+ echo '<th>' . $this->getLang('plugin_default') . '</th>';
+ echo '<th>' . $this->getLang('plugin_enabled') . '</th>';
+ echo '<th>' . $this->getLang('plugin_disabled') . '</th>';
+ echo '</tr>';
+
+ foreach($plugins as $plugin) {
+ echo '<tr>';
+ echo '<th>' . hsc($plugin['name']) . '</th>';
+
+ echo '<td>';
+ $attr = array();
+ $attr['type'] = 'radio';
+ $attr['name'] = 'bulk_plugins[' . $plugin['name'] . ']';
+ $attr['value'] = '-1';
+ if($plugin['isdefault']) {
+ $attr['checked'] = 'checked';
+ }
+ echo '<label>';
+ echo '<input ' . buildAttributes($attr) . ' />';
+ if($plugin['default']) {
+ echo ' (' . $this->getLang('plugin_on') . ')';
+ } else {
+ echo ' (' . $this->getLang('plugin_off') . ')';
+ }
+ echo '</label>';
+ echo '</td>';
+
+ echo '<td>';
+ $attr = array();
+ $attr['type'] = 'radio';
+ $attr['name'] = 'bulk_plugins[' . $plugin['name'] . ']';
+ $attr['value'] = '1';
+ if(!$plugin['isdefault'] && $plugin['actual']) {
+ $attr['checked'] = 'checked';
+ }
+ echo '<label>';
+ echo '<input ' . buildAttributes($attr) . ' />';
+ echo ' ' . $this->getLang('plugin_on');
+ echo '</label>';
+ echo '</td>';
+
+ echo '<td>';
+ $attr = array();
+ $attr['type'] = 'radio';
+ $attr['name'] = 'bulk_plugins[' . $plugin['name'] . ']';
+ $attr['value'] = '0';
+ if(!$plugin['isdefault'] && !$plugin['actual']) {
+ $attr['checked'] = 'checked';
+ }
+ echo '<label>';
+ echo '<input ' . buildAttributes($attr) . ' />';
+ echo ' ' . $this->getLang('plugin_off');
+ echo '</label>';
+ echo '</td>';
+
+ echo '</tr>';
+ }
+ }
+
+}
+
diff --git a/platform/www/lib/plugins/farmer/action/disable.php b/platform/www/lib/plugins/farmer/action/disable.php
new file mode 100644
index 0000000..bf0c48a
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/action/disable.php
@@ -0,0 +1,56 @@
+<?php
+/**
+ * DokuWiki Plugin farmer (Action Component)
+ *
+ * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
+ * @author Michael Große <grosse@cosmocode.de>
+ * @author Andreas Gohr <gohr@cosmocode.de>
+ */
+
+if(!defined('DOKU_INC')) die();
+
+/**
+ * Disable Plugins on install
+ */
+class action_plugin_farmer_disable extends DokuWiki_Action_Plugin {
+
+ /**
+ * plugin should use this method to register its handlers with the DokuWiki's event controller
+ *
+ * @param Doku_Event_Handler $controller DokuWiki's event controller object. Also available as global $EVENT_HANDLER
+ *
+ */
+ public function register(Doku_Event_Handler $controller) {
+ /** @var helper_plugin_farmer $farmer */
+ $farmer = plugin_load('helper', 'farmer');
+ if($farmer->getAnimal()) return;
+
+ if($this->getConf('disable_new_plugins')) {
+ $controller->register_hook('PLUGIN_EXTENSION_CHANGE', 'AFTER', $this, 'handle_install');
+ }
+ }
+
+ /**
+ * handle install of new plugin
+ *
+ * @param Doku_Event $event
+ * @param $param
+ */
+ public function handle_install(Doku_Event $event, $param) {
+ if($event->data['action'] != 'install') return;
+
+ /* @var Doku_Plugin_Controller $plugin_controller */
+ global $plugin_controller;
+ $plugin_controller = new Doku_Plugin_Controller(); // we need to refresh the status
+
+ /** @var helper_plugin_extension_extension $ext */
+ $ext = $event->data['extension'];
+ $disabled = $ext->disable();
+ if($disabled === true) {
+ msg($this->getLang('disable_new_plugins'));
+ } else {
+ msg(hsc($disabled), -1);
+ }
+ }
+}
+
diff --git a/platform/www/lib/plugins/farmer/action/startup.php b/platform/www/lib/plugins/farmer/action/startup.php
new file mode 100644
index 0000000..4b9d38e
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/action/startup.php
@@ -0,0 +1,90 @@
+<?php
+/**
+ * DokuWiki Plugin farmer (Action Component)
+ *
+ * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
+ * @author Michael Große <grosse@cosmocode.de>
+ * @author Andreas Gohr <gohr@cosmocode.de>
+ */
+
+if(!defined('DOKU_INC')) die();
+
+/**
+ * Handles Farm mechanisms on DokuWiki startup
+ */
+class action_plugin_farmer_startup extends DokuWiki_Action_Plugin {
+
+ /** @var helper_plugin_farmer */
+ protected $helper;
+
+ /**
+ * action_plugin_farmer_startup constructor.
+ */
+ public function __construct() {
+ $this->helper = plugin_load('helper', 'farmer');
+ }
+
+ /**
+ * plugin should use this method to register its handlers with the DokuWiki's event controller
+ *
+ * @param Doku_Event_Handler $controller DokuWiki's event controller object. Also available as global $EVENT_HANDLER
+ *
+ */
+ public function register(Doku_Event_Handler $controller) {
+ $controller->register_hook('DOKUWIKI_STARTED', 'BEFORE', $this, 'before_start');
+ }
+
+ public function before_start(Doku_Event $event, $param) {
+ if($this->helper->wasNotfound()) $this->handleNotFound();
+ }
+
+ /**
+ * Handles the animal not found case
+ *
+ * Will abort the current script unless the farmer is wanted
+ */
+ protected function handleNotFound() {
+ /** @noinspection PhpUnusedLocalVariableInspection */
+ global $conf, $lang;
+ $config = $this->helper->getConfig();
+ $show = $config['notfound']['show'];
+ $url = $config['notfound']['url'];
+ if($show == 'farmer') return;
+
+ if($show == '404' || $show == 'list') {
+ http_status(404);
+ $body = $this->locale_xhtml('notfound_' . $show);
+ /** @noinspection PhpUnusedLocalVariableInspection */
+ $title = '404';
+ if($show == 'list') {
+ /** @noinspection PhpUnusedLocalVariableInspection */
+ $body .= $this->animalList();
+ }
+
+ include __DIR__ . '/../includes/template.php';
+ exit;
+ }
+
+ if($show == 'redirect' && $url) {
+ send_redirect($url);
+ }
+ }
+
+ /**
+ * Retrun a HTML list of animals
+ *
+ * @return string
+ */
+ protected function animalList() {
+ $html = '<ul>';
+ $animals = $this->helper->getAllAnimals();
+ foreach($animals as $animal) {
+ $link = $this->helper->getAnimalURL($animal);
+ $html .= '<li><div class="li"><a href="' . $link . '">' . hsc($animal) . '</a></div></li>';
+ }
+ $html .= '</ul>';
+ return $html;
+ }
+
+}
+
diff --git a/platform/www/lib/plugins/farmer/admin.php b/platform/www/lib/plugins/farmer/admin.php
new file mode 100644
index 0000000..6f00039
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/admin.php
@@ -0,0 +1,115 @@
+<?php
+
+/**
+ * DokuWiki Plugin farmer (Admin Component)
+ *
+ * This is the main admin page. It displays the tabs and then loads the sub components
+ * according to the selected tab
+ *
+ * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
+ * @author Michael Große <grosse@cosmocode.de>
+ * @author Andreas Gohr <gohr@cosmocode.de>
+ */
+class admin_plugin_farmer extends DokuWiki_Admin_Plugin {
+
+ /** @var helper_plugin_farmer */
+ protected $helper;
+ /** @var array The available pages for the current user in the current wiki */
+ protected $pages;
+ /** @var string The currently selected page */
+ protected $page;
+ /** @var DokuWiki_Admin_Plugin the plugin to use for the current page */
+ protected $adminplugin;
+
+ /**
+ * @return bool we're available for managers and admins
+ */
+ public function forAdminOnly() {
+ return false;
+ }
+
+ /**
+ * Initialize current page
+ */
+ public function __construct() {
+ global $INPUT;
+ $this->helper = plugin_load('helper', 'farmer');
+
+ // set available pages depending on user and animal
+ $isanimal = (bool) $this->helper->getAnimal();
+ if($isanimal || !auth_isadmin()) {
+ $this->pages = array(
+ 'info'
+ );
+ } else {
+ if(!$this->helper->checkFarmSetup()) {
+ $this->pages = array(
+ 'setup'
+ );
+ } else {
+ $this->pages = array(
+ 'info',
+ 'config',
+ 'new',
+ 'plugins',
+ 'delete'
+ );
+ }
+ }
+
+ // make sure current page requested is available
+ $this->page = $INPUT->str('sub');
+ if(!in_array($this->page, $this->pages)) {
+ $this->page = $this->pages[0];
+ }
+
+ // load the sub component
+ $this->adminplugin = plugin_load('admin', 'farmer_' . $this->page);
+ if(!$this->adminplugin) nice_die('Something went wrong loading the plugin component for ' . hsc($this->page));
+ }
+
+ /**
+ * handle user request
+ */
+ public function handle() {
+ $this->adminplugin->handle();
+ }
+
+ /**
+ * output appropriate tab
+ */
+ public function html() {
+ global $ID;
+
+ echo '<div id="plugin__farmer_admin">';
+ echo '<h1>' . $this->getLang('menu') . '</h1>';
+
+ echo '<ul class="tabs" id="plugin__farmer_tabs">';
+ foreach($this->pages as $page) {
+ $link = wl($ID, array('do' => 'admin', 'page' => 'farmer', 'sub' => $page));
+ $class = ($page == $this->page) ? 'active' : '';
+
+ echo '<li class="' . $class . '"><a href="' . $link . '">' . $this->getLang('tab_' . $page) . '</a></li>';
+ }
+ echo '</ul>';
+ echo '<div class="panelHeader">';
+ echo $this->locale_xhtml('tab_' . $this->page);
+ echo '</div>';
+ echo '<div class="panelMain">';
+ $this->adminplugin->html();
+ echo '</div>';
+ echo '<div class="panelFooter">';
+ echo $this->locale_xhtml('tab_' . $this->page . '_help');
+ echo '</div>';
+ echo '</div>';
+ }
+
+ /**
+ * @return int
+ */
+ public function getMenuSort() {
+ return 42;
+ }
+
+}
+
diff --git a/platform/www/lib/plugins/farmer/admin.svg b/platform/www/lib/plugins/farmer/admin.svg
new file mode 100644
index 0000000..e774207
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/admin.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M10.5 18a.5.5 0 0 1 .5.5.5.5 0 0 1-.5.5.5.5 0 0 1-.5-.5.5.5 0 0 1 .5-.5m3 0a.5.5 0 0 1 .5.5.5.5 0 0 1-.5.5.5.5 0 0 1-.5-.5.5.5 0 0 1 .5-.5M10 11a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1m4 0a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1m4 7c0 2.21-2.69 4-6 4s-6-1.79-6-4c0-.9.45-1.73 1.2-2.4-.75-1-1.2-2.25-1.2-3.6l.12-1.22c-.54.15-1.19.15-1.72 0-1.02-.28-2.56-1.43-2.33-2.23.23-.8 2.14-.95 3.16-.65.59.17 1.22.6 1.59 1.06l.57-.81C6.79 7.05 7 4 10 3l-.09.14c-.28.44-1 1.83-.24 3.33a6.02 6.02 0 0 1 4.66 0c.76-1.5.04-2.89-.24-3.33L14 3c3 1 3.21 4.05 2.61 5.15l.57.81c.37-.46 1-.89 1.59-1.06 1.02-.3 2.93-.15 3.16.65.23.8-1.31 1.95-2.33 2.23-.53.15-1.18.15-1.72 0L18 12c0 1.35-.45 2.6-1.2 3.6.75.67 1.2 1.5 1.2 2.4m-6-2c-2.21 0-4 .9-4 2s1.79 2 4 2 4-.9 4-2-1.79-2-4-2m0-2c1.12 0 2.17.21 3.07.56.58-.69.93-1.56.93-2.56a4 4 0 0 0-4-4 4 4 0 0 0-4 4c0 1 .35 1.87.93 2.56.9-.35 1.95-.56 3.07-.56m2.09-10.86z"/></svg> \ No newline at end of file
diff --git a/platform/www/lib/plugins/farmer/admin/config.php b/platform/www/lib/plugins/farmer/admin/config.php
new file mode 100644
index 0000000..7d79970
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/admin/config.php
@@ -0,0 +1,126 @@
+<?php
+/**
+ * DokuWiki Plugin farmer (Admin Component)
+ *
+ * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
+ * @author Michael Große <grosse@cosmocode.de>
+ * @author Andreas Gohr <gohr@cosmocode.de>
+ */
+use dokuwiki\Form\Form;
+
+// must be run within Dokuwiki
+if(!defined('DOKU_INC')) die();
+
+/**
+ * Configuration Interface for farm.ini
+ */
+class admin_plugin_farmer_config extends DokuWiki_Admin_Plugin {
+
+ /** @var helper_plugin_farmer */
+ protected $helper;
+
+ /**
+ * @return bool admin only!
+ */
+ public function forAdminOnly() {
+ return false;
+ }
+
+ /**
+ * admin_plugin_farmer_config constructor.
+ */
+ public function __construct() {
+ $this->helper = plugin_load('helper', 'farmer');
+ }
+
+ /**
+ * Should carry out any processing required by the plugin.
+ */
+ public function handle() {
+ global $INPUT;
+ global $ID;
+ if(!$INPUT->has('farmconf')) return;
+ if(!checkSecurityToken()) return;
+
+ $farmconf = $this->helper->getConfig();
+ $farmdir = $farmconf['base']['farmdir'];
+ $farmconf = array_merge($farmconf, $INPUT->arr('farmconf'));
+ $farmconf['base']['farmdir'] = $farmdir;
+
+ $farmconf['base']['basedomain'] = trim(trim($farmconf['base']['basedomain'], '.'));
+
+ $ini = DOKU_INC . 'conf/farm.ini';
+ $data = "; Farm config created by the farmer plugin\n";
+ $data .= $this->createIni($farmconf);
+ io_saveFile($ini, $data);
+
+ $self = wl($ID, array('do' => 'admin', 'page' => 'farmer', 'sub' => 'config'), true, '&');
+ send_redirect($self);
+ }
+
+ /**
+ * Render HTML output, e.g. helpful text and a form
+ */
+ public function html() {
+ $farmconf = $this->helper->getConfig();
+
+ $form = new Form(array('method' => 'post'));
+
+ $form->addFieldsetOpen($this->getLang('base'));
+ $form->addHTML('<label><span>' . $this->getLang('farm dir') . '</span>' . DOKU_FARMDIR);
+ $form->addTextInput('farmconf[base][farmhost]', $this->getLang('farm host'))->val($farmconf['base']['farmhost']);
+ $form->addTextInput('farmconf[base][basedomain]', $this->getLang('base domain'))->val($farmconf['base']['basedomain']);
+ $form->addFieldsetClose();
+
+ $form->addFieldsetOpen($this->getLang('conf_inherit'));
+ foreach($farmconf['inherit'] as $key => $val) {
+ $form->setHiddenField("farmconf[inherit][$key]", 0);
+ $chk = $form->addCheckbox("farmconf[inherit][$key]", $this->getLang('conf_inherit_' . $key))->useInput(false);
+ if($val) $chk->attr('checked', 'checked');
+ }
+ $form->addFieldsetClose();
+
+ $options = array(
+ 'farmer' => $this->getLang('conf_notfound_farmer'),
+ '404' => $this->getLang('conf_notfound_404'),
+ 'list' => $this->getLang('conf_notfound_list'),
+ 'redirect' => $this->getLang('conf_notfound_redirect')
+ );
+
+ $form->addFieldsetOpen($this->getLang('conf_notfound'));
+ $form->addDropdown('farmconf[notfound][show]', $options, $this->getLang('conf_notfound'))->val($farmconf['notfound']['show']);
+ $form->addTextInput('farmconf[notfound][url]', $this->getLang('conf_notfound_url'))->val($farmconf['notfound']['url']);
+ $form->addFieldsetClose();
+
+ $form->addButton('save', $this->getLang('save'));
+ echo $form->toHTML();
+ }
+
+ /**
+ * Simple function to create an ini file
+ *
+ * Does no escaping, but should suffice for our use case
+ *
+ * @link http://stackoverflow.com/a/5695202/172068
+ * @param array $data The data to transform
+ * @return string
+ */
+ public function createIni($data) {
+ $res = array();
+ foreach($data as $key => $val) {
+ if(is_array($val)) {
+ $res[] = '';
+ $res[] = "[$key]";
+ foreach($val as $skey => $sval) {
+ $res[] = "$skey = " . (is_numeric($sval) ? $sval : '"' . $sval . '"');
+ }
+ } else {
+ $res[] = "$key = " . (is_numeric($val) ? $val : '"' . $val . '"');
+ }
+ }
+ $res[] = '';
+ return join("\n", $res);
+ }
+}
+
+// vim:ts=4:sw=4:et:
diff --git a/platform/www/lib/plugins/farmer/admin/delete.php b/platform/www/lib/plugins/farmer/admin/delete.php
new file mode 100644
index 0000000..d8b3450
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/admin/delete.php
@@ -0,0 +1,95 @@
+<?php
+/**
+ * DokuWiki Plugin farmer (Admin Component)
+ *
+ * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
+ * @author Michael Große <grosse@cosmocode.de>
+ * @author Andreas Gohr <gohr@cosmocode.de>
+ */
+
+// must be run within Dokuwiki
+use dokuwiki\Form\Form;
+
+if(!defined('DOKU_INC')) die();
+
+/**
+ * Information about the farm and the current instance
+ */
+class admin_plugin_farmer_delete extends DokuWiki_Admin_Plugin {
+
+ /** @var helper_plugin_farmer */
+ protected $helper;
+
+ /**
+ * admin_plugin_farmer_info constructor.
+ */
+ public function __construct() {
+ $this->helper = plugin_load('helper', 'farmer');
+ }
+
+ /**
+ * @return bool admin only!
+ */
+ public function forAdminOnly() {
+ return true;
+ }
+
+ /**
+ * Should carry out any processing required by the plugin.
+ */
+ public function handle() {
+ global $INPUT;
+ global $ID;
+ if(!$INPUT->has('delete')) return;
+
+ if($INPUT->filter('trim')->str('delanimal') === '') {
+ msg($this->getLang('delete_noanimal'), -1);
+ return;
+ }
+
+ if($INPUT->str('delanimal') != $INPUT->str('confirm')) {
+ msg($this->getLang('delete_mismatch'), -1);
+ return;
+ }
+
+ $animaldir = DOKU_FARMDIR . $INPUT->str('delanimal');
+
+ if(!$this->helper->isInPath($animaldir, DOKU_FARMDIR) || !is_dir($animaldir)) {
+ msg($this->getLang('delete_invalid'), -1);
+ return;
+ }
+
+ // let's delete it
+ $ok = io_rmdir($animaldir, true);
+ if($ok) {
+ msg($this->getLang('delete_success'), 1);
+ } else {
+ msg($this->getLang('delete_fail'), -1);
+ }
+
+ $link = wl($ID, array('do'=>'admin', 'page'=>'farmer', 'sub' => 'delete'), true, '&');
+ send_redirect($link);
+ }
+
+ /**
+ * Render HTML output, e.g. helpful text and a form
+ */
+ public function html() {
+
+ $form = new Form();
+ $form->addFieldsetOpen($this->getLang('delete_animal'));
+
+ $animals = $this->helper->getAllAnimals();
+ array_unshift($animals, '');
+ $form->addDropdown('delanimal', $animals)->addClass('farmer_chosen_animals');
+ $form->addTextInput('confirm', $this->getLang('delete_confirm'));
+ $form->addButton('delete', $this->getLang('delete'));
+ $form->addFieldsetClose();
+ echo $form->toHTML();
+
+ }
+
+
+}
+
+// vim:ts=4:sw=4:et:
diff --git a/platform/www/lib/plugins/farmer/admin/info.php b/platform/www/lib/plugins/farmer/admin/info.php
new file mode 100644
index 0000000..3bf2938
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/admin/info.php
@@ -0,0 +1,116 @@
+<?php
+/**
+ * DokuWiki Plugin farmer (Admin Component)
+ *
+ * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
+ * @author Michael Große <grosse@cosmocode.de>
+ * @author Andreas Gohr <gohr@cosmocode.de>
+ */
+
+// must be run within Dokuwiki
+if(!defined('DOKU_INC')) die();
+
+/**
+ * Information about the farm and the current instance
+ */
+class admin_plugin_farmer_info extends DokuWiki_Admin_Plugin {
+
+ /** @var helper_plugin_farmer */
+ protected $helper;
+
+ /**
+ * admin_plugin_farmer_info constructor.
+ */
+ public function __construct() {
+ $this->helper = plugin_load('helper', 'farmer');
+ }
+
+ /**
+ * @return bool admin only!
+ */
+ public function forAdminOnly() {
+ return false;
+ }
+
+ /**
+ * Should carry out any processing required by the plugin.
+ */
+ public function handle() {
+ }
+
+ /**
+ * Render HTML output, e.g. helpful text and a form
+ */
+ public function html() {
+ global $conf;
+ global $INPUT;
+
+ $animal = $this->helper->getAnimal();
+ $config = $this->helper->getConfig();
+
+ echo '<table class="inline">';
+
+ $this->line('thisis', $animal ? $this->getLang('thisis.animal') : $this->getLang('thisis.farmer'));
+ if($animal) {
+ $this->line('animal', $animal);
+ }
+ $this->line('confdir', fullpath(DOKU_CONF) . '/');
+ $this->line('savedir', fullpath($conf['savedir']) . '/');
+ $this->line('baseinstall', DOKU_INC);
+ $this->line('farm host', $config['base']['farmhost']);
+ $this->line('farm dir', DOKU_FARMDIR);
+
+ $this->line('animals', $this->animals($INPUT->bool('list')));
+
+ foreach($config['inherit'] as $key => $value) {
+ $this->line('conf_inherit_' . $key, $this->getLang($value ? 'conf_inherit_yes' : 'conf_inherit_no'));
+ }
+
+ $this->line('plugins', join(', ', $this->helper->getAllPlugins(false)));
+
+ echo '</table>';
+ }
+
+ /**
+ * List or count the animals
+ *
+ * @param bool $list
+ * @return string
+ */
+ protected function animals($list) {
+ global $ID;
+
+ $animals = $this->helper->getAllAnimals();
+ $html = '';
+ if(!$list) {
+ $html = count($animals);
+ $self = wl($ID, array('do' => 'admin', 'page' => 'farmer', 'sub' => 'info', 'list' => 1));
+ $html .= ' [<a href="' . $self . '">' . $this->getLang('conf_notfound_list') . '</a>]';
+ return $html;
+ }
+
+ $html .= '<ol>';
+ foreach($animals as $animal) {
+ $link = $this->helper->getAnimalURL($animal);
+ $html .= '<li><div class="li"><a href="' . $link . '">' . $animal . '</a></div></li>';
+ }
+ $html .= '</ol>';
+ return $html;
+ }
+
+ /**
+ * Output a table line
+ *
+ * @param string $langkey
+ * @param string $value
+ */
+ protected function line($langkey, $value) {
+ echo '<tr>';
+ echo '<th>' . $this->getLang($langkey) . '</th>';
+ echo '<td>' . $value . '</td>';
+ echo '</tr>';
+ }
+
+}
+
+// vim:ts=4:sw=4:et:
diff --git a/platform/www/lib/plugins/farmer/admin/new.php b/platform/www/lib/plugins/farmer/admin/new.php
new file mode 100644
index 0000000..8cbf94f
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/admin/new.php
@@ -0,0 +1,338 @@
+<?php
+/**
+ * DokuWiki Plugin farmer (Admin Component)
+ *
+ * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
+ * @author Michael Große <grosse@cosmocode.de>
+ */
+
+// must be run within Dokuwiki
+if(!defined('DOKU_INC')) die();
+
+class admin_plugin_farmer_new extends DokuWiki_Admin_Plugin {
+
+ /** @var helper_plugin_farmer $helper */
+ protected $helper;
+
+ /**
+ * @return bool true if only access for superuser, false is for superusers and moderators
+ */
+ public function forAdminOnly() {
+ return true;
+ }
+
+ /**
+ * admin_plugin_farmer_new constructor.
+ */
+ public function __construct() {
+ $this->helper = plugin_load('helper', 'farmer');
+ }
+
+ /**
+ * Should carry out any processing required by the plugin.
+ */
+ public function handle() {
+ global $INPUT;
+ global $ID;
+ if(!$INPUT->has('farmer__submit')) return;
+
+ $data = $this->validateAnimalData();
+ if(!$data) return;
+ if($this->createNewAnimal($data['name'], $data['admin'], $data['pass'], $data['template'], $data['aclpolicy'], $data['allowreg'])) {
+ $url = $this->helper->getAnimalURL($data['name']);
+ $link = '<a href="' . $url . '">' . hsc($data['name']) . '</a>';
+
+ msg(sprintf($this->getLang('animal creation success'), $link), 1);
+ $link = wl($ID, array('do' => 'admin', 'page' => 'farmer', 'sub' => 'new'), true, '&');
+ send_redirect($link);
+ }
+ }
+
+ /**
+ * Render HTML output, e.g. helpful text and a form
+ */
+ public function html() {
+ global $lang;
+ $farmconfig = $this->helper->getConfig();
+
+ $form = new \dokuwiki\Form\Form();
+ $form->addClass('plugin_farmer')->id('farmer__create_animal_form');
+
+ $form->addFieldsetOpen($this->getLang('animal configuration'));
+ $form->addTextInput('animalname', $this->getLang('animal'));
+ $form->addFieldsetClose();
+
+ $animals = $this->helper->getAllAnimals();
+ array_unshift($animals, '');
+ $form->addFieldsetOpen($this->getLang('animal template'));
+ $form->addDropdown('animaltemplate', $animals)->addClass('farmer_chosen_animals');
+ $form->addFieldsetClose();
+
+ $form->addFieldsetOpen($lang['i_policy'])->attr('id', 'aclPolicyFieldset');
+ $policyOptions = array('open' => $lang['i_pol0'],'public' => $lang['i_pol1'], 'closed' => $lang['i_pol2']);
+ $form->addDropdown('aclpolicy', $policyOptions)->addClass('acl_chosen');
+ if ($farmconfig['inherit']['main']) {
+ $form->addRadioButton('allowreg',$this->getLang('inherit user registration'))->val('inherit')->attr('checked', 'checked');
+ $form->addRadioButton('allowreg',$this->getLang('enable user registration'))->val('allow');
+ $form->addRadioButton('allowreg',$this->getLang('disable user registration'))->val('disable');
+ } else {
+ $form->addCheckbox('allowreg', $lang['i_allowreg'])->attr('checked', 'checked');
+ }
+
+ $form->addFieldsetClose();
+
+ $form->addFieldsetOpen($this->getLang('animal administrator'));
+ $btn = $form->addRadioButton('adminsetup', $this->getLang('noUsers'))->val('noUsers');
+ if($farmconfig['inherit']['users']) {
+ $btn->attr('checked', 'checked'); // default when inherit available
+ } else {
+ // no user copying when inheriting
+ $form->addRadioButton('adminsetup', $this->getLang('importUsers'))->val('importUsers');
+ $form->addRadioButton('adminsetup', $this->getLang('currentAdmin'))->val('currentAdmin');
+ }
+ $btn = $form->addRadioButton('adminsetup', $this->getLang('newAdmin'))->val('newAdmin');
+ if(!$farmconfig['inherit']['users']) {
+ $btn->attr('checked', 'checked'); // default when inherit not available
+ }
+ $form->addPasswordInput('adminPassword', $this->getLang('admin password'));
+ $form->addFieldsetClose();
+
+ $form->addButton('farmer__submit', $this->getLang('submit'))->attr('type', 'submit')->val('newAnimal');
+ echo $form->toHTML();
+ }
+
+ /**
+ * Validate the data for a new animal
+ *
+ * @return array|bool false on errors, clean data otherwise
+ */
+ protected function validateAnimalData() {
+ global $INPUT;
+
+ $animalname = $INPUT->filter('trim')->str('animalname');
+ $adminsetup = $INPUT->str('adminsetup');
+ $adminpass = $INPUT->filter('trim')->str('adminPassword');
+ $template = $INPUT->filter('trim')->str('animaltemplate');
+ $aclpolicy = $INPUT->filter('trim')->str('aclpolicy');
+ $allowreg = $INPUT->str('allowreg');
+
+ $errors = array();
+
+ if($animalname === '') {
+ $errors[] = $this->getLang('animalname_missing');
+ } elseif(!$this->helper->validateAnimalName($animalname)) {
+ $errors[] = $this->getLang('animalname_invalid');
+ }
+
+ if($adminsetup === 'newAdmin' && $adminpass === '') {
+ $errors[] = $this->getLang('adminPassword_empty');
+ }
+
+ if($animalname !== '' && file_exists(DOKU_FARMDIR . '/' . $animalname)) {
+ $errors[] = $this->getLang('animalname_preexisting');
+ }
+
+ if (!is_dir(DOKU_FARMDIR . $template) && !in_array($aclpolicy,array('open', 'public', 'closed'))) {
+ $errors[] = $this->getLang('aclpolicy missing/bad');
+ }
+
+ if($errors) {
+ foreach($errors as $error) {
+ msg($error, -1);
+ }
+ return false;
+ }
+
+ if(!is_dir(DOKU_FARMDIR . $template)) {
+ $template = '';
+ }
+ if ($template != '') {
+ $aclpolicy = '';
+ }
+
+ return array(
+ 'name' => $animalname,
+ 'admin' => $adminsetup,
+ 'pass' => $adminpass,
+ 'template' => $template,
+ 'aclpolicy' => $aclpolicy,
+ 'allowreg' => $allowreg
+ );
+ }
+
+ /**
+ * Create a new animal
+ *
+ * @param string $name name/title of the animal, will be the directory name for htaccess setup
+ * @param string $adminSetup newAdmin, currentAdmin or importUsers
+ * @param string $adminPassword required if $adminSetup is newAdmin
+ * @param string $template name of animal to copy
+ * @param $aclpolicy
+ * @param $userreg
+ * @return bool true if successful
+ * @throws Exception
+ */
+ protected function createNewAnimal($name, $adminSetup, $adminPassword, $template, $aclpolicy, $userreg) {
+ $animaldir = DOKU_FARMDIR . $name;
+
+ // copy basic template
+ $ok = $this->helper->io_copyDir(__DIR__ . '/../_animal', $animaldir);
+ if(!$ok) {
+ msg($this->getLang('animal creation error'), -1);
+ return false;
+ }
+
+ // copy animal template
+ if($template != '') {
+ foreach(array('conf', 'data/pages', 'data/media', 'data/meta', 'data/media_meta', 'index') as $dir) {
+ $templatedir = DOKU_FARMDIR . $template . '/' . $dir;
+ if(!is_dir($templatedir)) continue;
+ // do not copy changelogs in meta
+ if(substr($dir, -4) == 'meta') {
+ $exclude = '/\.changes$/';
+ } else {
+ $exclude = '';
+ }
+ if(!$this->helper->io_copyDir($templatedir, $animaldir . '/' . $dir, $exclude)) {
+ msg(sprintf($this->getLang('animal template copy error'), $dir), -1);
+ // we go on anyway
+ }
+ }
+ }
+
+ // append title to local config
+ $ok &= io_saveFile($animaldir . '/conf/local.php', "\n" . '$conf[\'title\'] = \'' . $name . '\';' . "\n", true);
+
+ // create a random logo and favicon
+ if(!class_exists('\splitbrain\RingIcon\RingIcon', false)) {
+ require(__DIR__ . '/../3rdparty/RingIcon.php');
+ }
+ if(!class_exists('\chrisbliss18\phpico\PHPIco', false)) {
+ require(__DIR__ . '/../3rdparty/PHPIco.php');
+ }
+ try {
+ if(function_exists('imagecreatetruecolor')) {
+ $logo = $animaldir . '/data/media/wiki/logo.png';
+ if(!file_exists($logo)) {
+ $ringicon = new \splitbrain\RingIcon\RingIcon(64);
+ $ringicon->createImage($animaldir, $logo);
+ }
+
+ $icon = $animaldir . '/data/media/wiki/favicon.ico';
+ if(!file_exists($icon)) {
+ $icongen = new \chrisbliss18\phpico\PHPIco($logo);
+ $icongen->save_ico($icon);
+ }
+ }
+ } catch(\Exception $ignore) {
+ // something went wrong, but we don't care. this is a nice to have feature only
+ }
+
+ // create admin user
+ if($adminSetup === 'newAdmin') {
+ $users = "# <?php exit()?>\n" . $this->makeAdminLine($adminPassword) . "\n";
+ } elseif($adminSetup === 'currentAdmin') {
+ $users = "# <?php exit()?>\n" . $this->getAdminLine() . "\n";
+ } elseif($adminSetup === 'noUsers') {
+ if(file_exists($animaldir . '/conf/users.auth.php')) {
+ // a user file exists already, probably from animal template - don't overwrite
+ $users = '';
+ } else {
+ // create empty user file
+ $users = "# <?php exit()?>\n";
+ }
+ } else {
+ $users = io_readFile(DOKU_CONF . 'users.auth.php');
+ }
+ if($users) {
+ $ok &= io_saveFile($animaldir . '/conf/users.auth.php', $users);
+ }
+
+ if ($aclpolicy != '') {
+ $aclfile = file($animaldir . '/conf/acl.auth.php');
+ $aclfile = array_map('trim', $aclfile);
+ array_pop($aclfile);
+ switch ($aclpolicy) {
+ case 'open':
+ $aclfile[] = "* @ALL 8";
+ break;
+ case 'public':
+ $aclfile[] = "* @ALL 1";
+ $aclfile[] = "* @user 8";
+ break;
+ case 'closed':
+ $aclfile[] = "* @ALL 0";
+ $aclfile[] = "* @user 8";
+ break;
+ default:
+ throw new Exception('Undefined aclpolicy given');
+ }
+ $ok &= io_saveFile($animaldir . '/conf/acl.auth.php', join("\n", $aclfile)."\n");
+
+ global $conf;
+ switch ($userreg) {
+ case 'allow':
+ $disableactions = join(',', array_diff(explode(',', $conf['disableactions']), array('register')));
+ $ok &= io_saveFile($animaldir . '/conf/local.php', "\n" . '$conf[\'disableactions\'] = \''.$disableactions.'\';' . "\n", true);
+ break;
+ case 'disable':
+ $disableactions = join(',', array_merge(explode(',', $conf['disableactions']), array('register')));
+ $ok &= io_saveFile($animaldir . '/conf/local.php', "\n" . '$conf[\'disableactions\'] = \''.$disableactions.'\';' . "\n", true);
+ break;
+ case 'inherit':
+ case true:
+ // nothing needs to be done
+ break;
+ default:
+ $ok &= io_saveFile($animaldir . '/conf/local.php', "\n" . '$conf[\'disableactions\'] = \'register\';' . "\n", true);
+ }
+ }
+
+ // deactivate plugins by default FIXME this should be nicer
+ $deactivatedPluginsList = explode(',', $this->getConf('deactivated plugins'));
+ $deactivatedPluginsList = array_map('trim', $deactivatedPluginsList);
+ $deactivatedPluginsList = array_unique($deactivatedPluginsList);
+ $deactivatedPluginsList = array_filter($deactivatedPluginsList);
+ foreach($deactivatedPluginsList as $plugin) {
+ $this->helper->setPluginState(trim($plugin), $name, 0);
+ }
+
+ return $ok;
+ }
+
+ /**
+ * Creates a new user line
+ *
+ * @param $password
+ * @return string
+ */
+ protected function makeAdminLine($password) {
+ $pass = auth_cryptPassword($password);
+ $line = join(
+ ':', array(
+ 'admin',
+ $pass,
+ 'Administrator',
+ 'admin@example.org',
+ 'admin,user'
+ )
+ );
+ return $line;
+ }
+
+ /**
+ * Copies the current user as new admin line
+ *
+ * @return string
+ */
+ protected function getAdminLine() {
+ $currentAdmin = $_SERVER['REMOTE_USER'];
+ $masterUsers = file_get_contents(DOKU_CONF . 'users.auth.php');
+ $masterUsers = ltrim(strstr($masterUsers, "\n" . $currentAdmin . ":"));
+ $newAdmin = substr($masterUsers, 0, strpos($masterUsers, "\n") + 1);
+ return $newAdmin;
+ }
+
+}
+
+// vim:ts=4:sw=4:et:
diff --git a/platform/www/lib/plugins/farmer/admin/plugins.php b/platform/www/lib/plugins/farmer/admin/plugins.php
new file mode 100644
index 0000000..74f0e60
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/admin/plugins.php
@@ -0,0 +1,104 @@
+<?php
+/**
+ * DokuWiki Plugin farmer (Admin Component)
+ *
+ * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
+ * @author Michael Große <grosse@cosmocode.de>
+ * @author Andreas Gohr <gohr@cosmocode.de>
+ */
+
+// must be run within Dokuwiki
+if(!defined('DOKU_INC')) die();
+
+/**
+ * Manage Animal Plugin settings
+ */
+class admin_plugin_farmer_plugins extends DokuWiki_Admin_Plugin {
+
+ /** @var helper_plugin_farmer $helper */
+ private $helper;
+
+ public function __construct() {
+ $this->helper = plugin_load('helper', 'farmer');
+ }
+
+ /**
+ * handle user request
+ */
+ public function handle() {
+ global $INPUT;
+ global $ID;
+
+ $self = wl($ID, array('do' => 'admin', 'page' => 'farmer', 'sub' => 'plugins'), true, '&');
+
+ if($INPUT->has('bulk_plugin') && $INPUT->has('state')) {
+ $animals = $this->helper->getAllAnimals();
+ $plugin = $INPUT->str('bulk_plugin');
+ foreach($animals as $animal) {
+ $this->helper->setPluginState($plugin, $animal, $INPUT->int('state'));
+ }
+ msg($this->getLang('plugindone'), 1);
+ send_redirect($self);
+ }
+
+ if($INPUT->has('bulk_animal') && $INPUT->has('bulk_plugins')) {
+ $animal = $INPUT->str('bulk_animal');
+ $activePlugins = $INPUT->arr('bulk_plugins');
+ foreach($activePlugins as $plugin => $state) {
+ $this->helper->setPluginState($plugin, $animal, $state);
+ }
+ msg($this->getLang('plugindone'), 1);
+ send_redirect($self);
+ }
+ }
+
+ /**
+ * output appropriate html
+ */
+ public function html() {
+
+ echo $this->locale_xhtml('plugins');
+ $switchForm = new \dokuwiki\Form\Form();
+ $switchForm->addClass('plugin_farmer');
+ $switchForm->addFieldsetOpen($this->getLang('bulkSingleSwitcher'));
+ $switchForm->addRadioButton('bulkSingleSwitch', $this->getLang('bulkEdit'))->id('farmer__bulk')->attr('type', 'radio');
+ $switchForm->addRadioButton('bulkSingleSwitch', $this->getLang('singleEdit'))->id('farmer__single')->attr('type', 'radio');
+ $switchForm->addRadioButton('bulkSingleSwitch', $this->getLang('matrixEdit'))->id('farmer__matrix')->attr('type', 'radio');
+ $switchForm->addFieldsetClose();
+ echo $switchForm->toHTML();
+
+ /** @var helper_plugin_farmer $helper */
+ $helper = plugin_load('helper', 'farmer');
+ $plugins = $helper->getAllPlugins();
+ array_unshift($plugins, '');
+
+ // All Animals at once
+ $bulkForm = new \dokuwiki\Form\Form();
+ $bulkForm->id('farmer__pluginsforall');
+ $bulkForm->addFieldsetOpen($this->getLang('bulkEditForm'));
+ $bulkForm->addDropdown('bulk_plugin', $plugins);
+ $bulkForm->addButton('state', $this->getLang('default'))->attr('value', '-1')->attr('type', 'submit')->attr('disabled', 'disabled');
+ $bulkForm->addButton('state', $this->getLang('activate'))->attr('value', '1')->attr('type', 'submit')->attr('disabled', 'disabled');
+ $bulkForm->addButton('state', $this->getLang('deactivate'))->attr('value', '0')->attr('type', 'submit')->attr('disabled', 'disabled');
+ $bulkForm->addFieldsetClose();
+ echo $bulkForm->toHTML();
+
+ $animals = $helper->getAllAnimals();
+ array_unshift($animals, '');
+
+ // One Animal, all the plugins
+ $singleForm = new \dokuwiki\Form\Form();
+ $singleForm->id('farmer__pluginsforone');
+ $singleForm->addFieldsetOpen($this->getLang('singleEditForm'));
+ $singleForm->addDropdown('bulk_animal', $animals);
+ $singleForm->addTagOpen('div')->addClass('output');
+ $singleForm->addTagClose('div');
+ $singleForm->addButton('save', $this->getLang('save'))->attr('disabled', 'disabled');
+
+ echo $singleForm->toHTML();
+
+
+ echo '<div id="farmer__pluginmatrix"></div>';
+ }
+}
+
diff --git a/platform/www/lib/plugins/farmer/admin/setup.php b/platform/www/lib/plugins/farmer/admin/setup.php
new file mode 100644
index 0000000..f836ae5
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/admin/setup.php
@@ -0,0 +1,151 @@
+<?php
+/**
+ * DokuWiki Plugin farmer (Admin Component)
+ *
+ * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
+ * @author Michael Große <grosse@cosmocode.de>
+ * @author Andreas Gohr <gohr@cosmocode.de>
+ */
+
+// must be run within Dokuwiki
+if(!defined('DOKU_INC')) die();
+
+/**
+ * Setup the farm by creating preload.php etc
+ */
+class admin_plugin_farmer_setup extends DokuWiki_Admin_Plugin {
+
+ /** @var helper_plugin_farmer $helper */
+ private $helper;
+
+ /**
+ * @return bool admin only!
+ */
+ public function forAdminOnly() {
+ return true;
+ }
+
+ /**
+ * Should carry out any processing required by the plugin.
+ */
+ public function handle() {
+ global $INPUT;
+ global $ID;
+
+ if(!$INPUT->bool('farmdir')) return;
+ if(!checkSecurityToken()) return;
+
+ $this->helper = plugin_load('helper', 'farmer');
+
+ $farmdir = trim($INPUT->str('farmdir', ''));
+ if($farmdir[0] !== '/') $farmdir = DOKU_INC . $farmdir;
+ $farmdir = fullpath($farmdir);
+
+ $errors = array();
+ if($farmdir === '') {
+ $errors[] = $this->getLang('farmdir_missing');
+ } elseif($this->helper->isInPath($farmdir, DOKU_INC) !== false) {
+ $errors[] = sprintf($this->getLang('farmdir_in_dokuwiki'), hsc($farmdir), hsc(DOKU_INC));
+ } elseif(!io_mkdir_p($farmdir)) {
+ $errors[] = sprintf($this->getLang('farmdir_uncreatable'), hsc($farmdir));
+ } elseif(!is_writeable($farmdir)) {
+ $errors[] = sprintf($this->getLang('farmdir_unwritable'), hsc($farmdir));
+ } elseif(count(scandir($farmdir)) > 2) {
+ $errors[] = sprintf($this->getLang('farmdir_notEmpty'), hsc($farmdir));
+ }
+
+ if($errors) {
+ foreach($errors as $error) {
+ msg($error, -1);
+ }
+ return;
+ }
+
+ // create the files
+ $ok = $this->createPreloadPHP();
+ if($ok && $INPUT->bool('htaccess')) $ok &= $this->createHtaccess();
+ if($ok) $ok &= $this->createFarmIni($farmdir);
+
+ if($ok) {
+ msg($this->getLang('preload creation success'), 1);
+ $link = wl($ID, array('do' => 'admin', 'page' => 'farmer', 'sub' => 'config'), true, '&');
+ send_redirect($link);
+ } else {
+ msg($this->getLang('preload creation error'), -1);
+ }
+ }
+
+ /**
+ * Render HTML output, e.g. helpful text and a form
+ */
+ public function html() {
+ // Is preload.php already enabled?
+ if(file_exists(DOKU_INC . 'inc/preload.php')) {
+ msg($this->getLang('overwrite_preload'), -1);
+ }
+
+ $form = new \dokuwiki\Form\Form();
+ $form->addClass('plugin_farmer');
+ $form->addFieldsetOpen($this->getLang('preloadPHPForm'));
+ $form->addTextInput('farmdir', $this->getLang('farm dir'));
+ $form->addCheckbox('htaccess', $this->getLang('htaccess setup'))->attr('checked', 'checked');
+ $form->addButton('farmer__submit', $this->getLang('submit'))->attr('type', 'submit');
+ $form->addFieldsetClose();
+ echo $form->toHTML();
+
+ }
+
+ /**
+ * Creates the preload that loads our farm controller
+ * @return bool true if saving was successful
+ */
+ protected function createPreloadPHP() {
+ $content = "<?php\n";
+ $content .= "# farm setup by farmer plugin\n";
+ $content .= "if(file_exists(__DIR__ . '/../lib/plugins/farmer/DokuWikiFarmCore.php')) {\n";
+ $content .= " include(__DIR__ . '/../lib/plugins/farmer/DokuWikiFarmCore.php');\n";
+ $content .= "}\n";
+ return io_saveFile(DOKU_INC . 'inc/preload.php', $content);
+ }
+
+ /**
+ * Prepends the needed config to the main .htaccess for htaccess type setups
+ *
+ * @return bool true if saving was successful
+ */
+ protected function createHtaccess() {
+ // load existing or template
+ if(file_exists(DOKU_INC . '.htaccess')) {
+ $old = io_readFile(DOKU_INC . '.htaccess');
+ } elseif(file_exists(DOKU_INC . '.htaccess.dist')) {
+ $old = io_readFile(DOKU_INC . '.htaccess.dist');
+ } else {
+ $old = '';
+ }
+
+ $content = "# Options added for farm setup by farmer plugin:\n";
+ $content .= "RewriteEngine On\n";
+ $content .= 'RewriteRule ^!([^/]+)/(.*) $2?animal=$1 [QSA,DPI]' . "\n";
+ $content .= 'RewriteRule ^!([^/]+)$ ?animal=$1 [QSA,DPI]' . "\n";
+ $content .= 'Options +FollowSymLinks' . "\n";
+ $content .= '# end of farm configuration' . "\n\n";
+ $content .= $old;
+ return io_saveFile(DOKU_INC . '.htaccess', $content);
+ }
+
+ /**
+ * Creates the initial configuration
+ *
+ * @param $animalpath
+ * @return bool true if saving was successful
+ */
+ protected function createFarmIni($animalpath) {
+ $content = "; farm config created by the farmer plugin\n\n";
+ $content .= "[base]\n";
+ $content .= "farmdir = $animalpath\n";
+ $content .= "farmhost = {$_SERVER['HTTP_HOST']}\n";
+ return io_saveFile(DOKU_INC . 'conf/farm.ini', $content);
+ }
+}
+
+// vim:ts=4:sw=4:et:
diff --git a/platform/www/lib/plugins/farmer/all.less b/platform/www/lib/plugins/farmer/all.less
new file mode 100644
index 0000000..effff6f
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/all.less
@@ -0,0 +1,13 @@
+@import "css/chosen.less";
+
+.chosen-container-single .chosen-single abbr,
+.chosen-container-single .chosen-single div b,
+.chosen-container-single .chosen-search input[type="text"],
+.chosen-container-multi .chosen-choices li.search-choice .search-choice-close,
+.chosen-rtl .chosen-search input[type="text"] {
+ background-image: url(css/chosen-sprite.png);
+}
+
+@media only screen and (-webkit-min-device-pixel-ratio: 1.5), only screen and (min-resolution: 144dpi), only screen and (min-resolution: 1.5dppx) {
+ background-image: url(css/chosen-sprite.png);
+}
diff --git a/platform/www/lib/plugins/farmer/conf/default.php b/platform/www/lib/plugins/farmer/conf/default.php
new file mode 100644
index 0000000..33e10d4
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/conf/default.php
@@ -0,0 +1,9 @@
+<?php
+/**
+ * Default settings for the farmer plugin
+ *
+ * @author Michael Große <grosse@cosmocode.de>
+ */
+
+$conf['deactivated plugins'] = '';
+$conf['disable_new_plugins'] = 0;
diff --git a/platform/www/lib/plugins/farmer/conf/metadata.php b/platform/www/lib/plugins/farmer/conf/metadata.php
new file mode 100644
index 0000000..9b81338
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/conf/metadata.php
@@ -0,0 +1,9 @@
+<?php
+/**
+ * Options for the farmer plugin
+ *
+ * @author Michael Große <grosse@cosmocode.de>
+ */
+
+$meta['deactivated plugins'] = array('string');
+$meta['disable_new_plugins'] = array('onoff');
diff --git a/platform/www/lib/plugins/farmer/css/chosen-sprite.png b/platform/www/lib/plugins/farmer/css/chosen-sprite.png
new file mode 100644
index 0000000..c57da70
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/css/chosen-sprite.png
Binary files differ
diff --git a/platform/www/lib/plugins/farmer/css/chosen.less b/platform/www/lib/plugins/farmer/css/chosen.less
new file mode 100644
index 0000000..e7ea092
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/css/chosen.less
@@ -0,0 +1,450 @@
+/*!
+Chosen, a Select Box Enhancer for jQuery and Prototype
+by Patrick Filler for Harvest, http://getharvest.com
+
+Version 1.4.2
+Full source at https://github.com/harvesthq/chosen
+Copyright (c) 2011-2015 Harvest http://getharvest.com
+
+MIT License, https://github.com/harvesthq/chosen/blob/master/LICENSE.md
+This file is generated by `grunt build`, do not edit it by hand.
+*/
+
+/* @group Base */
+.chosen-container {
+ position: relative;
+ display: inline-block;
+ vertical-align: middle;
+ font-size: 13px;
+ zoom: 1;
+ *display: inline;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ user-select: none;
+}
+.chosen-container * {
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+}
+.chosen-container .chosen-drop {
+ position: absolute;
+ top: 100%;
+ left: -9999px;
+ z-index: 1010;
+ width: 100%;
+ border: 1px solid #aaa;
+ border-top: 0;
+ background: #fff;
+ box-shadow: 0 4px 5px rgba(0, 0, 0, 0.15);
+}
+.chosen-container.chosen-with-drop .chosen-drop {
+ left: 0;
+}
+.chosen-container a {
+ cursor: pointer;
+}
+.chosen-container .search-choice .group-name, .chosen-container .chosen-single .group-name {
+ margin-right: 4px;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ font-weight: normal;
+ color: #999999;
+}
+.chosen-container .search-choice .group-name:after, .chosen-container .chosen-single .group-name:after {
+ content: ":";
+ padding-left: 2px;
+ vertical-align: top;
+}
+
+/* @end */
+/* @group Single Chosen */
+.chosen-container-single .chosen-single {
+ position: relative;
+ display: block;
+ overflow: hidden;
+ padding: 0 0 0 8px;
+ height: 25px;
+ border: 1px solid #aaa;
+ border-radius: 5px;
+ background-color: #fff;
+ background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(20%, #ffffff), color-stop(50%, #f6f6f6), color-stop(52%, #eeeeee), color-stop(100%, #f4f4f4));
+ background: -webkit-linear-gradient(top, #ffffff 20%, #f6f6f6 50%, #eeeeee 52%, #f4f4f4 100%);
+ background: -moz-linear-gradient(top, #ffffff 20%, #f6f6f6 50%, #eeeeee 52%, #f4f4f4 100%);
+ background: -o-linear-gradient(top, #ffffff 20%, #f6f6f6 50%, #eeeeee 52%, #f4f4f4 100%);
+ background: linear-gradient(top, #ffffff 20%, #f6f6f6 50%, #eeeeee 52%, #f4f4f4 100%);
+ background-clip: padding-box;
+ box-shadow: 0 0 3px white inset, 0 1px 1px rgba(0, 0, 0, 0.1);
+ color: #444;
+ text-decoration: none;
+ white-space: nowrap;
+ line-height: 24px;
+}
+.chosen-container-single .chosen-default {
+ color: #999;
+}
+.chosen-container-single .chosen-single span {
+ display: block;
+ overflow: hidden;
+ margin-right: 26px;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+.chosen-container-single .chosen-single-with-deselect span {
+ margin-right: 38px;
+}
+.chosen-container-single .chosen-single abbr {
+ position: absolute;
+ top: 6px;
+ right: 26px;
+ display: block;
+ width: 12px;
+ height: 12px;
+ background: url('chosen-sprite.png') -42px 1px no-repeat;
+ font-size: 1px;
+}
+.chosen-container-single .chosen-single abbr:hover {
+ background-position: -42px -10px;
+}
+.chosen-container-single.chosen-disabled .chosen-single abbr:hover {
+ background-position: -42px -10px;
+}
+.chosen-container-single .chosen-single div {
+ position: absolute;
+ top: 0;
+ right: 0;
+ display: block;
+ width: 18px;
+ height: 100%;
+}
+.chosen-container-single .chosen-single div b {
+ display: block;
+ width: 100%;
+ height: 100%;
+ background: url('chosen-sprite.png') no-repeat 0px 2px;
+}
+.chosen-container-single .chosen-search {
+ position: relative;
+ z-index: 1010;
+ margin: 0;
+ padding: 3px 4px;
+ white-space: nowrap;
+}
+.chosen-container-single .chosen-search input[type="text"] {
+ margin: 1px 0;
+ padding: 4px 20px 4px 5px;
+ width: 100%;
+ height: auto;
+ outline: 0;
+ border: 1px solid #aaa;
+ background: white url('chosen-sprite.png') no-repeat 100% -20px;
+ background: url('chosen-sprite.png') no-repeat 100% -20px;
+ font-size: 1em;
+ font-family: sans-serif;
+ line-height: normal;
+ border-radius: 0;
+}
+.chosen-container-single .chosen-drop {
+ margin-top: -1px;
+ border-radius: 0 0 4px 4px;
+ background-clip: padding-box;
+}
+.chosen-container-single.chosen-container-single-nosearch .chosen-search {
+ position: absolute;
+ left: -9999px;
+}
+
+/* @end */
+/* @group Results */
+.chosen-container .chosen-results {
+ color: #444;
+ position: relative;
+ overflow-x: hidden;
+ overflow-y: auto;
+ margin: 0 4px 4px 0;
+ padding: 0 0 0 4px;
+ max-height: 240px;
+ -webkit-overflow-scrolling: touch;
+}
+.chosen-container .chosen-results li {
+ display: none;
+ margin: 0;
+ padding: 5px 6px;
+ list-style: none;
+ line-height: 15px;
+ word-wrap: break-word;
+ -webkit-touch-callout: none;
+}
+.chosen-container .chosen-results li.active-result {
+ display: list-item;
+ cursor: pointer;
+}
+.chosen-container .chosen-results li.disabled-result {
+ display: list-item;
+ color: #ccc;
+ cursor: default;
+}
+.chosen-container .chosen-results li.highlighted {
+ background-color: #3875d7;
+ background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(20%, #3875d7), color-stop(90%, #2a62bc));
+ background-image: -webkit-linear-gradient(#3875d7 20%, #2a62bc 90%);
+ background-image: -moz-linear-gradient(#3875d7 20%, #2a62bc 90%);
+ background-image: -o-linear-gradient(#3875d7 20%, #2a62bc 90%);
+ background-image: linear-gradient(#3875d7 20%, #2a62bc 90%);
+ color: #fff;
+}
+.chosen-container .chosen-results li.no-results {
+ color: #777;
+ display: list-item;
+ background: #f4f4f4;
+}
+.chosen-container .chosen-results li.group-result {
+ display: list-item;
+ font-weight: bold;
+ cursor: default;
+}
+.chosen-container .chosen-results li.group-option {
+ padding-left: 15px;
+}
+.chosen-container .chosen-results li em {
+ font-style: normal;
+ text-decoration: underline;
+}
+
+/* @end */
+/* @group Multi Chosen */
+.chosen-container-multi .chosen-choices {
+ position: relative;
+ overflow: hidden;
+ margin: 0;
+ padding: 0 5px;
+ width: 100%;
+ height: auto !important;
+ height: 1%;
+ border: 1px solid #aaa;
+ background-color: #fff;
+ background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(1%, #eeeeee), color-stop(15%, #ffffff));
+ background-image: -webkit-linear-gradient(#eeeeee 1%, #ffffff 15%);
+ background-image: -moz-linear-gradient(#eeeeee 1%, #ffffff 15%);
+ background-image: -o-linear-gradient(#eeeeee 1%, #ffffff 15%);
+ background-image: linear-gradient(#eeeeee 1%, #ffffff 15%);
+ cursor: text;
+}
+.chosen-container-multi .chosen-choices li {
+ float: left;
+ list-style: none;
+}
+.chosen-container-multi .chosen-choices li.search-field {
+ margin: 0;
+ padding: 0;
+ white-space: nowrap;
+}
+.chosen-container-multi .chosen-choices li.search-field input[type="text"] {
+ margin: 1px 0;
+ padding: 0;
+ height: 25px;
+ outline: 0;
+ border: 0 !important;
+ background: transparent !important;
+ box-shadow: none;
+ color: #999;
+ font-size: 100%;
+ font-family: sans-serif;
+ line-height: normal;
+ border-radius: 0;
+}
+.chosen-container-multi .chosen-choices li.search-choice {
+ position: relative;
+ margin: 3px 5px 3px 0;
+ padding: 3px 20px 3px 5px;
+ border: 1px solid #aaa;
+ max-width: 100%;
+ border-radius: 3px;
+ background-color: #eeeeee;
+ background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(20%, #f4f4f4), color-stop(50%, #f0f0f0), color-stop(52%, #e8e8e8), color-stop(100%, #eeeeee));
+ background-image: -webkit-linear-gradient(#f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);
+ background-image: -moz-linear-gradient(#f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);
+ background-image: -o-linear-gradient(#f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);
+ background-image: linear-gradient(#f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);
+ background-size: 100% 19px;
+ background-repeat: repeat-x;
+ background-clip: padding-box;
+ box-shadow: 0 0 2px white inset, 0 1px 0 rgba(0, 0, 0, 0.05);
+ color: #333;
+ line-height: 13px;
+ cursor: default;
+}
+.chosen-container-multi .chosen-choices li.search-choice span {
+ word-wrap: break-word;
+}
+.chosen-container-multi .chosen-choices li.search-choice .search-choice-close {
+ position: absolute;
+ top: 4px;
+ right: 3px;
+ display: block;
+ width: 12px;
+ height: 12px;
+ background: url('chosen-sprite.png') -42px 1px no-repeat;
+ font-size: 1px;
+}
+.chosen-container-multi .chosen-choices li.search-choice .search-choice-close:hover {
+ background-position: -42px -10px;
+}
+.chosen-container-multi .chosen-choices li.search-choice-disabled {
+ padding-right: 5px;
+ border: 1px solid #ccc;
+ background-color: #e4e4e4;
+ background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(20%, #f4f4f4), color-stop(50%, #f0f0f0), color-stop(52%, #e8e8e8), color-stop(100%, #eeeeee));
+ background-image: -webkit-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);
+ background-image: -moz-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);
+ background-image: -o-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);
+ background-image: linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);
+ color: #666;
+}
+.chosen-container-multi .chosen-choices li.search-choice-focus {
+ background: #d4d4d4;
+}
+.chosen-container-multi .chosen-choices li.search-choice-focus .search-choice-close {
+ background-position: -42px -10px;
+}
+.chosen-container-multi .chosen-results {
+ margin: 0;
+ padding: 0;
+}
+.chosen-container-multi .chosen-drop .result-selected {
+ display: list-item;
+ color: #ccc;
+ cursor: default;
+}
+
+/* @end */
+/* @group Active */
+.chosen-container-active .chosen-single {
+ border: 1px solid #5897fb;
+ box-shadow: 0 0 5px rgba(0, 0, 0, 0.3);
+}
+.chosen-container-active.chosen-with-drop .chosen-single {
+ border: 1px solid #aaa;
+ -moz-border-radius-bottomright: 0;
+ border-bottom-right-radius: 0;
+ -moz-border-radius-bottomleft: 0;
+ border-bottom-left-radius: 0;
+ background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(20%, #eeeeee), color-stop(80%, #ffffff));
+ background-image: -webkit-linear-gradient(#eeeeee 20%, #ffffff 80%);
+ background-image: -moz-linear-gradient(#eeeeee 20%, #ffffff 80%);
+ background-image: -o-linear-gradient(#eeeeee 20%, #ffffff 80%);
+ background-image: linear-gradient(#eeeeee 20%, #ffffff 80%);
+ box-shadow: 0 1px 0 #fff inset;
+}
+.chosen-container-active.chosen-with-drop .chosen-single div {
+ border-left: none;
+ background: transparent;
+}
+.chosen-container-active.chosen-with-drop .chosen-single div b {
+ background-position: -18px 2px;
+}
+.chosen-container-active .chosen-choices {
+ border: 1px solid #5897fb;
+ box-shadow: 0 0 5px rgba(0, 0, 0, 0.3);
+}
+.chosen-container-active .chosen-choices li.search-field input[type="text"] {
+ color: #222 !important;
+}
+
+/* @end */
+/* @group Disabled Support */
+.chosen-disabled {
+ opacity: 0.5 !important;
+ cursor: default;
+}
+.chosen-disabled .chosen-single {
+ cursor: default;
+}
+.chosen-disabled .chosen-choices .search-choice .search-choice-close {
+ cursor: default;
+}
+
+/* @end */
+/* @group Right to Left */
+.chosen-rtl {
+ text-align: right;
+}
+.chosen-rtl .chosen-single {
+ overflow: visible;
+ padding: 0 8px 0 0;
+}
+.chosen-rtl .chosen-single span {
+ margin-right: 0;
+ margin-left: 26px;
+ direction: rtl;
+}
+.chosen-rtl .chosen-single-with-deselect span {
+ margin-left: 38px;
+}
+.chosen-rtl .chosen-single div {
+ right: auto;
+ left: 3px;
+}
+.chosen-rtl .chosen-single abbr {
+ right: auto;
+ left: 26px;
+}
+.chosen-rtl .chosen-choices li {
+ float: right;
+}
+.chosen-rtl .chosen-choices li.search-field input[type="text"] {
+ direction: rtl;
+}
+.chosen-rtl .chosen-choices li.search-choice {
+ margin: 3px 5px 3px 0;
+ padding: 3px 5px 3px 19px;
+}
+.chosen-rtl .chosen-choices li.search-choice .search-choice-close {
+ right: auto;
+ left: 4px;
+}
+.chosen-rtl.chosen-container-single-nosearch .chosen-search,
+.chosen-rtl .chosen-drop {
+ left: 9999px;
+}
+.chosen-rtl.chosen-container-single .chosen-results {
+ margin: 0 0 4px 4px;
+ padding: 0 4px 0 0;
+}
+.chosen-rtl .chosen-results li.group-option {
+ padding-right: 15px;
+ padding-left: 0;
+}
+.chosen-rtl.chosen-container-active.chosen-with-drop .chosen-single div {
+ border-right: none;
+}
+.chosen-rtl .chosen-search input[type="text"] {
+ padding: 4px 5px 4px 20px;
+ background: white url('chosen-sprite.png') no-repeat -30px -20px;
+ background: url('chosen-sprite.png') no-repeat -30px -20px;
+ direction: rtl;
+}
+.chosen-rtl.chosen-container-single .chosen-single div b {
+ background-position: 6px 2px;
+}
+.chosen-rtl.chosen-container-single.chosen-with-drop .chosen-single div b {
+ background-position: -12px 2px;
+}
+
+/* @end */
+/* @group Retina compatibility */
+@media only screen and (-webkit-min-device-pixel-ratio: 1.5), only screen and (min-resolution: 144dpi), only screen and (min-resolution: 1.5dppx) {
+ .chosen-rtl .chosen-search input[type="text"],
+ .chosen-container-single .chosen-single abbr,
+ .chosen-container-single .chosen-single div b,
+ .chosen-container-single .chosen-search input[type="text"],
+ .chosen-container-multi .chosen-choices .search-choice .search-choice-close,
+ .chosen-container .chosen-results-scroll-down span,
+ .chosen-container .chosen-results-scroll-up span {
+ background-image: url('chosen-sprite@2x.png') !important;
+ background-size: 52px 37px !important;
+ background-repeat: no-repeat !important;
+ }
+}
+/* @end */
diff --git a/platform/www/lib/plugins/farmer/deleted.files b/platform/www/lib/plugins/farmer/deleted.files
new file mode 100644
index 0000000..fea8b5e
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/deleted.files
@@ -0,0 +1,11 @@
+# This is a list of files that were present in previous plugin releases
+# but were removed later. An up to date plugin should not have any of
+# the files installed
+_test/validation.test.php
+action/handleAjax.php
+admin/createAnimal.php
+farm.php
+lang/en/createAnimal.txt
+lang/en/preload.txt
+scripts/plugins.js
+style.css
diff --git a/platform/www/lib/plugins/farmer/helper.php b/platform/www/lib/plugins/farmer/helper.php
new file mode 100644
index 0000000..53e7153
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/helper.php
@@ -0,0 +1,334 @@
+<?php
+/**
+ * DokuWiki Plugin farmer (Helper Component)
+ *
+ * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
+ * @author Michael Große <grosse@cosmocode.de>
+ * @author Andreas Gohr <gohr@cosmocode.de>
+ */
+
+// must be run within Dokuwiki
+if(!defined('DOKU_INC')) die();
+
+class helper_plugin_farmer extends DokuWiki_Plugin {
+
+ protected $defaultPluginState = null;
+ protected $animalPluginState = array();
+
+ /**
+ * Returns the name of the current animal if any, false otherwise
+ *
+ * @return string|false
+ */
+ public function getAnimal() {
+ if(!isset($GLOBALS['FARMCORE'])) return false;
+ return $GLOBALS['FARMCORE']->getAnimal();
+ }
+
+ /**
+ * Get the farm config
+ *
+ * @return array
+ */
+ public function getConfig() {
+ if(!isset($GLOBALS['FARMCORE'])) return array();
+ return $GLOBALS['FARMCORE']->getConfig();
+ }
+
+ /**
+ * Was the current animal requested by host?
+ *
+ * @return bool
+ */
+ public function isHostbased() {
+ if(!isset($GLOBALS['FARMCORE'])) return false;
+ return $GLOBALS['FARMCORE']->isHostbased();
+ }
+
+ /**
+ * Was an animal requested that could not be found?
+ *
+ * @return bool
+ */
+ public function wasNotfound() {
+ if(!isset($GLOBALS['FARMCORE'])) return false;
+ return $GLOBALS['FARMCORE']->wasNotfound();
+ }
+
+ /**
+ * Guess the URL for an animal
+ *
+ * @param $animal
+ * @return string
+ */
+ public function getAnimalURL($animal) {
+ $config = $this->getConfig();
+
+ if(strpos($animal, '.') !== false) {
+ return 'http://' . $animal;
+ } elseif($config['base']['basedomain']) {
+ return 'http://' . $animal . '.' . $config['base']['basedomain'];
+ } else {
+ return DOKU_URL . '!' . $animal . '/';
+ }
+ }
+
+ /**
+ * List of all animals, i.e. directories within DOKU_FARMDIR without the template.
+ *
+ * @return array
+ */
+ public function getAllAnimals() {
+ $animals = array();
+ $list = glob(DOKU_FARMDIR . '*/conf/', GLOB_ONLYDIR);
+ foreach($list as $path) {
+ $animal = basename(dirname($path));
+ if($animal == '_animal') continue; // old template
+ $animals[] = $animal;
+ }
+ sort($animals);
+ return $animals;
+ }
+
+ /**
+ * checks wether $path is in under $container
+ *
+ * Also returns false if $path and $container are the same directory
+ *
+ * @param string $path
+ * @param string $container
+ * @return bool
+ */
+ public function isInPath($path, $container) {
+ $path = fullpath($path).'/';
+ $container = fullpath($container).'/';
+ if($path == $container) return false;
+ return (strpos($path, $container) === 0);
+ }
+
+ /**
+ * Check if the farm is correctly configured for this farmer plugin
+ *
+ * @return bool
+ */
+ public function checkFarmSetup() {
+ return defined('DOKU_FARMDIR') && isset($GLOBALS['FARMCORE']);
+ }
+
+ /**
+ * @param string $animalname
+ *
+ * @return bool
+ */
+ public function validateAnimalName($animalname) {
+ return preg_match("/^[a-z0-9]+([\\.\\-][a-z0-9]+)*$/i", $animalname) === 1;
+ }
+
+ /**
+ * Copy a file, or recursively copy a folder and its contents. Adapted for DokuWiki.
+ *
+ * @todo: needs tests
+ *
+ * @author Aidan Lister <aidan@php.net>
+ * @author Michael Große <grosse@cosmocode.de>
+ * @author Andreas Gohr <gohr@cosmocode.de>
+ * @link http://aidanlister.com/2004/04/recursively-copying-directories-in-php/
+ *
+ * @param string $source Source path
+ * @param string $destination Destination path
+ * @param string $exclude Regular expression to exclude files or directories (complete with delimiters)
+ * @return bool Returns TRUE on success, FALSE on failure
+ */
+ function io_copyDir($source, $destination, $exclude = '') {
+ if($exclude && preg_match($exclude, $source)) {
+ return true;
+ }
+
+ if(is_link($source)) {
+ io_lock($destination);
+ $result = symlink(readlink($source), $destination);
+ io_unlock($destination);
+ return $result;
+ }
+
+ if(is_file($source)) {
+ io_lock($destination);
+ $result = copy($source, $destination);
+ io_unlock($destination);
+ return $result;
+ }
+
+ if(!is_dir($destination)) {
+ io_mkdir_p($destination);
+ }
+
+ $dir = @dir($source);
+ if($dir === false) return false;
+ while(false !== ($entry = $dir->read())) {
+ if($entry == '.' || $entry == '..') {
+ continue;
+ }
+
+ // recurse into directories
+ $this->io_copyDir("$source/$entry", "$destination/$entry", $exclude);
+ }
+
+ $dir->close();
+ return true;
+ }
+
+ /**
+ * get a list of all Plugins installed in the farmer wiki, regardless whether they are active or not.
+ *
+ * @param bool $all get all plugins, even disabled ones
+ * @return array
+ */
+ public function getAllPlugins($all = true) {
+
+ /** @var Doku_Plugin_Controller $plugin_controller */
+ global $plugin_controller;
+
+ $plugins = $plugin_controller->getList('', $all);
+
+ // filter out a few plugins
+ $plugins = array_filter(
+ $plugins, function ($item) {
+ if($item == 'farmer') return false;
+ if($item == 'extension') return false;
+ if($item == 'upgrade') return false;
+ if($item == 'testing') return false;
+ return true;
+ }
+ );
+
+ sort($plugins);
+ return $plugins;
+ }
+
+ /**
+ * Get the plugin states configured locally in the given animal
+ *
+ * Response is cached
+ *
+ * @param $animal
+ * @return array
+ */
+ public function getAnimalPluginLocalStates($animal) {
+ if(isset($this->animalPluginState[$animal])) return $this->animalPluginState[$animal];
+
+ $localfile = DOKU_FARMDIR . $animal . '/conf/plugins.local.php';
+ $plugins = array();
+ if(file_exists($localfile)) {
+ include($localfile);
+ }
+
+ $this->animalPluginState[$animal] = $plugins;
+ return $plugins;
+ }
+
+ /**
+ * Return the default state plugins would have in animals
+ *
+ * Response is cached
+ *
+ * @return array
+ */
+ public function getDefaultPluginStates() {
+ if(!is_null($this->defaultPluginState)) return $this->defaultPluginState;
+
+ $farmconf = $this->getConfig();
+ $all = $this->getAllPlugins();
+
+ $plugins = array();
+ foreach($all as $one) {
+ if($farmconf['inherit']['plugins']) {
+ $plugins[$one] = !plugin_isdisabled($one);
+ } else {
+ $plugins[$one] = true; // default state is enabled
+ }
+ }
+
+ ksort($plugins);
+ $this->defaultPluginState = $plugins;
+ return $plugins;
+ }
+
+ /**
+ * Return a structure giving detailed info about the state of all plugins in an animal
+ *
+ * @param $animal
+ * @return array
+ */
+ public function getAnimalPluginRealState($animal) {
+ $info = array();
+
+ $defaults = $this->getDefaultPluginStates();
+ $local = $this->getAnimalPluginLocalStates($animal);
+
+ foreach($defaults as $plugin => $set) {
+ $current = array(
+ 'name' => $plugin,
+ 'default' => $set,
+ 'actual' => $set,
+ 'isdefault' => true
+ );
+
+ if(isset($local[$plugin])) {
+ $current['actual'] = (bool) $local[$plugin];
+ $current['isdefault'] = false;
+ }
+
+ $info[$plugin] = $current;
+ }
+
+ ksort($info);
+ return $info;
+ }
+
+ /**
+ * Set the state of a plugin in an animal
+ *
+ * @param string $plugin
+ * @param string $animal
+ * @param int $state -1 = default, 1 = enabled, 0 = disabled
+ */
+ public function setPluginState($plugin, $animal, $state) {
+ $state = (int) $state;
+
+ $plugins = $this->getAnimalPluginLocalStates($animal);
+ if($state < 0) {
+ if(isset($plugins[$plugin])) unset($plugins[$plugin]);
+ } else {
+ $plugins[$plugin] = $state;
+ }
+
+ $this->writePluginConf($plugins, $animal);
+
+ // clear state cache
+ if(isset($this->animalPluginState[$animal])) unset($this->animalPluginState[$animal]);
+ }
+
+ /**
+ * Write the list of (deactivated) plugins as plugin configuration of an animal to file
+ *
+ * updates the plugin state cache
+ *
+ * @param array $plugins associative array with the key being the plugin name and the value 0 or 1
+ * @param string $animal Directory of the animal within DOKU_FARMDIR
+ */
+ public function writePluginConf($plugins, $animal) {
+ $pluginConf = '<?php' . "\n# plugins enabled and disabled by the farmer plugin\n";
+ foreach($plugins as $plugin => $status) {
+ $pluginConf .= '$plugins[\'' . $plugin . '\'] = ' . $status . ";\n";
+ }
+ io_saveFile(DOKU_FARMDIR . $animal . '/conf/plugins.local.php', $pluginConf);
+ touch(DOKU_FARMDIR . $animal . '/conf/local.php');
+
+ if(function_exists('opcache_invalidate')) {
+ opcache_invalidate(DOKU_FARMDIR . $animal . '/conf/plugins.local.php');
+ opcache_invalidate(DOKU_FARMDIR . $animal . '/conf/local.php');
+ }
+
+ $this->animalPluginState[$animal] = $plugins;
+ }
+}
diff --git a/platform/www/lib/plugins/farmer/includes/config.php b/platform/www/lib/plugins/farmer/includes/config.php
new file mode 100644
index 0000000..cd9aa67
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/includes/config.php
@@ -0,0 +1,13 @@
+<?php
+/**
+ * This overrides some values for the animals without having to configure it
+ *
+ * This file is added to the protected cascade for animals only.
+ * You should not edit it!
+ */
+global $FARMCORE;
+$conf['savedir'] = $FARMCORE->getAnimalDataDir();
+$conf['basedir'] = $FARMCORE->getAnimalBaseDir();
+$conf['upgradecheck'] = 0;
+
+
diff --git a/platform/www/lib/plugins/farmer/includes/plugins.php b/platform/www/lib/plugins/farmer/includes/plugins.php
new file mode 100644
index 0000000..cd8d7b2
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/includes/plugins.php
@@ -0,0 +1,12 @@
+<?php
+/**
+ * This overrides some values for the animals without having to configure it
+ *
+ * This file is added to the protected cascade for animals only.
+ * You should not edit it!
+ */
+$plugins['extension'] = 0;
+$plugins['upgrade'] = 0;
+$plugins['testing'] = 0;
+$plugins['farmer'] = 1;
+$plugins['farmsync'] = 0;
diff --git a/platform/www/lib/plugins/farmer/includes/template.php b/platform/www/lib/plugins/farmer/includes/template.php
new file mode 100644
index 0000000..1b0075c
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/includes/template.php
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<html lang="<?php echo $conf['lang'] ?>" dir="<?php echo $lang['direction'] ?>" class="popup no-js">
+<head>
+ <meta charset="utf-8"/>
+ <title>
+ <?php echo $title ?>
+ </title>
+ <script>(function (H) {
+ H.className = H.className.replace(/\bno-js\b/, 'js')
+ })(document.documentElement)</script>
+ <?php tpl_metaheaders() ?>
+ <meta name="viewport" content="width=device-width,initial-scale=1"/>
+ <?php echo tpl_favicon(array('favicon', 'mobile')) ?>
+ <?php tpl_includeFile('meta.html') ?>
+</head>
+
+<body>
+<!--[if lte IE 8 ]>
+<div id="IE8"><![endif]-->
+<div class="dokuwiki">
+ <div class="page">
+ <?php echo $body ?>
+ </div>
+</div>
+<!--[if lte IE 8 ]></div><![endif]-->
+</body>
+</html>
diff --git a/platform/www/lib/plugins/farmer/lang/de/lang.php b/platform/www/lib/plugins/farmer/lang/de/lang.php
new file mode 100644
index 0000000..b97a850
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/lang/de/lang.php
@@ -0,0 +1,118 @@
+<?php
+/**
+ * German language file for farmer plugin
+ *
+ * @author Andreas Gohr <gohr@cosmocode.de>
+ */
+
+// menu entry for admin plugins
+$lang['menu'] = 'Farming';
+
+// tabs
+$lang['tab_setup'] = 'Farm Setup';
+$lang['tab_info'] = 'Info';
+$lang['tab_config'] = 'Konfiguration';
+$lang['tab_plugins'] = 'Plugins verwalten';
+$lang['tab_new'] = 'Neues Animal hinzufügen';
+$lang['tab_delete'] = 'Animal löschen';
+
+// setup
+$lang['preloadPHPForm'] = 'Farm aufsetzen';
+$lang['farm dir'] = 'Animal-Verzeichnis';
+$lang['htaccess setup'] = 'Farm code zu .htaccess hinzufügen?';
+$lang['submit'] = 'Abschicken';
+$lang['farmdir_missing'] = 'Bitte geben Sie das Verzeichnis an in dem die Animals gespeichert werden sollen.';
+$lang['farmdir_in_dokuwiki'] = 'Das Animal-Verzeichnis (%s) muss außerhalb des Farm-DokuWikis (%s) liegen.';
+$lang['farmdir_uncreatable'] = 'Das Animal-Verzeichnis (%s) konnte nicht erzeugt werden. Sind die Dateiberechtigungen korrekt?';
+$lang['farmdir_unwritable'] = 'Bitte stellen Sie sicher, dass der Webserver in das Animal-Verzeichnis (%s) schreiben darf!';
+$lang['farmdir_notEmpty'] = 'Das Animal-Verzeichnis (%s) muss leer sein!';
+$lang['preload creation success'] = 'Die Farm wurde erfolgreich angelegt.';
+$lang['preload creation error'] = 'Es ist ein Fehler beim Aufsetzen der Farm ausfgetreten.';
+$lang['overwrite_preload'] = 'Achtung: Ihre existierende: inc/preload.php wird überschrieben, wenn Sie diesen hier weitermachen!';
+
+// info
+$lang['animal'] = 'Animal Name / Domain';
+$lang['thisis'] = 'Diese Instanz ist';
+$lang['thisis.farmer'] = 'Der Farmer!';
+$lang['thisis.animal'] = 'Ein Animal!';
+$lang['baseinstall'] = 'Farmer Installation';
+$lang['animals'] = 'Animals';
+$lang['confdir'] = 'Konfigurationsverzeichnis dieser Instanz';
+$lang['savedir'] = 'data-Verzeichnis dieser Instanz';
+$lang['plugins'] = 'In dieser Instanz aktivierte Plugins';
+
+// config
+$lang['base'] = 'Grundkonfiguration';
+$lang['farm host'] = 'Farmer Host Name';
+$lang['base domain'] = 'Basis-Domain für Subdomain-Animals';
+$lang['conf_inherit'] = 'Farmer-Einstellungen die von Animals geerbt werden sollen';
+$lang['conf_inherit_main'] = 'Konfigurationseinstellungen';
+$lang['conf_inherit_acronyms'] = 'Abkürzungs-Definitionen';
+$lang['conf_inherit_entities'] = 'Entity-Definitionionen';
+$lang['conf_inherit_interwiki'] = 'Interwiki-Definitionen';
+$lang['conf_inherit_license'] = 'Lizenz-Definitionen';
+$lang['conf_inherit_mime'] = 'MIME-Type-Definitionen';
+$lang['conf_inherit_scheme'] = 'URL-Scheme-Definitionen';
+$lang['conf_inherit_smileys'] = 'Smiley-Definitionen';
+$lang['conf_inherit_wordblock'] = 'Spamfiltereinträge';
+$lang['conf_inherit_userstyle'] = 'Nutzer-Styles';
+$lang['conf_inherit_userscript'] = 'Nutzer-Scripts';
+$lang['conf_inherit_styleini'] = 'Anpassungen an Template-Styles';
+$lang['conf_inherit_users'] = 'Benutzer (nur Plain Auth)';
+$lang['conf_inherit_plugins'] = 'Plugin-Zustand';
+$lang['conf_inherit_yes'] = 'vom Farmer geerbt';
+$lang['conf_inherit_no'] = 'unabhängig vom Farmer';
+$lang['conf_notfound'] = 'Verhalten bei zugriff auf nicht-existierende Animals';
+$lang['conf_notfound_farmer'] = 'Zeige das Farmer-Wiki';
+$lang['conf_notfound_404'] = 'Zeige eine 404-Fehlerseite';
+$lang['conf_notfound_list'] = 'Zeige eine Liste der existierenden Animals';
+$lang['conf_notfound_redirect'] = 'Leite auf untenstehende URL um';
+$lang['conf_notfound_url'] = 'URL auf die umgeleitet werden soll wenn oben ausgewählt';
+$lang['save'] = 'Speichern';
+
+// new
+$lang['animal template'] = 'Bestehendes Animal kopieren';
+$lang['animal creation success'] = 'Das Animal "%s" wurde erfolgreich angelegt.';
+$lang['animal creation error'] = 'Es gab einen Fehler beim Anlegen des Animals.';
+$lang['animal configuration'] = 'Animal Grundkonfiguration';
+$lang['animal administrator'] = 'Animal Administrator';
+$lang['noUsers'] = 'Keine Benutzer erzeugen';
+$lang['importUsers'] = 'Alle Benutzeraccounts des Farmers in das neue Animal kopieren';
+$lang['currentAdmin'] = 'Den aktuellen Benutzer als Admin setzen';
+$lang['newAdmin'] = 'Neuen Benutzer "admin" anlegen';
+$lang['admin password'] = 'Passwort für den neuen Administrator';
+$lang['animalname_missing'] = 'Bitte geben Sie einen Namen für das neue Animal an.';
+$lang['animalname_invalid'] = ' Der Name des Animals darf nur aus Buchstaben und Ziffern sowie aus Bindestrichen und Punkten (nicht am Anfang oder Ende) bestehen.';
+$lang['animalname_preexisting'] = 'Ein Animal mit diesem Namen existiert bereits.';
+$lang['adminPassword_empty'] = 'Das Passwort für den neuen Administrator darf nicht leer sein.';
+$lang['animal template copy error'] = 'Es gab ein Problem beim Kopieren von %s aus dem existierenden Animal in das neue.';
+
+// plugins
+$lang['bulkSingleSwitcher'] = 'Ein einzelnes Animal bearbeiten oder alle aufeinmal?';
+$lang['bulkEdit'] = 'Alle Animals bearbeiten';
+$lang['singleEdit'] = 'Ein einzelnes Animal bearbeiten';
+$lang['bulkEditForm'] = 'Plugins in allen Animals ein- oder ausschalten';
+$lang['activate'] = 'Aktivieren';
+$lang['deactivate'] = 'Deaktivieren';
+$lang['singleEditForm'] = 'Plugins eines spezifischen Animals bearbeiten';
+$lang['plugindone'] = 'Plugin states updated';
+$lang['plugin'] = 'Plugin';
+$lang['plugin_on'] = 'an';
+$lang['plugin_off'] = 'aus';
+$lang['plugin_default'] = 'Voreinstellung';
+$lang['plugin_enabled'] = 'Aktiviert';
+$lang['plugin_disabled'] = 'Deaktiviert';
+$lang['js']['animalSelect'] = 'Wählen Sie ein Animal';
+$lang['js']['pluginSelect'] = 'Wählen Sie ein Plugin';
+
+// delete
+$lang['delete_animal'] = 'Animal zum Löschen auswählen';
+$lang['delete_confirm'] = 'Name des Animals erneut eingeben, um Löschen zu bestätigen';
+$lang['delete'] = 'Animal und alle darin gespeicherten Daten löschen';
+$lang['delete_noanimal'] = 'Bitte wählen sie ein Animal zum Löschen aus';
+$lang['delete_mismatch'] = 'Bestätigung stimmt nicht mit Animalnamen überein. Nicht gelöscht.';
+$lang['delete_invalid'] = 'Invalider Animalname. Nicht gelöscht.';
+$lang['delete_success'] = 'Animal erfolgreich gelöscht.';
+$lang['delete_fail'] = 'Einige Dateien konnten nicht gelöscht werden. Sie sollten diese manuell entfernen.';
+
+//Setup VIM: ex: et ts=4 :
diff --git a/platform/www/lib/plugins/farmer/lang/de/notfound_404.txt b/platform/www/lib/plugins/farmer/lang/de/notfound_404.txt
new file mode 100644
index 0000000..bac18d9
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/lang/de/notfound_404.txt
@@ -0,0 +1,3 @@
+====== 404 Nicht gefunden ======
+
+Die angeforderte Ressource konnte nicht gefunden werden
diff --git a/platform/www/lib/plugins/farmer/lang/de/notfound_list.txt b/platform/www/lib/plugins/farmer/lang/de/notfound_list.txt
new file mode 100644
index 0000000..300ba7f
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/lang/de/notfound_list.txt
@@ -0,0 +1,3 @@
+====== Wiki nicht gefunden ======
+
+Das angeforderte Wiki konnte nicht gefunden werden. Bitte wählen Sie aus der folgenden Liste der vorhandenen Wikis.
diff --git a/platform/www/lib/plugins/farmer/lang/de/settings.php b/platform/www/lib/plugins/farmer/lang/de/settings.php
new file mode 100644
index 0000000..661a3ec
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/lang/de/settings.php
@@ -0,0 +1,13 @@
+<?php
+/**
+ * German language file for farmer plugin
+ *
+ * @author Andreas Gohr <gohr@cosmocode.de>
+ */
+
+// keys need to match the config setting name
+$lang['deactivated plugins'] = 'Kommaseparierte Liste an Plugins die in neuen Animals automatisch deaktiviert werden sollen.';
+
+
+
+//Setup VIM: ex: et ts=4 :
diff --git a/platform/www/lib/plugins/farmer/lang/de/tab_config.txt b/platform/www/lib/plugins/farmer/lang/de/tab_config.txt
new file mode 100644
index 0000000..299c979
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/lang/de/tab_config.txt
@@ -0,0 +1 @@
+Hier können die grundlegenden Eigenschaften der Farm konfiguriert werden. Beachten Sie, dass alle Einstellungen hier alle Animals beeinflussen werden.
diff --git a/platform/www/lib/plugins/farmer/lang/de/tab_delete.txt b/platform/www/lib/plugins/farmer/lang/de/tab_delete.txt
new file mode 100644
index 0000000..795487a
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/lang/de/tab_delete.txt
@@ -0,0 +1 @@
+Sie können bestehende Animals hier löschen. Dies löscht **alle Daten, inklusive Seiten und Medieninhalte** des gewählten Animals. **Dies kann nicht rückgängig gemacht werden!**
diff --git a/platform/www/lib/plugins/farmer/lang/de/tab_info.txt b/platform/www/lib/plugins/farmer/lang/de/tab_info.txt
new file mode 100644
index 0000000..11faab9
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/lang/de/tab_info.txt
@@ -0,0 +1 @@
+Dieses Wiki ist Teil eines Farm-Setups. Nähere Details finden Sie unten.
diff --git a/platform/www/lib/plugins/farmer/lang/de/tab_new.txt b/platform/www/lib/plugins/farmer/lang/de/tab_new.txt
new file mode 100644
index 0000000..a62ef02
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/lang/de/tab_new.txt
@@ -0,0 +1 @@
+Hier können Sie ein neues Animal anlegen.
diff --git a/platform/www/lib/plugins/farmer/lang/de/tab_plugins.txt b/platform/www/lib/plugins/farmer/lang/de/tab_plugins.txt
new file mode 100644
index 0000000..8277c98
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/lang/de/tab_plugins.txt
@@ -0,0 +1 @@
+Hier können Sie Plugins für einzelne oder alle Animals ein- oder ausschalten.
diff --git a/platform/www/lib/plugins/farmer/lang/de/tab_setup.txt b/platform/www/lib/plugins/farmer/lang/de/tab_setup.txt
new file mode 100644
index 0000000..e0dc63b
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/lang/de/tab_setup.txt
@@ -0,0 +1 @@
+Ihr Wiki ist noch nicht für Farming mit dem Farmer-Plugin eingerichtet. Bitte nutzen Sie den folgenden Dialog zum Aufsetzen der Farm.
diff --git a/platform/www/lib/plugins/farmer/lang/en/lang.php b/platform/www/lib/plugins/farmer/lang/en/lang.php
new file mode 100644
index 0000000..da49836
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/lang/en/lang.php
@@ -0,0 +1,128 @@
+<?php
+/**
+ * English language file for farmer plugin
+ *
+ * @author Michael Große <grosse@cosmocode.de>
+ * @author Andreas Gohr <gohr@cosmocode.de>
+ */
+
+// menu entry for admin plugins
+$lang['menu'] = 'Farming';
+
+// tabs
+$lang['tab_setup'] = 'Farm Setup';
+$lang['tab_info'] = 'Info';
+$lang['tab_config'] = 'Configuration';
+$lang['tab_plugins'] = 'Manage Plugins';
+$lang['tab_new'] = 'Add new Animal';
+$lang['tab_delete'] = 'Delete Animal';
+
+// setup
+$lang['preloadPHPForm'] = 'Initialize Farming';
+$lang['farm dir'] = 'Animal directory';
+$lang['htaccess setup'] = 'Add farm code to .htaccess?';
+$lang['submit'] = 'Submit';
+$lang['farmdir_missing'] = 'Please enter a directory where the Animals should be stored.';
+$lang['farmdir_in_dokuwiki'] = 'The Animal directory (%s) must be outside of the Farm dokuwiki (%s).';
+$lang['farmdir_uncreatable'] = 'The Animal directory (%s) could not be created. Are the permissions correct?';
+$lang['farmdir_unwritable'] = 'Please make sure that the webserver has write access in the Animal directory (%s)!';
+$lang['farmdir_notEmpty'] = 'The Animal directory (%s) must be empty.';
+$lang['preload creation success'] = 'Farming has been succesfully initialized.';
+$lang['preload creation error'] = 'There was an error during Farming initialization.';
+$lang['overwrite_preload'] = 'Warning: Your existing inc/preload.php will be overwritten when continuing here!';
+
+// info
+$lang['animal'] = 'Animal Name / Domain';
+$lang['thisis'] = 'Instance is';
+$lang['thisis.farmer'] = 'The farmer!';
+$lang['thisis.animal'] = 'An Animal!';
+$lang['baseinstall'] = 'Farmer Install';
+$lang['animals'] = 'Animals';
+$lang['confdir'] = 'Instance Configuration Directory';
+$lang['savedir'] = 'Instance Data Directory';
+$lang['plugins'] = 'Plugins active in this instance';
+
+// config
+$lang['base'] = 'Base Configuration';
+$lang['farm host'] = 'Farmer Host Name';
+$lang['base domain'] = 'Base Domain for subdomain Animals';
+$lang['conf_inherit'] = 'Farmer Settings Animals should inherit';
+$lang['conf_inherit_main'] = 'Configuration Settings';
+$lang['conf_inherit_acronyms'] = 'Acronym Definitions';
+$lang['conf_inherit_entities'] = 'Entity Definitions';
+$lang['conf_inherit_interwiki'] = 'Interwiki Definitions';
+$lang['conf_inherit_license'] = 'License Definitions';
+$lang['conf_inherit_mime'] = 'MIME Type Definitions';
+$lang['conf_inherit_scheme'] = 'URL Scheme Definitions';
+$lang['conf_inherit_smileys'] = 'Smiley Definitions';
+$lang['conf_inherit_wordblock'] = 'Spam Blacklist Entries';
+$lang['conf_inherit_userstyle'] = 'User Styles';
+$lang['conf_inherit_userscript'] = 'User Scripts';
+$lang['conf_inherit_styleini'] = 'Template style customizations';
+$lang['conf_inherit_users'] = 'Users (Plain Auth only)';
+$lang['conf_inherit_plugins'] = 'Plugin State';
+$lang['conf_inherit_yes'] = 'inherited from farmer';
+$lang['conf_inherit_no'] = 'independent from farmer';
+$lang['conf_notfound'] = 'Behavior on accessing nonexistent Animals';
+$lang['conf_notfound_farmer'] = 'Show the farmer wiki';
+$lang['conf_notfound_404'] = 'Show a 404 error page';
+$lang['conf_notfound_list'] = 'Show a list of available animals';
+$lang['conf_notfound_redirect'] = 'Redirect to the URL below';
+$lang['conf_notfound_url'] = 'URL to redirect to if selected above';
+$lang['save'] = 'Save';
+
+// new
+$lang['animal template'] = 'Copy existing Animal';
+$lang['animal creation success'] = 'The Animal "%s" has been successfully created.';
+$lang['animal creation error'] = 'There was an error while creating the Animal.';
+$lang['animal configuration'] = 'Basic Animal configuration';
+$lang['inherit user registration'] = 'Inherit user registration setting from farmer';
+$lang['enable user registration'] = 'Allow users to register themselves';
+$lang['disable user registration'] = 'Disable user register';
+$lang['animal administrator'] = 'Animal administrator';
+$lang['noUsers'] = 'Do not create any users';
+$lang['importUsers'] = 'Import all users of the Farmer to the new Animal';
+$lang['currentAdmin'] = 'Set the current user as admin';
+$lang['newAdmin'] = 'Create new admin user "admin"';
+$lang['admin password'] = 'Password for the new admin';
+$lang['animalname_missing'] = 'Please enter a name for the new Animal.';
+$lang['animalname_invalid'] = 'The Animal name may only contain alphanumeric characters and dots/hyphens (but not as first or last character).';
+$lang['animalname_preexisting'] = 'An Animal with that name already exists.';
+$lang['adminPassword_empty'] = 'The password for the new admin account must not be empty.';
+$lang['animal template copy error'] = 'There was a problem copying %s from the existing Animal to the new one.';
+$lang['aclpolicy missing/bad'] = 'Please choose an initial ACL policy from the dropdown.';
+
+// plugins
+$lang['bulkSingleSwitcher'] = 'Edit a single Animal or all at once?';
+$lang['bulkEdit'] = 'Bulk edit all Animals';
+$lang['singleEdit'] = 'Edit a single Animal';
+$lang['bulkEditForm'] = 'Activate or deactivate a plugin in all Animals';
+$lang['matrixEdit'] = 'Edit Animal/Plugin matrix';
+$lang['default'] = 'Set to default';
+$lang['activate'] = 'Activate';
+$lang['deactivate'] = 'Deactivate';
+$lang['singleEditForm'] = 'Edit the plugins of a specific Animal';
+$lang['plugindone'] = 'Plugin states updated';
+$lang['plugin'] = 'Plugin';
+$lang['plugin_on'] = 'on';
+$lang['plugin_off'] = 'off';
+$lang['plugin_default'] = 'Default';
+$lang['plugin_enabled'] = 'Enabled';
+$lang['plugin_disabled'] = 'Disabled';
+$lang['js']['animalSelect'] = 'Select an animal';
+$lang['js']['pluginSelect'] = 'Select a plugin';
+$lang['disable_new_plugins'] = 'The plugin has been disabled by default. You can enable it here or in specific animals only.';
+
+
+// delete
+$lang['delete_animal'] = 'Select Animal to delete';
+$lang['delete_confirm'] = 'Please type the Animal name to confirm';
+$lang['delete'] = 'Delete the Animal and all its data';
+
+$lang['delete_noanimal'] = 'Please select an Animal to delete';
+$lang['delete_mismatch'] = 'Confirmation does not match Animal name. Not deleted.';
+$lang['delete_invalid'] = 'Invalid Animal name. Not deleted.';
+$lang['delete_success'] = 'Animal successfully deleted.';
+$lang['delete_fail'] = 'Some files could not be deleted, you should clean up manuallly.';
+
+//Setup VIM: ex: et ts=4 :
diff --git a/platform/www/lib/plugins/farmer/lang/en/notfound_404.txt b/platform/www/lib/plugins/farmer/lang/en/notfound_404.txt
new file mode 100644
index 0000000..b023e79
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/lang/en/notfound_404.txt
@@ -0,0 +1,3 @@
+====== 404 Not Found ======
+
+The requested resource could not be found.
diff --git a/platform/www/lib/plugins/farmer/lang/en/notfound_list.txt b/platform/www/lib/plugins/farmer/lang/en/notfound_list.txt
new file mode 100644
index 0000000..9e4633a
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/lang/en/notfound_list.txt
@@ -0,0 +1,3 @@
+====== Wiki Not Found ======
+
+The requested Wiki could not be found. Please refer to the list of available Wikis below.
diff --git a/platform/www/lib/plugins/farmer/lang/en/settings.php b/platform/www/lib/plugins/farmer/lang/en/settings.php
new file mode 100644
index 0000000..0e854ac
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/lang/en/settings.php
@@ -0,0 +1,13 @@
+<?php
+/**
+ * english language file for farmer plugin
+ *
+ * @author Michael Große <grosse@cosmocode.de>
+ */
+
+// keys need to match the config setting name
+$lang['deactivated plugins'] = 'Comma-separated list of plugins which are deactivated by default in new animals.';
+$lang['disable_new_plugins'] = 'Automatically disable plugins after they have been newly installed in the farmer? (only when installed via extension manager)';
+
+
+//Setup VIM: ex: et ts=4 :
diff --git a/platform/www/lib/plugins/farmer/lang/en/tab_config.txt b/platform/www/lib/plugins/farmer/lang/en/tab_config.txt
new file mode 100644
index 0000000..cedf536
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/lang/en/tab_config.txt
@@ -0,0 +1 @@
+Here the basic behavior of the farm is configured. Please be aware that changing options here will affect all animals.
diff --git a/platform/www/lib/plugins/farmer/lang/en/tab_config_help.txt b/platform/www/lib/plugins/farmer/lang/en/tab_config_help.txt
new file mode 100644
index 0000000..0ebf7c2
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/lang/en/tab_config_help.txt
@@ -0,0 +1,26 @@
+===== Farm Configuration =====
+
+All settings made here are saved to the Farmer's ''conf/farm.ini''.
+
+==== Base Configuration ====
+
+The **Farmer's host name** was set automatically during the setup, but you can change it here. It
+will be used to detect if a request was made directly to the Farmer when using host based farms. This
+should be a full qualified hostname (''foo.example.com'' instead of just ''foo'').
+
+When using a subdomain wildcard setup you should specify the main domain in the **Base Domain** setting.
+Eg. if you specify ''example.com'' it is assumed an animal named ''foo'' is reachable via ''foo.example.com''.
+The base domain is only appended to animal names not containing any dots.
+
+==== Inheritence ====
+
+Here you can specify what configuration settings made in the farmer should be used as defaults in the animals.
+Animals can still override the Farmer settings in their own configuration files. When inheritance is disabled,
+DokuWiki's default settings are the defaults for all animals as well.
+
+==== Non Existing Animals ====
+
+By default, when accessing a non-existing animal no error message is shown. You can pick between different
+behavior here. Be sure that your Farmer's host name is set up correctly above, before switching away from the
+default.
+
diff --git a/platform/www/lib/plugins/farmer/lang/en/tab_delete.txt b/platform/www/lib/plugins/farmer/lang/en/tab_delete.txt
new file mode 100644
index 0000000..cd7f67c
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/lang/en/tab_delete.txt
@@ -0,0 +1 @@
+You can delete existing animals here. This deletes **all data, including pages and media files** of the selected animal. **This is irreversible!**
diff --git a/platform/www/lib/plugins/farmer/lang/en/tab_info.txt b/platform/www/lib/plugins/farmer/lang/en/tab_info.txt
new file mode 100644
index 0000000..f3d7a83
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/lang/en/tab_info.txt
@@ -0,0 +1 @@
+This wiki is part of a farm setup. Check the details below.
diff --git a/platform/www/lib/plugins/farmer/lang/en/tab_new.txt b/platform/www/lib/plugins/farmer/lang/en/tab_new.txt
new file mode 100644
index 0000000..ffe899c
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/lang/en/tab_new.txt
@@ -0,0 +1 @@
+You can create a new animal here.
diff --git a/platform/www/lib/plugins/farmer/lang/en/tab_new_help.txt b/platform/www/lib/plugins/farmer/lang/en/tab_new_help.txt
new file mode 100644
index 0000000..0445e82
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/lang/en/tab_new_help.txt
@@ -0,0 +1,31 @@
+===== Animal Creation =====
+
+Animals are the independent subwikis of a Dokuwiki farm. To create a new animal a name has to be assigned.
+
+==== Animal Names ====
+
+For .htaccess based setups this should be a single word. For domain based setups you should provide either a full qualified domain name.
+If you use a wildcard subdomain setup you can also just provide the hostname part if you set a base domain in the configuration.
+
+Examples:
+
+ * .htaccess based: ''foo'' for an animal accessible at ''%%http://example.org/dokuwiki/!foo/%%''
+ * domain based: ''%%www.foo.com%%'' for an animal accessible at ''%%http://www.foo.com/%%''
+ * sub domain based: ''foo'' for an animal accessible at ''%%http://foo.example.com/%%''
+
+The latter two require the appropriate DNS setup!
+
+==== Copy Animal ====
+
+You can select an existing animal to base the new one on. All configuration, page, media and meta data will be copied
+to the new animal. Page and media revisions will not be copied.
+
+The title and logo image will be overwritten to make sure you can distinguish the new from the old wiki.
+
+==== Animal Administrator ====
+
+The Animal will be a fully functional wiki with it's own user base. You will need at least one administrative user
+to configure it. You can copy your current user or all users from the farmer or create a completely new user for the Animal.
+
+You can choose to not create any users. This should only be chosen when inheriting users from the Farmer was enabled in the
+configuration tab or you copied a different animal with existing users.
diff --git a/platform/www/lib/plugins/farmer/lang/en/tab_plugins.txt b/platform/www/lib/plugins/farmer/lang/en/tab_plugins.txt
new file mode 100644
index 0000000..78e8021
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/lang/en/tab_plugins.txt
@@ -0,0 +1 @@
+You can activate or deactivate either a plugin for all animals in a single bulk operation or you can edit the plugins of a specific animal.
diff --git a/platform/www/lib/plugins/farmer/lang/en/tab_plugins_help.txt b/platform/www/lib/plugins/farmer/lang/en/tab_plugins_help.txt
new file mode 100644
index 0000000..44af5fc
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/lang/en/tab_plugins_help.txt
@@ -0,0 +1,16 @@
+===== Plugin Management =====
+
+The extension manager is disabled in all Animals. Plugins can only be installed in the Farmer and only the Farmer
+may control what plugins are enabled or disabled in all Animals. This is done through this interface.
+
+A plugin can have three states in an Animal. It can be enabled (on) or disabled (off), or it can have the default state. The
+default means that the state of a Plugin was not explicitly set for an Animal. Normally the default state is on.
+
+When Animals are configured to inherit the Plugin state of the Farmer, a plugin in default state will have the same
+state as in the Farmer. Eg. when you disable a plugin in the Farmer it will disable the plugin for all Animals which
+did not have the state of this plugin explicitly set.
+
+There are three ways to manage plugins: You can either pick a single plugin and set its state to the same value in all animals
+or you can pick a specific animal and configure all the plugin states within that animal only. The third option allows you to
+see and edit the state of all plugins in all animals at once. This option may not be feasible of you have a large amount
+of animals.
diff --git a/platform/www/lib/plugins/farmer/lang/en/tab_setup.txt b/platform/www/lib/plugins/farmer/lang/en/tab_setup.txt
new file mode 100644
index 0000000..19ae730
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/lang/en/tab_setup.txt
@@ -0,0 +1 @@
+Your Wiki is not set up as a farm managed by the Farmer Plugin, yet. Please use the wizard below to enable farming.
diff --git a/platform/www/lib/plugins/farmer/lang/en/tab_setup_help.txt b/platform/www/lib/plugins/farmer/lang/en/tab_setup_help.txt
new file mode 100644
index 0000000..f81532f
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/lang/en/tab_setup_help.txt
@@ -0,0 +1,37 @@
+===== About Farms =====
+
+Farms allow you to have a single DokuWiki installation (The "Farmer") that powers an arbitrary number of
+other wikis (The "Animals"). You install plugins and templates in the Farmer only and then make them available
+through to the animals. You only need to keep one wiki uptodate and all other wikis just use the same code base.
+
+After completing this setup step your current DokuWiki (the one you're looking at) will be known as the "Farmer".
+
+===== What does this Setup do? =====
+
+This setup wizard will do three things:
+
+ - create a ''inc/preload.php'' file
+ - create a ''conf/farm.ini'' file
+ - optionally append to the ''.htaccess'' file
+
+The ''preload.php'' is a file that is loaded at the very beginning of loading DokuWiki. Here the farm mechanism is
+inititialized. The Farmer plugin will detect if the current request should access an Animal or the Farmer and
+reconfigure everything accordingly.
+
+The ''conf/farm.ini'' contains the basic configuration of the farm setup. Most importantly it will contain the
+location where all the animal's data will be stored.
+
+The ''.htaccess'' modification makes animals accessible through the //bang!// mechanism. (See below)
+
+===== What to fill in? =====
+
+The **Animal Directory** is where a new directory is created for each Animal you create. This directory has to be
+outside your current DokuWiki. You can specify a relative directory like ''../animals''.
+
+Enabling the **.htaccess** support is recommended. This feature requires Apache with mod_rewrite and .htaccess support.
+
+When enabled, your animals will be accessible under the farmer's URL using the //bang!// mechanism. Eg. if your farmer is
+running at ''%%http://www.example.com/dokuwiki/%%'', an animal will be accessible at
+''%%http://www.example.com/dokuwiki/!animal%%''.
+
+If you do not enable this, you will have to configure your Webserver and DNS to access the animals.
diff --git a/platform/www/lib/plugins/farmer/lang/fr/lang.php b/platform/www/lib/plugins/farmer/lang/fr/lang.php
new file mode 100644
index 0000000..8171952
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/lang/fr/lang.php
@@ -0,0 +1,109 @@
+<?php
+
+/**
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ * @author Schplurtz le Déboulonné <schplurtz@laposte.net>
+ * @author ubibene <services.m@benard.info>
+ */
+$lang['menu'] = 'Élevage';
+$lang['tab_setup'] = 'Configuration de la ferme';
+$lang['tab_info'] = 'Info';
+$lang['tab_config'] = 'Configuration';
+$lang['tab_plugins'] = 'Gestion des greffons';
+$lang['tab_new'] = 'Ajouter un animal';
+$lang['tab_delete'] = 'Supprimer un animal';
+$lang['preloadPHPForm'] = 'Initialiser la ferme';
+$lang['farm dir'] = 'Dossier des animaux';
+$lang['htaccess setup'] = 'Ajouter le code d\'élevage au .htaccess ?';
+$lang['submit'] = 'Envoyer';
+$lang['farmdir_missing'] = 'Veuillez entrer un dossier où stocker les animaux.';
+$lang['farmdir_in_dokuwiki'] = 'Le dossier des animaux (%s) doit se trouver hors de la ferme DokuWiki (%s).';
+$lang['farmdir_uncreatable'] = 'Impossible de créer le dossier (%s). Les permissions sont-elles correctes ?';
+$lang['farmdir_unwritable'] = 'Veuillez vous assurer que le serveur web dispose d\'un accès en écriture au dossier des animaux (%s) !';
+$lang['farmdir_notEmpty'] = 'Le dossier des animaux (%s) doit être vide.';
+$lang['preload creation success'] = 'Configuration de la ferme réalisée.';
+$lang['preload creation error'] = 'Il y a eu une erreur lors de l\'initialisation de l\'élevage.';
+$lang['overwrite_preload'] = 'Attention, votre fichier existant inc/preload.php va être écrasé si vous continuez !';
+$lang['animal'] = 'Nom d\'animal / Domaine';
+$lang['thisis'] = 'L\'instance est';
+$lang['thisis.farmer'] = 'Le fermier!';
+$lang['thisis.animal'] = 'Un animal!';
+$lang['baseinstall'] = 'Installation de la ferme';
+$lang['animals'] = 'Animaux';
+$lang['confdir'] = 'Dossier de configuration de l\'instance';
+$lang['savedir'] = 'Dossier des données de l\'instance';
+$lang['plugins'] = 'Greffons actifs dans cette instance';
+$lang['base'] = 'Configuration de base';
+$lang['farm host'] = 'Nom d\'hôte du fermier';
+$lang['base domain'] = 'Domaine de base pour les animaux par sous-domaine';
+$lang['conf_inherit'] = 'Réglage dont les animaux vont hériter';
+$lang['conf_inherit_main'] = 'Options de configuraton';
+$lang['conf_inherit_acronyms'] = 'Définitions des acronymes';
+$lang['conf_inherit_entities'] = 'Définitions des entités';
+$lang['conf_inherit_interwiki'] = 'Liens interwiki';
+$lang['conf_inherit_license'] = 'Licence du contenu';
+$lang['conf_inherit_mime'] = 'Types MIME';
+$lang['conf_inherit_scheme'] = 'Schémas d\'URL';
+$lang['conf_inherit_smileys'] = 'Frimousses';
+$lang['conf_inherit_wordblock'] = 'Liste noire des spammeurs';
+$lang['conf_inherit_userstyle'] = 'Styles utilisateur';
+$lang['conf_inherit_userscript'] = 'Scripts utilisateur';
+$lang['conf_inherit_styleini'] = 'Personnalisations du style du thème ';
+$lang['conf_inherit_users'] = 'utilisateurs (Auth texte seulement)';
+$lang['conf_inherit_plugins'] = 'État des greffons';
+$lang['conf_inherit_yes'] = 'hérité du fermier';
+$lang['conf_inherit_no'] = 'indépendant du fermier';
+$lang['conf_notfound'] = 'Comportement lors d\'un accès à un animal inexistant';
+$lang['conf_notfound_farmer'] = 'Montrer le wiki fermier';
+$lang['conf_notfound_404'] = 'Montrer une page 404';
+$lang['conf_notfound_list'] = 'Montrer la liste des animaux disponibles';
+$lang['conf_notfound_redirect'] = 'Rediriger vers l\'URL ci-dessous';
+$lang['conf_notfound_url'] = 'URL de redirection si sélectionné ci-dessus';
+$lang['save'] = 'Enregistrer';
+$lang['animal template'] = 'Copier un animal existant';
+$lang['animal creation success'] = 'L\'animal "%s" a été créé avec succès.';
+$lang['animal creation error'] = 'Il y a eu une erreur lors de la création de l\'animal.';
+$lang['animal configuration'] = 'Configuration de base de l\'animal';
+$lang['inherit user registration'] = 'Hériter le réglage d\'enregistrement des utilisateurs.';
+$lang['enable user registration'] = 'Autoriser les utilisateurs à s\'enregistrer';
+$lang['disable user registration'] = 'Désactiver l\'enregistrement des utilisateurs';
+$lang['animal administrator'] = 'Administrateur d\'animal';
+$lang['noUsers'] = 'Ne pas créer d\'utilisateur';
+$lang['importUsers'] = 'Exporter les utilisateurs du fermier vers l\'animal';
+$lang['currentAdmin'] = 'Définir l\'utilisateur courant comme admin';
+$lang['newAdmin'] = 'Créer un nouvel administrateur "admin"';
+$lang['admin password'] = 'Mot de passe du nouvel admin';
+$lang['animalname_missing'] = 'Veuillez saisir le nom du nouvel animal.';
+$lang['animalname_invalid'] = 'Le nom de l\'animal ne doit contenir que des caractères alphanumériques et les points et tirets (mais pas en premier ou en dernier).';
+$lang['animalname_preexisting'] = 'Un animal avec ce nom existe déjà.';
+$lang['adminPassword_empty'] = 'Le mot de passe du nouvel administrateur ne peux pas être vide.';
+$lang['animal template copy error'] = 'Il y a eu un problème en copiant %s de l\'animal existant vers le nouveau.';
+$lang['aclpolicy missing/bad'] = 'Veuillez choisir une politique d\'ACL initiale.';
+$lang['bulkSingleSwitcher'] = 'Modifier un seul animal ou tout le troupeau ?';
+$lang['bulkEdit'] = 'Modifier tout le troupeau';
+$lang['singleEdit'] = 'Modifier un seul animal';
+$lang['bulkEditForm'] = 'Activer ou désactiver un greffon sur tout le troupeau.';
+$lang['matrixEdit'] = 'Modifier la matrice Animal/Greffon';
+$lang['default'] = 'Hériter du fermier';
+$lang['activate'] = 'Activer';
+$lang['deactivate'] = 'Désactiver';
+$lang['singleEditForm'] = 'Éditer les greffons d\'un animal particulier';
+$lang['plugindone'] = 'État du greffon mis à jour';
+$lang['plugin'] = 'Greffon';
+$lang['plugin_on'] = 'marche';
+$lang['plugin_off'] = 'arrêt';
+$lang['plugin_default'] = 'Herité';
+$lang['plugin_enabled'] = 'Activé';
+$lang['plugin_disabled'] = 'Désactivé';
+$lang['js']['animalSelect'] = 'Sélectionnez un animal';
+$lang['js']['pluginSelect'] = 'Sélectionnez un greffon';
+$lang['disable_new_plugins'] = 'Le greffon est par défaut désactivé. Vous pouvez l\'activer ici ou pour certains animaux spécifiques seulement.';
+$lang['delete_animal'] = 'Sélectionnez l\'animal à détruire';
+$lang['delete_confirm'] = 'Veuillez taper le nom de l\'animal pour confirmer';
+$lang['delete'] = 'Détruire l\'animal et toutes ses données';
+$lang['delete_noanimal'] = 'Veuillez sélectionner l\'animal à détruire';
+$lang['delete_mismatch'] = 'La confirmation ne correspond pas à l\'animal. Destruction annulée.';
+$lang['delete_invalid'] = 'Nom d\'animal invalide. Destruction annulée';
+$lang['delete_success'] = 'Animal supprimé avec succès.';
+$lang['delete_fail'] = 'Impossible de supprimer certains fichiers. Vous devriez faire le ménage à la main.';
diff --git a/platform/www/lib/plugins/farmer/lang/fr/notfound_404.txt b/platform/www/lib/plugins/farmer/lang/fr/notfound_404.txt
new file mode 100644
index 0000000..5aaa62d
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/lang/fr/notfound_404.txt
@@ -0,0 +1,3 @@
+====== 404 Non trouvé ======
+
+La ressource demandée ne peut être trouvée.
diff --git a/platform/www/lib/plugins/farmer/lang/fr/notfound_list.txt b/platform/www/lib/plugins/farmer/lang/fr/notfound_list.txt
new file mode 100644
index 0000000..3a6acc7
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/lang/fr/notfound_list.txt
@@ -0,0 +1,4 @@
+====== Wiki non trouvé ======
+
+Impossible de trouver le wiki que vous avez demandé. Veuillez
+vous référer à la liste ci-dessous.
diff --git a/platform/www/lib/plugins/farmer/lang/fr/settings.php b/platform/www/lib/plugins/farmer/lang/fr/settings.php
new file mode 100644
index 0000000..ed4465c
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/lang/fr/settings.php
@@ -0,0 +1,9 @@
+<?php
+
+/**
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ * @author Schplurtz le Déboulonné <Schplurtz@laposte.net>
+ */
+$lang['deactivated plugins'] = 'Liste à virgule des greffons qui sont désactivés par défaut dans les nouveaux animaux.';
+$lang['disable_new_plugins'] = 'Désactiver automatiquement les greffons après qu\'il sont installés dans le fermier. Valable seulement pour les greffons installés par le gestionnaire d\'extensions.';
diff --git a/platform/www/lib/plugins/farmer/lang/fr/tab_config.txt b/platform/www/lib/plugins/farmer/lang/fr/tab_config.txt
new file mode 100644
index 0000000..edee83f
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/lang/fr/tab_config.txt
@@ -0,0 +1 @@
+Ici, le comportement de base de la ferme est configurée. Veuillez garder à l'esprit que changer des options ici affecte tous les animaux.
diff --git a/platform/www/lib/plugins/farmer/lang/fr/tab_config_help.txt b/platform/www/lib/plugins/farmer/lang/fr/tab_config_help.txt
new file mode 100644
index 0000000..28790e4
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/lang/fr/tab_config_help.txt
@@ -0,0 +1,37 @@
+===== Configuration de la ferme =====
+
+Tous les réglages faits ici sont enregistrés dans le fichier ''conf/farm.ini'' du fermier.
+
+==== Configuration de base ====
+
+Le réglage de **Nom d'hôte du fermier** a été effectué automatiquement
+pendant l'initialisation, mais on peut le changer ici. Il sera utilisé
+pour déterminer si une requête est dirigée directement vers le fermier,
+lors de l'utilisation d'une ferme par nom. Ce devrait être un FQDN,
+''toto.example.com'' au lieu de simplement ''toto''.
+
+Lors de l'utilisation d'une configuration avec sous domaine
+générique((//wildcard subdomain//)), vous devez spécifier le
+nom principal dans le réglage **Domaine de base pour les animaux
+par sous domaine**. Par exemple, si vous indiquez ''example.com'',
+on part du principe qu'un animal nommé ''toto'' est joignable via
+''toto.example.com''. Le **Domaine de base** est simplement ajouté aux
+noms des animaux ne contenant aucun point.
+
+==== Héritage ====
+
+Vous pouvez indiquer ici quels réglages de configuration réalisés dans
+le fermier devraient être utilisés comme défaut dans les animaux.
+les animaux peuvent toujours écraser ces réglages dans leurs propres
+fichiers de configuration. Lorsque l'héritage est désactivé, les
+valeurs par défaut de DokuWiki sont également les valeurs par défaut
+pour les nouveaux animaux.
+
+==== Animaux non existant ====
+
+Par défaut, lors d'une tentative d'accès à un animal inexistant,
+aucun message d'erreur n'est affiché. Ici, vous pouvez choisir entre
+différents comportements. Assurez vous que le nom d'hôte du fermier
+soit correctement réglé ci-dessus avant de vous écarter des valeurs
+par défaut.
+
diff --git a/platform/www/lib/plugins/farmer/lang/fr/tab_delete.txt b/platform/www/lib/plugins/farmer/lang/fr/tab_delete.txt
new file mode 100644
index 0000000..73fb7ff
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/lang/fr/tab_delete.txt
@@ -0,0 +1 @@
+Vous pouvez détruire les animaux existant ici. Cela détruit **toutes les données, y compris les pages et médias** des animaux sélectionnés. **Ceci est irréversible** !
diff --git a/platform/www/lib/plugins/farmer/lang/fr/tab_info.txt b/platform/www/lib/plugins/farmer/lang/fr/tab_info.txt
new file mode 100644
index 0000000..e1a4ee0
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/lang/fr/tab_info.txt
@@ -0,0 +1 @@
+Ce wiki fait partie d'une ferme. Consultez les détails ci-dessous.
diff --git a/platform/www/lib/plugins/farmer/lang/fr/tab_new.txt b/platform/www/lib/plugins/farmer/lang/fr/tab_new.txt
new file mode 100644
index 0000000..269260d
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/lang/fr/tab_new.txt
@@ -0,0 +1 @@
+Vous pouvez créer un nouvel animal ici.
diff --git a/platform/www/lib/plugins/farmer/lang/fr/tab_new_help.txt b/platform/www/lib/plugins/farmer/lang/fr/tab_new_help.txt
new file mode 100644
index 0000000..7405a3b
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/lang/fr/tab_new_help.txt
@@ -0,0 +1,43 @@
+===== Création d'animaux =====
+
+Les animaux sont des sous-wikis indépendants d'une ferme de DokuWiki. Pour créer un nouvel animal, il faut lui assigner un nom.
+
+==== Les noms d'animaux ====
+
+Pour les fermes basées sur la réécriture par fichier .htaccess
+cela devrait être un seul mot.
+Pour les configurations basées sur les noms de domaine vous devriez indiquer
+un nom de domaine totalement qualifié (FQDN).
+Pour les fermes à base de sous domaine générique((//wildcard subdomain//
+en bon anglais)), un nom d'hôte suffit, à condition
+d'indiquer également un domaine de base.
+
+Exemples:
+
+ * basé sur .htaccess : ''toto'' pour un animal accessible à ''%%http://example.org/dokuwiki/!toto/%%''
+ * basé sur le domaine : ''%%www.toto.com%%'' pour un animal accessible à ''%%http://www.toto.com/%%''
+ * basé sur le sous domaine : ''toto'' pour un animal accessible à ''%%http://toto.example.com/%%''
+
+Les deux derniers nécessitent un réglage du DNS approprié !
+
+==== Copie d'animal ====
+
+Vous pouvez sélectionner un animal existant comme base d'un
+nouvel animal. Toute la configuration, toutes les pages et fichiers
+médias, ainsi que les méta données seront copiés vers le nouvel
+animal. L'historique des pages et médias ne sera pas copié.
+
+Le titre et l'image de logo seront modifiés de manière à être certain
+que vous pourrez distinguer la nouvelle copie de l'ancien.
+
+==== Administrateur de l'animal ====
+
+L'animal sera un wiki complètement fonctionnel, avec sa propre base
+d'utilisateurs. Vous aurez besoin d'au moins un administrateur pour le
+gérer. Vous pouvez copier l'utilisateur courant, tous les utilisateurs
+du fermier, ou créer un tout nouvel utilisateur pour l'animal.
+
+Vous pouvez choisir de ne pas créer d'utilisateur. Vous ne devriez
+choisir cette possibilité que si l'héritage d'utilisateurs du fermier
+est activé dans l'onglet de configuration, ou si vous avez copié un
+animal déjà muni d'utilisateurs.
diff --git a/platform/www/lib/plugins/farmer/lang/fr/tab_plugins.txt b/platform/www/lib/plugins/farmer/lang/fr/tab_plugins.txt
new file mode 100644
index 0000000..f7c49e5
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/lang/fr/tab_plugins.txt
@@ -0,0 +1 @@
+Vous pouvez activer ou désactiver soit un même greffon pour tous les animaux en une seule opération de masse, soit les greffons d'un animal particulier.
diff --git a/platform/www/lib/plugins/farmer/lang/fr/tab_plugins_help.txt b/platform/www/lib/plugins/farmer/lang/fr/tab_plugins_help.txt
new file mode 100644
index 0000000..179ab63
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/lang/fr/tab_plugins_help.txt
@@ -0,0 +1,22 @@
+===== Gestion des greffons =====
+
+Le gestionnaire d'extensions est désactivé dans tous les animaux. On peut
+uniquement installer les thèmes et greffons dans le fermier.
+Seul le fermier peut contrôler quels greffons
+sont activés dans les animaux. La présente interface permet d'effectuer
+ces réglages.
+
+Un greffon peut avoir trois états dans un animal. Il peut être activé, ou désactivé. Il peut également avoir l'état par défaut. Cette dernière option signifie que l'état du greffon n'est pas explicitement choisi pour un animal. Habituellement, l'état par défaut est <<activé>>.
+
+Lorsque les animaux sont configurés pour hériter l'état du fermier, un
+greffon dans l'état par défaut aura le même état que dans le fermier.
+C'est à dire que désactiver un greffon dans le fermier le désactivera
+aussi dans tous les animaux pour lesquels l'état du greffon n'a pas été
+explicitement spécifié. Si vous l'activez à nouveau dans le fermier,
+le greffon s'activera de la même manière dans tous les animaux
+concernés.
+
+Il existe trois manières de gérer les greffons :
+ - Choisir un greffon et fixer son état à la même valeur dans tous les animaux,
+ - Choisir un animal particulier et gérer l'état de chacun de ses greffons.
+ - Voir et éditer l'état de tous les greffons dans tous les animaux d'un seul coup. Cette option pourrait ne pas être praticable si vous avez un grand nombre d'animaux.
diff --git a/platform/www/lib/plugins/farmer/lang/fr/tab_setup.txt b/platform/www/lib/plugins/farmer/lang/fr/tab_setup.txt
new file mode 100644
index 0000000..f6dbdb3
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/lang/fr/tab_setup.txt
@@ -0,0 +1 @@
+Votre Wiki est pas configuré pour l'élevage avec le greffon //farmer//. Veuillez utiliser la boîte de dialogue suivante pour la mise en place de la ferme.
diff --git a/platform/www/lib/plugins/farmer/lang/fr/tab_setup_help.txt b/platform/www/lib/plugins/farmer/lang/fr/tab_setup_help.txt
new file mode 100644
index 0000000..66a3ccc
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/lang/fr/tab_setup_help.txt
@@ -0,0 +1,52 @@
+===== À propos de l'élevage =====
+
+L'élevage est une technique qui permet, à partir d'une unique
+installation de DokuWiki, connue comme le <<fermier>>, de propulser
+un nombre quelconque d'autres wikis appelés les animaux. Vous installez les
+greffons et thèmes uniquement dans le fermier, et les rendez disponibles
+à votre guise dans des animaux. Il suffit de mettre à jour le wiki principal et
+tous les autres wikis, qui partagent la même base de code, sont également mis
+à jour.
+
+Après avoir complété cette étape de configuration, votre wiki actuel,
+celui que vous lisez en ce moment même, sera connu comme le <<fermier>>.
+
+===== Que fait cette initialisation ? =====
+
+Cet assistant de configuration fera trois choses :
+
+ - Créer un fichier ''inc/preload.php'',
+ - créer un fichier ''conf/farm.ini''
+ - facultativement, ajouter un fichier ''.htaccess''.
+
+''preload.php'' est un fichier qui est chargé au tout début du chargement de DokuWiki.
+Là, le mécanisme de ferme est initialisé. Le greffon //farmer// détectera si la requête
+doit être adressée à un animal ou au fermier, et reconfigurera l'ensemble de manière
+appropriée.
+
+''conf/farm.ini'' contient les configurations de base du système de ferme. En particulier,
+ce fichier contient l'emplacement où les données des animaux seront
+enregistrées.
+
+La modification du fichier ''.htaccess'' permet de rendre accessible les animaux via
+le mécanisme ''!bang'' (voir ci-dessous).
+
+===== Que remplir ? =====
+
+Le **Dossier des animaux** est le dossier où un nouveau dossier est créé pour
+chaque nouvel animal. Ce dossier **doit** se trouver en dehors de l'arborescence
+de votre DokuWiki actuel. Vous pouvez utiliser un dossier relatif, tel que
+''../animaux'' ou un chemin absolu.
+
+Il est recommandé d'activer l'utilisation du fichier ''.htaccess''. Cette fonctionnalité
+nécessite un serveur [[https://httpd.apache.org/|Apache]] avec le module
+mod_rewrite et la prise en charge des fichiers .htaccess.
+
+Lorsque vous activez le .htaccess, les animaux sont accessibles sous l'URL du
+wiki fermier en utilisant le suffixe //!bang//. Par exemple, si votre fermier
+est accessible à l'URL
+''%%http://www.example.com/dokuwiki/%%'', l'animal toto sera accessible à
+l'URL ''%%http://www.example.com/dokuwiki/!toto%%''.
+
+Si vous n'activez pas ce mécanisme, vous devrez configurer votre serveur web
+et votre DNS pour pouvoir accéder aux animaux.
diff --git a/platform/www/lib/plugins/farmer/lang/ja/lang.php b/platform/www/lib/plugins/farmer/lang/ja/lang.php
new file mode 100644
index 0000000..f837a3a
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/lang/ja/lang.php
@@ -0,0 +1,106 @@
+<?php
+
+/**
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ * @author Hideaki SAWADA <chuno@live.jp>
+ */
+$lang['menu'] = '牧場設定';
+$lang['tab_setup'] = '牧場設定';
+$lang['tab_info'] = '情報';
+$lang['tab_config'] = '設定';
+$lang['tab_plugins'] = 'プラグイン管理';
+$lang['tab_new'] = '新しい動物の追加';
+$lang['tab_delete'] = '動物の削除';
+$lang['preloadPHPForm'] = '牧場の初期化';
+$lang['farm dir'] = '動物用ディレクトリ';
+$lang['htaccess setup'] = '.htaccess に牧場用のコードを追加しますか?';
+$lang['submit'] = '実行';
+$lang['farmdir_missing'] = '動物を保存するディレクトリを入力してください。';
+$lang['farmdir_in_dokuwiki'] = '動物用ディレクトリは牧場 dokuwiki の外にある必要があります。';
+$lang['farmdir_uncreatable'] = '動物用ディレクトリを作成できませんでした。権限は適切ですか?';
+$lang['farmdir_unwritable'] = 'Web サーバが動物用ディレクトリに書き込み権を持っていることを確認してください!';
+$lang['farmdir_notEmpty'] = '動物用ディレクトリは空でなければなりません。';
+$lang['preload creation success'] = '牧場は正常に初期化されました。';
+$lang['preload creation error'] = '牧場の初期化中にエラーが発生しました。';
+$lang['overwrite_preload'] = '警告:続行すると、既存の inc/preload.php が上書きされます!';
+$lang['animal'] = '動物名・ドメイン';
+$lang['thisis'] = 'インスタンスは';
+$lang['thisis.farmer'] = '牧場主です!';
+$lang['thisis.animal'] = '動物です!';
+$lang['baseinstall'] = '牧場主のインストール先';
+$lang['animals'] = '動物';
+$lang['confdir'] = 'インスタンスの設定ディレクトリ';
+$lang['savedir'] = 'インスタンスのデータディレクトリ';
+$lang['plugins'] = 'このインスタンスで有効なプラグイン';
+$lang['base'] = '基本設定';
+$lang['farm host'] = '牧場主ホスト名';
+$lang['base domain'] = '動物サブドメイン用のベースドメイン';
+$lang['conf_inherit'] = '動物が継承する牧場主設定';
+$lang['conf_inherit_main'] = 'DokuWiki 設定';
+$lang['conf_inherit_acronyms'] = '略字と頭字語';
+$lang['conf_inherit_entities'] = '記号への変換';
+$lang['conf_inherit_interwiki'] = 'InterWiki リンク';
+$lang['conf_inherit_license'] = 'ライセンス';
+$lang['conf_inherit_mime'] = 'MIME の設定';
+$lang['conf_inherit_scheme'] = 'URL スキーム';
+$lang['conf_inherit_smileys'] = 'スマイリー';
+$lang['conf_inherit_wordblock'] = 'ブラックリスト';
+$lang['conf_inherit_userstyle'] = 'ユーザースタイル';
+$lang['conf_inherit_users'] = 'ユーザー(テキスト認証の場合のみ)';
+$lang['conf_inherit_plugins'] = 'プラグインの状態';
+$lang['conf_inherit_yes'] = '牧場主から継承';
+$lang['conf_inherit_no'] = '牧場主から独立';
+$lang['conf_notfound'] = '実在しない動物へのアクセス時の動作';
+$lang['conf_notfound_farmer'] = '牧場主を表示';
+$lang['conf_notfound_404'] = '404 エラーページを表示';
+$lang['conf_notfound_list'] = '利用可能な動物一覧を表示';
+$lang['conf_notfound_redirect'] = '以下の URL へ転送';
+$lang['conf_notfound_url'] = '上記選択した場合、転送先の URL';
+$lang['save'] = '保存';
+$lang['animal template'] = '既存の動物をコピーする';
+$lang['animal creation success'] = '動物 "%s" は正常に作成されました。';
+$lang['animal creation error'] = '動物の作成中にエラーが発生しました。';
+$lang['animal configuration'] = '基本的な動物設定';
+$lang['inherit user registration'] = '牧場主からユーザー登録設定を継承する';
+$lang['enable user registration'] = '自分でユーザー登録することを許可する';
+$lang['disable user registration'] = 'ユーザー登録を無効化する';
+$lang['animal administrator'] = '動物の管理者';
+$lang['noUsers'] = 'ユーザーを作成しない';
+$lang['importUsers'] = '牧場主の全てのユーザーを新しい動物に入力する';
+$lang['currentAdmin'] = '現在のユーザーを admin として設定する';
+$lang['newAdmin'] = '新しい管理者ユーザー "admin" を作成する';
+$lang['admin password'] = '新しい管理者のパスワード';
+$lang['animalname_missing'] = '新しい動物の名前を入力してください。';
+$lang['animalname_invalid'] = '動物の名前には、英数字とドット・ハイフン(最初または最後の文字以外)のみを使用できます。';
+$lang['animalname_preexisting'] = 'その名前の動物はすでに存在しています。';
+$lang['adminPassword_empty'] = '新しい管理者アカウントのパスワードは空ではいけません。';
+$lang['animal template copy error'] = '既存の動物から %s をコピーする際に問題が発生しました。';
+$lang['aclpolicy missing/bad'] = 'ドロップダウンから最初の ACL ポリシーを選択してください。';
+$lang['bulkSingleSwitcher'] = '特定の動物を設定しますか?全動物を一括設定しますか?';
+$lang['bulkEdit'] = '全動物を一括設定する';
+$lang['singleEdit'] = '特定の動物を設定する';
+$lang['bulkEditForm'] = '特定のプラグインを全動物一括で設定する';
+$lang['matrixEdit'] = '一覧(動物×プラグイン)で設定する';
+$lang['default'] = 'デフォルト値を設定';
+$lang['activate'] = '有効化';
+$lang['deactivate'] = '無効化';
+$lang['singleEditForm'] = '特定の動物のプラグインを設定する';
+$lang['plugindone'] = 'プラグインの状態は更新されました';
+$lang['plugin'] = 'プラグイン';
+$lang['plugin_on'] = '有効';
+$lang['plugin_off'] = '無効';
+$lang['plugin_default'] = 'デフォルト値';
+$lang['plugin_enabled'] = '個別に有効化';
+$lang['plugin_disabled'] = '個別に無効化';
+$lang['js']['animalSelect'] = '動物を選択する';
+$lang['js']['pluginSelect'] = 'プラグインを選択する';
+$lang['disable_new_plugins'] = 'プラグインはデフォルトで無効になっています。デフォルト値を有効にするか特定の動物に対して有効にすることができます。';
+$lang['delete_animal'] = '選択した動物の削除';
+$lang['delete_confirm'] = '確認のために動物の名前を入力してください';
+$lang['delete'] = '動物とそのすべてのデータを削除します';
+$lang['delete_noanimal'] = '削除する動物を選択してください';
+$lang['delete_mismatch'] = '確認入力は動物の名前と一致しません。 削除されません。';
+$lang['delete_invalid'] = '無効な動物の名前です。 削除されません。';
+$lang['delete_success'] = '動物は正常に削除されました。';
+$lang['delete_fail'] = '一部のファイルを削除できなかったため、手動できれいにする必要があります。';
diff --git a/platform/www/lib/plugins/farmer/lang/ja/notfound_404.txt b/platform/www/lib/plugins/farmer/lang/ja/notfound_404.txt
new file mode 100644
index 0000000..3f575d6
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/lang/ja/notfound_404.txt
@@ -0,0 +1,3 @@
+====== 404 ありません ======
+
+要求されたリソースがありません。
diff --git a/platform/www/lib/plugins/farmer/lang/ja/notfound_list.txt b/platform/www/lib/plugins/farmer/lang/ja/notfound_list.txt
new file mode 100644
index 0000000..f3e633c
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/lang/ja/notfound_list.txt
@@ -0,0 +1,3 @@
+====== Wiki がありません ======
+
+要求された Wiki がありません。以下の利用可能な Wiki の一覧を参照してください。
diff --git a/platform/www/lib/plugins/farmer/lang/ja/settings.php b/platform/www/lib/plugins/farmer/lang/ja/settings.php
new file mode 100644
index 0000000..dd9b6a6
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/lang/ja/settings.php
@@ -0,0 +1,9 @@
+<?php
+
+/**
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ * @author Hideaki SAWADA <chuno@live.jp>
+ */
+$lang['deactivated plugins'] = '新しい動物を作成した場合、デフォルトで無効とするプラグインのカンマ区切り一覧。';
+$lang['disable_new_plugins'] = '牧場にプラグインを新規にインストールした後、プラグインを自動的に無効化しますか?(拡張機能管理でインストールした場合のみ)';
diff --git a/platform/www/lib/plugins/farmer/lang/ja/tab_config.txt b/platform/www/lib/plugins/farmer/lang/ja/tab_config.txt
new file mode 100644
index 0000000..4d3619b
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/lang/ja/tab_config.txt
@@ -0,0 +1 @@
+牧場の基本的な動作を設定します。 ここのオプションを変更すると、全ての動物に影響が及ぶのでご注意ください。 \ No newline at end of file
diff --git a/platform/www/lib/plugins/farmer/lang/ja/tab_config_help.txt b/platform/www/lib/plugins/farmer/lang/ja/tab_config_help.txt
new file mode 100644
index 0000000..5e9e4b8
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/lang/ja/tab_config_help.txt
@@ -0,0 +1,25 @@
+===== 牧場設定 =====
+
+ここで入力した設定内容はすべて牧場主の ''conf/farm.ini'' に保存されます。
+
+==== 基本設定 ====
+
+**牧場主ホスト名**は、設定時に自動的に設定されますが、ここで変更できます。
+ホスト方式牧場を使用する場合、牧場主に直接要求されたかどうかの検出に使用します。
+これは完全修飾ホスト名でなければなりません。(''foo'' のみではなく ''foo.example.com'')
+
+サブドメインのワイルドカード設定を使用する場合、**ベースドメイン**設定でメインドメインを指定する必要があります。
+例えば ''example.com'' を指定した場合、''foo'' という名の動物は ''foo.example.com'' 経由で届く想定します。
+ベースドメインにはドットを含まない動物名だけが付加されます。
+
+==== 継承 ====
+
+ここでは、牧場主上で作成したどの設定を動物内でデフォルトとして使用するかを指定できます。
+動物は、独自の設定ファイルで牧場主の設定を上書きすることもできます。
+継承が無効になっている場合、DokuWiki のデフォルト設定が、すべての動物のデフォルトです。
+
+==== 実在しない動物 ====
+
+デフォルトでは、実在しない動物へのアクセス時にエラーメッセージを表示しません。
+ここで異なる動作を選ぶことができます。
+デフォルトから切替える前に、牧場主ホスト名が上記に正しく設定されていることを確認して下さい。
diff --git a/platform/www/lib/plugins/farmer/lang/ja/tab_delete.txt b/platform/www/lib/plugins/farmer/lang/ja/tab_delete.txt
new file mode 100644
index 0000000..bde0596
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/lang/ja/tab_delete.txt
@@ -0,0 +1 @@
+既存の動物を削除できます。 選択した動物の**ページやメディアファイルを含む全てのデータ**を削除します。 **元に戻せないので注意して下さい!** \ No newline at end of file
diff --git a/platform/www/lib/plugins/farmer/lang/ja/tab_info.txt b/platform/www/lib/plugins/farmer/lang/ja/tab_info.txt
new file mode 100644
index 0000000..78ea6c7
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/lang/ja/tab_info.txt
@@ -0,0 +1 @@
+この Wiki は、牧場設定の一部分です。 以下の詳細を確認してください。 \ No newline at end of file
diff --git a/platform/www/lib/plugins/farmer/lang/ja/tab_new.txt b/platform/www/lib/plugins/farmer/lang/ja/tab_new.txt
new file mode 100644
index 0000000..78d5975
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/lang/ja/tab_new.txt
@@ -0,0 +1 @@
+新しい動物を作成できます。 \ No newline at end of file
diff --git a/platform/www/lib/plugins/farmer/lang/ja/tab_new_help.txt b/platform/www/lib/plugins/farmer/lang/ja/tab_new_help.txt
new file mode 100644
index 0000000..9287e1f
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/lang/ja/tab_new_help.txt
@@ -0,0 +1,38 @@
+===== 動物の作成 =====
+
+動物は、Dokuwiki 牧場の独立したサブ wiki です。
+新しい動物を作成するには名前を割り当てなければなりません。
+
+==== 動物名 ====
+
+.htaccess 方式の設定の場合、動物名は一つの単語でなければなりません。
+ドメイン方式の設定の場合、完全修飾ドメイン名を提供する必要があります。
+ワイルドカードサブドメインの設定を使用している場合、ベースドメインが設定済みであれば、単にホスト名部分を提供するだけです。
+
+例:
+
+ * .htaccess 方式:''%%http://example.org/dokuwiki/!foo/%%'' にアクセス可能な動物用の ''foo''
+ * ドメイン方式:''%%http://www.foo.com/%%'' にアクセス可能な動物用の ''%%www.foo.com%%''
+ * サブドメイン方式: ''%%http://foo.example.com/%%'' にアクセス可能な動物用の ''foo''
+
+後の二つは、適切な DNS 設定が必要です!
+
+==== 動物のコピー ====
+
+新しい動物の基礎にするために既存の動物を選択することができます。
+すべての設定、ページ、メディア、メタデータが新しい動物にコピーされます。
+ページやメディアの履歴はコピーされません。
+
+タイトルとロゴイメージは上書きして、コピー元と区別できることを確認してください。
+
+==== 動物の管理者 ====
+
+動物は独自のユーザー基準で完全な機能の wiki になります。
+ユーザーを設定するために少なくとも1つの管理ユーザーが必要になります。
+牧場主から現在のユーザーまたはすべてのユーザーをコピーしたり、動物のために完全に新しいユーザーを作成できます。
+
+ユーザーを作成しないことも選択できます。
+設定タブで牧場主からユーザーの継承を有効にした場合、既存のユーザーを別の動物からコピーした場合のみこれを選択すべきです。
+
+/lang/en/tab_plugins.txt
+一括操作で全ての動物に対するプラグインの有効化・無効化や、特定の動物に対するプラグインの編集ができます。
diff --git a/platform/www/lib/plugins/farmer/lang/ja/tab_plugins.txt b/platform/www/lib/plugins/farmer/lang/ja/tab_plugins.txt
new file mode 100644
index 0000000..ee4acd5
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/lang/ja/tab_plugins.txt
@@ -0,0 +1 @@
+一度の操作で全ての動物のプラグインを有効または無効にすることや、個々の動物のプラグインを編集することができます。 \ No newline at end of file
diff --git a/platform/www/lib/plugins/farmer/lang/ja/tab_plugins_help.txt b/platform/www/lib/plugins/farmer/lang/ja/tab_plugins_help.txt
new file mode 100644
index 0000000..fe4aa72
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/lang/ja/tab_plugins_help.txt
@@ -0,0 +1,17 @@
+===== プラグイン管理 =====
+
+拡張機能管理は、すべての動物の中で無効になっています。
+プラグインは牧場主にのみインストールすることができ、牧場主だけがすべての動物の中でどのプラグインを有効にするか無効にするかを制御できます。
+この画面を使ってこの制御を行います。
+
+各プラグインは動物毎に三つの状態を指定できます。
+有効、無効、デフォルト状態です。
+デフォルトは動物に対してプラグインの状態を明示的に設定していないことを意味します。
+通常はデフォルトの状態は有効になっています。
+
+動物が牧場主のプラグインの状態を継承するように設定した場合、デフォルト状態のプラグインは牧場主と同じ状態になります。
+例えば、牧場主のプラグインを無効にした場合、このプラグインの状態を明示的に設定していなかったすべての動物内でプラグインを無効にします。
+
+プラグイン管理には二種類の方法があります:
+ * 一つのプラグインを選択し、すべての動物に対して同じ状態を設定する。
+ * 特定の動物を選び、その動物内のみに対するすべてのプラグインの状態を設定する。
diff --git a/platform/www/lib/plugins/farmer/lang/ja/tab_setup.txt b/platform/www/lib/plugins/farmer/lang/ja/tab_setup.txt
new file mode 100644
index 0000000..a5cf09c
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/lang/ja/tab_setup.txt
@@ -0,0 +1 @@
+この Wiki は牧場主プラグインの牧場設定をされていません。以下の入力画面で牧場設定をして下さい。 \ No newline at end of file
diff --git a/platform/www/lib/plugins/farmer/lang/ja/tab_setup_help.txt b/platform/www/lib/plugins/farmer/lang/ja/tab_setup_help.txt
new file mode 100644
index 0000000..d2cd61f
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/lang/ja/tab_setup_help.txt
@@ -0,0 +1,38 @@
+===== 牧場について =====
+
+牧場は複数の他の wiki(「動物」 "Animals")を動かす単一の DokuWiki インストール(「牧場主」 "Farmer")を使えるようにします。
+プラグインとテンプレートは牧場主にだけインストールすれば、動物もそれらを利用できるようになります。
+一つの wiki だけを最新状態に保つ必要があり、他のすべての wiki は同じコードベースを使用するだけになります。
+
+この設定手順を完了した後、現在の DokuWiki(あなたが見ている DokuWiki)が、「牧場主」として認識されるようになります。
+
+===== この設定は何をしますか? =====
+
+この設定ウィザードでは、以下の三つを行います:
+
+ - ''inc/preload.php'' ファイルの作成
+ - ''conf/farm.ini'' ファイルの作成
+ - 必要に応じて ''.htaccess'' ファイルに追加
+
+''preload.php'' は DokuWiki 読込み時の最初に読み込まれるファイルです。
+ここで牧場機能が初期化されます。
+現在の要求が動物か牧場主をアクセスし、それに応じて全てを再構成する必要がある場合、牧場主プラグインを検出します。
+
+''conf/farm.ini'' には牧場の基本的な設定が含まれています。
+最も重要なことは、すべての動物データが格納される場所が含まれていることです。
+
+''.htaccess'' を変更することで //バン !// 機能を使って動物にアクセスできるようになります。(下記参照)
+
+===== 入力方法? =====
+
+**動物用のディレクトリ**は、各動物用に新しいディレクトリを作成する場所です。
+このディレクトリは、現在の DokuWiki の外でなければなりません。
+''../animals'' のような相対的なディレクトリを指定できます。
+
+**.htaccess** 対応を有効にすることを推奨します。
+この機能は、Apache の mod_rewrite と .htaccess 対応が必要です。
+
+有効にすると、動物は //バン !// 機能を使って牧場主の URL の下でアクセスできるようになります。
+例えば、''%%http://www.example.com/dokuwiki/%%'' で牧場主が実行されている場合、''%%http://www.example.com/dokuwiki/!animal%%'' で動物にアクセスできるようになります。
+
+このオプションを有効にしない場合、動物にアクセスするには、Web サーバーや DNS を設定するが必要があります。
diff --git a/platform/www/lib/plugins/farmer/lang/nl/lang.php b/platform/www/lib/plugins/farmer/lang/nl/lang.php
new file mode 100644
index 0000000..cef0c84
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/lang/nl/lang.php
@@ -0,0 +1,20 @@
+<?php
+
+/**
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ * @author Harriet Neitz <harrietneitz@gmail.com>
+ */
+$lang['tab_info'] = 'Info';
+$lang['tab_config'] = 'Configuratie';
+$lang['tab_plugins'] = 'Beheer plugins';
+$lang['tab_new'] = 'Voeg nieuw dier toe';
+$lang['tab_delete'] = 'Verwijder dier';
+$lang['farmdir_in_dokuwiki'] = 'De dier directory (%s) moet buiten de Boerderij dokuwiki (%s) staan.';
+$lang['farmdir_unwritable'] = 'Controleer dat de webserver schrijf-toegang heeft tot de dier directory (%s)!';
+$lang['overwrite_preload'] = 'Waarschuwing: Je bestaande inc/preload.php wordt overschreven wanneer je hier doorgaat!';
+$lang['conf_notfound_404'] = 'Toon een 404 fout pagina';
+$lang['conf_notfound_list'] = 'Toon een lijst van bestaande dieren';
+$lang['conf_notfound_redirect'] = 'Redirect naar onderstaande URL';
+$lang['save'] = 'Opslaan';
+$lang['animal template'] = 'Kopieer bestaand dier';
diff --git a/platform/www/lib/plugins/farmer/lang/pl/lang.php b/platform/www/lib/plugins/farmer/lang/pl/lang.php
new file mode 100644
index 0000000..375d919
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/lang/pl/lang.php
@@ -0,0 +1,102 @@
+<?php
+
+/**
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ * @author Bartek S <sadupl@gmail.com>
+ */
+$lang['menu'] = 'Farma';
+$lang['tab_setup'] = 'Ustawienia Farmy';
+$lang['tab_info'] = 'Informacje';
+$lang['tab_config'] = 'Konfiguracja';
+$lang['tab_plugins'] = 'Zarządzaj pluginami';
+$lang['tab_new'] = 'Dodaj nowe zwierzę';
+$lang['tab_delete'] = 'Usuń zwierzę';
+$lang['preloadPHPForm'] = 'Zainicjuj Farmę';
+$lang['farm dir'] = 'Katalog zwierząt';
+$lang['htaccess setup'] = 'Dodać kod farmy do .htaccess?';
+$lang['submit'] = 'Zatwierdź';
+$lang['farmdir_missing'] = 'Proszę podać katalog gdzie Zwierzęta powinny być przechowywane.';
+$lang['farmdir_in_dokuwiki'] = 'Katalog Zwierząt (%s) musi być poza Farmą dokuwiki (%s).';
+$lang['farmdir_uncreatable'] = 'Katalog Zwierząt (%s) nie został stworzony. Czy uprawnienia są prawidłowe?';
+$lang['farmdir_unwritable'] = 'Proszę się upenić, że webserver posiada prawa zapisu do katalogu Zwierząt.';
+$lang['farmdir_notEmpty'] = 'Katalog Zwierząt (%s) musi być pusty.';
+$lang['preload creation success'] = 'Farma została zainicjowana pomyślnie.';
+$lang['preload creation error'] = 'Wystąpił błąd podczas inicjowania Farmy.';
+$lang['overwrite_preload'] = 'Uwaga: Dotychczasowy plik inc/preload.php będzie zastąpiony podczas kontynuowania!';
+$lang['animal'] = 'Imię Zwierzęta / Domeny';
+$lang['thisis'] = 'Instancja to';
+$lang['thisis.farmer'] = 'Farmer!';
+$lang['thisis.animal'] = 'Zwierzę!';
+$lang['baseinstall'] = 'Instalacja Farmera';
+$lang['animals'] = 'Zwierzęta';
+$lang['confdir'] = 'Katalog konfiguracji instancji';
+$lang['savedir'] = 'Katalog danych instancji';
+$lang['plugins'] = 'Aktywne pluginy tej instancji';
+$lang['base'] = 'Konfiguracja Bazy';
+$lang['farm host'] = 'Host Name Farmera';
+$lang['base domain'] = 'Domena podstawowa dla subdomeny Zwierzęta';
+$lang['conf_inherit'] = 'Zwierzęta powinny dziedziczyć ustawienia farmera';
+$lang['conf_inherit_main'] = 'Ustawienia konfiguracji';
+$lang['conf_inherit_acronyms'] = 'Definicje akronimów';
+$lang['conf_inherit_entities'] = 'Definicje jednostek';
+$lang['conf_inherit_interwiki'] = 'Definicje Interwiki';
+$lang['conf_inherit_license'] = 'Definicje licencji';
+$lang['conf_inherit_smileys'] = 'Definicje uśmieszków';
+$lang['conf_inherit_wordblock'] = 'Wpisy SPAM na czarnej liście';
+$lang['conf_inherit_userstyle'] = 'Style użytkownika';
+$lang['conf_inherit_userscript'] = 'Skrypty użytkownika';
+$lang['conf_inherit_styleini'] = 'Dostosowanie stylu szablonu';
+$lang['conf_inherit_users'] = 'Użytkownicy (tylko zwykłe uwierzytelnianie)';
+$lang['conf_inherit_plugins'] = 'Stan wtyczki';
+$lang['conf_inherit_yes'] = 'odziedziczony po farmerze';
+$lang['conf_inherit_no'] = 'niezależny od farmera';
+$lang['conf_notfound'] = 'Zachowanie podczas uzyskiwania dostępu do nieistniejących zwierząt';
+$lang['conf_notfound_farmer'] = 'Pokaż wiki farmera';
+$lang['conf_notfound_404'] = 'Pokaż stronę błędu 404';
+$lang['conf_notfound_list'] = 'Pokaż listę dostępnych zwierząt';
+$lang['conf_notfound_redirect'] = 'Przekieruj na poniższy adres URL';
+$lang['conf_notfound_url'] = 'Adres URL do przekierowania, jeśli wybrano powyżej';
+$lang['save'] = 'Zapisz';
+$lang['animal template'] = 'Kopiuj istniejące Zwierzę';
+$lang['animal creation success'] = 'Zwierzę "%s" zostało pomyślnie utworzone.';
+$lang['animal creation error'] = 'Wystąpił błąd podczas tworzenia zwierzęcia.';
+$lang['animal configuration'] = 'Podstawowa konfiguracja Zwierząt';
+$lang['inherit user registration'] = 'Dziedzicz ustawienia rejestracji użytkownika od farmera';
+$lang['enable user registration'] = 'Pozwól użytkownikom się zarejestrować';
+$lang['disable user registration'] = 'Wyłącz rejestrację użytkowników';
+$lang['animal administrator'] = 'Administrator zwięrząt';
+$lang['noUsers'] = 'Nie twórz żadnych użytkowników';
+$lang['importUsers'] = 'Zaimportuj wszystkich użytkowników Farmera do nowego zwierzęcia';
+$lang['currentAdmin'] = 'Ustaw bieżącego użytkownika jako admin';
+$lang['newAdmin'] = 'Utwórz nowego administratora „admin”';
+$lang['admin password'] = 'Hasło dla nowego administratora';
+$lang['animalname_missing'] = 'Wprowadź nazwę nowego zwierzęcia.';
+$lang['animalname_preexisting'] = 'Zwierzę o tej nazwie już istnieje.';
+$lang['adminPassword_empty'] = 'Hasło do nowego konta administratora nie może być puste.';
+$lang['bulkSingleSwitcher'] = 'Edytować jedno Zwierzę czy wszystkie naraz?';
+$lang['bulkEdit'] = 'Zbiorcza edycja wszystkich Zwierząt';
+$lang['singleEdit'] = 'Edytuj jedno Zwierzę';
+$lang['bulkEditForm'] = 'Aktywuj lub dezaktywuj wtyczkę we wszystkich Zwierzętach';
+$lang['default'] = 'Ustaw na domyślny';
+$lang['activate'] = 'Aktywuj';
+$lang['deactivate'] = 'Dezaktywuj';
+$lang['singleEditForm'] = 'Edytuj wtyczki określonego Zwierzęcia';
+$lang['plugindone'] = 'Stany wtyczek zaktualizowane';
+$lang['plugin'] = 'Plugin';
+$lang['plugin_on'] = 'on
+(it means \'włączone\', but in PL we use also ON/OFF)';
+$lang['plugin_off'] = 'off
+(it means \'wyłączone\', but in PL we use also ON/OFF)';
+$lang['plugin_default'] = 'Domyślne';
+$lang['plugin_enabled'] = 'Włączone';
+$lang['plugin_disabled'] = 'Wyłączone';
+$lang['js']['animalSelect'] = 'Wybierz zwierzę';
+$lang['js']['pluginSelect'] = 'Wybierz plugin';
+$lang['delete_animal'] = 'Wybierz Zwierzę do usunięcia';
+$lang['delete_confirm'] = 'Wpisz nazwę Zwierzęcia, aby potwierdzić';
+$lang['delete'] = 'Usuń Zwierzę i wszystkie jego dane';
+$lang['delete_noanimal'] = 'Proszę wybrać Zwierzę do usunięcia';
+$lang['delete_mismatch'] = 'Potwierdzenie nie pasuje do nazwy Zwierzęcia. Nie usunięte.';
+$lang['delete_invalid'] = 'Nieprawidłowa nazwa Zwierzęcia. Nie usunięte';
+$lang['delete_success'] = 'Zwierzę zostało pomyślnie usunięte.';
diff --git a/platform/www/lib/plugins/farmer/plugin.info.txt b/platform/www/lib/plugins/farmer/plugin.info.txt
new file mode 100644
index 0000000..2a26566
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/plugin.info.txt
@@ -0,0 +1,7 @@
+base farmer
+author Michael Große, Andreas Gohr
+email dokuwiki@cosmocode.de
+date 2021-01-08
+name farmer plugin
+desc A plugin to help with creating and administring wiki farm animals
+url https://dokuwiki.org/plugin:farmer
diff --git a/platform/www/lib/plugins/farmer/script.js b/platform/www/lib/plugins/farmer/script.js
new file mode 100644
index 0000000..d5cea10
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/script.js
@@ -0,0 +1,2 @@
+/* DOKUWIKI:include_once script/jquery.chosen.js */
+/* DOKUWIKI:include script/plugins.js */
diff --git a/platform/www/lib/plugins/farmer/script/jquery.chosen.js b/platform/www/lib/plugins/farmer/script/jquery.chosen.js
new file mode 100644
index 0000000..929a9ca
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/script/jquery.chosen.js
@@ -0,0 +1,1257 @@
+/*!
+Chosen, a Select Box Enhancer for jQuery and Prototype
+by Patrick Filler for Harvest, http://getharvest.com
+
+Version 1.4.2
+Full source at https://github.com/harvesthq/chosen
+Copyright (c) 2011-2015 Harvest http://getharvest.com
+
+MIT License, https://github.com/harvesthq/chosen/blob/master/LICENSE.md
+This file is generated by `grunt build`, do not edit it by hand.
+*/
+
+(function() {
+ var $, AbstractChosen, Chosen, SelectParser, _ref,
+ __hasProp = {}.hasOwnProperty,
+ __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
+
+ SelectParser = (function() {
+ function SelectParser() {
+ this.options_index = 0;
+ this.parsed = [];
+ }
+
+ SelectParser.prototype.add_node = function(child) {
+ if (child.nodeName.toUpperCase() === "OPTGROUP") {
+ return this.add_group(child);
+ } else {
+ return this.add_option(child);
+ }
+ };
+
+ SelectParser.prototype.add_group = function(group) {
+ var group_position, option, _i, _len, _ref, _results;
+ group_position = this.parsed.length;
+ this.parsed.push({
+ array_index: group_position,
+ group: true,
+ label: this.escapeExpression(group.label),
+ title: group.title ? group.title : void 0,
+ children: 0,
+ disabled: group.disabled,
+ classes: group.className
+ });
+ _ref = group.childNodes;
+ _results = [];
+ for (_i = 0, _len = _ref.length; _i < _len; _i++) {
+ option = _ref[_i];
+ _results.push(this.add_option(option, group_position, group.disabled));
+ }
+ return _results;
+ };
+
+ SelectParser.prototype.add_option = function(option, group_position, group_disabled) {
+ if (option.nodeName.toUpperCase() === "OPTION") {
+ if (option.text !== "") {
+ if (group_position != null) {
+ this.parsed[group_position].children += 1;
+ }
+ this.parsed.push({
+ array_index: this.parsed.length,
+ options_index: this.options_index,
+ value: option.value,
+ text: option.text,
+ html: option.innerHTML,
+ title: option.title ? option.title : void 0,
+ selected: option.selected,
+ disabled: group_disabled === true ? group_disabled : option.disabled,
+ group_array_index: group_position,
+ group_label: group_position != null ? this.parsed[group_position].label : null,
+ classes: option.className,
+ style: option.style.cssText
+ });
+ } else {
+ this.parsed.push({
+ array_index: this.parsed.length,
+ options_index: this.options_index,
+ empty: true
+ });
+ }
+ return this.options_index += 1;
+ }
+ };
+
+ SelectParser.prototype.escapeExpression = function(text) {
+ var map, unsafe_chars;
+ if ((text == null) || text === false) {
+ return "";
+ }
+ if (!/[\&\<\>\"\'\`]/.test(text)) {
+ return text;
+ }
+ map = {
+ "<": "&lt;",
+ ">": "&gt;",
+ '"': "&quot;",
+ "'": "&#x27;",
+ "`": "&#x60;"
+ };
+ unsafe_chars = /&(?!\w+;)|[\<\>\"\'\`]/g;
+ return text.replace(unsafe_chars, function(chr) {
+ return map[chr] || "&amp;";
+ });
+ };
+
+ return SelectParser;
+
+ })();
+
+ SelectParser.select_to_array = function(select) {
+ var child, parser, _i, _len, _ref;
+ parser = new SelectParser();
+ _ref = select.childNodes;
+ for (_i = 0, _len = _ref.length; _i < _len; _i++) {
+ child = _ref[_i];
+ parser.add_node(child);
+ }
+ return parser.parsed;
+ };
+
+ AbstractChosen = (function() {
+ function AbstractChosen(form_field, options) {
+ this.form_field = form_field;
+ this.options = options != null ? options : {};
+ if (!AbstractChosen.browser_is_supported()) {
+ return;
+ }
+ this.is_multiple = this.form_field.multiple;
+ this.set_default_text();
+ this.set_default_values();
+ this.setup();
+ this.set_up_html();
+ this.register_observers();
+ this.on_ready();
+ }
+
+ AbstractChosen.prototype.set_default_values = function() {
+ var _this = this;
+ this.click_test_action = function(evt) {
+ return _this.test_active_click(evt);
+ };
+ this.activate_action = function(evt) {
+ return _this.activate_field(evt);
+ };
+ this.active_field = false;
+ this.mouse_on_container = false;
+ this.results_showing = false;
+ this.result_highlighted = null;
+ this.allow_single_deselect = (this.options.allow_single_deselect != null) && (this.form_field.options[0] != null) && this.form_field.options[0].text === "" ? this.options.allow_single_deselect : false;
+ this.disable_search_threshold = this.options.disable_search_threshold || 0;
+ this.disable_search = this.options.disable_search || false;
+ this.enable_split_word_search = this.options.enable_split_word_search != null ? this.options.enable_split_word_search : true;
+ this.group_search = this.options.group_search != null ? this.options.group_search : true;
+ this.search_contains = this.options.search_contains || false;
+ this.single_backstroke_delete = this.options.single_backstroke_delete != null ? this.options.single_backstroke_delete : true;
+ this.max_selected_options = this.options.max_selected_options || Infinity;
+ this.inherit_select_classes = this.options.inherit_select_classes || false;
+ this.display_selected_options = this.options.display_selected_options != null ? this.options.display_selected_options : true;
+ this.display_disabled_options = this.options.display_disabled_options != null ? this.options.display_disabled_options : true;
+ return this.include_group_label_in_selected = this.options.include_group_label_in_selected || false;
+ };
+
+ AbstractChosen.prototype.set_default_text = function() {
+ if (this.form_field.getAttribute("data-placeholder")) {
+ this.default_text = this.form_field.getAttribute("data-placeholder");
+ } else if (this.is_multiple) {
+ this.default_text = this.options.placeholder_text_multiple || this.options.placeholder_text || AbstractChosen.default_multiple_text;
+ } else {
+ this.default_text = this.options.placeholder_text_single || this.options.placeholder_text || AbstractChosen.default_single_text;
+ }
+ return this.results_none_found = this.form_field.getAttribute("data-no_results_text") || this.options.no_results_text || AbstractChosen.default_no_result_text;
+ };
+
+ AbstractChosen.prototype.choice_label = function(item) {
+ if (this.include_group_label_in_selected && (item.group_label != null)) {
+ return "<b class='group-name'>" + item.group_label + "</b>" + item.html;
+ } else {
+ return item.html;
+ }
+ };
+
+ AbstractChosen.prototype.mouse_enter = function() {
+ return this.mouse_on_container = true;
+ };
+
+ AbstractChosen.prototype.mouse_leave = function() {
+ return this.mouse_on_container = false;
+ };
+
+ AbstractChosen.prototype.input_focus = function(evt) {
+ var _this = this;
+ if (this.is_multiple) {
+ if (!this.active_field) {
+ return setTimeout((function() {
+ return _this.container_mousedown();
+ }), 50);
+ }
+ } else {
+ if (!this.active_field) {
+ return this.activate_field();
+ }
+ }
+ };
+
+ AbstractChosen.prototype.input_blur = function(evt) {
+ var _this = this;
+ if (!this.mouse_on_container) {
+ this.active_field = false;
+ return setTimeout((function() {
+ return _this.blur_test();
+ }), 100);
+ }
+ };
+
+ AbstractChosen.prototype.results_option_build = function(options) {
+ var content, data, _i, _len, _ref;
+ content = '';
+ _ref = this.results_data;
+ for (_i = 0, _len = _ref.length; _i < _len; _i++) {
+ data = _ref[_i];
+ if (data.group) {
+ content += this.result_add_group(data);
+ } else {
+ content += this.result_add_option(data);
+ }
+ if (options != null ? options.first : void 0) {
+ if (data.selected && this.is_multiple) {
+ this.choice_build(data);
+ } else if (data.selected && !this.is_multiple) {
+ this.single_set_selected_text(this.choice_label(data));
+ }
+ }
+ }
+ return content;
+ };
+
+ AbstractChosen.prototype.result_add_option = function(option) {
+ var classes, option_el;
+ if (!option.search_match) {
+ return '';
+ }
+ if (!this.include_option_in_results(option)) {
+ return '';
+ }
+ classes = [];
+ if (!option.disabled && !(option.selected && this.is_multiple)) {
+ classes.push("active-result");
+ }
+ if (option.disabled && !(option.selected && this.is_multiple)) {
+ classes.push("disabled-result");
+ }
+ if (option.selected) {
+ classes.push("result-selected");
+ }
+ if (option.group_array_index != null) {
+ classes.push("group-option");
+ }
+ if (option.classes !== "") {
+ classes.push(option.classes);
+ }
+ option_el = document.createElement("li");
+ option_el.className = classes.join(" ");
+ option_el.style.cssText = option.style;
+ option_el.setAttribute("data-option-array-index", option.array_index);
+ option_el.innerHTML = option.search_text;
+ if (option.title) {
+ option_el.title = option.title;
+ }
+ return this.outerHTML(option_el);
+ };
+
+ AbstractChosen.prototype.result_add_group = function(group) {
+ var classes, group_el;
+ if (!(group.search_match || group.group_match)) {
+ return '';
+ }
+ if (!(group.active_options > 0)) {
+ return '';
+ }
+ classes = [];
+ classes.push("group-result");
+ if (group.classes) {
+ classes.push(group.classes);
+ }
+ group_el = document.createElement("li");
+ group_el.className = classes.join(" ");
+ group_el.innerHTML = group.search_text;
+ if (group.title) {
+ group_el.title = group.title;
+ }
+ return this.outerHTML(group_el);
+ };
+
+ AbstractChosen.prototype.results_update_field = function() {
+ this.set_default_text();
+ if (!this.is_multiple) {
+ this.results_reset_cleanup();
+ }
+ this.result_clear_highlight();
+ this.results_build();
+ if (this.results_showing) {
+ return this.winnow_results();
+ }
+ };
+
+ AbstractChosen.prototype.reset_single_select_options = function() {
+ var result, _i, _len, _ref, _results;
+ _ref = this.results_data;
+ _results = [];
+ for (_i = 0, _len = _ref.length; _i < _len; _i++) {
+ result = _ref[_i];
+ if (result.selected) {
+ _results.push(result.selected = false);
+ } else {
+ _results.push(void 0);
+ }
+ }
+ return _results;
+ };
+
+ AbstractChosen.prototype.results_toggle = function() {
+ if (this.results_showing) {
+ return this.results_hide();
+ } else {
+ return this.results_show();
+ }
+ };
+
+ AbstractChosen.prototype.results_search = function(evt) {
+ if (this.results_showing) {
+ return this.winnow_results();
+ } else {
+ return this.results_show();
+ }
+ };
+
+ AbstractChosen.prototype.winnow_results = function() {
+ var escapedSearchText, option, regex, results, results_group, searchText, startpos, text, zregex, _i, _len, _ref;
+ this.no_results_clear();
+ results = 0;
+ searchText = this.get_search_text();
+ escapedSearchText = searchText.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
+ zregex = new RegExp(escapedSearchText, 'i');
+ regex = this.get_search_regex(escapedSearchText);
+ _ref = this.results_data;
+ for (_i = 0, _len = _ref.length; _i < _len; _i++) {
+ option = _ref[_i];
+ option.search_match = false;
+ results_group = null;
+ if (this.include_option_in_results(option)) {
+ if (option.group) {
+ option.group_match = false;
+ option.active_options = 0;
+ }
+ if ((option.group_array_index != null) && this.results_data[option.group_array_index]) {
+ results_group = this.results_data[option.group_array_index];
+ if (results_group.active_options === 0 && results_group.search_match) {
+ results += 1;
+ }
+ results_group.active_options += 1;
+ }
+ option.search_text = option.group ? option.label : option.html;
+ if (!(option.group && !this.group_search)) {
+ option.search_match = this.search_string_match(option.search_text, regex);
+ if (option.search_match && !option.group) {
+ results += 1;
+ }
+ if (option.search_match) {
+ if (searchText.length) {
+ startpos = option.search_text.search(zregex);
+ text = option.search_text.substr(0, startpos + searchText.length) + '</em>' + option.search_text.substr(startpos + searchText.length);
+ option.search_text = text.substr(0, startpos) + '<em>' + text.substr(startpos);
+ }
+ if (results_group != null) {
+ results_group.group_match = true;
+ }
+ } else if ((option.group_array_index != null) && this.results_data[option.group_array_index].search_match) {
+ option.search_match = true;
+ }
+ }
+ }
+ }
+ this.result_clear_highlight();
+ if (results < 1 && searchText.length) {
+ this.update_results_content("");
+ return this.no_results(searchText);
+ } else {
+ this.update_results_content(this.results_option_build());
+ return this.winnow_results_set_highlight();
+ }
+ };
+
+ AbstractChosen.prototype.get_search_regex = function(escaped_search_string) {
+ var regex_anchor;
+ regex_anchor = this.search_contains ? "" : "^";
+ return new RegExp(regex_anchor + escaped_search_string, 'i');
+ };
+
+ AbstractChosen.prototype.search_string_match = function(search_string, regex) {
+ var part, parts, _i, _len;
+ if (regex.test(search_string)) {
+ return true;
+ } else if (this.enable_split_word_search && (search_string.indexOf(" ") >= 0 || search_string.indexOf("[") === 0)) {
+ parts = search_string.replace(/\[|\]/g, "").split(" ");
+ if (parts.length) {
+ for (_i = 0, _len = parts.length; _i < _len; _i++) {
+ part = parts[_i];
+ if (regex.test(part)) {
+ return true;
+ }
+ }
+ }
+ }
+ };
+
+ AbstractChosen.prototype.choices_count = function() {
+ var option, _i, _len, _ref;
+ if (this.selected_option_count != null) {
+ return this.selected_option_count;
+ }
+ this.selected_option_count = 0;
+ _ref = this.form_field.options;
+ for (_i = 0, _len = _ref.length; _i < _len; _i++) {
+ option = _ref[_i];
+ if (option.selected) {
+ this.selected_option_count += 1;
+ }
+ }
+ return this.selected_option_count;
+ };
+
+ AbstractChosen.prototype.choices_click = function(evt) {
+ evt.preventDefault();
+ if (!(this.results_showing || this.is_disabled)) {
+ return this.results_show();
+ }
+ };
+
+ AbstractChosen.prototype.keyup_checker = function(evt) {
+ var stroke, _ref;
+ stroke = (_ref = evt.which) != null ? _ref : evt.keyCode;
+ this.search_field_scale();
+ switch (stroke) {
+ case 8:
+ if (this.is_multiple && this.backstroke_length < 1 && this.choices_count() > 0) {
+ return this.keydown_backstroke();
+ } else if (!this.pending_backstroke) {
+ this.result_clear_highlight();
+ return this.results_search();
+ }
+ break;
+ case 13:
+ evt.preventDefault();
+ if (this.results_showing) {
+ return this.result_select(evt);
+ }
+ break;
+ case 27:
+ if (this.results_showing) {
+ this.results_hide();
+ }
+ return true;
+ case 9:
+ case 38:
+ case 40:
+ case 16:
+ case 91:
+ case 17:
+ break;
+ default:
+ return this.results_search();
+ }
+ };
+
+ AbstractChosen.prototype.clipboard_event_checker = function(evt) {
+ var _this = this;
+ return setTimeout((function() {
+ return _this.results_search();
+ }), 50);
+ };
+
+ AbstractChosen.prototype.container_width = function() {
+ if (this.options.width != null) {
+ return this.options.width;
+ } else {
+ return "" + this.form_field.offsetWidth + "px";
+ }
+ };
+
+ AbstractChosen.prototype.include_option_in_results = function(option) {
+ if (this.is_multiple && (!this.display_selected_options && option.selected)) {
+ return false;
+ }
+ if (!this.display_disabled_options && option.disabled) {
+ return false;
+ }
+ if (option.empty) {
+ return false;
+ }
+ return true;
+ };
+
+ AbstractChosen.prototype.search_results_touchstart = function(evt) {
+ this.touch_started = true;
+ return this.search_results_mouseover(evt);
+ };
+
+ AbstractChosen.prototype.search_results_touchmove = function(evt) {
+ this.touch_started = false;
+ return this.search_results_mouseout(evt);
+ };
+
+ AbstractChosen.prototype.search_results_touchend = function(evt) {
+ if (this.touch_started) {
+ return this.search_results_mouseup(evt);
+ }
+ };
+
+ AbstractChosen.prototype.outerHTML = function(element) {
+ var tmp;
+ if (element.outerHTML) {
+ return element.outerHTML;
+ }
+ tmp = document.createElement("div");
+ tmp.appendChild(element);
+ return tmp.innerHTML;
+ };
+
+ AbstractChosen.browser_is_supported = function() {
+ if (window.navigator.appName === "Microsoft Internet Explorer") {
+ return document.documentMode >= 8;
+ }
+ if (/iP(od|hone)/i.test(window.navigator.userAgent)) {
+ return false;
+ }
+ if (/Android/i.test(window.navigator.userAgent)) {
+ if (/Mobile/i.test(window.navigator.userAgent)) {
+ return false;
+ }
+ }
+ return true;
+ };
+
+ AbstractChosen.default_multiple_text = "Select Some Options";
+
+ AbstractChosen.default_single_text = "Select an Option";
+
+ AbstractChosen.default_no_result_text = "No results match";
+
+ return AbstractChosen;
+
+ })();
+
+ $ = jQuery;
+
+ $.fn.extend({
+ chosen: function(options) {
+ if (!AbstractChosen.browser_is_supported()) {
+ return this;
+ }
+ return this.each(function(input_field) {
+ var $this, chosen;
+ $this = $(this);
+ chosen = $this.data('chosen');
+ if (options === 'destroy' && chosen instanceof Chosen) {
+ chosen.destroy();
+ } else if (!(chosen instanceof Chosen)) {
+ $this.data('chosen', new Chosen(this, options));
+ }
+ });
+ }
+ });
+
+ Chosen = (function(_super) {
+ __extends(Chosen, _super);
+
+ function Chosen() {
+ _ref = Chosen.__super__.constructor.apply(this, arguments);
+ return _ref;
+ }
+
+ Chosen.prototype.setup = function() {
+ this.form_field_jq = $(this.form_field);
+ this.current_selectedIndex = this.form_field.selectedIndex;
+ return this.is_rtl = this.form_field_jq.hasClass("chosen-rtl");
+ };
+
+ Chosen.prototype.set_up_html = function() {
+ var container_classes, container_props;
+ container_classes = ["chosen-container"];
+ container_classes.push("chosen-container-" + (this.is_multiple ? "multi" : "single"));
+ if (this.inherit_select_classes && this.form_field.className) {
+ container_classes.push(this.form_field.className);
+ }
+ if (this.is_rtl) {
+ container_classes.push("chosen-rtl");
+ }
+ container_props = {
+ 'class': container_classes.join(' '),
+ 'style': "width: " + (this.container_width()) + ";",
+ 'title': this.form_field.title
+ };
+ if (this.form_field.id.length) {
+ container_props.id = this.form_field.id.replace(/[^\w]/g, '_') + "_chosen";
+ }
+ this.container = $("<div />", container_props);
+ if (this.is_multiple) {
+ this.container.html('<ul class="chosen-choices"><li class="search-field"><input type="text" value="' + this.default_text + '" class="default" autocomplete="off" style="width:25px;" /></li></ul><div class="chosen-drop"><ul class="chosen-results"></ul></div>');
+ } else {
+ this.container.html('<a class="chosen-single chosen-default" tabindex="-1"><span>' + this.default_text + '</span><div><b></b></div></a><div class="chosen-drop"><div class="chosen-search"><input type="text" autocomplete="off" /></div><ul class="chosen-results"></ul></div>');
+ }
+ this.form_field_jq.hide().after(this.container);
+ this.dropdown = this.container.find('div.chosen-drop').first();
+ this.search_field = this.container.find('input').first();
+ this.search_results = this.container.find('ul.chosen-results').first();
+ this.search_field_scale();
+ this.search_no_results = this.container.find('li.no-results').first();
+ if (this.is_multiple) {
+ this.search_choices = this.container.find('ul.chosen-choices').first();
+ this.search_container = this.container.find('li.search-field').first();
+ } else {
+ this.search_container = this.container.find('div.chosen-search').first();
+ this.selected_item = this.container.find('.chosen-single').first();
+ }
+ this.results_build();
+ this.set_tab_index();
+ return this.set_label_behavior();
+ };
+
+ Chosen.prototype.on_ready = function() {
+ return this.form_field_jq.trigger("chosen:ready", {
+ chosen: this
+ });
+ };
+
+ Chosen.prototype.register_observers = function() {
+ var _this = this;
+ this.container.bind('touchstart.chosen', function(evt) {
+ _this.container_mousedown(evt);
+ return evt.preventDefault();
+ });
+ this.container.bind('touchend.chosen', function(evt) {
+ _this.container_mouseup(evt);
+ return evt.preventDefault();
+ });
+ this.container.bind('mousedown.chosen', function(evt) {
+ _this.container_mousedown(evt);
+ });
+ this.container.bind('mouseup.chosen', function(evt) {
+ _this.container_mouseup(evt);
+ });
+ this.container.bind('mouseenter.chosen', function(evt) {
+ _this.mouse_enter(evt);
+ });
+ this.container.bind('mouseleave.chosen', function(evt) {
+ _this.mouse_leave(evt);
+ });
+ this.search_results.bind('mouseup.chosen', function(evt) {
+ _this.search_results_mouseup(evt);
+ });
+ this.search_results.bind('mouseover.chosen', function(evt) {
+ _this.search_results_mouseover(evt);
+ });
+ this.search_results.bind('mouseout.chosen', function(evt) {
+ _this.search_results_mouseout(evt);
+ });
+ this.search_results.bind('mousewheel.chosen DOMMouseScroll.chosen', function(evt) {
+ _this.search_results_mousewheel(evt);
+ });
+ this.search_results.bind('touchstart.chosen', function(evt) {
+ _this.search_results_touchstart(evt);
+ });
+ this.search_results.bind('touchmove.chosen', function(evt) {
+ _this.search_results_touchmove(evt);
+ });
+ this.search_results.bind('touchend.chosen', function(evt) {
+ _this.search_results_touchend(evt);
+ });
+ this.form_field_jq.bind("chosen:updated.chosen", function(evt) {
+ _this.results_update_field(evt);
+ });
+ this.form_field_jq.bind("chosen:activate.chosen", function(evt) {
+ _this.activate_field(evt);
+ });
+ this.form_field_jq.bind("chosen:open.chosen", function(evt) {
+ _this.container_mousedown(evt);
+ });
+ this.form_field_jq.bind("chosen:close.chosen", function(evt) {
+ _this.input_blur(evt);
+ });
+ this.search_field.bind('blur.chosen', function(evt) {
+ _this.input_blur(evt);
+ });
+ this.search_field.bind('keyup.chosen', function(evt) {
+ _this.keyup_checker(evt);
+ });
+ this.search_field.bind('keydown.chosen', function(evt) {
+ _this.keydown_checker(evt);
+ });
+ this.search_field.bind('focus.chosen', function(evt) {
+ _this.input_focus(evt);
+ });
+ this.search_field.bind('cut.chosen', function(evt) {
+ _this.clipboard_event_checker(evt);
+ });
+ this.search_field.bind('paste.chosen', function(evt) {
+ _this.clipboard_event_checker(evt);
+ });
+ if (this.is_multiple) {
+ return this.search_choices.bind('click.chosen', function(evt) {
+ _this.choices_click(evt);
+ });
+ } else {
+ return this.container.bind('click.chosen', function(evt) {
+ evt.preventDefault();
+ });
+ }
+ };
+
+ Chosen.prototype.destroy = function() {
+ $(this.container[0].ownerDocument).unbind("click.chosen", this.click_test_action);
+ if (this.search_field[0].tabIndex) {
+ this.form_field_jq[0].tabIndex = this.search_field[0].tabIndex;
+ }
+ this.container.remove();
+ this.form_field_jq.removeData('chosen');
+ return this.form_field_jq.show();
+ };
+
+ Chosen.prototype.search_field_disabled = function() {
+ this.is_disabled = this.form_field_jq[0].disabled;
+ if (this.is_disabled) {
+ this.container.addClass('chosen-disabled');
+ this.search_field[0].disabled = true;
+ if (!this.is_multiple) {
+ this.selected_item.unbind("focus.chosen", this.activate_action);
+ }
+ return this.close_field();
+ } else {
+ this.container.removeClass('chosen-disabled');
+ this.search_field[0].disabled = false;
+ if (!this.is_multiple) {
+ return this.selected_item.bind("focus.chosen", this.activate_action);
+ }
+ }
+ };
+
+ Chosen.prototype.container_mousedown = function(evt) {
+ if (!this.is_disabled) {
+ if (evt && evt.type === "mousedown" && !this.results_showing) {
+ evt.preventDefault();
+ }
+ if (!((evt != null) && ($(evt.target)).hasClass("search-choice-close"))) {
+ if (!this.active_field) {
+ if (this.is_multiple) {
+ this.search_field.val("");
+ }
+ $(this.container[0].ownerDocument).bind('click.chosen', this.click_test_action);
+ this.results_show();
+ } else if (!this.is_multiple && evt && (($(evt.target)[0] === this.selected_item[0]) || $(evt.target).parents("a.chosen-single").length)) {
+ evt.preventDefault();
+ this.results_toggle();
+ }
+ return this.activate_field();
+ }
+ }
+ };
+
+ Chosen.prototype.container_mouseup = function(evt) {
+ if (evt.target.nodeName === "ABBR" && !this.is_disabled) {
+ return this.results_reset(evt);
+ }
+ };
+
+ Chosen.prototype.search_results_mousewheel = function(evt) {
+ var delta;
+ if (evt.originalEvent) {
+ delta = evt.originalEvent.deltaY || -evt.originalEvent.wheelDelta || evt.originalEvent.detail;
+ }
+ if (delta != null) {
+ evt.preventDefault();
+ if (evt.type === 'DOMMouseScroll') {
+ delta = delta * 40;
+ }
+ return this.search_results.scrollTop(delta + this.search_results.scrollTop());
+ }
+ };
+
+ Chosen.prototype.blur_test = function(evt) {
+ if (!this.active_field && this.container.hasClass("chosen-container-active")) {
+ return this.close_field();
+ }
+ };
+
+ Chosen.prototype.close_field = function() {
+ $(this.container[0].ownerDocument).unbind("click.chosen", this.click_test_action);
+ this.active_field = false;
+ this.results_hide();
+ this.container.removeClass("chosen-container-active");
+ this.clear_backstroke();
+ this.show_search_field_default();
+ return this.search_field_scale();
+ };
+
+ Chosen.prototype.activate_field = function() {
+ this.container.addClass("chosen-container-active");
+ this.active_field = true;
+ this.search_field.val(this.search_field.val());
+ return this.search_field.focus();
+ };
+
+ Chosen.prototype.test_active_click = function(evt) {
+ var active_container;
+ active_container = $(evt.target).closest('.chosen-container');
+ if (active_container.length && this.container[0] === active_container[0]) {
+ return this.active_field = true;
+ } else {
+ return this.close_field();
+ }
+ };
+
+ Chosen.prototype.results_build = function() {
+ this.parsing = true;
+ this.selected_option_count = null;
+ this.results_data = SelectParser.select_to_array(this.form_field);
+ if (this.is_multiple) {
+ this.search_choices.find("li.search-choice").remove();
+ } else if (!this.is_multiple) {
+ this.single_set_selected_text();
+ if (this.disable_search || this.form_field.options.length <= this.disable_search_threshold) {
+ this.search_field[0].readOnly = true;
+ this.container.addClass("chosen-container-single-nosearch");
+ } else {
+ this.search_field[0].readOnly = false;
+ this.container.removeClass("chosen-container-single-nosearch");
+ }
+ }
+ this.update_results_content(this.results_option_build({
+ first: true
+ }));
+ this.search_field_disabled();
+ this.show_search_field_default();
+ this.search_field_scale();
+ return this.parsing = false;
+ };
+
+ Chosen.prototype.result_do_highlight = function(el) {
+ var high_bottom, high_top, maxHeight, visible_bottom, visible_top;
+ if (el.length) {
+ this.result_clear_highlight();
+ this.result_highlight = el;
+ this.result_highlight.addClass("highlighted");
+ maxHeight = parseInt(this.search_results.css("maxHeight"), 10);
+ visible_top = this.search_results.scrollTop();
+ visible_bottom = maxHeight + visible_top;
+ high_top = this.result_highlight.position().top + this.search_results.scrollTop();
+ high_bottom = high_top + this.result_highlight.outerHeight();
+ if (high_bottom >= visible_bottom) {
+ return this.search_results.scrollTop((high_bottom - maxHeight) > 0 ? high_bottom - maxHeight : 0);
+ } else if (high_top < visible_top) {
+ return this.search_results.scrollTop(high_top);
+ }
+ }
+ };
+
+ Chosen.prototype.result_clear_highlight = function() {
+ if (this.result_highlight) {
+ this.result_highlight.removeClass("highlighted");
+ }
+ return this.result_highlight = null;
+ };
+
+ Chosen.prototype.results_show = function() {
+ if (this.is_multiple && this.max_selected_options <= this.choices_count()) {
+ this.form_field_jq.trigger("chosen:maxselected", {
+ chosen: this
+ });
+ return false;
+ }
+ this.container.addClass("chosen-with-drop");
+ this.results_showing = true;
+ this.search_field.focus();
+ this.search_field.val(this.search_field.val());
+ this.winnow_results();
+ return this.form_field_jq.trigger("chosen:showing_dropdown", {
+ chosen: this
+ });
+ };
+
+ Chosen.prototype.update_results_content = function(content) {
+ return this.search_results.html(content);
+ };
+
+ Chosen.prototype.results_hide = function() {
+ if (this.results_showing) {
+ this.result_clear_highlight();
+ this.container.removeClass("chosen-with-drop");
+ this.form_field_jq.trigger("chosen:hiding_dropdown", {
+ chosen: this
+ });
+ }
+ return this.results_showing = false;
+ };
+
+ Chosen.prototype.set_tab_index = function(el) {
+ var ti;
+ if (this.form_field.tabIndex) {
+ ti = this.form_field.tabIndex;
+ this.form_field.tabIndex = -1;
+ return this.search_field[0].tabIndex = ti;
+ }
+ };
+
+ Chosen.prototype.set_label_behavior = function() {
+ var _this = this;
+ this.form_field_label = this.form_field_jq.parents("label");
+ if (!this.form_field_label.length && this.form_field.id.length) {
+ this.form_field_label = $("label[for='" + this.form_field.id + "']");
+ }
+ if (this.form_field_label.length > 0) {
+ return this.form_field_label.bind('click.chosen', function(evt) {
+ if (_this.is_multiple) {
+ return _this.container_mousedown(evt);
+ } else {
+ return _this.activate_field();
+ }
+ });
+ }
+ };
+
+ Chosen.prototype.show_search_field_default = function() {
+ if (this.is_multiple && this.choices_count() < 1 && !this.active_field) {
+ this.search_field.val(this.default_text);
+ return this.search_field.addClass("default");
+ } else {
+ this.search_field.val("");
+ return this.search_field.removeClass("default");
+ }
+ };
+
+ Chosen.prototype.search_results_mouseup = function(evt) {
+ var target;
+ target = $(evt.target).hasClass("active-result") ? $(evt.target) : $(evt.target).parents(".active-result").first();
+ if (target.length) {
+ this.result_highlight = target;
+ this.result_select(evt);
+ return this.search_field.focus();
+ }
+ };
+
+ Chosen.prototype.search_results_mouseover = function(evt) {
+ var target;
+ target = $(evt.target).hasClass("active-result") ? $(evt.target) : $(evt.target).parents(".active-result").first();
+ if (target) {
+ return this.result_do_highlight(target);
+ }
+ };
+
+ Chosen.prototype.search_results_mouseout = function(evt) {
+ if ($(evt.target).hasClass("active-result" || $(evt.target).parents('.active-result').first())) {
+ return this.result_clear_highlight();
+ }
+ };
+
+ Chosen.prototype.choice_build = function(item) {
+ var choice, close_link,
+ _this = this;
+ choice = $('<li />', {
+ "class": "search-choice"
+ }).html("<span>" + (this.choice_label(item)) + "</span>");
+ if (item.disabled) {
+ choice.addClass('search-choice-disabled');
+ } else {
+ close_link = $('<a />', {
+ "class": 'search-choice-close',
+ 'data-option-array-index': item.array_index
+ });
+ close_link.bind('click.chosen', function(evt) {
+ return _this.choice_destroy_link_click(evt);
+ });
+ choice.append(close_link);
+ }
+ return this.search_container.before(choice);
+ };
+
+ Chosen.prototype.choice_destroy_link_click = function(evt) {
+ evt.preventDefault();
+ evt.stopPropagation();
+ if (!this.is_disabled) {
+ return this.choice_destroy($(evt.target));
+ }
+ };
+
+ Chosen.prototype.choice_destroy = function(link) {
+ if (this.result_deselect(link[0].getAttribute("data-option-array-index"))) {
+ this.show_search_field_default();
+ if (this.is_multiple && this.choices_count() > 0 && this.search_field.val().length < 1) {
+ this.results_hide();
+ }
+ link.parents('li').first().remove();
+ return this.search_field_scale();
+ }
+ };
+
+ Chosen.prototype.results_reset = function() {
+ this.reset_single_select_options();
+ this.form_field.options[0].selected = true;
+ this.single_set_selected_text();
+ this.show_search_field_default();
+ this.results_reset_cleanup();
+ this.form_field_jq.trigger("change");
+ if (this.active_field) {
+ return this.results_hide();
+ }
+ };
+
+ Chosen.prototype.results_reset_cleanup = function() {
+ this.current_selectedIndex = this.form_field.selectedIndex;
+ return this.selected_item.find("abbr").remove();
+ };
+
+ Chosen.prototype.result_select = function(evt) {
+ var high, item;
+ if (this.result_highlight) {
+ high = this.result_highlight;
+ this.result_clear_highlight();
+ if (this.is_multiple && this.max_selected_options <= this.choices_count()) {
+ this.form_field_jq.trigger("chosen:maxselected", {
+ chosen: this
+ });
+ return false;
+ }
+ if (this.is_multiple) {
+ high.removeClass("active-result");
+ } else {
+ this.reset_single_select_options();
+ }
+ high.addClass("result-selected");
+ item = this.results_data[high[0].getAttribute("data-option-array-index")];
+ item.selected = true;
+ this.form_field.options[item.options_index].selected = true;
+ this.selected_option_count = null;
+ if (this.is_multiple) {
+ this.choice_build(item);
+ } else {
+ this.single_set_selected_text(this.choice_label(item));
+ }
+ if (!((evt.metaKey || evt.ctrlKey) && this.is_multiple)) {
+ this.results_hide();
+ }
+ this.search_field.val("");
+ if (this.is_multiple || this.form_field.selectedIndex !== this.current_selectedIndex) {
+ this.form_field_jq.trigger("change", {
+ 'selected': this.form_field.options[item.options_index].value
+ });
+ }
+ this.current_selectedIndex = this.form_field.selectedIndex;
+ evt.preventDefault();
+ return this.search_field_scale();
+ }
+ };
+
+ Chosen.prototype.single_set_selected_text = function(text) {
+ if (text == null) {
+ text = this.default_text;
+ }
+ if (text === this.default_text) {
+ this.selected_item.addClass("chosen-default");
+ } else {
+ this.single_deselect_control_build();
+ this.selected_item.removeClass("chosen-default");
+ }
+ return this.selected_item.find("span").html(text);
+ };
+
+ Chosen.prototype.result_deselect = function(pos) {
+ var result_data;
+ result_data = this.results_data[pos];
+ if (!this.form_field.options[result_data.options_index].disabled) {
+ result_data.selected = false;
+ this.form_field.options[result_data.options_index].selected = false;
+ this.selected_option_count = null;
+ this.result_clear_highlight();
+ if (this.results_showing) {
+ this.winnow_results();
+ }
+ this.form_field_jq.trigger("change", {
+ deselected: this.form_field.options[result_data.options_index].value
+ });
+ this.search_field_scale();
+ return true;
+ } else {
+ return false;
+ }
+ };
+
+ Chosen.prototype.single_deselect_control_build = function() {
+ if (!this.allow_single_deselect) {
+ return;
+ }
+ if (!this.selected_item.find("abbr").length) {
+ this.selected_item.find("span").first().after("<abbr class=\"search-choice-close\"></abbr>");
+ }
+ return this.selected_item.addClass("chosen-single-with-deselect");
+ };
+
+ Chosen.prototype.get_search_text = function() {
+ return $('<div/>').text($.trim(this.search_field.val())).html();
+ };
+
+ Chosen.prototype.winnow_results_set_highlight = function() {
+ var do_high, selected_results;
+ selected_results = !this.is_multiple ? this.search_results.find(".result-selected.active-result") : [];
+ do_high = selected_results.length ? selected_results.first() : this.search_results.find(".active-result").first();
+ if (do_high != null) {
+ return this.result_do_highlight(do_high);
+ }
+ };
+
+ Chosen.prototype.no_results = function(terms) {
+ var no_results_html;
+ no_results_html = $('<li class="no-results">' + this.results_none_found + ' "<span></span>"</li>');
+ no_results_html.find("span").first().html(terms);
+ this.search_results.append(no_results_html);
+ return this.form_field_jq.trigger("chosen:no_results", {
+ chosen: this
+ });
+ };
+
+ Chosen.prototype.no_results_clear = function() {
+ return this.search_results.find(".no-results").remove();
+ };
+
+ Chosen.prototype.keydown_arrow = function() {
+ var next_sib;
+ if (this.results_showing && this.result_highlight) {
+ next_sib = this.result_highlight.nextAll("li.active-result").first();
+ if (next_sib) {
+ return this.result_do_highlight(next_sib);
+ }
+ } else {
+ return this.results_show();
+ }
+ };
+
+ Chosen.prototype.keyup_arrow = function() {
+ var prev_sibs;
+ if (!this.results_showing && !this.is_multiple) {
+ return this.results_show();
+ } else if (this.result_highlight) {
+ prev_sibs = this.result_highlight.prevAll("li.active-result");
+ if (prev_sibs.length) {
+ return this.result_do_highlight(prev_sibs.first());
+ } else {
+ if (this.choices_count() > 0) {
+ this.results_hide();
+ }
+ return this.result_clear_highlight();
+ }
+ }
+ };
+
+ Chosen.prototype.keydown_backstroke = function() {
+ var next_available_destroy;
+ if (this.pending_backstroke) {
+ this.choice_destroy(this.pending_backstroke.find("a").first());
+ return this.clear_backstroke();
+ } else {
+ next_available_destroy = this.search_container.siblings("li.search-choice").last();
+ if (next_available_destroy.length && !next_available_destroy.hasClass("search-choice-disabled")) {
+ this.pending_backstroke = next_available_destroy;
+ if (this.single_backstroke_delete) {
+ return this.keydown_backstroke();
+ } else {
+ return this.pending_backstroke.addClass("search-choice-focus");
+ }
+ }
+ }
+ };
+
+ Chosen.prototype.clear_backstroke = function() {
+ if (this.pending_backstroke) {
+ this.pending_backstroke.removeClass("search-choice-focus");
+ }
+ return this.pending_backstroke = null;
+ };
+
+ Chosen.prototype.keydown_checker = function(evt) {
+ var stroke, _ref1;
+ stroke = (_ref1 = evt.which) != null ? _ref1 : evt.keyCode;
+ this.search_field_scale();
+ if (stroke !== 8 && this.pending_backstroke) {
+ this.clear_backstroke();
+ }
+ switch (stroke) {
+ case 8:
+ this.backstroke_length = this.search_field.val().length;
+ break;
+ case 9:
+ if (this.results_showing && !this.is_multiple) {
+ this.result_select(evt);
+ }
+ this.mouse_on_container = false;
+ break;
+ case 13:
+ if (this.results_showing) {
+ evt.preventDefault();
+ }
+ break;
+ case 32:
+ if (this.disable_search) {
+ evt.preventDefault();
+ }
+ break;
+ case 38:
+ evt.preventDefault();
+ this.keyup_arrow();
+ break;
+ case 40:
+ evt.preventDefault();
+ this.keydown_arrow();
+ break;
+ }
+ };
+
+ Chosen.prototype.search_field_scale = function() {
+ var div, f_width, h, style, style_block, styles, w, _i, _len;
+ if (this.is_multiple) {
+ h = 0;
+ w = 0;
+ style_block = "position:absolute; left: -1000px; top: -1000px; display:none;";
+ styles = ['font-size', 'font-style', 'font-weight', 'font-family', 'line-height', 'text-transform', 'letter-spacing'];
+ for (_i = 0, _len = styles.length; _i < _len; _i++) {
+ style = styles[_i];
+ style_block += style + ":" + this.search_field.css(style) + ";";
+ }
+ div = $('<div />', {
+ 'style': style_block
+ });
+ div.text(this.search_field.val());
+ $('body').append(div);
+ w = div.width() + 25;
+ div.remove();
+ f_width = this.container.outerWidth();
+ if (w > f_width - 10) {
+ w = f_width - 10;
+ }
+ return this.search_field.css({
+ 'width': w + 'px'
+ });
+ }
+ };
+
+ return Chosen;
+
+ })(AbstractChosen);
+
+}).call(this);
diff --git a/platform/www/lib/plugins/farmer/script/plugins.js b/platform/www/lib/plugins/farmer/script/plugins.js
new file mode 100644
index 0000000..f092b39
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/script/plugins.js
@@ -0,0 +1,149 @@
+/**
+ * DokuWiki Plugin farmer (JS for plugin management)
+ *
+ * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
+ * @author Michael Große <grosse@cosmocode.de>
+ * @author Andreas Gohr <gohr@cosmocode.de>
+ */
+(function () {
+ 'use strict';
+
+ jQuery(function () {
+ // general animal select
+ var $animalSelect = jQuery('select.farmer_chosen_animals');
+ $animalSelect.chosen({
+ width: '100%',
+ search_contains: true,
+ allow_single_deselect: true,
+ "placeholder_text_single": LANG.plugins.farmer.animalSelect
+ });
+
+ jQuery('select.acl_chosen').chosen({
+ disable_search: true,
+ width: '100%'
+ });
+
+
+ // Plugin Management for all Animals
+ var $formAllAnimals = jQuery('#farmer__pluginsforall');
+ $formAllAnimals.find('select')
+ .change(function () {
+ $formAllAnimals.find('button').prop('disabled', false);
+ })
+ .chosen({
+ width: '100%',
+ search_contains: true,
+ "placeholder_text_single": LANG.plugins.farmer.pluginSelect
+ })
+ ;
+
+ // Plugin Management for single Animals
+ var $formSingleAnimal = jQuery('#farmer__pluginsforone');
+ $formSingleAnimal.find('select')
+ .change(function () {
+ var animal = jQuery(this).val();
+ $formSingleAnimal.find('button').prop('disabled', true);
+ jQuery.post(
+ DOKU_BASE + 'lib/exe/ajax.php',
+ {
+ call: 'plugin_farmer_getPlugins_' + animal
+ },
+ function (data) {
+ $formSingleAnimal.find('div.output').html(data);
+ $formSingleAnimal.find('button').prop('disabled', false);
+ },
+ 'html'
+ )}
+ )
+ .chosen({
+ width: '100%',
+ search_contains: true,
+ "placeholder_text_single": LANG.plugins.farmer.animalSelect
+ })
+ ;
+
+ /**
+ * Handle clicks on the matrix
+ */
+ var $formPluginMatrix = jQuery('#farmer__pluginmatrix').hide();
+ $formPluginMatrix.on('click', 'td', function () {
+ var $td = jQuery(this);
+ $td.html('⌛').css('background-color','transparent');
+ jQuery.post(
+ DOKU_BASE + 'lib/exe/ajax.php',
+ {
+ call: 'plugin_farmer_modPlugin',
+ plugin: $td.data('plugin'),
+ ani: $td.data('animal')
+ },
+ function (data) {
+ $td.replaceWith(data);
+ },
+ 'html'
+ );
+ });
+
+ /**
+ * show the matrix interface
+ */
+ function showMatrix() {
+ jQuery.post(
+ DOKU_BASE + 'lib/exe/ajax.php',
+ {
+ call: 'plugin_farmer_getPluginMatrix'
+ },
+ function (data) {
+ $formPluginMatrix.html(data);
+ $formPluginMatrix.show();
+ },
+ 'html'
+ )
+ }
+
+ // make sure there's enough space for the dropdown
+ $animalSelect.on('chosen:showing_dropdown', function (evt, params) {
+ jQuery(evt.target).parent('fieldset').animate({
+ "padding-bottom": '20em'
+ }, 400);
+ }).on('chosen:hiding_dropdown', function (evt, params) {
+ jQuery(evt.target).parent('fieldset').animate({
+ "padding-bottom": '7px'
+ }, 400);
+ });
+
+ var $aclPolicyFieldset = jQuery('#aclPolicyFieldset');
+ if ($aclPolicyFieldset.length) {
+ $animalSelect.on('change', function (evt, params) {
+ var $this = jQuery(this);
+ if ($this.val() === '') {
+ $aclPolicyFieldset.slideDown();
+ } else {
+ $aclPolicyFieldset.slideUp();
+ }
+ });
+ }
+
+
+
+
+ jQuery("input[name=bulkSingleSwitch]:radio").change(function () {
+ if (jQuery('#farmer__bulk').prop("checked")) {
+ $formAllAnimals.show();
+ $formSingleAnimal.hide();
+ $formPluginMatrix.hide();
+ } else if (jQuery('#farmer__single').prop("checked")) {
+ $formAllAnimals.hide();
+ $formSingleAnimal.show();
+ $formPluginMatrix.hide();
+ } else {
+ $formAllAnimals.hide();
+ $formSingleAnimal.hide();
+ showMatrix();
+ }
+ });
+ jQuery('#farmer__bulk').click();
+
+
+ });
+
+})();
diff --git a/platform/www/lib/plugins/farmer/style.less b/platform/www/lib/plugins/farmer/style.less
new file mode 100644
index 0000000..905078c
--- /dev/null
+++ b/platform/www/lib/plugins/farmer/style.less
@@ -0,0 +1,104 @@
+#plugin__farmer_admin {
+
+ .panelHeader {
+ background-color: #eee;
+ margin: 0;
+ padding: 10px 10px 8px;
+ overflow: hidden;
+ border-right: 1px solid @ini_border;
+ border-left: 1px solid @ini_border;
+ }
+
+ .panelMain,
+ .panelFooter {
+ padding: 1em;
+ border-right: 1px solid @ini_border;
+ border-left: 1px solid @ini_border;
+ }
+
+ .panelFooter {
+ border-bottom: 1px solid @ini_border;
+ }
+
+ form {
+ display: block;
+ text-align: center;
+
+ fieldset {
+ width: 80%;
+ padding: 1em;
+ }
+
+ label {
+ text-align: left;
+ display: block;
+ margin-bottom: 0.5em;
+
+ &:hover {
+ background-color: @ini_background_alt;
+ }
+
+ span {
+ display: inline-block;
+ width: 40%;
+ }
+ }
+
+ .chosen-container {
+ margin-bottom: 0.5em;
+ }
+
+ button {
+ margin: 1em;
+ }
+ }
+
+ .pluginmatrix {
+ width: 80%;
+ margin: 1em auto;
+
+ table {
+ thead th {
+ position: relative;
+ height: 8em;
+ vertical-align: bottom;
+ overflow: visible;
+ background-color: transparent;
+ border: none;
+
+ div {
+ transform-origin: bottom left;
+ transform: rotate(-60deg);
+ position: absolute;
+ left: 1em;
+ bottom: 0;
+ z-index: 5;
+ }
+ }
+
+ tbody {
+ position: relative;
+
+ td {
+ cursor: pointer;
+ }
+
+ th.off,
+ td.off {
+ background-color: #c33;
+ }
+
+ th.on,
+ td.on {
+ background-color: #3c3;
+ }
+
+ th.default,
+ td.default {
+ opacity: 0.5;
+ }
+ }
+ }
+ }
+
+}
diff --git a/platform/www/lib/plugins/fastwiki b/platform/www/lib/plugins/fastwiki
new file mode 160000
+Subproject 82b64eac47dbf50c2668b11eeaf901be63994a8
diff --git a/platform/www/lib/plugins/index.html b/platform/www/lib/plugins/index.html
new file mode 100644
index 0000000..977f90e
--- /dev/null
+++ b/platform/www/lib/plugins/index.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta http-equiv="refresh" content="0; URL=../../" />
+<meta name="robots" content="noindex" />
+<title>nothing here...</title>
+</head>
+<body>
+<!-- this is just here to prevent directory browsing -->
+</body>
+</html>
diff --git a/platform/www/lib/plugins/info/plugin.info.txt b/platform/www/lib/plugins/info/plugin.info.txt
new file mode 100644
index 0000000..f4c6201
--- /dev/null
+++ b/platform/www/lib/plugins/info/plugin.info.txt
@@ -0,0 +1,7 @@
+base info
+author Andreas Gohr
+email andi@splitbrain.org
+date 2020-06-04
+name Info Plugin
+desc Displays information about various DokuWiki internals
+url http://dokuwiki.org/plugin:info
diff --git a/platform/www/lib/plugins/info/syntax.php b/platform/www/lib/plugins/info/syntax.php
new file mode 100644
index 0000000..0d1e389
--- /dev/null
+++ b/platform/www/lib/plugins/info/syntax.php
@@ -0,0 +1,302 @@
+<?php
+/**
+ * Info Plugin: Displays information about various DokuWiki internals
+ *
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @author Esther Brunner <wikidesign@gmail.com>
+ */
+class syntax_plugin_info extends DokuWiki_Syntax_Plugin
+{
+
+ /**
+ * What kind of syntax are we?
+ */
+ public function getType()
+ {
+ return 'substition';
+ }
+
+ /**
+ * What about paragraphs?
+ */
+ public function getPType()
+ {
+ return 'block';
+ }
+
+ /**
+ * Where to sort in?
+ */
+ public function getSort()
+ {
+ return 155;
+ }
+
+
+ /**
+ * Connect pattern to lexer
+ */
+ public function connectTo($mode)
+ {
+ $this->Lexer->addSpecialPattern('~~INFO:\w+~~', $mode, 'plugin_info');
+ }
+
+ /**
+ * Handle the match
+ *
+ * @param string $match The text matched by the patterns
+ * @param int $state The lexer state for the match
+ * @param int $pos The character position of the matched text
+ * @param Doku_Handler $handler The Doku_Handler object
+ * @return array Return an array with all data you want to use in render
+ */
+ public function handle($match, $state, $pos, Doku_Handler $handler)
+ {
+ $match = substr($match, 7, -2); //strip ~~INFO: from start and ~~ from end
+ return array(strtolower($match));
+ }
+
+ /**
+ * Create output
+ *
+ * @param string $format string output format being rendered
+ * @param Doku_Renderer $renderer the current renderer object
+ * @param array $data data created by handler()
+ * @return boolean rendered correctly?
+ */
+ public function render($format, Doku_Renderer $renderer, $data)
+ {
+ if ($format == 'xhtml') {
+ /** @var Doku_Renderer_xhtml $renderer */
+ //handle various info stuff
+ switch ($data[0]) {
+ case 'syntaxmodes':
+ $renderer->doc .= $this->renderSyntaxModes();
+ break;
+ case 'syntaxtypes':
+ $renderer->doc .= $this->renderSyntaxTypes();
+ break;
+ case 'syntaxplugins':
+ $this->renderPlugins('syntax', $renderer);
+ break;
+ case 'adminplugins':
+ $this->renderPlugins('admin', $renderer);
+ break;
+ case 'actionplugins':
+ $this->renderPlugins('action', $renderer);
+ break;
+ case 'rendererplugins':
+ $this->renderPlugins('renderer', $renderer);
+ break;
+ case 'helperplugins':
+ $this->renderPlugins('helper', $renderer);
+ break;
+ case 'authplugins':
+ $this->renderPlugins('auth', $renderer);
+ break;
+ case 'remoteplugins':
+ $this->renderPlugins('remote', $renderer);
+ break;
+ case 'helpermethods':
+ $this->renderHelperMethods($renderer);
+ break;
+ case 'datetime':
+ $renderer->doc .= date('r');
+ break;
+ default:
+ $renderer->doc .= "no info about ".htmlspecialchars($data[0]);
+ }
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * list all installed plugins
+ *
+ * uses some of the original renderer methods
+ *
+ * @param string $type
+ * @param Doku_Renderer_xhtml $renderer
+ */
+ protected function renderPlugins($type, Doku_Renderer_xhtml $renderer)
+ {
+ global $lang;
+ $renderer->doc .= '<ul>';
+
+ $plugins = plugin_list($type);
+ $plginfo = array();
+
+ // remove subparts
+ foreach ($plugins as $p) {
+ if (!$po = plugin_load($type, $p)) continue;
+ list($name,/* $part */) = explode('_', $p, 2);
+ $plginfo[$name] = $po->getInfo();
+ }
+
+ // list them
+ foreach ($plginfo as $info) {
+ $renderer->doc .= '<li><div class="li">';
+ $renderer->externallink($info['url'], $info['name']);
+ $renderer->doc .= ' ';
+ $renderer->doc .= '<em>'.$info['date'].'</em>';
+ $renderer->doc .= ' ';
+ $renderer->doc .= $lang['by'];
+ $renderer->doc .= ' ';
+ $renderer->emaillink($info['email'], $info['author']);
+ $renderer->doc .= '<br />';
+ $renderer->doc .= strtr(hsc($info['desc']), array("\n"=>"<br />"));
+ $renderer->doc .= '</div></li>';
+ unset($po);
+ }
+
+ $renderer->doc .= '</ul>';
+ }
+
+ /**
+ * list all installed plugins
+ *
+ * uses some of the original renderer methods
+ *
+ * @param Doku_Renderer_xhtml $renderer
+ */
+ protected function renderHelperMethods(Doku_Renderer_xhtml $renderer)
+ {
+ $plugins = plugin_list('helper');
+ foreach ($plugins as $p) {
+ if (!$po = plugin_load('helper', $p)) continue;
+
+ if (!method_exists($po, 'getMethods')) continue;
+ $methods = $po->getMethods();
+ $info = $po->getInfo();
+
+ $hid = $this->addToToc($info['name'], 2, $renderer);
+ $doc = '<h2><a name="'.$hid.'" id="'.$hid.'">'.hsc($info['name']).'</a></h2>';
+ $doc .= '<div class="level2">';
+ $doc .= '<p>'.strtr(hsc($info['desc']), array("\n"=>"<br />")).'</p>';
+ $doc .= '<pre class="code">$'.$p." = plugin_load('helper', '".$p."');</pre>";
+ $doc .= '</div>';
+ foreach ($methods as $method) {
+ $title = '$'.$p.'->'.$method['name'].'()';
+ $hid = $this->addToToc($title, 3, $renderer);
+ $doc .= '<h3><a name="'.$hid.'" id="'.$hid.'">'.hsc($title).'</a></h3>';
+ $doc .= '<div class="level3">';
+ $doc .= '<div class="table"><table class="inline"><tbody>';
+ $doc .= '<tr><th>Description</th><td colspan="2">'.$method['desc'].
+ '</td></tr>';
+ if ($method['params']) {
+ $c = count($method['params']);
+ $doc .= '<tr><th rowspan="'.$c.'">Parameters</th><td>';
+ $params = array();
+ foreach ($method['params'] as $desc => $type) {
+ $params[] = hsc($desc).'</td><td>'.hsc($type);
+ }
+ $doc .= join('</td></tr><tr><td>', $params).'</td></tr>';
+ }
+ if ($method['return']) {
+ $doc .= '<tr><th>Return value</th><td>'.hsc(key($method['return'])).
+ '</td><td>'.hsc(current($method['return'])).'</td></tr>';
+ }
+ $doc .= '</tbody></table></div>';
+ $doc .= '</div>';
+ }
+ unset($po);
+
+ $renderer->doc .= $doc;
+ }
+ }
+
+ /**
+ * lists all known syntax types and their registered modes
+ *
+ * @return string
+ */
+ protected function renderSyntaxTypes()
+ {
+ global $PARSER_MODES;
+ $doc = '';
+
+ $doc .= '<div class="table"><table class="inline"><tbody>';
+ foreach ($PARSER_MODES as $mode => $modes) {
+ $doc .= '<tr>';
+ $doc .= '<td class="leftalign">';
+ $doc .= $mode;
+ $doc .= '</td>';
+ $doc .= '<td class="leftalign">';
+ $doc .= join(', ', $modes);
+ $doc .= '</td>';
+ $doc .= '</tr>';
+ }
+ $doc .= '</tbody></table></div>';
+ return $doc;
+ }
+
+ /**
+ * lists all known syntax modes and their sorting value
+ *
+ * @return string
+ */
+ protected function renderSyntaxModes()
+ {
+ $modes = p_get_parsermodes();
+
+ $compactmodes = array();
+ foreach ($modes as $mode) {
+ $compactmodes[$mode['sort']][] = $mode['mode'];
+ }
+ $doc = '';
+ $doc .= '<div class="table"><table class="inline"><tbody>';
+
+ foreach ($compactmodes as $sort => $modes) {
+ $rowspan = '';
+ if (count($modes) > 1) {
+ $rowspan = ' rowspan="'.count($modes).'"';
+ }
+
+ foreach ($modes as $index => $mode) {
+ $doc .= '<tr>';
+ $doc .= '<td class="leftalign">';
+ $doc .= $mode;
+ $doc .= '</td>';
+
+ if ($index === 0) {
+ $doc .= '<td class="rightalign" '.$rowspan.'>';
+ $doc .= $sort;
+ $doc .= '</td>';
+ }
+ $doc .= '</tr>';
+ }
+ }
+
+ $doc .= '</tbody></table></div>';
+ return $doc;
+ }
+
+ /**
+ * Adds a TOC item
+ *
+ * @param string $text
+ * @param int $level
+ * @param Doku_Renderer_xhtml $renderer
+ * @return string
+ */
+ protected function addToToc($text, $level, Doku_Renderer_xhtml $renderer)
+ {
+ global $conf;
+
+ $hid = '';
+ if (($level >= $conf['toptoclevel']) && ($level <= $conf['maxtoclevel'])) {
+ $hid = $renderer->_headerToLink($text, true);
+ $renderer->toc[] = array(
+ 'hid' => $hid,
+ 'title' => $text,
+ 'type' => 'ul',
+ 'level' => $level - $conf['toptoclevel'] + 1
+ );
+ }
+ return $hid;
+ }
+}
+
+//Setup VIM: ex: et ts=4 :
diff --git a/platform/www/lib/plugins/markdowku/LICENSE b/platform/www/lib/plugins/markdowku/LICENSE
new file mode 100644
index 0000000..40ff04a
--- /dev/null
+++ b/platform/www/lib/plugins/markdowku/LICENSE
@@ -0,0 +1,27 @@
+BSD 2-Clause License
+
+Copyright (c) 2017, Julian Fagir <gnrp@komkon2.de>
+Copyright (c) 2020, Raphael Wimmer <raphael.wimmer@ur.de>
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+2. 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.
+
+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 HOLDER 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/platform/www/lib/plugins/markdowku/README.md b/platform/www/lib/plugins/markdowku/README.md
new file mode 100644
index 0000000..d5b199a
--- /dev/null
+++ b/platform/www/lib/plugins/markdowku/README.md
@@ -0,0 +1,2 @@
+# dokuwiki-plugin-markdowku
+Markdowku - markdown syntax plugin for DokuWiki
diff --git a/platform/www/lib/plugins/markdowku/manager.dat b/platform/www/lib/plugins/markdowku/manager.dat
new file mode 100644
index 0000000..664b6c7
--- /dev/null
+++ b/platform/www/lib/plugins/markdowku/manager.dat
@@ -0,0 +1,2 @@
+downloadurl=https://github.com/Medieninformatik-Regensburg/dokuwiki-plugin-markdowku/archive/refs/heads/master.zip
+installed=Sun, 06 Mar 2022 22:47:06 +0000
diff --git a/platform/www/lib/plugins/markdowku/plugin.info.txt b/platform/www/lib/plugins/markdowku/plugin.info.txt
new file mode 100644
index 0000000..7682646
--- /dev/null
+++ b/platform/www/lib/plugins/markdowku/plugin.info.txt
@@ -0,0 +1,7 @@
+base markdowku
+author Julian Fagir, Raphael Wimmer (maintainer)
+email raphael.wimmer@ur.de
+date 2021-12-04
+name Markdowku
+desc Integrates Markdown into Dokuwiki syntax
+url https://www.dokuwiki.org/plugin:markdowku
diff --git a/platform/www/lib/plugins/markdowku/syntax/anchorsinline.php b/platform/www/lib/plugins/markdowku/syntax/anchorsinline.php
new file mode 100644
index 0000000..b16f2b4
--- /dev/null
+++ b/platform/www/lib/plugins/markdowku/syntax/anchorsinline.php
@@ -0,0 +1,46 @@
+<?php
+/*
+ * Inline links [name](target "title")
+ */
+
+if(!defined('DOKU_INC')) die();
+if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/');
+require_once(DOKU_PLUGIN.'syntax.php');
+
+class syntax_plugin_markdowku_anchorsinline extends DokuWiki_Syntax_Plugin {
+
+ function getType() { return 'substition'; }
+ function getPType() { return 'normal'; }
+ function getSort() { return 102; }
+
+ function connectTo($mode) {
+ $this->nested_brackets_re =
+ str_repeat('(?>[^\[\]]+|\[', 6).
+ str_repeat('\])*', 6);
+ $this->Lexer->addSpecialPattern(
+ '\['.$this->nested_brackets_re.'\]\([ \t]*<?.+?>?[ \t]*(?:[\'"].*?[\'"])?\)',
+ $mode,
+ 'plugin_markdowku_anchorsinline'
+ );
+ }
+
+ function handle($match, $state, $pos, Doku_Handler $handler) {
+ if ($state == DOKU_LEXER_SPECIAL) {
+ $text = preg_match(
+ '/^\[('.$this->nested_brackets_re.')\]\([ \t]*<?(.+?)>?[ \t]*(?:[\'"](.*?)[\'"])?[ \t]*?\)$/',
+ $match,
+ $matches);
+ $target = $matches[2] == '' ? $matches[3] : $matches[2];
+ $title = $matches[1];
+
+ $target = preg_replace('/^mailto:/', '', $target);
+ $handler->internallink($target.'|'.$title, $state, $pos);
+ }
+ return true;
+ }
+
+ function render($mode, Doku_Renderer $renderer, $data) {
+ return true;
+ }
+}
+//Setup VIM: ex: et ts=4 enc=utf-8 :
diff --git a/platform/www/lib/plugins/markdowku/syntax/anchorsreference.php b/platform/www/lib/plugins/markdowku/syntax/anchorsreference.php
new file mode 100644
index 0000000..be8cdd6
--- /dev/null
+++ b/platform/www/lib/plugins/markdowku/syntax/anchorsreference.php
@@ -0,0 +1,62 @@
+<?php
+/*
+ * Reference links, i.e.
+ * ... [name][id] ...
+ * ... [id][] ...
+ * ...
+ * [id]: http://example.com (handled by markdowku_references)
+ */
+
+if(!defined('DOKU_INC')) die();
+if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/');
+require_once(DOKU_PLUGIN.'syntax.php');
+
+class syntax_plugin_markdowku_anchorsreference extends DokuWiki_Syntax_Plugin {
+
+ function getType() { return 'substition'; }
+ function getPType() { return 'normal'; }
+ function getSort() { return 102; }
+
+ function connectTo($mode) {
+ $this->nested_brackets_re =
+ str_repeat('(?>[^\[\]]+|\[', 3).
+ str_repeat('\])*', 3);
+ $this->Lexer->addSpecialPattern(
+ '\['.$this->nested_brackets_re.'\][ ]?(?:\n[ ]*)?\[[^\[\]\n]*?\]',
+ $mode,
+ 'plugin_markdowku_anchorsreference');
+ }
+
+ function handle($match, $state, $pos, Doku_Handler $handler) {
+ return array($state, $match);
+ }
+
+ function render($mode, Doku_Renderer$renderer, $data) {
+ global $ID;
+ preg_match(
+ '/^\[('.$this->nested_brackets_re.')\][ ]?(?:\n[ ]*)?\[(.*?)\]$/',
+ $data[1],
+ $matches);
+
+ $title = $matches[1];
+
+ if ($matches[2] == '')
+ $rid = $matches[1];
+ else
+ $rid = $matches[2];
+
+ $rid = preg_replace("/ /", ".", $rid);
+ $target = p_get_metadata($ID, 'markdowku_references_'.$rid, METADATA_RENDER_USING_CACHE);
+ if ($target == '') {
+ $renderer->cdata($data[1]);
+ } else if (preg_match('/^mailto:/', $target) or
+ preg_match('<'.PREG_PATTERN_VALID_EMAIL.'>', $target)) {
+ $target = preg_replace('/^mailto:/', '', $target);
+ $renderer->emaillink($target, $title);
+ } else {
+ $renderer->externallink($target, $title);
+ }
+ return true;
+ }
+}
+//Setup VIM: ex: et ts=4 enc=utf-8 :
diff --git a/platform/www/lib/plugins/markdowku/syntax/autolinks.php b/platform/www/lib/plugins/markdowku/syntax/autolinks.php
new file mode 100644
index 0000000..b6ed523
--- /dev/null
+++ b/platform/www/lib/plugins/markdowku/syntax/autolinks.php
@@ -0,0 +1,40 @@
+<?php
+/*
+ * Autolinks enclosed in <...>
+ */
+
+if(!defined('DOKU_INC')) die();
+if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/');
+require_once(DOKU_PLUGIN.'syntax.php');
+
+class syntax_plugin_markdowku_autolinks extends DokuWiki_Syntax_Plugin {
+
+ function getType() { return 'substition'; }
+ function getPType() { return 'normal'; }
+ function getSort() { return 102; }
+
+ function connectTo($mode) {
+ $this->Lexer->addSpecialPattern(
+ '<(?:https?|ftp|mailto):[^\'">\s]+?>',
+ $mode,
+ 'plugin_markdowku_autolinks'
+ );
+ }
+
+ function handle($match, $state, $pos, Doku_Handler $handler) {
+ if (preg_match('/^<mailto:/', $match)) {
+ $match = substr($match, 8, -1);
+ $handler->_addCall('emaillink', array($match, NULL), $pos);
+ } else {
+ $match = substr($match, 1, -1);
+ $handler->_addCall('externallink', array($match, NULL), $pos);
+ }
+
+ return true;
+ }
+
+ function render($mode, Doku_Renderer $renderer, $data) {
+ return true;
+ }
+}
+//Setup VIM: ex: et ts=4 enc=utf-8 :
diff --git a/platform/www/lib/plugins/markdowku/syntax/blockquotes.php b/platform/www/lib/plugins/markdowku/syntax/blockquotes.php
new file mode 100644
index 0000000..463c049
--- /dev/null
+++ b/platform/www/lib/plugins/markdowku/syntax/blockquotes.php
@@ -0,0 +1,115 @@
+<?php
+
+if(!defined('DOKU_INC')) die();
+if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/');
+require_once(DOKU_PLUGIN.'syntax.php');
+include_once('headeratx.php');
+
+use dokuwiki\Parsing\Handler\Quote;
+
+class syntax_plugin_markdowku_blockquotes extends DokuWiki_Syntax_Plugin {
+
+ function getType() { return 'container'; }
+ function getPType() { return 'block'; }
+ function getSort() { return 219; }
+ function getAllowedTypes() {
+ return array('formatting', 'substition', 'disabled', 'protected',
+ 'container');
+ }
+
+ function connectTo($mode) {
+ $this->Lexer->addEntryPattern(
+ // (?<=\n)[ \t]*>[ \t]?(?=(?:\n[ ]*[^>]|\Z))
+// '\n[ \t]*>[ \t]?.+?\n(?:.+\n)*',
+// '(?:\n|\A)[ \t]*>(?:[ >\t]*)?', //[ \t]?(?=[^\n]+?\n)',
+ '(?:\n|\A)[ \t]*>(?:[ >\t]*)?.*?(?=\n)', //[ \t]?(?=[^\n]+?\n)',
+ $mode,
+ 'plugin_markdowku_blockquotes');
+
+ /* Setext headers need two lines */
+ $this->Lexer->addPattern(
+ '\n[ \t]*>(?:[ \t>]*>)?[ \t]?[^\n]+?[ \t]*\n[ \t]*>(?:[ \t>]*>)?[ \t]?=+[ \t]*',
+ 'plugin_markdowku_blockquotes');
+
+ $this->Lexer->addPattern(
+ '\n[ \t]*>(?:[ \t>]*>)?[ \t]?[^\n]+?[ \t]*\n[ \t]*>(?:[ \t>]*>)?[ \t]?-+[ \t]*',
+ 'plugin_markdowku_blockquotes');
+
+ $this->Lexer->addPattern(
+// '\n[ \t]*>(?:[ \t>]*>)?[ \t]?', //[ \t]?(?=[^\n]+?\n)',
+ '\n[ \t]*>(?:[ \t>]*>)?[ \t]?.*?(?=\n)', //[ \t]?(?=[^\n]+?\n)',
+ 'plugin_markdowku_blockquotes');
+ }
+
+ function postConnect() {
+ $this->Lexer->addExitPattern(
+ '(?:\n[^>]|\Z)',
+ 'plugin_markdowku_blockquotes');
+ }
+
+ function handle($match, $state, $pos, Doku_Handler $handler) {
+ global $DOKU_PLUGINS;
+
+ preg_match('/^\n[ \t]*>(?:[ \t>]*>)?[ \t]?/', $match, $quotearg);
+ $quoteinarg = preg_replace('/^\n[ \t]*>(?:[ \t>]*>)?[ \t]?/', '', $match);
+
+ if ($state == DOKU_LEXER_ENTER) {
+ $ReWriter = new Doku_Handler_Markdown_Quote($handler->getCallWriter());
+ $handler->setCallWriter($ReWriter);
+ $handler->_addCall('quote_start', $quotearg, $pos);
+ } elseif ($state == DOKU_LEXER_EXIT) {
+ $handler->_addCall('quote_end', array(), $pos);
+ $handler->getCallWriter()->process();
+ $ReWriter = & $handler->getCallWriter();
+ $handler->setCallWriter($ReWriter->getCallWriter());
+ }
+
+ if ($quoteinarg == '') {
+ $handler->_addCall('quote_newline', $quotearg, $pos);
+ /* ATX headers (headeratx) */
+ } elseif (preg_match('/^\#{1,6}[ \t]*.+?[ \t]*\#*/', $quoteinarg)) {
+ $plugin =& plugin_load('syntax', 'markdowku_headeratx');
+ $plugin->handle($quoteinarg, $state, $pos, $handler);
+ /* Horizontal rulers (hr) */
+ } elseif (preg_match('/[ ]{0,2}(?:[ ]?_[ ]?){3,}[ \t]*/', $quoteinarg)
+ or preg_match('/[ ]{0,2}(?:[ ]?-[ ]?){3,}[ \t]*/', $quoteinarg)
+ or preg_match('/[ ]{0,2}(?:[ ]?\*[ ]?){3,}[ \t]*/', $quoteinarg)) {
+ $plugin =& plugin_load('syntax', 'markdowku_hr');
+ $plugin->handle($quoteinarg, $state, $pos, $handler);
+ /* Setext headers (headersetext) */
+ } elseif (preg_match('/^[^\n]+?[ \t]*\n[ \t]*>(?:[ \t>]*>)?[ \t]?=+[ \t]*/', $quoteinarg)
+ or preg_match('/^[^\n]+?[ \t]*\n[ \t]*>(?:[ \t>]*>)?[ \t]?-+[ \t]*/', $quoteinarg)) {
+ $quoteinarg = preg_replace('/(?<=\n)[ \t]*>(?:[ \t>]*>)?[ \t]?/', '', $quoteinarg);
+ $plugin =& plugin_load('syntax', 'markdowku_headersetext');
+ $plugin->handle($quoteinarg, $state, $pos, $handler);
+ } else {
+ $handler->_addCall('cdata', array($quoteinarg), $pos);
+ }
+
+ return true;
+ }
+
+ function render($mode, Doku_Renderer $renderer, $data) {
+ return true;
+ }
+}
+
+class Doku_Handler_Markdown_Quote extends Quote {
+ function getDepth($marker) {
+ $quoteLength = 0;
+ $position = 0;
+ $text = preg_replace('/^\n*/', '', $marker);
+ while (TRUE) {
+ if (preg_match('/^[ \t]/', substr($text, $position)) > 0) {
+ $position++;
+ } elseif (preg_match('/^>/', substr($text, $position)) > 0) {
+ $position++;
+ $quoteLength++;
+ } else {
+ break;
+ }
+ }
+ return $quoteLength;
+ }
+}
+//Setup VIM: ex: et ts=4 enc=utf-8 :
diff --git a/platform/www/lib/plugins/markdowku/syntax/boldasterisk.php b/platform/www/lib/plugins/markdowku/syntax/boldasterisk.php
new file mode 100644
index 0000000..0dcad52
--- /dev/null
+++ b/platform/www/lib/plugins/markdowku/syntax/boldasterisk.php
@@ -0,0 +1,45 @@
+<?php
+/*
+ * Bold text enclosed in asterisks: **...**
+ */
+
+if(!defined('DOKU_INC')) die();
+if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/');
+require_once(DOKU_PLUGIN.'syntax.php');
+
+class syntax_plugin_markdowku_boldasterisk extends DokuWiki_Syntax_Plugin {
+
+ function getType() { return 'formatting'; }
+ function getPType() { return 'normal'; }
+ function getSort() { return 69; }
+ function getAllowedTypes() { return array('formatting', 'substition'); }
+
+ function connectTo($mode) {
+ $this->Lexer->addEntryPattern(
+ '(?<![\\\\*])\*\*(?![ ])(?=(?:(?!\n\n).)+?[^\\\\ ]\*\*)',
+ $mode,
+ 'plugin_markdowku_boldasterisk');
+ }
+
+ function postConnect() {
+ $this->Lexer->addExitPattern(
+ '(?<![\\\\ ])\*\*',
+ 'plugin_markdowku_boldasterisk');
+ }
+
+ function handle($match, $state, $pos, Doku_Handler $handler) {
+ return array($state, $match);
+ }
+
+ function render($mode, Doku_Renderer $renderer, $data) {
+ if ($data[0] == DOKU_LEXER_ENTER)
+ $renderer->strong_open();
+ elseif ($data[0] == DOKU_LEXER_EXIT)
+ $renderer->strong_close();
+ else
+ $renderer->cdata($data[1]);
+
+ return true;
+ }
+}
+//Setup VIM: ex: et ts=4 enc=utf-8 :
diff --git a/platform/www/lib/plugins/markdowku/syntax/codeblocks.php b/platform/www/lib/plugins/markdowku/syntax/codeblocks.php
new file mode 100644
index 0000000..bbda567
--- /dev/null
+++ b/platform/www/lib/plugins/markdowku/syntax/codeblocks.php
@@ -0,0 +1,63 @@
+<?php
+/*
+ * Codeblocks, indented by four spaces
+ */
+
+if(!defined('DOKU_INC')) die();
+if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/');
+require_once(DOKU_PLUGIN.'syntax.php');
+
+use dokuwiki\Parsing\Handler\Preformatted;
+
+class syntax_plugin_markdowku_codeblocks extends DokuWiki_Syntax_Plugin {
+
+ function getType() { return 'protected'; }
+ function getPType() { return 'block'; }
+ function getSort() { return 199; }
+
+ function connectTo($mode) {
+ $this->Lexer->addEntryPattern(
+ '(?:\n\n|\A\n?) ',
+ $mode,
+ 'plugin_markdowku_codeblocks');
+
+ $this->Lexer->addPattern(
+ '\n ',
+ 'plugin_markdowku_codeblocks');
+ }
+
+ function postConnect() {
+ $this->Lexer->addExitPattern(
+ '\n(?:(?=\n*[ ]{0,3}\S)|\Z)',
+ 'plugin_markdowku_codeblocks');
+ }
+
+ function handle($match, $state, $pos, Doku_Handler $handler) {
+ switch ($state) {
+ case DOKU_LEXER_ENTER:
+ $ReWriter = new Preformatted($handler->getCallWriter());
+ $handler->setCallWriter($ReWriter);
+ $handler->_addCall('preformatted_start', array($match), $pos);
+ break;
+ case DOKU_LEXER_MATCHED:
+ $handler->_addCall('preformatted_newline', array($match), $pos);
+ break;
+ case DOKU_LEXER_UNMATCHED:
+ $handler->_addCall('preformatted_content', array($match), $pos);
+ break;
+ case DOKU_LEXER_EXIT:
+ $handler->_addCall('preformatted_end', array(), $pos);
+ $handler->_addCall('preformatted_content', array($match), $pos);
+ $handler->getCallWriter()->process();
+ $ReWriter = & $handler->getCallWriter();
+ $handler->setCallWriter($ReWriter->getCallWriter());
+ break;
+ }
+ return true;
+ }
+
+ function render($mode, Doku_Renderer $renderer, $data) {
+ return false;
+ }
+}
+//Setup VIM: ex: et ts=4 enc=utf-8 :
diff --git a/platform/www/lib/plugins/markdowku/syntax/codespans1.php b/platform/www/lib/plugins/markdowku/syntax/codespans1.php
new file mode 100644
index 0000000..6465288
--- /dev/null
+++ b/platform/www/lib/plugins/markdowku/syntax/codespans1.php
@@ -0,0 +1,35 @@
+<?php
+/*
+ * Codespans enclosed with one backtick: `...`
+ */
+
+if(!defined('DOKU_INC')) die();
+if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/');
+require_once(DOKU_PLUGIN.'syntax.php');
+
+class syntax_plugin_markdowku_codespans1 extends DokuWiki_Syntax_Plugin {
+
+ function getType() { return 'formatting'; }
+ function getPType() { return 'normal'; }
+ function getSort() { return 99; }
+ function getAllowedTypes() { return array(); }
+
+ function connectTo($mode) {
+ $this->Lexer->addSpecialPattern(
+ '(?<!`)`(?!`).+?(?<!`)`(?!`)',
+ $mode,
+ 'plugin_markdowku_codespans1');
+ }
+
+ function handle($match, $state, $pos, Doku_Handler $handler) {
+ return array($match);
+ }
+
+ function render($mode, Doku_Renderer $renderer, $data) {
+ $renderer->monospace_open();
+ $renderer->cdata(substr($data[0], 1, -1));
+ $renderer->monospace_close();
+ return true;
+ }
+}
+//Setup VIM: ex: et ts=4 enc=utf-8 :
diff --git a/platform/www/lib/plugins/markdowku/syntax/codespans2.php b/platform/www/lib/plugins/markdowku/syntax/codespans2.php
new file mode 100644
index 0000000..596fc28
--- /dev/null
+++ b/platform/www/lib/plugins/markdowku/syntax/codespans2.php
@@ -0,0 +1,34 @@
+<?php
+/*
+ * Codespans enclosed with two backticks: ``...``
+ */
+
+if(!defined('DOKU_INC')) die();
+if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/');
+require_once(DOKU_PLUGIN.'syntax.php');
+
+class syntax_plugin_markdowku_codespans2 extends DokuWiki_Syntax_Plugin {
+
+ function getType() { return 'formatting'; }
+ function getPType() { return 'normal'; }
+ function getSort() { return 98; }
+
+ function connectTo($mode) {
+ $this->Lexer->addSpecialPattern(
+ '(?<!`)``(?!`).+?(?<!`)``(?!`)',
+ $mode,
+ 'plugin_markdowku_codespans2');
+ }
+
+ function handle($match, $state, $pos, Doku_Handler $handler) {
+ return array($match);
+ }
+
+ function render($mode, Doku_Renderer $renderer, $data) {
+ $renderer->monospace_open();
+ $renderer->cdata(substr($data[0], 2, -2));
+ $renderer->monospace_close();
+ return true;
+ }
+}
+//Setup VIM: ex: et ts=4 enc=utf-8 :
diff --git a/platform/www/lib/plugins/markdowku/syntax/codespans3.php b/platform/www/lib/plugins/markdowku/syntax/codespans3.php
new file mode 100644
index 0000000..b6b342b
--- /dev/null
+++ b/platform/www/lib/plugins/markdowku/syntax/codespans3.php
@@ -0,0 +1,34 @@
+<?php
+/*
+ * Codespans enclosed within three backticks: ```...```
+ */
+
+if(!defined('DOKU_INC')) die();
+if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/');
+require_once(DOKU_PLUGIN.'syntax.php');
+
+class syntax_plugin_markdowku_codespans3 extends DokuWiki_Syntax_Plugin {
+
+ function getType() { return 'formatting'; }
+ function getPType() { return 'normal'; }
+ function getSort() { return 97; }
+
+ function connectTo($mode) {
+ $this->Lexer->addSpecialPattern(
+ '(?<!`)```(?!`).+?(?<!`)```(?!`)',
+ $mode,
+ 'plugin_markdowku_codespans3');
+ }
+
+ function handle($match, $state, $pos, Doku_Handler $handler) {
+ return array($match);
+ }
+
+ function render($mode, Doku_Renderer $renderer, $data) {
+ $renderer->monospace_open();
+ $renderer->cdata(substr($data[0], 3, -3));
+ $renderer->monospace_close();
+ return true;
+ }
+}
+//Setup VIM: ex: et ts=4 enc=utf-8 :
diff --git a/platform/www/lib/plugins/markdowku/syntax/codespans4.php b/platform/www/lib/plugins/markdowku/syntax/codespans4.php
new file mode 100644
index 0000000..4166d7f
--- /dev/null
+++ b/platform/www/lib/plugins/markdowku/syntax/codespans4.php
@@ -0,0 +1,34 @@
+<?php
+/*
+ * Codespans enclosed within four backticks: ````...````
+ */
+
+if(!defined('DOKU_INC')) die();
+if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/');
+require_once(DOKU_PLUGIN.'syntax.php');
+
+class syntax_plugin_markdowku_codespans4 extends DokuWiki_Syntax_Plugin {
+
+ function getType() { return 'formatting'; }
+ function getPType() { return 'normal'; }
+ function getSort() { return 96; }
+
+ function connectTo($mode) {
+ $this->Lexer->addSpecialPattern(
+ '(?<!`)````(?!`).+?(?<!`)````(?!`)',
+ $mode,
+ 'plugin_markdowku_codespans4');
+ }
+
+ function handle($match, $state, $pos, Doku_Handler $handler) {
+ return array($match);
+ }
+
+ function render($mode, Doku_Renderer $renderer, $data) {
+ $renderer->monospace_open();
+ $renderer->cdata(substr($data[0], 4, -4));
+ $renderer->monospace_close();
+ return true;
+ }
+}
+//Setup VIM: ex: et ts=4 enc=utf-8 :
diff --git a/platform/www/lib/plugins/markdowku/syntax/codespans5.php b/platform/www/lib/plugins/markdowku/syntax/codespans5.php
new file mode 100644
index 0000000..00f33d2
--- /dev/null
+++ b/platform/www/lib/plugins/markdowku/syntax/codespans5.php
@@ -0,0 +1,34 @@
+<?php
+/*
+ * Codespans enclosed within five brackets: `````...`````
+ */
+
+if(!defined('DOKU_INC')) die();
+if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/');
+require_once(DOKU_PLUGIN.'syntax.php');
+
+class syntax_plugin_markdowku_codespans5 extends DokuWiki_Syntax_Plugin {
+
+ function getType() { return 'formatting'; }
+ function getPType() { return 'normal'; }
+ function getSort() { return 95; }
+
+ function connectTo($mode) {
+ $this->Lexer->addSpecialPattern(
+ '(?<!`)`````(?!`).+?(?<!`)`````(?!`)',
+ $mode,
+ 'plugin_markdowku_codespans5');
+ }
+
+ function handle($match, $state, $pos, Doku_Handler $handler) {
+ return array($match);
+ }
+
+ function render($mode, Doku_Renderer $renderer, $data) {
+ $renderer->monospace_open();
+ $renderer->cdata(substr($data[0], 5, -5));
+ $renderer->monospace_close();
+ return true;
+ }
+}
+//Setup VIM: ex: et ts=4 enc=utf-8 :
diff --git a/platform/www/lib/plugins/markdowku/syntax/escapespecialchars.php b/platform/www/lib/plugins/markdowku/syntax/escapespecialchars.php
new file mode 100644
index 0000000..b27b7c0
--- /dev/null
+++ b/platform/www/lib/plugins/markdowku/syntax/escapespecialchars.php
@@ -0,0 +1,94 @@
+<?php
+/*
+ * Unescape escaped backslash. \\\\ -> \
+ * This is in a separate class as it needs a higher priority than the other
+ * escapes.
+ */
+
+if(!defined('DOKU_INC')) die();
+if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/');
+require_once(DOKU_PLUGIN.'syntax.php');
+
+class syntax_plugin_markdowku_escapespecialchars extends DokuWiki_Syntax_Plugin {
+
+ function getType() { return 'substition'; }
+ function getPType() { return 'normal'; }
+ function getSort() { return 61; }
+
+ function connectTo($mode) {
+ $this->Lexer->addSpecialPattern(
+ '(?<!\\\\)\\\\`',
+ $mode,
+ 'plugin_markdowku_escapespecialchars');
+ $this->Lexer->addSpecialPattern(
+ '(?<!\\\\)\\\\\*',
+ $mode,
+ 'plugin_markdowku_escapespecialchars');
+ $this->Lexer->addSpecialPattern(
+ '(?<!\\\\)\\\\_',
+ $mode,
+ 'plugin_markdowku_escapespecialchars');
+ $this->Lexer->addSpecialPattern(
+ '(?<!\\\\)\\\\\{',
+ $mode,
+ 'plugin_markdowku_escapespecialchars');
+ $this->Lexer->addSpecialPattern(
+ '(?<!\\\\)\\\\\}',
+ $mode,
+ 'plugin_markdowku_escapespecialchars');
+ $this->Lexer->addSpecialPattern(
+ '(?<!\\\\)\\\\\[',
+ $mode,
+ 'plugin_markdowku_escapespecialchars');
+ $this->Lexer->addSpecialPattern(
+ '(?<!\\\\)\\\\\]',
+ $mode,
+ 'plugin_markdowku_escapespecialchars');
+ $this->Lexer->addSpecialPattern(
+ '(?<!\\\\)\\\\\(',
+ $mode,
+ 'plugin_markdowku_escapespecialchars');
+ $this->Lexer->addSpecialPattern(
+ '(?<!\\\\)\\\\\)',
+ $mode,
+ 'plugin_markdowku_escapespecialchars');
+ $this->Lexer->addSpecialPattern(
+ '(?<!\\\\)\\\\>',
+ $mode,
+ 'plugin_markdowku_escapespecialchars');
+ $this->Lexer->addSpecialPattern(
+ '(?<!\\\\)\\\\\#',
+ $mode,
+ 'plugin_markdowku_escapespecialchars');
+ $this->Lexer->addSpecialPattern(
+ '(?<!\\\\)\\\\\+',
+ $mode,
+ 'plugin_markdowku_escapespecialchars');
+ $this->Lexer->addSpecialPattern(
+ '(?<!\\\\)\\\\\-',
+ $mode,
+ 'plugin_markdowku_escapespecialchars');
+ $this->Lexer->addSpecialPattern(
+ '(?<!\\\\)\\\\\-',
+ $mode,
+ 'plugin_markdowku_escapespecialchars');
+ $this->Lexer->addSpecialPattern(
+ '(?<!\\\\)\\\\\.',
+ $mode,
+ 'plugin_markdowku_escapespecialchars');
+ $this->Lexer->addSpecialPattern(
+ '(?<!\\\\)\\\\!',
+ $mode,
+ 'plugin_markdowku_escapespecialchars');
+ }
+
+ function handle($match, $state, $pos, Doku_Handler $handler) {
+ return array($state, $match);
+ }
+
+ function render($mode, Doku_Renderer $renderer, $data) {
+ $renderer->doc .= substr($data[1], -1);
+ return true;
+ }
+}
+//Setup VIM: ex: et ts=4 enc=utf-8 :
diff --git a/platform/www/lib/plugins/markdowku/syntax/githubcodeblocks.php b/platform/www/lib/plugins/markdowku/syntax/githubcodeblocks.php
new file mode 100644
index 0000000..c715c55
--- /dev/null
+++ b/platform/www/lib/plugins/markdowku/syntax/githubcodeblocks.php
@@ -0,0 +1,48 @@
+<?php
+/*
+ * Github style codeblocks, starting and ending with three backticks, optionally
+ * providing a language to be used for syntax highlighting.
+ *
+ * ```php
+ * ...
+ * ```
+ */
+
+if(!defined('DOKU_INC')) die();
+if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/');
+require_once(DOKU_PLUGIN.'syntax.php');
+
+class syntax_plugin_markdowku_githubcodeblocks extends DokuWiki_Syntax_Plugin {
+
+ function getType() { return 'protected'; }
+ function getPType() { return 'block'; }
+ function getSort() { return 91; }
+
+ function connectTo($mode) {
+ $this->Lexer->addSpecialPattern(
+ '\n```[a-z0-9_]*\n.+?\n```(?=\n)',
+ $mode,
+ 'plugin_markdowku_githubcodeblocks');
+ }
+
+ function handle($match, $state, $pos, Doku_Handler $handler) {
+ if (preg_match('/^\n```([a-z0-9_]+)\n/', $match, $matches) > 0) {
+ $lang = $matches[1];
+ } else {
+ $lang = NULL;
+ }
+
+ $text = preg_replace('/^```[a-z0-9_]+\n/m', '', $match);
+ $text = preg_replace('/^```$/m', '', $text);
+ if ($lang)
+ $handler->_addCall('file', array($text, $lang, 'snippet.'.$lang), $pos);
+ else
+ $handler->_addCall('code', array($text, $lang), $pos);
+ return true;
+ }
+
+ function render($mode, Doku_Renderer $renderer, $data) {
+ return true;
+ }
+}
+//Setup VIM: ex: et ts=4 enc=utf-8 :
diff --git a/platform/www/lib/plugins/markdowku/syntax/headeratx.php b/platform/www/lib/plugins/markdowku/syntax/headeratx.php
new file mode 100644
index 0000000..b55c38c
--- /dev/null
+++ b/platform/www/lib/plugins/markdowku/syntax/headeratx.php
@@ -0,0 +1,57 @@
+<?php
+/*
+ * Header in ATX style, i.e. '# Header1', '## Header2', ...
+ */
+
+if (!defined('DOKU_INC')) die();
+if (!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN', DOKU_INC . 'lib/plugins/');
+require_once (DOKU_PLUGIN . 'syntax.php');
+
+class syntax_plugin_markdowku_headeratx extends DokuWiki_Syntax_Plugin {
+
+ function getType() { return 'baseonly'; }
+ function getPType() { return 'block'; }
+ function getSort() { return 49; }
+ function getAllowedTypes() {
+ return array('formatting', 'substition', 'disabled', 'protected');
+ }
+
+ function connectTo($mode) {
+ $this->Lexer->addSpecialPattern(
+ '\n\#{1,6}[ \t]*.+?[ \t]*\#*(?=\n+)',
+ 'base',
+ 'plugin_markdowku_headeratx');
+ }
+
+ function handle($match, $state, $pos, Doku_Handler $handler) {
+ global $conf;
+
+ $title = trim($match);
+ $level = strspn($title, '#');
+ $title = trim($title, '#');
+ $title = trim($title);
+
+ if ($level < 1)
+ $level = 1;
+ elseif ($level > 6)
+ $level = 6;
+
+ if ($handler->getStatus('section'))
+ $handler->_addCall('section_close', array(), $pos);
+ if ($level <= $conf['maxseclevel']) {
+ $handler->setStatus('section_edit_start', $pos);
+ $handler->setStatus('section_edit_level', $level);
+ $handler->setStatus('section_edit_title', $title);
+ }
+ $handler->_addCall('header', array($title, $level, $pos), $pos);
+ $handler->_addCall('section_open', array($level), $pos);
+ $handler->setStatus('section', true);
+
+ return true;
+ }
+
+ function render($mode, Doku_Renderer $renderer, $data) {
+ return true;
+ }
+}
+//Setup VIM: ex: et ts=4 enc=utf-8 :
diff --git a/platform/www/lib/plugins/markdowku/syntax/headersetext.php b/platform/www/lib/plugins/markdowku/syntax/headersetext.php
new file mode 100644
index 0000000..43e9551
--- /dev/null
+++ b/platform/www/lib/plugins/markdowku/syntax/headersetext.php
@@ -0,0 +1,56 @@
+<?php
+/*
+ * Setext style headers:
+ * Header
+ * ======
+ */
+
+if (!defined('DOKU_INC')) die();
+if (!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN', DOKU_INC . 'lib/plugins/');
+require_once (DOKU_PLUGIN . 'syntax.php');
+
+class syntax_plugin_markdowku_headersetext extends DokuWiki_Syntax_Plugin {
+
+ function getType() { return 'baseonly'; }
+ function getPType() { return 'block'; }
+ function getSort() { return 49; }
+ function getAllowedTypes() {
+ return array('formatting', 'substition', 'disabled', 'protected');
+ }
+
+ function connectTo($mode) {
+ $this->Lexer->addSpecialPattern(
+ '\n[^\n]+[ \t]*\n=+[ \t]*(?=\n)',
+ 'base',
+ 'plugin_markdowku_headersetext');
+
+ $this->Lexer->addSpecialPattern(
+ '\n[^\n]+[ \t]*\n-+[ \t]*(?=\n)',
+ 'base',
+ 'plugin_markdowku_headersetext');
+ }
+
+ function handle($match, $state, $pos, Doku_Handler $handler) {
+ $title = preg_replace('/^\n(.+?)[ \t]*\n.*/', '\1', $match);
+ $title = trim($title);
+ if (preg_match('/^\n(.+?)[ \t]*\n=/', $match))
+ $level = 1;
+ if (preg_match('/^\n(.+?)[ \t]*\n-/', $match))
+ $level = 2;
+
+ if ($handler->getStatus('section'))
+ $handler->_addCall('section_close', array(), $pos);
+ $handler->setStatus('section_edit_start', $pos);
+ $handler->setStatus('section_edit_level', $level);
+ $handler->setStatus('section_edit_title', $title);
+ $handler->_addCall('header', array($title, $level, $pos), $pos);
+ $handler->_addCall('section_open', array($level), $pos);
+ $handler->setStatus('section', true);
+ return true;
+ }
+
+ function render($mode, Doku_Renderer $renderer, $data) {
+ return true;
+ }
+}
+//Setup VIM: ex: et ts=4 enc=utf-8 :
diff --git a/platform/www/lib/plugins/markdowku/syntax/hr.php b/platform/www/lib/plugins/markdowku/syntax/hr.php
new file mode 100644
index 0000000..f9da2c0
--- /dev/null
+++ b/platform/www/lib/plugins/markdowku/syntax/hr.php
@@ -0,0 +1,47 @@
+<?php
+/*
+ * Horizontal rulers:
+ * * * *
+ * ---
+ * ___
+ */
+
+if(!defined('DOKU_INC')) die();
+if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/');
+require_once(DOKU_PLUGIN.'syntax.php');
+
+class syntax_plugin_markdowku_hr extends DokuWiki_Syntax_Plugin {
+
+ function getType() { return 'container'; }
+ function getPType() { return 'block'; }
+ function getSort() { return 8; } /* Before list block parsing. */
+
+ function connectTo($mode) {
+ /* We use two newlines, as we don't want to conflict with setext header
+ * parsing, but also have to be before list blocks. */
+ $this->Lexer->addSpecialPattern(
+ '\n[ ]{0,2}(?:[ ]?\*[ ]?){3,}[ \t]*(?=\n)',
+ $mode,
+ 'plugin_markdowku_hr');
+
+ $this->Lexer->addSpecialPattern(
+ '\n[ ]{0,2}(?:[ ]?-[ ]?){3,}[ \t]*(?=\n)',
+ $mode,
+ 'plugin_markdowku_hr');
+
+ $this->Lexer->addSpecialPattern(
+ '\n[ ]{0,2}(?:[ ]?_[ ]?){3,}[ \t]*(?=\n)',
+ $mode,
+ 'plugin_markdowku_hr');
+ }
+
+ function handle($match, $state, $pos, Doku_Handler $handler) {
+ $handler->_addCall('hr', array(), $pos);
+ return true;
+ }
+
+ function render($mode, Doku_Renderer $renderer, $data) {
+ return true;
+ }
+}
+//Setup VIM: ex: et ts=4 enc=utf-8 :
diff --git a/platform/www/lib/plugins/markdowku/syntax/imagesinline.php b/platform/www/lib/plugins/markdowku/syntax/imagesinline.php
new file mode 100644
index 0000000..28b7d75
--- /dev/null
+++ b/platform/www/lib/plugins/markdowku/syntax/imagesinline.php
@@ -0,0 +1,43 @@
+<?php
+/*
+ * Inline images: ![source](description "title")
+ */
+
+if(!defined('DOKU_INC')) die();
+if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/');
+require_once(DOKU_PLUGIN.'syntax.php');
+
+class syntax_plugin_markdowku_imagesinline extends DokuWiki_Syntax_Plugin {
+
+ function getType() { return 'substition'; }
+ function getPType() { return 'block'; }
+ function getSort() { return 101; }
+
+ function connectTo($mode) {
+ $this->nested_brackets_re =
+ str_repeat('(?>[^\[\]]+|\[', 6).
+ str_repeat('\])*', 6);
+ $this->Lexer->addSpecialPattern(
+ '\!\['.$this->nested_brackets_re.'\]\([ \t]*<?.+?>?[ \t]*(?:[\'"].*?[\'"])?\)',
+ $mode,
+ 'plugin_markdowku_imagesinline');
+ }
+
+ function handle($match, $state, $pos, Doku_Handler $handler) {
+ if ($state == DOKU_LEXER_SPECIAL) {
+ $text = preg_match(
+ '/^\!\[('.$this->nested_brackets_re.')\]\([ \t]*<?(.+?)>?[ \t]*(?:[\'"](.*?)[\'"])?[ \t]*?\)$/',
+ $match,
+ $matches);
+ $target = $matches[2] == '' ? $matches[3] : $matches[2];
+ $title = $matches[1];
+ $handler->media($target.'|'.$title, $state, $pos);
+ }
+ return true;
+ }
+
+ function render($mode, Doku_Renderer $renderer, $data) {
+ return true;
+ }
+}
+//Setup VIM: ex: et ts=4 enc=utf-8 :
diff --git a/platform/www/lib/plugins/markdowku/syntax/imagesreference.php b/platform/www/lib/plugins/markdowku/syntax/imagesreference.php
new file mode 100644
index 0000000..9382d2a
--- /dev/null
+++ b/platform/www/lib/plugins/markdowku/syntax/imagesreference.php
@@ -0,0 +1,58 @@
+<?php
+/*
+ * Reference links, i.e.
+ * ... [name][id] ...
+ * ... [id][] ...
+ * ...
+ * [id]: http://example.com (handled by markdowku_references)
+ */
+
+if(!defined('DOKU_INC')) die();
+if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/');
+require_once(DOKU_PLUGIN.'syntax.php');
+
+class syntax_plugin_markdowku_imagesreference extends DokuWiki_Syntax_Plugin {
+
+ function getType() { return 'substition'; }
+ function getPType() { return 'normal'; }
+ function getSort() { return 102; }
+
+ function connectTo($mode) {
+ $this->nested_brackets_re =
+ str_repeat('(?>[^\[\]]+|\[', 3).
+ str_repeat('\])*', 3);
+ $this->Lexer->addSpecialPattern(
+ '\!\['.$this->nested_brackets_re.'\][ ]?(?:\n[ ]*)?\[[^\n]*?\]',
+ $mode,
+ 'plugin_markdowku_imagesreference');
+ }
+
+ function handle($match, $state, $pos, Doku_Handler $handler) {
+ return array($state, $match);
+ }
+
+ function render($mode, Doku_Renderer $renderer, $data) {
+ global $ID;
+ preg_match(
+ '/^\!\[('.$this->nested_brackets_re.')\][ ]?(?:\n[ ]*)?\[(.*?)\]$/',
+ $data[1],
+ $matches);
+
+ $title = $matches[1];
+
+ if ($matches[2] == '')
+ $rid = $matches[1];
+ else
+ $rid = $matches[2];
+
+ $rid = preg_replace("/ /", ".", $rid);
+ $target = p_get_metadata($ID, 'markdowku_references_'.$rid, METADATA_RENDER_USING_CACHE);
+ if ($target == '')
+ $renderer->cdata($data[1]);
+ else
+ $renderer->_media($target, $title);
+
+ return true;
+ }
+}
+//Setup VIM: ex: et ts=4 enc=utf-8 :
diff --git a/platform/www/lib/plugins/markdowku/syntax/italicasterisk.php b/platform/www/lib/plugins/markdowku/syntax/italicasterisk.php
new file mode 100644
index 0000000..bebc45f
--- /dev/null
+++ b/platform/www/lib/plugins/markdowku/syntax/italicasterisk.php
@@ -0,0 +1,49 @@
+<?php
+/*
+ * Italic text enclosed by asterisks, i.e. *...*
+ */
+
+if(!defined('DOKU_INC')) die();
+if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/');
+require_once(DOKU_PLUGIN.'syntax.php');
+
+# Fix for Hogfather RC2 - see https://github.com/dwp-forge/columns/issues/5#issuecomment-638467603
+require_once(DOKU_INC.'inc/Parsing/Lexer/Lexer.php');
+
+class syntax_plugin_markdowku_italicasterisk extends DokuWiki_Syntax_Plugin {
+
+ function getType() { return 'formatting'; }
+ function getPType() { return 'normal'; }
+ function getSort() { return 79; }
+ function getAllowedTypes() {
+ return Array('formatting', 'substition');
+ }
+
+ function connectTo($mode) {
+ $this->Lexer->addEntryPattern(
+ '(?<![\\\\])\*(?![ *])(?=(?:(?!\n\n).)+?(?<![\\\\ *])\*)',
+ $mode,
+ 'plugin_markdowku_italicasterisk');
+ }
+
+ function postConnect() {
+ $this->Lexer->addExitPattern(
+ '(?<![\\\\* ])\*',
+ 'plugin_markdowku_italicasterisk');
+ }
+
+ function handle($match, $state, $pos, Doku_Handler $handler) {
+ return array($state, $match);
+ }
+
+ function render($mode, Doku_Renderer $renderer, $data) {
+ if ($data[0] == DOKU_LEXER_ENTER)
+ $renderer->emphasis_open();
+ elseif ($data[0] == DOKU_LEXER_EXIT)
+ $renderer->emphasis_close();
+ else
+ $renderer->cdata($data[1]);
+
+ return true;
+ }
+}
diff --git a/platform/www/lib/plugins/markdowku/syntax/italicunderline.php b/platform/www/lib/plugins/markdowku/syntax/italicunderline.php
new file mode 100644
index 0000000..13a5a02
--- /dev/null
+++ b/platform/www/lib/plugins/markdowku/syntax/italicunderline.php
@@ -0,0 +1,56 @@
+<?php
+/*
+ * Italic text enclosed in underlines: _..._
+ */
+
+if(!defined('DOKU_INC')) die();
+if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/');
+require_once(DOKU_PLUGIN.'syntax.php');
+
+# Fix for Hogfather RC2 - see https://github.com/dwp-forge/columns/issues/5#issuecomment-638467603
+require_once(DOKU_INC.'inc/Parsing/Lexer/Lexer.php');
+
+class syntax_plugin_markdowku_italicunderline extends DokuWiki_Syntax_Plugin {
+
+ function getType() { return 'formatting'; }
+ function getPType() { return 'normal'; }
+ function getSort() { return 79; }
+ function getAllowedTypes() {
+ return Array('formatting', 'substition');
+ }
+
+ function connectTo($mode) {
+ $this->Lexer->addEntryPattern(
+ '(?<![\\\\])_(?![ _])(?=(?:(?!\n\n).)+?[^\\\\ _]_)',
+ $mode,
+ 'plugin_markdowku_italicunderline');
+// $this->Lexer->addSpecialPattern(
+// '\w+_\w+_\w[\w_]*',
+// $mode,
+// 'plugin_markdowku_italicunderline');
+ }
+
+ function postConnect() {
+ $this->Lexer->addExitPattern(
+ '(?<![\\\\_ ])_',
+ 'plugin_markdowku_italicunderline');
+ }
+
+ function handle($match, $state, $pos, Doku_Handler $handler) {
+ return array($state, $match);
+ }
+
+ function render($mode, Doku_Renderer $renderer, $data) {
+
+ if ($data[0] == DOKU_LEXER_ENTER)
+ $renderer->emphasis_open();
+ elseif ($data[0] == DOKU_LEXER_EXIT)
+ $renderer->emphasis_close();
+ elseif ($data[0] == DOKU_LEXER_UNMATCHED)
+ $renderer->cdata($data[1]);
+ elseif ($data[0] == DOKU_LEXER_SPECIAL)
+ $renderer->cdata($data[1]);
+
+ return true;
+ }
+}
diff --git a/platform/www/lib/plugins/markdowku/syntax/linebreak.php b/platform/www/lib/plugins/markdowku/syntax/linebreak.php
new file mode 100644
index 0000000..ef2f1df
--- /dev/null
+++ b/platform/www/lib/plugins/markdowku/syntax/linebreak.php
@@ -0,0 +1,32 @@
+<?php
+/*
+ * Linebreaks, determined by two spaces at the line end.
+ */
+
+if(!defined('DOKU_INC')) die();
+if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/');
+require_once(DOKU_PLUGIN.'syntax.php');
+
+class syntax_plugin_markdowku_linebreak extends DokuWiki_Syntax_Plugin {
+
+ function getType() { return 'substition'; }
+ function getPType() { return 'normal'; }
+ function getSort() { return 139; }
+
+ function connectTo($mode) {
+ $this->Lexer->addSpecialPattern(
+ '[ ]{2,}\n',
+ $mode,
+ 'plugin_markdowku_linebreak');
+ }
+
+ function handle($match, $state, $pos, Doku_Handler $handler) {
+ return array($match, $state, $pos);
+ }
+
+ function render($mode, Doku_Renderer $renderer, $data) {
+ $renderer->linebreak();
+ return true;
+ }
+}
+//Setup VIM: ex: et ts=4 enc=utf-8 :
diff --git a/platform/www/lib/plugins/markdowku/syntax/olists.php b/platform/www/lib/plugins/markdowku/syntax/olists.php
new file mode 100644
index 0000000..853232a
--- /dev/null
+++ b/platform/www/lib/plugins/markdowku/syntax/olists.php
@@ -0,0 +1,98 @@
+<?php
+/*
+ * Ordered lists:
+ * 1. ...
+ * 2. ...
+ */
+
+if(!defined('DOKU_INC')) die();
+if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/');
+require_once(DOKU_PLUGIN.'syntax.php');
+
+use dokuwiki\Parsing\Handler\Lists;
+
+class syntax_plugin_markdowku_olists extends DokuWiki_Syntax_Plugin {
+ function getType() { return 'container'; }
+ function getPType() { return 'block'; }
+ function getSort() { return 9; }
+ function getAllowedTypes() {
+ return array('formatting', 'substition', 'paragraphs', 'baseonly');
+ }
+
+ function connectTo($mode) {
+ $this->Lexer->addEntryPattern(
+ '\n\n[ ]{0,3}\d+\.[ \t]',
+ $mode,
+ 'plugin_markdowku_olists');
+
+ $this->Lexer->addPattern(
+ '\n^[ \t]*\d+\.[ \t]',
+ 'plugin_markdowku_olists');
+ }
+
+ function postConnect() {
+ $this->Lexer->addExitPattern(
+ '(?:\Z|\n{1,}(?=\n\S)(?!\n[ \t]*\d+\.[ \t]))',
+ 'plugin_markdowku_olists');
+ }
+
+ function handle($match, $state, $pos, Doku_Handler $handler) {
+ switch ($state) {
+ case DOKU_LEXER_ENTER:
+ $ReWriter = new Doku_Handler_Markdown_Ordered_List($handler->getCallWriter());
+ $handler->setCallWriter($ReWriter);
+ $handler->_addCall('list_open', array($match), $pos);
+ break;
+ case DOKU_LEXER_MATCHED:
+ $handler->_addCall('list_item', array($match), $pos);
+ break;
+ case DOKU_LEXER_UNMATCHED:
+ $handler->_addCall('cdata', array($match), $pos);
+ break;
+ case DOKU_LEXER_EXIT:
+ $handler->_addCall('list_close', array(), $pos);
+ $handler->getCallWriter()->process();
+ $ReWriter = & $handler->getCallWriter();
+ $handler->setCallWriter($ReWriter->getCallWriter());
+ break;
+ }
+ return true;
+ }
+
+ function render($mode, Doku_Renderer $renderer, $data) {
+ return true;
+ }
+}
+
+class Doku_Handler_Markdown_Ordered_List extends Lists {
+ private $depth = array(0, 4);
+
+ function interpretSyntax($match, &$type) {
+ $type="o";
+ $listlevel = 1;
+ $real_position = 0;
+ $logical_position = 0;
+ $text = preg_replace('/^\n*/', '', $match);
+
+ while (TRUE) {
+ if (preg_match('/^[ ]{'.$this->depth[$listlevel].'}/', substr($text, $real_position)) > 0) {
+ $real_position += $this->depth[$listlevel];
+ $logical_position += $this->depth[$listlevel];
+ $listlevel += 1;
+ continue;
+ }
+ if (preg_match('/^\t/', substr($text, $real_position)) > 0) {
+ $real_position += 1;
+ $logical_position += 4;
+ $listlevel += 1;
+ continue;
+ }
+ if (preg_match('/^[ ]{0,3}\d+\.[ \t]/', substr($text, $real_position)) > 0) {
+ $this->depth[$listlevel] = strlen(substr($text, $real_position)) - 1;
+ }
+ break;
+ }
+ return $listlevel;
+ }
+}
+//Setup VIM: ex: et ts=4 enc=utf-8 :
diff --git a/platform/www/lib/plugins/markdowku/syntax/references.php b/platform/www/lib/plugins/markdowku/syntax/references.php
new file mode 100644
index 0000000..fe50a09
--- /dev/null
+++ b/platform/www/lib/plugins/markdowku/syntax/references.php
@@ -0,0 +1,41 @@
+<?php
+/*
+ * References for links or images, i.e.
+ * [id]: http://example.com
+ */
+
+if(!defined('DOKU_INC')) die();
+if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/');
+require_once(DOKU_PLUGIN.'syntax.php');
+
+class syntax_plugin_markdowku_references extends DokuWiki_Syntax_Plugin {
+
+ function getType() { return 'substition'; }
+ function getPType() { return 'normal'; }
+ function getSort() { return 100; }
+
+ function connectTo($mode) {
+ $this->Lexer->addSpecialPattern(
+ '\n[ ]{0,3}\[[^\n]+?\]:[ \t]*\n?[ \t]*<?\S+?>?[ \t]*\n?[ \t]*(?:(?<=\s)["(].+?[")][\t]*)?(?=\n)',
+ $mode,
+ 'plugin_markdowku_references');
+ }
+
+ function handle($match, $state, $pos, Doku_Handler $handler) {
+ return array($state, $match);
+ }
+
+ function render($mode, Doku_Renderer $renderer, $data) {
+ if ($mode != 'metadata')
+ return false;
+
+ preg_match(
+ '/\[(.+)\]:[ \t]*\n?[ \t]*<?(\S+)>?[ \t]*\n?[ \t]*(?:(?<=\s)["(](.+?)[")][\t]*)?/',
+ $data[1],
+ $matches);
+ $key = 'markdowku_references_'.preg_replace("/ /", ".", $matches[1]);
+ $renderer->meta[$key] = $matches[2];
+ return true;
+ }
+}
+//Setup VIM: ex: et ts=4 enc=utf-8 :
diff --git a/platform/www/lib/plugins/markdowku/syntax/ulists.php b/platform/www/lib/plugins/markdowku/syntax/ulists.php
new file mode 100644
index 0000000..79393c2
--- /dev/null
+++ b/platform/www/lib/plugins/markdowku/syntax/ulists.php
@@ -0,0 +1,99 @@
+<?php
+/*
+ * Unorderd lists:
+ * * ...
+ * * ...
+ */
+
+if(!defined('DOKU_INC')) die();
+if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/');
+require_once(DOKU_PLUGIN.'syntax.php');
+
+use dokuwiki\Parsing\Handler\Lists;
+
+class syntax_plugin_markdowku_ulists extends DokuWiki_Syntax_Plugin {
+ function getType() { return 'container'; }
+ function getPType() { return 'block'; }
+ function getSort() { return 9; }
+ function getAllowedTypes() {
+ return array('formatting', 'substition', 'paragraphs', 'baseonly', 'container');
+ }
+
+ function connectTo($mode) {
+ /* The negative lookahead is for not conflicting with hrs. */
+ $this->Lexer->addEntryPattern(
+ '\n\n[ ]{0,3}[*+-][ \t](?!(?:[ ]?[*+-][ ]?){2,}[ \t]*\n)',
+ $mode,
+ 'plugin_markdowku_ulists');
+
+ $this->Lexer->addPattern(
+ '\n[ \t]*[*+-][ \t](?!(?:[ ]?[*+-][ ]?){2,}[ \t]*\n)',
+ 'plugin_markdowku_ulists');
+ }
+
+ function postConnect() {
+ $this->Lexer->addExitPattern(
+ '(?:\Z|\n{1,}(?=\n\S)(?!\n[ \t]*[*+-][ \t]))',
+ 'plugin_markdowku_ulists');
+ }
+
+ function handle($match, $state, $pos, Doku_Handler $handler) {
+ switch ($state) {
+ case DOKU_LEXER_ENTER:
+ $ReWriter = new Doku_Handler_Markdown_Unordered_List($handler->getCallWriter());
+ $handler->setCallWriter($ReWriter);
+ $handler->_addCall('list_open', array($match), $pos);
+ break;
+ case DOKU_LEXER_MATCHED:
+ $handler->_addCall('list_item', array($match), $pos);
+ break;
+ case DOKU_LEXER_UNMATCHED:
+ $handler->_addCall('cdata', array($match), $pos);
+ break;
+ case DOKU_LEXER_EXIT:
+ $handler->_addCall('list_close', array(), $pos);
+ $handler->getCallWriter()->process();
+ $ReWriter = & $handler->getCallWriter();
+ $handler->setCallWriter($ReWriter->getCallWriter());
+ break;
+ }
+ return true;
+ }
+
+ function render($mode, Doku_Renderer $renderer, $data) {
+ return true;
+ }
+}
+
+class Doku_Handler_Markdown_Unordered_List extends Lists {
+ private $depth = array(0, 4);
+
+ function interpretSyntax($match, &$type) {
+ $type="u";
+ $listlevel = 1;
+ $real_position = 0;
+ $logical_position = 0;
+ $text = preg_replace('/^\n*/', '', $match);
+
+ while (TRUE) {
+ if (preg_match('/^[ ]{'.$this->depth[$listlevel].'}/', substr($text, $real_position)) > 0) {
+ $real_position += $this->depth[$listlevel];
+ $logical_position += $this->depth[$listlevel];
+ $listlevel += 1;
+ continue;
+ }
+ if (preg_match('/^\t/', substr($text, $real_position)) > 0) {
+ $real_position += 1;
+ $logical_position += 4;
+ $listlevel += 1;
+ continue;
+ }
+ if (preg_match('/^[ ]{0,3}[*+-][ \t]/', substr($text, $real_position)) > 0) {
+ $this->depth[$listlevel] = strlen(substr($text, $real_position)) - 1;
+ }
+ break;
+ }
+ return $listlevel + 1;
+ }
+}
+//Setup VIM: ex: et ts=4 enc=utf-8 :
diff --git a/platform/www/lib/plugins/master.zip b/platform/www/lib/plugins/master.zip
new file mode 100644
index 0000000..0c9a008
--- /dev/null
+++ b/platform/www/lib/plugins/master.zip
Binary files differ
diff --git a/platform/www/lib/plugins/popularity/action.php b/platform/www/lib/plugins/popularity/action.php
new file mode 100644
index 0000000..fac6107
--- /dev/null
+++ b/platform/www/lib/plugins/popularity/action.php
@@ -0,0 +1,66 @@
+<?php
+/**
+ * Popularity Feedback Plugin
+ *
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ */
+
+class action_plugin_popularity extends DokuWiki_Action_Plugin
+{
+
+ /**
+ * @var helper_plugin_popularity
+ */
+ protected $helper;
+
+ public function __construct()
+ {
+ $this->helper = $this->loadHelper('popularity', false);
+ }
+
+ /** @inheritdoc */
+ public function register(Doku_Event_Handler $controller)
+ {
+ $controller->register_hook('INDEXER_TASKS_RUN', 'AFTER', $this, 'autosubmit', array());
+ }
+
+ /**
+ * Event handler
+ *
+ * @param Doku_Event $event
+ * @param $param
+ */
+ public function autosubmit(Doku_Event &$event, $param)
+ {
+ //Do we have to send the data now
+ if (!$this->helper->isAutosubmitEnabled() || $this->isTooEarlyToSubmit()) {
+ return;
+ }
+
+ //Actually send it
+ $status = $this->helper->sendData($this->helper->gatherAsString());
+
+ if ($status !== '') {
+ //If an error occured, log it
+ io_saveFile($this->helper->autosubmitErrorFile, $status);
+ } else {
+ //If the data has been sent successfully, previous log of errors are useless
+ @unlink($this->helper->autosubmitErrorFile);
+ //Update the last time we sent data
+ touch($this->helper->autosubmitFile);
+ }
+
+ $event->stopPropagation();
+ $event->preventDefault();
+ }
+
+ /**
+ * Check if it's time to send autosubmit data
+ * (we should have check if autosubmit is enabled first)
+ */
+ protected function isTooEarlyToSubmit()
+ {
+ $lastSubmit = $this->helper->lastSentTime();
+ return $lastSubmit + 24*60*60*30 > time();
+ }
+}
diff --git a/platform/www/lib/plugins/popularity/admin.php b/platform/www/lib/plugins/popularity/admin.php
new file mode 100644
index 0000000..61d8cc3
--- /dev/null
+++ b/platform/www/lib/plugins/popularity/admin.php
@@ -0,0 +1,157 @@
+<?php
+/**
+ * Popularity Feedback Plugin
+ *
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+class admin_plugin_popularity extends DokuWiki_Admin_Plugin
+{
+
+ /** @var helper_plugin_popularity */
+ protected $helper;
+ protected $sentStatus = null;
+
+ /**
+ * admin_plugin_popularity constructor.
+ */
+ public function __construct()
+ {
+ $this->helper = $this->loadHelper('popularity', false);
+ }
+
+ /**
+ * return prompt for admin menu
+ * @param $language
+ * @return string
+ */
+ public function getMenuText($language)
+ {
+ return $this->getLang('name');
+ }
+
+ /**
+ * return sort order for position in admin menu
+ */
+ public function getMenuSort()
+ {
+ return 2000;
+ }
+
+ /**
+ * Accessible for managers
+ */
+ public function forAdminOnly()
+ {
+ return false;
+ }
+
+
+ /**
+ * handle user request
+ */
+ public function handle()
+ {
+ global $INPUT;
+
+ //Send the data
+ if ($INPUT->has('data')) {
+ $this->sentStatus = $this->helper->sendData($INPUT->str('data'));
+ if ($this->sentStatus === '') {
+ //Update the last time we sent the data
+ touch($this->helper->popularityLastSubmitFile);
+ }
+ //Deal with the autosubmit option
+ $this->enableAutosubmit($INPUT->has('autosubmit'));
+ }
+ }
+
+ /**
+ * Enable or disable autosubmit
+ * @param bool $enable If TRUE, it will enable autosubmit. Else, it will disable it.
+ */
+ protected function enableAutosubmit($enable)
+ {
+ if ($enable) {
+ io_saveFile($this->helper->autosubmitFile, ' ');
+ } else {
+ @unlink($this->helper->autosubmitFile);
+ }
+ }
+
+ /**
+ * Output HTML form
+ */
+ public function html()
+ {
+ global $INPUT;
+
+ if (! $INPUT->has('data')) {
+ echo $this->locale_xhtml('intro');
+
+ //If there was an error the last time we tried to autosubmit, warn the user
+ if ($this->helper->isAutoSubmitEnabled()) {
+ if (file_exists($this->helper->autosubmitErrorFile)) {
+ echo $this->getLang('autosubmitError');
+ echo io_readFile($this->helper->autosubmitErrorFile);
+ }
+ }
+
+ flush();
+ echo $this->buildForm('server');
+
+ //Print the last time the data was sent
+ $lastSent = $this->helper->lastSentTime();
+ if ($lastSent !== 0) {
+ echo $this->getLang('lastSent') . ' ' . datetime_h($lastSent);
+ }
+ } else {
+ //If we just submitted the form
+ if ($this->sentStatus === '') {
+ //If we successfully sent the data
+ echo $this->locale_xhtml('submitted');
+ } else {
+ //If we failed to submit the data, try directly with the browser
+ echo $this->getLang('submissionFailed') . $this->sentStatus . '<br />';
+ echo $this->getLang('submitDirectly');
+ echo $this->buildForm('browser', $INPUT->str('data'));
+ }
+ }
+ }
+
+
+ /**
+ * Build the form which presents the data to be sent
+ * @param string $submissionMode How is the data supposed to be sent? (may be: 'browser' or 'server')
+ * @param string $data The popularity data, if it has already been computed. NULL otherwise.
+ * @return string The form, as an html string
+ */
+ protected function buildForm($submissionMode, $data = null)
+ {
+ $url = ($submissionMode === 'browser' ? $this->helper->submitUrl : script());
+ if (is_null($data)) {
+ $data = $this->helper->gatherAsString();
+ }
+
+ $form = '<form method="post" action="'. $url .'" accept-charset="utf-8">'
+ .'<fieldset style="width: 60%;">'
+ .'<textarea class="edit" rows="10" cols="80" readonly="readonly" name="data">'
+ .$data
+ .'</textarea><br />';
+
+ //If we submit via the server, we give the opportunity to suscribe to the autosubmission option
+ if ($submissionMode !== 'browser') {
+ $form .= '<label for="autosubmit">'
+ .'<input type="checkbox" name="autosubmit" id="autosubmit" '
+ .($this->helper->isAutosubmitEnabled() ? 'checked' : '' )
+ .'/> ' . $this->getLang('autosubmit') .'<br />'
+ .'</label>'
+ .'<input type="hidden" name="do" value="admin" />'
+ .'<input type="hidden" name="page" value="popularity" />';
+ }
+ $form .= '<button type="submit">'.$this->getLang('submit').'</button>'
+ .'</fieldset>'
+ .'</form>';
+ return $form;
+ }
+}
diff --git a/platform/www/lib/plugins/popularity/admin.svg b/platform/www/lib/plugins/popularity/admin.svg
new file mode 100644
index 0000000..820fc8c
--- /dev/null
+++ b/platform/www/lib/plugins/popularity/admin.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M2 21l21-9L2 3v7l15 2-15 2v7z"/></svg> \ No newline at end of file
diff --git a/platform/www/lib/plugins/popularity/helper.php b/platform/www/lib/plugins/popularity/helper.php
new file mode 100644
index 0000000..4537976
--- /dev/null
+++ b/platform/www/lib/plugins/popularity/helper.php
@@ -0,0 +1,292 @@
+<?php
+
+use dokuwiki\HTTP\DokuHTTPClient;
+use dokuwiki\Extension\Event;
+
+/**
+ * Popularity Feedback Plugin
+ *
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ */
+class helper_plugin_popularity extends Dokuwiki_Plugin
+{
+ /**
+ * The url where the data should be sent
+ */
+ public $submitUrl = 'http://update.dokuwiki.org/popularity.php';
+
+ /**
+ * Name of the file which determine if the the autosubmit is enabled,
+ * and when it was submited for the last time
+ */
+ public $autosubmitFile;
+
+ /**
+ * File where the last error which happened when we tried to autosubmit, will be log
+ */
+ public $autosubmitErrorFile;
+
+ /**
+ * Name of the file which determine when the popularity data was manually
+ * submitted for the last time
+ * (If this file doesn't exist, the data has never been sent)
+ */
+ public $popularityLastSubmitFile;
+
+ /**
+ * helper_plugin_popularity constructor.
+ */
+ public function __construct()
+ {
+ global $conf;
+ $this->autosubmitFile = $conf['cachedir'].'/autosubmit.txt';
+ $this->autosubmitErrorFile = $conf['cachedir'].'/autosubmitError.txt';
+ $this->popularityLastSubmitFile = $conf['cachedir'].'/lastSubmitTime.txt';
+ }
+
+ /**
+ * Check if autosubmit is enabled
+ *
+ * @return boolean TRUE if we should send data once a month, FALSE otherwise
+ */
+ public function isAutoSubmitEnabled()
+ {
+ return file_exists($this->autosubmitFile);
+ }
+
+ /**
+ * Send the data, to the submit url
+ *
+ * @param string $data The popularity data
+ * @return string An empty string if everything worked fine, a string describing the error otherwise
+ */
+ public function sendData($data)
+ {
+ $error = '';
+ $httpClient = new DokuHTTPClient();
+ $status = $httpClient->sendRequest($this->submitUrl, array('data' => $data), 'POST');
+ if (! $status) {
+ $error = $httpClient->error;
+ }
+ return $error;
+ }
+
+ /**
+ * Compute the last time the data was sent. If it has never been sent, we return 0.
+ *
+ * @return int
+ */
+ public function lastSentTime()
+ {
+ $manualSubmission = @filemtime($this->popularityLastSubmitFile);
+ $autoSubmission = @filemtime($this->autosubmitFile);
+
+ return max((int) $manualSubmission, (int) $autoSubmission);
+ }
+
+ /**
+ * Gather all information
+ *
+ * @return string The popularity data as a string
+ */
+ public function gatherAsString()
+ {
+ $data = $this->gather();
+ $string = '';
+ foreach ($data as $key => $val) {
+ if (is_array($val)) foreach ($val as $v) {
+ $string .= hsc($key)."\t".hsc($v)."\n";
+ } else {
+ $string .= hsc($key)."\t".hsc($val)."\n";
+ }
+ }
+ return $string;
+ }
+
+ /**
+ * Gather all information
+ *
+ * @return array The popularity data as an array
+ */
+ protected function gather()
+ {
+ global $conf;
+ /** @var $auth DokuWiki_Auth_Plugin */
+ global $auth;
+ $data = array();
+ $phptime = ini_get('max_execution_time');
+ @set_time_limit(0);
+ $pluginInfo = $this->getInfo();
+
+ // version
+ $data['anon_id'] = md5(auth_cookiesalt());
+ $data['version'] = getVersion();
+ $data['popversion'] = $pluginInfo['date'];
+ $data['language'] = $conf['lang'];
+ $data['now'] = time();
+ $data['popauto'] = (int) $this->isAutoSubmitEnabled();
+
+ // some config values
+ $data['conf_useacl'] = $conf['useacl'];
+ $data['conf_authtype'] = $conf['authtype'];
+ $data['conf_template'] = $conf['template'];
+
+ // number and size of pages
+ $list = array();
+ search($list, $conf['datadir'], array($this, 'searchCountCallback'), array('all'=>false), '');
+ $data['page_count'] = $list['file_count'];
+ $data['page_size'] = $list['file_size'];
+ $data['page_biggest'] = $list['file_max'];
+ $data['page_smallest'] = $list['file_min'];
+ $data['page_nscount'] = $list['dir_count'];
+ $data['page_nsnest'] = $list['dir_nest'];
+ if ($list['file_count']) $data['page_avg'] = $list['file_size'] / $list['file_count'];
+ $data['page_oldest'] = $list['file_oldest'];
+ unset($list);
+
+ // number and size of media
+ $list = array();
+ search($list, $conf['mediadir'], array($this, 'searchCountCallback'), array('all'=>true));
+ $data['media_count'] = $list['file_count'];
+ $data['media_size'] = $list['file_size'];
+ $data['media_biggest'] = $list['file_max'];
+ $data['media_smallest'] = $list['file_min'];
+ $data['media_nscount'] = $list['dir_count'];
+ $data['media_nsnest'] = $list['dir_nest'];
+ if ($list['file_count']) $data['media_avg'] = $list['file_size'] / $list['file_count'];
+ unset($list);
+
+ // number and size of cache
+ $list = array();
+ search($list, $conf['cachedir'], array($this, 'searchCountCallback'), array('all'=>true));
+ $data['cache_count'] = $list['file_count'];
+ $data['cache_size'] = $list['file_size'];
+ $data['cache_biggest'] = $list['file_max'];
+ $data['cache_smallest'] = $list['file_min'];
+ if ($list['file_count']) $data['cache_avg'] = $list['file_size'] / $list['file_count'];
+ unset($list);
+
+ // number and size of index
+ $list = array();
+ search($list, $conf['indexdir'], array($this, 'searchCountCallback'), array('all'=>true));
+ $data['index_count'] = $list['file_count'];
+ $data['index_size'] = $list['file_size'];
+ $data['index_biggest'] = $list['file_max'];
+ $data['index_smallest'] = $list['file_min'];
+ if ($list['file_count']) $data['index_avg'] = $list['file_size'] / $list['file_count'];
+ unset($list);
+
+ // number and size of meta
+ $list = array();
+ search($list, $conf['metadir'], array($this, 'searchCountCallback'), array('all'=>true));
+ $data['meta_count'] = $list['file_count'];
+ $data['meta_size'] = $list['file_size'];
+ $data['meta_biggest'] = $list['file_max'];
+ $data['meta_smallest'] = $list['file_min'];
+ if ($list['file_count']) $data['meta_avg'] = $list['file_size'] / $list['file_count'];
+ unset($list);
+
+ // number and size of attic
+ $list = array();
+ search($list, $conf['olddir'], array($this, 'searchCountCallback'), array('all'=>true));
+ $data['attic_count'] = $list['file_count'];
+ $data['attic_size'] = $list['file_size'];
+ $data['attic_biggest'] = $list['file_max'];
+ $data['attic_smallest'] = $list['file_min'];
+ if ($list['file_count']) $data['attic_avg'] = $list['file_size'] / $list['file_count'];
+ $data['attic_oldest'] = $list['file_oldest'];
+ unset($list);
+
+ // user count
+ if ($auth && $auth->canDo('getUserCount')) {
+ $data['user_count'] = $auth->getUserCount();
+ }
+
+ // calculate edits per day
+ $list = @file($conf['metadir'].'/_dokuwiki.changes');
+ $count = count($list);
+ if ($count > 2) {
+ $first = (int) substr(array_shift($list), 0, 10);
+ $last = (int) substr(array_pop($list), 0, 10);
+ $dur = ($last - $first)/(60*60*24); // number of days in the changelog
+ $data['edits_per_day'] = $count/$dur;
+ }
+ unset($list);
+
+ // plugins
+ $data['plugin'] = plugin_list();
+
+ // pcre info
+ if (defined('PCRE_VERSION')) $data['pcre_version'] = PCRE_VERSION;
+ $data['pcre_backtrack'] = ini_get('pcre.backtrack_limit');
+ $data['pcre_recursion'] = ini_get('pcre.recursion_limit');
+
+ // php info
+ $data['os'] = PHP_OS;
+ $data['webserver'] = $_SERVER['SERVER_SOFTWARE'];
+ $data['php_version'] = phpversion();
+ $data['php_sapi'] = php_sapi_name();
+ $data['php_memory'] = php_to_byte(ini_get('memory_limit'));
+ $data['php_exectime'] = $phptime;
+ $data['php_extension'] = get_loaded_extensions();
+
+ // plugin usage data
+ $this->addPluginUsageData($data);
+
+ return $data;
+ }
+
+ /**
+ * Triggers event to let plugins add their own data
+ *
+ * @param $data
+ */
+ protected function addPluginUsageData(&$data)
+ {
+ $pluginsData = array();
+ Event::createAndTrigger('PLUGIN_POPULARITY_DATA_SETUP', $pluginsData);
+ foreach ($pluginsData as $plugin => $d) {
+ if (is_array($d)) {
+ foreach ($d as $key => $value) {
+ $data['plugin_' . $plugin . '_' . $key] = $value;
+ }
+ } else {
+ $data['plugin_' . $plugin] = $d;
+ }
+ }
+ }
+
+ /**
+ * Callback to search and count the content of directories in DokuWiki
+ *
+ * @param array &$data Reference to the result data structure
+ * @param string $base Base usually $conf['datadir']
+ * @param string $file current file or directory relative to $base
+ * @param string $type Type either 'd' for directory or 'f' for file
+ * @param int $lvl Current recursion depht
+ * @param array $opts option array as given to search()
+ * @return bool
+ */
+ public function searchCountCallback(&$data, $base, $file, $type, $lvl, $opts)
+ {
+ // traverse
+ if ($type == 'd') {
+ if ($data['dir_nest'] < $lvl) $data['dir_nest'] = $lvl;
+ $data['dir_count']++;
+ return true;
+ }
+
+ //only search txt files if 'all' option not set
+ if ($opts['all'] || substr($file, -4) == '.txt') {
+ $size = filesize($base.'/'.$file);
+ $date = filemtime($base.'/'.$file);
+ $data['file_count']++;
+ $data['file_size'] += $size;
+ if (!isset($data['file_min']) || $data['file_min'] > $size) $data['file_min'] = $size;
+ if ($data['file_max'] < $size) $data['file_max'] = $size;
+ if (!isset($data['file_oldest']) || $data['file_oldest'] > $date) $data['file_oldest'] = $date;
+ }
+
+ return false;
+ }
+}
diff --git a/platform/www/lib/plugins/popularity/lang/en/intro.txt b/platform/www/lib/plugins/popularity/lang/en/intro.txt
new file mode 100644
index 0000000..e1d6d94
--- /dev/null
+++ b/platform/www/lib/plugins/popularity/lang/en/intro.txt
@@ -0,0 +1,11 @@
+====== Popularity Feedback ======
+
+This [[doku>popularity|tool]] gathers anonymous data about your wiki and allows you to send it back to the DokuWiki developers. This helps them to understand them how DokuWiki is used by its users and makes sure future development decisions are backed up by real world usage statistics.
+
+You are encouraged to repeat this step from time to time to keep developers informed when your wiki grows. Your repeated data sets will be identified by an anonymous ID.
+
+Data collected contains information like your DokuWiki version, the number and size of your pages and files, installed plugins and information about your PHP install.
+
+The raw data that will be send is shown below. Please use the "Send Data" button to transfer the information.
+
+
diff --git a/platform/www/lib/plugins/popularity/lang/en/lang.php b/platform/www/lib/plugins/popularity/lang/en/lang.php
new file mode 100644
index 0000000..af6797c
--- /dev/null
+++ b/platform/www/lib/plugins/popularity/lang/en/lang.php
@@ -0,0 +1,9 @@
+<?php
+
+$lang['name'] = 'Popularity Feedback (may take some time to load)';
+$lang['submit'] = 'Send Data';
+$lang['autosubmit'] = 'Automatically send data once a month';
+$lang['submissionFailed'] = 'The data couldn\'t be sent due to the following error:';
+$lang['submitDirectly'] = 'You can send the data manually by submitting the following form.';
+$lang['autosubmitError'] = 'The last autosubmit failed, because of the following error: ';
+$lang['lastSent'] = 'The data has been sent';
diff --git a/platform/www/lib/plugins/popularity/lang/en/submitted.txt b/platform/www/lib/plugins/popularity/lang/en/submitted.txt
new file mode 100644
index 0000000..30f2784
--- /dev/null
+++ b/platform/www/lib/plugins/popularity/lang/en/submitted.txt
@@ -0,0 +1,3 @@
+====== Popularity Feedback ======
+
+The data has been sent succesfully.
diff --git a/platform/www/lib/plugins/popularity/plugin.info.txt b/platform/www/lib/plugins/popularity/plugin.info.txt
new file mode 100644
index 0000000..8ffc136
--- /dev/null
+++ b/platform/www/lib/plugins/popularity/plugin.info.txt
@@ -0,0 +1,7 @@
+base popularity
+author Andreas Gohr
+email andi@splitbrain.org
+date 2015-07-15
+name Popularity Feedback Plugin
+desc Send anonymous data about your wiki to the DokuWiki developers
+url http://www.dokuwiki.org/plugin:popularity
diff --git a/platform/www/lib/plugins/remote.php b/platform/www/lib/plugins/remote.php
new file mode 100644
index 0000000..a3cbec7
--- /dev/null
+++ b/platform/www/lib/plugins/remote.php
@@ -0,0 +1,2 @@
+<?php
+dbg_deprecated('Autoloading. Do not require() files yourself.');
diff --git a/platform/www/lib/plugins/revert/admin.php b/platform/www/lib/plugins/revert/admin.php
new file mode 100644
index 0000000..2d11dc0
--- /dev/null
+++ b/platform/www/lib/plugins/revert/admin.php
@@ -0,0 +1,193 @@
+<?php
+
+use dokuwiki\ChangeLog\PageChangeLog;
+
+/**
+ * All DokuWiki plugins to extend the admin function
+ * need to inherit from this class
+ */
+class admin_plugin_revert extends DokuWiki_Admin_Plugin
+{
+ protected $cmd;
+ // some vars which might need tuning later
+ protected $max_lines = 800; // lines to read from changelog
+ protected $max_revs = 20; // numer of old revisions to check
+
+
+ /**
+ * Constructor
+ */
+ public function __construct()
+ {
+ $this->setupLocale();
+ }
+
+ /**
+ * access for managers
+ */
+ public function forAdminOnly()
+ {
+ return false;
+ }
+
+ /**
+ * return sort order for position in admin menu
+ */
+ public function getMenuSort()
+ {
+ return 40;
+ }
+
+ /**
+ * handle user request
+ */
+ public function handle()
+ {
+ }
+
+ /**
+ * output appropriate html
+ */
+ public function html()
+ {
+ global $INPUT;
+
+ echo $this->locale_xhtml('intro');
+
+ $this->printSearchForm();
+
+ if (is_array($INPUT->param('revert')) && checkSecurityToken()) {
+ $this->revertEdits($INPUT->arr('revert'), $INPUT->str('filter'));
+ } elseif ($INPUT->has('filter')) {
+ $this->listEdits($INPUT->str('filter'));
+ }
+ }
+
+ /**
+ * Display the form for searching spam pages
+ */
+ protected function printSearchForm()
+ {
+ global $lang, $INPUT;
+ echo '<form action="" method="post"><div class="no">';
+ echo '<label>'.$this->getLang('filter').': </label>';
+ echo '<input type="text" name="filter" class="edit" value="'.hsc($INPUT->str('filter')).'" /> ';
+ echo '<button type="submit">'.$lang['btn_search'].'</button> ';
+ echo '<span>'.$this->getLang('note1').'</span>';
+ echo '</div></form><br /><br />';
+ }
+
+ /**
+ * Start the reversion process
+ */
+ protected function revertEdits($revert, $filter)
+ {
+ echo '<hr /><br />';
+ echo '<p>'.$this->getLang('revstart').'</p>';
+
+ echo '<ul>';
+ foreach ($revert as $id) {
+ global $REV;
+
+ // find the last non-spammy revision
+ $data = '';
+ $pagelog = new PageChangeLog($id);
+ $old = $pagelog->getRevisions(0, $this->max_revs);
+ if (count($old)) {
+ foreach ($old as $REV) {
+ $data = rawWiki($id, $REV);
+ if (strpos($data, $filter) === false) break;
+ }
+ }
+
+ if ($data) {
+ saveWikiText($id, $data, 'old revision restored', false);
+ printf('<li><div class="li">'.$this->getLang('reverted').'</div></li>', $id, $REV);
+ } else {
+ saveWikiText($id, '', '', false);
+ printf('<li><div class="li">'.$this->getLang('removed').'</div></li>', $id);
+ }
+ @set_time_limit(10);
+ flush();
+ }
+ echo '</ul>';
+
+ echo '<p>'.$this->getLang('revstop').'</p>';
+ }
+
+ /**
+ * List recent edits matching the given filter
+ */
+ protected function listEdits($filter)
+ {
+ global $conf;
+ global $lang;
+ echo '<hr /><br />';
+ echo '<form action="" method="post"><div class="no">';
+ echo '<input type="hidden" name="filter" value="'.hsc($filter).'" />';
+ formSecurityToken();
+
+ $recents = getRecents(0, $this->max_lines);
+ echo '<ul>';
+
+ $cnt = 0;
+ foreach ($recents as $recent) {
+ if ($filter) {
+ if (strpos(rawWiki($recent['id']), $filter) === false) continue;
+ }
+
+ $cnt++;
+ $date = dformat($recent['date']);
+
+ echo ($recent['type']===DOKU_CHANGE_TYPE_MINOR_EDIT) ? '<li class="minor">' : '<li>';
+ echo '<div class="li">';
+ echo '<input type="checkbox" name="revert[]" value="'.hsc($recent['id']).
+ '" checked="checked" id="revert__'.$cnt.'" />';
+ echo ' <label for="revert__'.$cnt.'">'.$date.'</label> ';
+
+ echo '<a href="'.wl($recent['id'], "do=diff").'">';
+ $p = array();
+ $p['src'] = DOKU_BASE.'lib/images/diff.png';
+ $p['width'] = 15;
+ $p['height'] = 11;
+ $p['title'] = $lang['diff'];
+ $p['alt'] = $lang['diff'];
+ $att = buildAttributes($p);
+ echo "<img $att />";
+ echo '</a> ';
+
+ echo '<a href="'.wl($recent['id'], "do=revisions").'">';
+ $p = array();
+ $p['src'] = DOKU_BASE.'lib/images/history.png';
+ $p['width'] = 12;
+ $p['height'] = 14;
+ $p['title'] = $lang['btn_revs'];
+ $p['alt'] = $lang['btn_revs'];
+ $att = buildAttributes($p);
+ echo "<img $att />";
+ echo '</a> ';
+
+ echo html_wikilink(':'.$recent['id'], (useHeading('navigation'))?null:$recent['id']);
+ echo ' – '.htmlspecialchars($recent['sum']);
+
+ echo ' <span class="user">';
+ echo $recent['user'].' '.$recent['ip'];
+ echo '</span>';
+
+ echo '</div>';
+ echo '</li>';
+
+ @set_time_limit(10);
+ flush();
+ }
+ echo '</ul>';
+
+ echo '<p>';
+ echo '<button type="submit">'.$this->getLang('revert').'</button> ';
+ printf($this->getLang('note2'), hsc($filter));
+ echo '</p>';
+
+ echo '</div></form>';
+ }
+}
+//Setup VIM: ex: et ts=4 :
diff --git a/platform/www/lib/plugins/revert/admin.svg b/platform/www/lib/plugins/revert/admin.svg
new file mode 100644
index 0000000..2129d2d
--- /dev/null
+++ b/platform/www/lib/plugins/revert/admin.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M13.5 7a6.5 6.5 0 0 1 6.5 6.5 6.5 6.5 0 0 1-6.5 6.5H10v-2h3.5c2.5 0 4.5-2 4.5-4.5S16 9 13.5 9H7.83l3.08 3.09L9.5 13.5 4 8l5.5-5.5 1.42 1.41L7.83 7h5.67M6 18h2v2H6v-2z"/></svg> \ No newline at end of file
diff --git a/platform/www/lib/plugins/revert/lang/en/intro.txt b/platform/www/lib/plugins/revert/lang/en/intro.txt
new file mode 100644
index 0000000..b8f3558
--- /dev/null
+++ b/platform/www/lib/plugins/revert/lang/en/intro.txt
@@ -0,0 +1,3 @@
+====== Revert Manager ======
+
+This page helps you with the automatic reversion of a spam attack. To find a list of spammy pages first enter a search string (eg. a spam URL), then confirm that the found pages are really spam and revert the edits.
diff --git a/platform/www/lib/plugins/revert/lang/en/lang.php b/platform/www/lib/plugins/revert/lang/en/lang.php
new file mode 100644
index 0000000..6bf867d
--- /dev/null
+++ b/platform/www/lib/plugins/revert/lang/en/lang.php
@@ -0,0 +1,23 @@
+<?php
+/**
+ * english language file
+ */
+
+// for admin plugins, the menu prompt to be displayed in the admin menu
+// if set here, the plugin doesn't need to override the getMenuText() method
+$lang['menu'] = 'Revert Manager';
+
+// custom language strings for the plugin
+
+$lang['filter'] = 'Search spammy pages';
+$lang['revert'] = 'Revert selected pages';
+$lang['reverted'] = '%s reverted to revision %s';
+$lang['removed'] = '%s removed';
+$lang['revstart'] = 'Reversion process started. This can take a long time. If the
+ script times out before finishing, you need to revert in smaller
+ chunks.';
+$lang['revstop'] = 'Reversion process finished successfully.';
+$lang['note1'] = 'Note: this search is case sensitive';
+$lang['note2'] = 'Note: the page will be reverted to the last version not containing the given spam term <i>%s</i>.';
+
+//Setup VIM: ex: et ts=4 :
diff --git a/platform/www/lib/plugins/revert/plugin.info.txt b/platform/www/lib/plugins/revert/plugin.info.txt
new file mode 100644
index 0000000..bba939d
--- /dev/null
+++ b/platform/www/lib/plugins/revert/plugin.info.txt
@@ -0,0 +1,7 @@
+base revert
+author Andreas Gohr
+email andi@splitbrain.org
+date 2015-07-15
+name Revert Manager
+desc Allows you to mass revert recent edits to remove Spam or vandalism
+url http://dokuwiki.org/plugin:revert
diff --git a/platform/www/lib/plugins/safefnrecode/action.php b/platform/www/lib/plugins/safefnrecode/action.php
new file mode 100644
index 0000000..952d95c
--- /dev/null
+++ b/platform/www/lib/plugins/safefnrecode/action.php
@@ -0,0 +1,68 @@
+<?php
+/**
+ * DokuWiki Plugin safefnrecode (Action Component)
+ *
+ * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+
+class action_plugin_safefnrecode extends DokuWiki_Action_Plugin
+{
+
+ /** @inheritdoc */
+ public function register(Doku_Event_Handler $controller)
+ {
+ $controller->register_hook('INDEXER_TASKS_RUN', 'BEFORE', $this, 'handleIndexerTasksRun');
+ }
+
+ /**
+ * Handle indexer event
+ *
+ * @param Doku_Event $event
+ * @param $param
+ */
+ public function handleIndexerTasksRun(Doku_Event $event, $param)
+ {
+ global $conf;
+ if ($conf['fnencode'] != 'safe') return;
+
+ if (!file_exists($conf['datadir'].'_safefn.recoded')) {
+ $this->recode($conf['datadir']);
+ touch($conf['datadir'].'_safefn.recoded');
+ }
+
+ if (!file_exists($conf['olddir'].'_safefn.recoded')) {
+ $this->recode($conf['olddir']);
+ touch($conf['olddir'].'_safefn.recoded');
+ }
+
+ if (!file_exists($conf['metadir'].'_safefn.recoded')) {
+ $this->recode($conf['metadir']);
+ touch($conf['metadir'].'_safefn.recoded');
+ }
+
+ if (!file_exists($conf['mediadir'].'_safefn.recoded')) {
+ $this->recode($conf['mediadir']);
+ touch($conf['mediadir'].'_safefn.recoded');
+ }
+ }
+
+ /**
+ * Recursive function to rename all safe encoded files to use the new
+ * square bracket post indicator
+ */
+ private function recode($dir)
+ {
+ $dh = opendir($dir);
+ if (!$dh) return;
+ while (($file = readdir($dh)) !== false) {
+ if ($file == '.' || $file == '..') continue; # cur and upper dir
+ if (is_dir("$dir/$file")) $this->recode("$dir/$file"); #recurse
+ if (strpos($file, '%') === false) continue; # no encoding used
+ $new = preg_replace('/(%[^\]]*?)\./', '\1]', $file); # new post indicator
+ if (preg_match('/%[^\]]+$/', $new)) $new .= ']'; # fix end FS#2122
+ rename("$dir/$file", "$dir/$new"); # rename it
+ }
+ closedir($dh);
+ }
+}
diff --git a/platform/www/lib/plugins/safefnrecode/plugin.info.txt b/platform/www/lib/plugins/safefnrecode/plugin.info.txt
new file mode 100644
index 0000000..3c6249d
--- /dev/null
+++ b/platform/www/lib/plugins/safefnrecode/plugin.info.txt
@@ -0,0 +1,7 @@
+base safefnrecode
+author Andreas Gohr
+email andi@splitbrain.org
+date 2012-07-28
+name safefnrecode plugin
+desc Changes existing page and foldernames for the change in the safe filename encoding
+url http://www.dokuwiki.org/plugin:safefnrecode
diff --git a/platform/www/lib/plugins/styling/README b/platform/www/lib/plugins/styling/README
new file mode 100644
index 0000000..a1a5e89
--- /dev/null
+++ b/platform/www/lib/plugins/styling/README
@@ -0,0 +1,27 @@
+styling Plugin for DokuWiki
+
+Allows to edit style.ini replacements
+
+All documentation for this plugin can be found at
+https://www.dokuwiki.org/plugin:styling
+
+If you install this plugin manually, make sure it is installed in
+lib/plugins/styling/ - if the folder is called different it
+will not work!
+
+Please refer to http://www.dokuwiki.org/plugins for additional info
+on how to install plugins in DokuWiki.
+
+----
+Copyright (C) Andreas Gohr <andi@splitbrain.org>
+
+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; version 2 of the License
+
+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.
+
+See the COPYING file in your DokuWiki folder for details
diff --git a/platform/www/lib/plugins/styling/action.php b/platform/www/lib/plugins/styling/action.php
new file mode 100644
index 0000000..46245ca
--- /dev/null
+++ b/platform/www/lib/plugins/styling/action.php
@@ -0,0 +1,51 @@
+<?php
+/**
+ * DokuWiki Plugin styling (Action Component)
+ *
+ * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+class action_plugin_styling extends DokuWiki_Action_Plugin
+{
+
+ /**
+ * Registers a callback functions
+ *
+ * @param Doku_Event_Handler $controller DokuWiki's event controller object
+ * @return void
+ */
+ public function register(Doku_Event_Handler $controller)
+ {
+ $controller->register_hook('TPL_METAHEADER_OUTPUT', 'BEFORE', $this, 'handleHeader');
+ }
+
+ /**
+ * Adds the preview parameter to the stylesheet loading in non-js mode
+ *
+ * @param Doku_Event $event event object by reference
+ * @param mixed $param [the parameters passed as fifth argument to register_hook() when this
+ * handler was registered]
+ * @return void
+ */
+ public function handleHeader(Doku_Event &$event, $param)
+ {
+ global $ACT;
+ global $INPUT;
+ if ($ACT != 'admin' || $INPUT->str('page') != 'styling') return;
+ /** @var admin_plugin_styling $admin */
+ $admin = plugin_load('admin', 'styling');
+ if (!$admin->isAccessibleByCurrentUser()) return;
+
+ // set preview
+ $len = count($event->data['link']);
+ for ($i = 0; $i < $len; $i++) {
+ if ($event->data['link'][$i]['rel'] == 'stylesheet' &&
+ strpos($event->data['link'][$i]['href'], 'lib/exe/css.php') !== false
+ ) {
+ $event->data['link'][$i]['href'] .= '&preview=1&tseed='.time();
+ }
+ }
+ }
+}
+
+// vim:ts=4:sw=4:et:
diff --git a/platform/www/lib/plugins/styling/admin.php b/platform/www/lib/plugins/styling/admin.php
new file mode 100644
index 0000000..d454422
--- /dev/null
+++ b/platform/www/lib/plugins/styling/admin.php
@@ -0,0 +1,224 @@
+<?php
+/**
+ * DokuWiki Plugin styling (Admin Component)
+ *
+ * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+class admin_plugin_styling extends DokuWiki_Admin_Plugin
+{
+
+ public $ispopup = false;
+
+ /**
+ * @return int sort number in admin menu
+ */
+ public function getMenuSort()
+ {
+ return 1000;
+ }
+
+ /**
+ * @return bool true if only access for superuser, false is for superusers and moderators
+ */
+ public function forAdminOnly()
+ {
+ return true;
+ }
+
+ /**
+ * handle the different actions (also called from ajax)
+ */
+ public function handle()
+ {
+ global $INPUT;
+ $run = $INPUT->extract('run')->str('run');
+ if (!$run) return;
+ $run = 'run'.ucfirst($run);
+ $this->$run();
+ }
+
+ /**
+ * Render HTML output, e.g. helpful text and a form
+ */
+ public function html()
+ {
+ $class = 'nopopup';
+ if ($this->ispopup) $class = 'ispopup page';
+
+ echo '<div id="plugin__styling" class="'.$class.'">';
+ ptln('<h1>'.$this->getLang('menu').'</h1>');
+ $this->form();
+ echo '</div>';
+ }
+
+ /**
+ * Create the actual editing form
+ */
+ public function form()
+ {
+ global $conf;
+ global $ID;
+
+ $styleUtil = new \dokuwiki\StyleUtils($conf['template'], true, true);
+ $styleini = $styleUtil->cssStyleini();
+ $replacements = $styleini['replacements'];
+
+ if ($this->ispopup) {
+ $target = DOKU_BASE.'lib/plugins/styling/popup.php';
+ } else {
+ $target = wl($ID, array('do' => 'admin', 'page' => 'styling'));
+ }
+
+ if (empty($replacements)) {
+ echo '<p class="error">'.$this->getLang('error').'</p>';
+ } else {
+ echo $this->locale_xhtml('intro');
+
+ echo '<form class="styling" method="post" action="'.$target.'">';
+
+ echo '<table><tbody>';
+ foreach ($replacements as $key => $value) {
+ $name = tpl_getLang($key);
+ if (empty($name)) $name = $this->getLang($key);
+ if (empty($name)) $name = $key;
+
+ echo '<tr>';
+ echo '<td><label for="tpl__'.hsc($key).'">'.$name.'</label></td>';
+ echo '<td><input type="'.$this->colorType($value).'" name="tpl['.hsc($key).']" id="tpl__'.hsc($key).'"
+ value="'.hsc($this->colorValue($value)).'" dir="ltr" /></td>';
+ echo '</tr>';
+ }
+ echo '</tbody></table>';
+
+ echo '<p>';
+ echo '<button type="submit" name="run[preview]" class="btn_preview primary">'.
+ $this->getLang('btn_preview').'</button> ';
+ #FIXME only if preview.ini exists:
+ echo '<button type="submit" name="run[reset]">'.$this->getLang('btn_reset').'</button>';
+ echo '</p>';
+
+ echo '<p>';
+ echo '<button type="submit" name="run[save]" class="primary">'.$this->getLang('btn_save').'</button>';
+ echo '</p>';
+
+ echo '<p>';
+ #FIXME only if local.ini exists:
+ echo '<button type="submit" name="run[revert]">'.$this->getLang('btn_revert').'</button>';
+ echo '</p>';
+
+ echo '</form>';
+
+ echo tpl_locale_xhtml('style');
+ }
+ }
+
+ /**
+ * Adjust three char color codes to the 6 char one supported by browser's color input
+ *
+ * @param string $value
+ * @return string
+ */
+ protected function colorValue($value)
+ {
+ if (preg_match('/^#([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])$/', $value, $match)) {
+ return '#' . $match[1] . $match[1] . $match[2] . $match[2] . $match[3] . $match[3];
+ }
+ return $value;
+ }
+
+ /**
+ * Decide the input type based on the value
+ *
+ * @param string $value
+ * @return string color|text
+ */
+ protected function colorType($value)
+ {
+ if (preg_match('/^#([0-9a-fA-F]{3}){1,2}$/', $value)) {
+ return 'color';
+ } else {
+ return 'text';
+ }
+ }
+
+ /**
+ * saves the preview.ini (alos called from ajax directly)
+ */
+ public function runPreview()
+ {
+ global $conf;
+ $ini = $conf['cachedir'].'/preview.ini';
+ io_saveFile($ini, $this->makeini());
+ }
+
+ /**
+ * deletes the preview.ini
+ */
+ protected function runReset()
+ {
+ global $conf;
+ $ini = $conf['cachedir'].'/preview.ini';
+ io_saveFile($ini, '');
+ }
+
+ /**
+ * deletes the local style.ini replacements
+ */
+ protected function runRevert()
+ {
+ $this->replaceIni('');
+ $this->runReset();
+ }
+
+ /**
+ * save the local style.ini replacements
+ */
+ protected function runSave()
+ {
+ $this->replaceIni($this->makeini());
+ $this->runReset();
+ }
+
+ /**
+ * create the replacement part of a style.ini from submitted data
+ *
+ * @return string
+ */
+ protected function makeini()
+ {
+ global $INPUT;
+
+ $ini = "[replacements]\n";
+ $ini .= ";These overwrites have been generated from the Template styling Admin interface\n";
+ $ini .= ";Any values in this section will be overwritten by that tool again\n";
+ foreach ($INPUT->arr('tpl') as $key => $val) {
+ $ini .= $key.' = "'.addslashes($val).'"'."\n";
+ }
+
+ return $ini;
+ }
+
+ /**
+ * replaces the replacement parts in the local ini
+ *
+ * @param string $new the new ini contents
+ */
+ protected function replaceIni($new)
+ {
+ global $conf;
+ $ini = DOKU_CONF."tpl/".$conf['template']."/style.ini";
+ if (file_exists($ini)) {
+ $old = io_readFile($ini);
+ $old = preg_replace('/\[replacements\]\n.*?(\n\[.*]|$)/s', '\\1', $old);
+ $old = trim($old);
+ } else {
+ $old = '';
+ }
+
+ io_makeFileDir($ini);
+ io_saveFile($ini, "$old\n\n$new");
+ }
+}
+
+// vim:ts=4:sw=4:et:
diff --git a/platform/www/lib/plugins/styling/admin.svg b/platform/www/lib/plugins/styling/admin.svg
new file mode 100644
index 0000000..5d73870
--- /dev/null
+++ b/platform/www/lib/plugins/styling/admin.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M17.5 12a1.5 1.5 0 0 1-1.5-1.5A1.5 1.5 0 0 1 17.5 9a1.5 1.5 0 0 1 1.5 1.5 1.5 1.5 0 0 1-1.5 1.5m-3-4A1.5 1.5 0 0 1 13 6.5 1.5 1.5 0 0 1 14.5 5 1.5 1.5 0 0 1 16 6.5 1.5 1.5 0 0 1 14.5 8m-5 0A1.5 1.5 0 0 1 8 6.5 1.5 1.5 0 0 1 9.5 5 1.5 1.5 0 0 1 11 6.5 1.5 1.5 0 0 1 9.5 8m-3 4A1.5 1.5 0 0 1 5 10.5 1.5 1.5 0 0 1 6.5 9 1.5 1.5 0 0 1 8 10.5 1.5 1.5 0 0 1 6.5 12M12 3a9 9 0 0 0-9 9 9 9 0 0 0 9 9 1.5 1.5 0 0 0 1.5-1.5c0-.39-.15-.74-.39-1-.23-.27-.38-.62-.38-1a1.5 1.5 0 0 1 1.5-1.5H16a5 5 0 0 0 5-5c0-4.42-4.03-8-9-8z"/></svg> \ No newline at end of file
diff --git a/platform/www/lib/plugins/styling/lang/en/intro.txt b/platform/www/lib/plugins/styling/lang/en/intro.txt
new file mode 100644
index 0000000..4ea5517
--- /dev/null
+++ b/platform/www/lib/plugins/styling/lang/en/intro.txt
@@ -0,0 +1,2 @@
+This tool allows you to change certain style settings of your currently selected template.
+All changes are stored in a local configuration file and are upgrade safe. \ No newline at end of file
diff --git a/platform/www/lib/plugins/styling/lang/en/lang.php b/platform/www/lib/plugins/styling/lang/en/lang.php
new file mode 100644
index 0000000..e0011eb
--- /dev/null
+++ b/platform/www/lib/plugins/styling/lang/en/lang.php
@@ -0,0 +1,35 @@
+<?php
+/**
+ * English language file for styling plugin
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+
+// menu entry for admin plugins
+$lang['menu'] = 'Template Style Settings';
+
+$lang['js']['loader'] = 'Preview is loading...<br />if this does not goes away, your values may be faulty';
+$lang['js']['popup'] = 'Open as a popup';
+
+// custom language strings for the plugin
+$lang['error'] = 'Sorry, this template does not support this functionality.';
+
+$lang['btn_preview'] = 'Preview changes';
+$lang['btn_save'] = 'Save changes';
+$lang['btn_reset'] = 'Reset current changes';
+$lang['btn_revert'] = 'Revert styles back to template\'s default';
+
+// default guaranteed placeholders
+$lang['__text__'] = 'Main text color';
+$lang['__background__'] = 'Main background color';
+$lang['__text_alt__'] = 'Alternative text color';
+$lang['__background_alt__'] = 'Alternative background color';
+$lang['__text_neu__'] = 'Neutral text color';
+$lang['__background_neu__'] = 'Neutral background color';
+$lang['__border__'] = 'Border color';
+$lang['__highlight__'] = 'Highlight color (for search results mainly)';
+
+
+
+
+//Setup VIM: ex: et ts=4 :
diff --git a/platform/www/lib/plugins/styling/plugin.info.txt b/platform/www/lib/plugins/styling/plugin.info.txt
new file mode 100644
index 0000000..e374eaf
--- /dev/null
+++ b/platform/www/lib/plugins/styling/plugin.info.txt
@@ -0,0 +1,7 @@
+base styling
+author Andreas Gohr
+email andi@splitbrain.org
+date 2020-06-14
+name styling plugin
+desc Allows to edit style.ini replacements
+url https://www.dokuwiki.org/plugin:styling
diff --git a/platform/www/lib/plugins/styling/popup.php b/platform/www/lib/plugins/styling/popup.php
new file mode 100644
index 0000000..079062e
--- /dev/null
+++ b/platform/www/lib/plugins/styling/popup.php
@@ -0,0 +1,31 @@
+<?php
+// phpcs:disable PSR1.Files.SideEffects
+if (!defined('DOKU_INC')) define('DOKU_INC', dirname(__FILE__) . '/../../../');
+require_once(DOKU_INC . 'inc/init.php');
+//close session
+session_write_close();
+header('Content-Type: text/html; charset=utf-8');
+header('X-UA-Compatible: IE=edge,chrome=1');
+
+/** @var admin_plugin_styling $plugin */
+$plugin = plugin_load('admin', 'styling');
+if (!$plugin->isAccessibleByCurrentUser()) die('only admins allowed');
+$plugin->ispopup = true;
+
+// handle posts
+$plugin->handle();
+
+// output plugin in a very minimal template:
+?><!DOCTYPE html>
+<html lang="<?php echo $conf['lang'] ?>" dir="<?php echo $lang['direction'] ?>">
+<head>
+ <meta charset="utf-8" />
+ <title><?php echo $plugin->getLang('menu') ?></title>
+ <?php tpl_metaheaders(false) ?>
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
+ <?php echo tpl_favicon(array('favicon')) ?>
+</head>
+<body class="dokuwiki">
+ <?php $plugin->html() ?>
+</body>
+</html>
diff --git a/platform/www/lib/plugins/styling/script.js b/platform/www/lib/plugins/styling/script.js
new file mode 100644
index 0000000..7fa8b25
--- /dev/null
+++ b/platform/www/lib/plugins/styling/script.js
@@ -0,0 +1,92 @@
+jQuery(function () {
+
+ /**
+ * Function to reload the preview styles in the main window
+ *
+ * @param {Window} target the main window
+ */
+ function applyPreview(target) {
+ // remove style
+ var $style = target.jQuery('link[rel=stylesheet][href*="lib/exe/css.php"]');
+ $style.attr('href', '');
+
+ // append the loader screen
+ var $loader = target.jQuery('#plugin__styling_loader');
+ if (!$loader.length) {
+ $loader = target.jQuery('<div id="plugin__styling_loader">' + LANG.plugins.styling.loader + '</div>');
+ $loader.css({
+ 'position': 'absolute',
+ 'width': '100%',
+ 'height': '100%',
+ 'top': 0,
+ 'left': 0,
+ 'z-index': 5000,
+ 'background-color': '#fff',
+ 'opacity': '0.7',
+ 'color': '#000',
+ 'font-size': '2.5em',
+ 'text-align': 'center',
+ 'line-height': 1.5,
+ 'padding-top': '2em'
+ });
+ target.jQuery('body').append($loader);
+ }
+
+ // load preview in main window (timeout works around chrome updating CSS weirdness)
+ setTimeout(function () {
+ var now = new Date().getTime();
+ $style.attr('href', DOKU_BASE + 'lib/exe/css.php?preview=1&tseed=' + now);
+ }, 500);
+ }
+
+ var doreload = 1;
+ var $styling_plugin = jQuery('#plugin__styling');
+
+ // if we are not on the plugin page (either main or popup)
+ if (!$styling_plugin.length) {
+ // handle the preview cookie
+ if(DokuCookie.getValue('styling_plugin') == 1) {
+ applyPreview(window);
+ }
+ return; // nothing more to do here
+ }
+
+ /* ---- from here on we're in the popup or admin page ---- */
+
+ // add button on main page
+ if (!$styling_plugin.hasClass('ispopup')) {
+ var $form = $styling_plugin.find('form.styling').first();
+ var $btn = jQuery('<button>' + LANG.plugins.styling.popup + '</button>');
+ $form.prepend($btn);
+
+ $btn.on('click', function (e) {
+ var windowFeatures = "menubar=no,location=no,resizable=yes,scrollbars=yes,status=false,width=500,height=500";
+ window.open(DOKU_BASE + 'lib/plugins/styling/popup.php', 'styling_popup', windowFeatures);
+ e.preventDefault();
+ e.stopPropagation();
+ }).wrap('<p></p>');
+ return; // we exit here if this is not the popup
+ }
+
+ /* ---- from here on we're in the popup only ---- */
+
+ // reload the main page on close
+ window.onunload = function(e) {
+ if(doreload) {
+ DokuCookie.setValue('styling_plugin', 0);
+ if(window.opener) window.opener.document.location.reload();
+ }
+ return null;
+ };
+
+ // don't reload on our own buttons
+ jQuery(':button').click(function(e){
+ doreload = false;
+ });
+
+ // on first load apply preview
+ if(window.opener) applyPreview(window.opener);
+
+ // enable the preview cookie
+ DokuCookie.setValue('styling_plugin', 1);
+});
diff --git a/platform/www/lib/plugins/styling/style.less b/platform/www/lib/plugins/styling/style.less
new file mode 100644
index 0000000..be0e16a
--- /dev/null
+++ b/platform/www/lib/plugins/styling/style.less
@@ -0,0 +1,13 @@
+#plugin__styling {
+ button.primary {
+ font-weight: bold;
+ }
+
+ [dir=rtl] & table input {
+ text-align: right;
+ }
+}
+
+#plugin__styling_loader {
+ display: none;
+}
diff --git a/platform/www/lib/plugins/syntax.php b/platform/www/lib/plugins/syntax.php
new file mode 100644
index 0000000..a3cbec7
--- /dev/null
+++ b/platform/www/lib/plugins/syntax.php
@@ -0,0 +1,2 @@
+<?php
+dbg_deprecated('Autoloading. Do not require() files yourself.');
diff --git a/platform/www/lib/plugins/textinsert/README b/platform/www/lib/plugins/textinsert/README
new file mode 100644
index 0000000..ed0c173
--- /dev/null
+++ b/platform/www/lib/plugins/textinsert/README
@@ -0,0 +1,55 @@
+This is a macro substitution plugin which enables substitutions of both words and longer
+strings of text.
+
+Basic Syntax: #@Macro_Name@#
+
+It provides an administrative panel which makes for simple adding, editing and deletion
+of macros.
+
+Much of the functionality of this plugin can be more easily implemented with Dokuwiki's
+own entities facility: http://www.dokuwiki.org/entities. The advantage of TextInsert
+comes when dealing with extended blocks of text and with its ability to include other
+macros inside the primary macro definition. That is, it can accept:
+
+MACRO_1 This macro can include #@MACR0_2@# inside it.
+MACRO_2 a second macro
+
+The result is:
+ This macro can include a second macro inside it.
+
+It accepts only one level of macro inclusion, so that if MACRO_3 were included in
+MACRO_2, MACRO_3 would not be rendered.
+
+The macro definitions will also accept entities defined dokuwiki's conf/entities.conf and
+user-defined entities conf/entities.local.conf. Entities will be replaced in both the
+primary and the included macros.
+
+HTML Support
+#Macro_HTML@#
+A macro name with the _HTML suffix will be output as HTML, whereas without the _HTML suffix the
+HTML markup will be treated as literals. With it, <p> creates a paragraph; wthout it the <p>
+markup is printed to the screen.
+
+Translation Support
+#@LANG_name@#
+If a macro has this format, textinsert checks the file's namespace for a language specified
+as an ISO abbreviation, for instance nl for Dutch or de for German -- as in de:my_file. If it finds
+a language specification, it will check the textinsert/lang directory for either macros.php or a lang.php
+file in the relevant language directory, for instance lang/de/lang.php. It will then look for a
+$lang['name'] entry in lang.php or a $lang_<iso>['name'] entry in macros.php, and if
+it finds one, it will substitute this for the macro. Otherwise, it will substitute the entry for
+LANG_name in the textinsert database. So, there must be a default entry in that database.
+
+Translation macros can be embeded in other macros, including other translation macros.
+And other macros can emdedded in translation macros. They also accept entities, as
+described above.
+
+For HTML output, HTML tags are accepted in translation macros, but for the HTML
+to become effective, these must be included in a standard HTML macro definition
+string. In effect, they must be doubly bound to the HTML
+
+See http://dokuwiki.org/plugin:textinsert for further information.
+
+
+
+
diff --git a/platform/www/lib/plugins/textinsert/admin.php b/platform/www/lib/plugins/textinsert/admin.php
new file mode 100644
index 0000000..3d85347
--- /dev/null
+++ b/platform/www/lib/plugins/textinsert/admin.php
@@ -0,0 +1,292 @@
+<?php
+/**
+ *
+ *
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ * @author Myron Turner <turnermm02@shaw.ca>
+ */
+if(!defined('DOKU_INC')) die();
+
+define('REPLACE_DIR', DOKU_INC . 'data/meta/macros/');
+
+/**
+ * All DokuWiki plugins to extend the admin function
+ * need to inherit from this class
+ */
+class admin_plugin_textinsert extends DokuWiki_Admin_Plugin {
+
+ var $output = false;
+ var $macros_file;
+ var $macros_data; //used for html listings
+ /**
+ * handle user request
+ */
+ function __construct() {
+ if(!$this->getConf('farm')) {
+ define('MACROS_FILE', REPLACE_DIR . 'macros.ser');
+ }
+ else {
+ define('MACROS_FILE', metaFN('macros','.ser'));
+ }
+
+ }
+ function handle() {
+
+ $this->macros_file=MACROS_FILE;
+
+ if (!isset($_REQUEST['cmd'])) return; // first time - nothing to do
+
+ $this->output = '';
+ if (!checkSecurityToken()) return;
+ if (!is_array($_REQUEST['cmd'])) return;
+ $action = "";
+
+ // verify valid values
+ switch (key($_REQUEST['cmd'])) {
+ case 'add' :
+ $action = 'add';
+ $a = $this->add();
+ break;
+
+ case 'delete' :
+ $a = $this->del();
+ break;
+ case 'edit':
+ $a = $this->edit();
+ break;
+ }
+ // $this->output = print_r($a,true);
+ // $this->output .= print_r($_REQUEST,true);
+ }
+
+ function add() {
+ $a = $this->get_macros();
+ $macros = $_REQUEST['macro'];
+ $words = $_REQUEST['word'];
+
+ foreach ($macros AS $key=>$value) {
+ if(isset($value) && trim($value)) {
+ if(isset($words[$key]) && trim($words[$key])) {
+ $value = utf8_deaccent($value);
+ $a[$value] = htmlspecialchars (($words[$key]),ENT_NOQUOTES, 'UTF-8');
+ }
+ }
+ }
+
+ io_saveFile(MACROS_FILE,serialize($a));
+ return $a;
+ }
+
+ function del() {
+ $macros = $this->get_macros();
+
+ $deletions = $_REQUEST['delete'];
+ $keys = array_keys($deletions);
+ foreach ($keys AS $_key) {
+ unset($macros[$_key]);
+ }
+
+ io_saveFile(MACROS_FILE,serialize($macros));
+ return $macros;
+ }
+
+ function edit() {
+ $macros = $this->get_macros();
+
+ $encoded = $_REQUEST['encoded'];
+ $encoded = array_map('urldecode',$encoded);
+ foreach($encoded AS $k=>$val) {
+ $macros[$k] = htmlspecialchars ($val,ENT_NOQUOTES, 'UTF-8', false);
+ }
+ io_saveFile(MACROS_FILE,serialize($macros));
+ return $macros;
+ }
+
+ function get_macros() {
+ if(file_exists(MACROS_FILE)) {
+ $a = unserialize(file_get_contents(MACROS_FILE));
+ if(!is_array($a)) return array();
+ ksort($a);
+ return $a;
+ }
+ return array();
+ }
+
+ function get_delete_list() {
+ $macros = $this->macros_data;
+ ptln('<table cellspacing="4px" width="90%">');
+ foreach($macros as $macro=>$subst) {
+ ptln("<tr><td><input type='checkbox' name='delete[$macro]' value='$subst'>");
+ ptln( "<td style='padding:4px;'>$macro<td>$subst</td>");
+
+ }
+ ptln('</table>');
+ }
+
+ function get_edit_list() {
+ $macros = $this->macros_data;
+ ptln('<table cellspacing="4"><tr><th align="center">Macro</th><th align="center">' . $this->getLang('col_subst') .'</th></tr>');
+ foreach($macros as $macro=>$subst) {
+ ptln("<tr><td align='center'>$macro&nbsp;</td><td>");
+ $encoded = urlencode($subst);
+ if($subst != $encoded) {
+ ptln("<input type = 'hidden' name='encoded[$macro]' value='$encoded'>");
+ }
+ if(strlen($subst) > 80) {
+ ptln ("<textarea cols='55' rows='3' name='edit[$macro]' onchange='replace_encode(this)'>$subst</textarea></td></tr>");
+
+ }
+ else {
+ ptln ("<input type='text' size='80' name='edit[$macro]' onchange='replace_encode(this)' value='$subst'></td></tr>");
+ }
+
+ }
+ ptln('</table>');
+ }
+
+ function view_entries() {
+ $macros = $this->macros_data;
+ ptln('<table cellpadding="8px" width="90%">');
+ foreach($macros as $macro=>$subst) {
+ ptln( "<tr><td align='center'>$macro<td style='padding: 4px; border-bottom: 1px solid black;'>$subst</tr>");
+
+ }
+ ptln('</table>');
+ }
+
+ function js() {
+
+echo <<<JSFN
+
+ <script type="text/javascript">
+ //<![CDATA[
+ var replace_divs= new Array('macro_add','macro_del','macro_edit','ti_info','macro_list');
+ /**
+ * Edit onChange handler
+ * @param el input element which has been changed
+ * @desc if an encode hidden input already exists, its value
+ * is re-encoded from the text input's value
+ * If not, a new encoded hidden input is created with the encoded
+ * value. The encode input value is used to substitute the new edit values
+ * in the php edit() function
+ */
+ function replace_encode (el) {
+ var matches = el.name.match(/\[(.*)\]/);
+ if(matches[1]) {
+ var name = 'encoded['+matches[1]+']';
+ var val = el.value;
+ val = val.replace(/>/g,"&gt;");
+ val = val.replace(/</g,"&lt;");
+ if(!el.form[name]) {
+ var encoder = document.createElement('input');
+ encoder.type = 'hidden';
+ encoder.name = name;
+ encoder.value = encodeURIComponent(val);
+ el.form.appendChild(encoder);
+ }
+ else if(el.form[name]) {
+ el.form[name].value = encodeURIComponent(val);
+ }
+ }
+ }
+
+ function ti_getEL(n) {
+ return document.getElementById(n);
+ }
+
+ function replace_show(which) {
+ for(var i in replace_divs) {
+ ti_getEL(replace_divs[i]).style.display='none';
+ }
+ ti_getEL(which).style.display='block';
+ ti_getEL('ti_info_btn').style.display='inline';
+ }
+//]]>
+ </script>
+JSFN;
+
+ }
+ /**
+ * output appropriate html
+ */
+ function html() {
+ $this->macros_data = $this->get_macros();
+ $this->js();
+ if($this->output) {
+ ptln('<pre>' . $this->output . '</pre>');
+ }
+ ptln('<div style="padding:4px" id="ti_info">');
+ ptln('<div style="text-align:right;">');
+ ptln('<button class="button" style="padding:0px;margin:0px;" onclick="replace_show(\'ti_info_btn\');">');
+ ptln($this->getLang('hide_info') .'</button>&nbsp;&nbsp;&nbsp;&nbsp;');
+ ptln('</div>');
+ ptln('<h2>Info</h2>');
+ ptln( $this->locale_xhtml('intro') . '</div>');
+
+ ptln('<div style="padding-bottom:8px;">');
+ ptln('<button class="button" onclick="replace_show(\'macro_add\'); ">');
+ ptln($this->getLang('add_macros') .'</button>&nbsp;&nbsp;');
+
+ ptln('<button class="button" onclick="replace_show(\'macro_del\'); ">');
+ ptln($this->getLang('delete_macros') .'</button>&nbsp;&nbsp;');
+
+ ptln('<button class="button" onclick="replace_show(\'macro_edit\'); ">');
+ ptln($this->getLang('edit_macros') .'</button>&nbsp;&nbsp;');
+
+ ptln('<button class="button" onclick="ti_getEL(\'macro_list\').style.display=\'block\';ti_getEL(\'macro_list\').scrollIntoView();">');
+ ptln($this->getLang('view_macros') .'</button>&nbsp;&nbsp;');
+
+ ptln('<button class="button" onclick="ti_getEL(\'macro_list\').style.display=\'none\';">');
+ ptln($this->getLang('hide_macros') .'</button>&nbsp;&nbsp;');
+
+ ptln('<button class="button" id="ti_info_btn" style="display:none" onclick="ti_getEL(\'ti_info\').style.display=\'block\';">');
+ ptln($this->getLang('show_info') .'</button>');
+
+ ptln('</div>');
+ ptln('<form action="'.wl($ID).'" method="post">');
+
+ // output hidden values to ensure dokuwiki will return back to this plugin
+ ptln(' <input type="hidden" name="do" value="admin" />');
+ ptln(' <input type="hidden" name="page" value="'.$this->getPluginName().'" />');
+ formSecurityToken();
+
+ ptln('<div id="macro_add" style="display:none">');
+ ptln('<h2>' . $this->getLang('label_add') . '</h2>');
+ ptln( '<table cellspacing="8px"><tr><th>Macro</th><th>' . $this->getLang('col_subst') . '</th></tr>');
+ ptln('<tr><td> <input type="text" name="macro[A]" id="m_A" value="" /></td>');
+ ptln('<td> <input type="text" name="word[A]" size="80" id="w_A" value="" /></td></tr>');
+ ptln('<tr><td> <input type="text" name="macro[B]" id="m_B" value="" /></td>');
+ ptln('<td> <input type="text" name="word[B]" size="80" id="w_B" value="" /></td></tr>');
+ ptln('<tr><td> <input type="text" name="macro[C]" id="m_C" value="" /></td>');
+ ptln('<td> <input type="text" name="word[C]" size="80" id="w_C" value="" /></td></tr>');
+ ptln('<tr><td> <input type="text" name="macro[D]" id="m_D" value="" /></td>');
+ ptln('<td> <input type="text" name="word[D]" size="80" id="w_C" value="" /></td></tr>');
+ ptln('<tr><td> <input type="text" name="macro[E]" id="m_E" value="" /></td>');
+ ptln('<td> <input type="text" name="word[E]" size="80" id="w_E" value="" /></td>');
+ ptln('<tr><td> <input type="text" name="macro[F]" id="m_F" value="" /></td>');
+ ptln('<td> <textarea cols="45" name="word[F]" rows="4" id="w_F"></textarea></td>');
+ ptln('</table>');
+ ptln(' <input type="submit" name="cmd[add]" value="'.$this->getLang('btn_add').'" />');
+ ptln('</div><br />');
+
+ ptln('<div id="macro_del" style="display:none">');
+ ptln('<h2>' . $this->getLang('label_del') . '</h2>');
+ $this->get_delete_list();
+ ptln('<br /><input type="submit" name="cmd[delete]" value="'.$this->getLang('btn_del').'" />');
+ ptln('</div>');
+
+ ptln('<div id="macro_edit" style="display:none; padding: 8px;">');
+ ptln('<h2>' . $this->getLang('label_edit') . '</h2>');
+ $this->get_edit_list();
+ ptln('<br /><input type="submit" name="cmd[edit]" value="'.$this->getLang('btn_edit').'" />');
+ ptln('</div>');
+
+ ptln('</form>');
+
+ ptln('<br /><div id="macro_list" style="overflow:auto;display:block;">');
+ ptln('<h2>' . $this->getLang('label_list') . '</h2>');
+ $this->view_entries();
+ ptln('</div>');
+ }
+
+}
diff --git a/platform/www/lib/plugins/textinsert/conf/default.php b/platform/www/lib/plugins/textinsert/conf/default.php
new file mode 100644
index 0000000..efe3db4
--- /dev/null
+++ b/platform/www/lib/plugins/textinsert/conf/default.php
@@ -0,0 +1,5 @@
+<?php
+$conf['stdreplace'] = 1;
+$conf['farm'] = 0;
+
+
diff --git a/platform/www/lib/plugins/textinsert/conf/metadata.php b/platform/www/lib/plugins/textinsert/conf/metadata.php
new file mode 100644
index 0000000..56168e8
--- /dev/null
+++ b/platform/www/lib/plugins/textinsert/conf/metadata.php
@@ -0,0 +1,3 @@
+<?php
+$meta['stdreplace'] = array('onoff');
+$meta['farm'] = array('onoff'); \ No newline at end of file
diff --git a/platform/www/lib/plugins/textinsert/lang/de/lang.php b/platform/www/lib/plugins/textinsert/lang/de/lang.php
new file mode 100644
index 0000000..907fe6f
--- /dev/null
+++ b/platform/www/lib/plugins/textinsert/lang/de/lang.php
@@ -0,0 +1,22 @@
+<?php
+
+/**
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ * @author Thor Weinreich <thorweinreich@nefkom.net>
+ */
+$lang['btn_add'] = 'Hinzufügen';
+$lang['btn_del'] = 'Löschen';
+$lang['btn_edit'] = 'Änderungen speichern';
+$lang['label_list'] = 'Makro-Liste';
+$lang['label_add'] = 'Makros hinzufügen';
+$lang['label_del'] = 'Makros löschen';
+$lang['label_edit'] = 'Makros bearbeiten';
+$lang['col_subst'] = 'Makros ersetzen';
+$lang['add_macros'] = 'Makros hinzufügen';
+$lang['delete_macros'] = 'Makros löschen';
+$lang['edit_macros'] = 'Makros bearbeiten';
+$lang['view_macros'] = 'Makro-Liste anzeigen';
+$lang['hide_macros'] = 'Makro-Liste verbergen';
+$lang['hide_info'] = 'Info schließen';
+$lang['show_info'] = 'Info anzeigen';
diff --git a/platform/www/lib/plugins/textinsert/lang/de/settings.php b/platform/www/lib/plugins/textinsert/lang/de/settings.php
new file mode 100644
index 0000000..ca9e0e1
--- /dev/null
+++ b/platform/www/lib/plugins/textinsert/lang/de/settings.php
@@ -0,0 +1,8 @@
+<?php
+
+/**
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ * @author Thor Weinreich <thorweinreich@nefkom.net>
+ */
+$lang['stdreplace'] = 'Standard Makro-Ersetzungen annehmen, wie sie in Namensraum-Templates benutzt werden';
diff --git a/platform/www/lib/plugins/textinsert/lang/en/intro.txt b/platform/www/lib/plugins/textinsert/lang/en/intro.txt
new file mode 100644
index 0000000..a89d5e5
--- /dev/null
+++ b/platform/www/lib/plugins/textinsert/lang/en/intro.txt
@@ -0,0 +1,16 @@
+This panel enables you to add and delete macros and their replacement texts, and to edit them after they have been saved. A macro name can contain letters, numbers, underscores, hyphens and periods. For example: ''Macro_one.txt''. The replacement texts can accept HTML and can be of any length.
+
+You can add up to six macros at a time. The sixth is a text area which will allow for extended texts.
+Enter the macro name in the **Macro** column and the texts which they represent in the
+**Substitution** column.
+
+
+Deletions are unlimited; check off the box(es) next the macro(s) to be deleted and click
+the **Delete** button at the bottom of the screen. Editing is done through the Edit screen, where you are presented
+with all your macros. You can edit the texts of any number of macros.
+
+
+The macro list will not refresh until after you have submitted your edits, additions, or deletions
+by clicking the appropriate button at the bottom of the screen.
+
+
diff --git a/platform/www/lib/plugins/textinsert/lang/en/lang.php b/platform/www/lib/plugins/textinsert/lang/en/lang.php
new file mode 100644
index 0000000..7a01bc6
--- /dev/null
+++ b/platform/www/lib/plugins/textinsert/lang/en/lang.php
@@ -0,0 +1,31 @@
+<?php
+/**
+ * English language file
+ *
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ * @author Myron turner <turnermm02@shaw.ca>
+ */
+
+// for admin plugins, the menu prompt to be displayed in the admin menu
+// if set here, the plugin doesn't need to override the getMenuText() method
+$lang['menu'] = 'TextInsert Macro Replacement Plugin';
+
+$lang['btn_add'] = 'Add';
+$lang['btn_del'] = 'Delete';
+$lang['btn_edit'] = 'Submit Edits';
+
+$lang['label_list'] = "Macro List";
+$lang['label_add'] = "Add Macros";
+$lang['label_del'] = "Delete Macros";
+$lang['label_edit'] = "Edit Macros";
+
+$lang['col_subst'] = 'Macro Substitution';
+$lang['add_macros'] = 'Add Macros';
+$lang['delete_macros'] = 'Delete Macros';
+$lang['edit_macros'] = 'Edit Macros';
+$lang['view_macros'] = 'View Macro List';
+$lang['hide_macros'] = 'Hide Macro List';
+$lang['hide_info'] = 'Close Info';
+$lang['show_info'] = 'Show Info';
+$lang['not_found'] = 'macro was not found in the macros database';
+
diff --git a/platform/www/lib/plugins/textinsert/lang/en/macros.php b/platform/www/lib/plugins/textinsert/lang/en/macros.php
new file mode 120000
index 0000000..21b1645
--- /dev/null
+++ b/platform/www/lib/plugins/textinsert/lang/en/macros.php
@@ -0,0 +1 @@
+../../../../../../i18n/textinsert_strings.php \ No newline at end of file
diff --git a/platform/www/lib/plugins/textinsert/lang/en/settings.php b/platform/www/lib/plugins/textinsert/lang/en/settings.php
new file mode 100644
index 0000000..4083a83
--- /dev/null
+++ b/platform/www/lib/plugins/textinsert/lang/en/settings.php
@@ -0,0 +1,3 @@
+<?php
+$lang['stdreplace'] = 'Accept standard macro replacements as used in namespace templates';
+$lang['farm'] = 'If this is a farm and you and would like each animal to have its own macro database, please select this option; otherwise all animals will share the same (i.e. the farmer\'s) database.';
diff --git a/platform/www/lib/plugins/textinsert/lang/es/intro.txt b/platform/www/lib/plugins/textinsert/lang/es/intro.txt
new file mode 100644
index 0000000..a89d5e5
--- /dev/null
+++ b/platform/www/lib/plugins/textinsert/lang/es/intro.txt
@@ -0,0 +1,16 @@
+This panel enables you to add and delete macros and their replacement texts, and to edit them after they have been saved. A macro name can contain letters, numbers, underscores, hyphens and periods. For example: ''Macro_one.txt''. The replacement texts can accept HTML and can be of any length.
+
+You can add up to six macros at a time. The sixth is a text area which will allow for extended texts.
+Enter the macro name in the **Macro** column and the texts which they represent in the
+**Substitution** column.
+
+
+Deletions are unlimited; check off the box(es) next the macro(s) to be deleted and click
+the **Delete** button at the bottom of the screen. Editing is done through the Edit screen, where you are presented
+with all your macros. You can edit the texts of any number of macros.
+
+
+The macro list will not refresh until after you have submitted your edits, additions, or deletions
+by clicking the appropriate button at the bottom of the screen.
+
+
diff --git a/platform/www/lib/plugins/textinsert/lang/es/lang.php b/platform/www/lib/plugins/textinsert/lang/es/lang.php
new file mode 100644
index 0000000..7a01bc6
--- /dev/null
+++ b/platform/www/lib/plugins/textinsert/lang/es/lang.php
@@ -0,0 +1,31 @@
+<?php
+/**
+ * English language file
+ *
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ * @author Myron turner <turnermm02@shaw.ca>
+ */
+
+// for admin plugins, the menu prompt to be displayed in the admin menu
+// if set here, the plugin doesn't need to override the getMenuText() method
+$lang['menu'] = 'TextInsert Macro Replacement Plugin';
+
+$lang['btn_add'] = 'Add';
+$lang['btn_del'] = 'Delete';
+$lang['btn_edit'] = 'Submit Edits';
+
+$lang['label_list'] = "Macro List";
+$lang['label_add'] = "Add Macros";
+$lang['label_del'] = "Delete Macros";
+$lang['label_edit'] = "Edit Macros";
+
+$lang['col_subst'] = 'Macro Substitution';
+$lang['add_macros'] = 'Add Macros';
+$lang['delete_macros'] = 'Delete Macros';
+$lang['edit_macros'] = 'Edit Macros';
+$lang['view_macros'] = 'View Macro List';
+$lang['hide_macros'] = 'Hide Macro List';
+$lang['hide_info'] = 'Close Info';
+$lang['show_info'] = 'Show Info';
+$lang['not_found'] = 'macro was not found in the macros database';
+
diff --git a/platform/www/lib/plugins/textinsert/lang/es/macros.php b/platform/www/lib/plugins/textinsert/lang/es/macros.php
new file mode 120000
index 0000000..21b1645
--- /dev/null
+++ b/platform/www/lib/plugins/textinsert/lang/es/macros.php
@@ -0,0 +1 @@
+../../../../../../i18n/textinsert_strings.php \ No newline at end of file
diff --git a/platform/www/lib/plugins/textinsert/lang/es/settings.php b/platform/www/lib/plugins/textinsert/lang/es/settings.php
new file mode 100644
index 0000000..4083a83
--- /dev/null
+++ b/platform/www/lib/plugins/textinsert/lang/es/settings.php
@@ -0,0 +1,3 @@
+<?php
+$lang['stdreplace'] = 'Accept standard macro replacements as used in namespace templates';
+$lang['farm'] = 'If this is a farm and you and would like each animal to have its own macro database, please select this option; otherwise all animals will share the same (i.e. the farmer\'s) database.';
diff --git a/platform/www/lib/plugins/textinsert/lang/fr/intro.txt b/platform/www/lib/plugins/textinsert/lang/fr/intro.txt
new file mode 100644
index 0000000..ce05cbd
--- /dev/null
+++ b/platform/www/lib/plugins/textinsert/lang/fr/intro.txt
@@ -0,0 +1,10 @@
+Ce panneau vous permet d'ajouter et supprimer des macros et leur textes de remplacement, de les modifier après leur enregistrement. Un nom de macro peut contenir des lettres, des nombres, des tirets bas, traits d'union et points. Par exemple : ''Macro_one.txt''. Le texte de remplacement peut accepter le HTML et peut être de n'importe quelle longueur.
+
+Vous pouvez ajouter jusqu'à six macros à la fois. Le sixième est une zone de texte qui permettra de longs textes.
+Entrez le nom de la macro dans la colonne **Macro** et les textes qu'ils représentent dans la colonne **Remplacement**.
+
+
+Les suppressions sont illimitées ; cochez la devant la macro à supprimer et cliquez sur le bouton **Supprimer** en bas de l'écran. La modification se fait à travers l'écran de modification, où vous sont présenté toutes vos macros. Vous pouvez modifier les textes de n'importe quel nombre de macros.
+
+
+La liste macro ne s'actualise pas jusqu'à ce que vous avez envoyer vos modifications, ajouts ou suppressions en cliquant sur le bouton approprié en bas de l'écran. \ No newline at end of file
diff --git a/platform/www/lib/plugins/textinsert/lang/fr/lang.php b/platform/www/lib/plugins/textinsert/lang/fr/lang.php
new file mode 100644
index 0000000..2570f93
--- /dev/null
+++ b/platform/www/lib/plugins/textinsert/lang/fr/lang.php
@@ -0,0 +1,25 @@
+<?php
+
+/**
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ * @author Hérisson grognon <dodoperso@laposte.net>
+ * @author Ner0lph <forums@ner0lph.tk>
+ */
+$lang['menu'] = 'TextInsert Macro Replacement Plugin';
+$lang['btn_add'] = 'Ajouter';
+$lang['btn_del'] = 'Supprimer';
+$lang['btn_edit'] = 'Envoyer les modifications';
+$lang['label_list'] = 'Liste des macros';
+$lang['label_add'] = 'Ajouter des macros';
+$lang['label_del'] = 'Supprimer des macros';
+$lang['label_edit'] = 'Modifier des macros';
+$lang['col_subst'] = 'Substitution de macro';
+$lang['add_macros'] = 'Ajouter des macros';
+$lang['delete_macros'] = 'Supprimer des macros';
+$lang['edit_macros'] = 'Modifier des macros';
+$lang['view_macros'] = 'Afficher la liste des macros';
+$lang['hide_macros'] = 'Cacher la liste des macros';
+$lang['hide_info'] = 'Fermer les informations';
+$lang['show_info'] = 'Montrer les informations';
+$lang['not_found'] = 'Le macro n\'a pas été trouvé dans la base de données des macros.';
diff --git a/platform/www/lib/plugins/textinsert/lang/fr/settings.php b/platform/www/lib/plugins/textinsert/lang/fr/settings.php
new file mode 100644
index 0000000..92bd788
--- /dev/null
+++ b/platform/www/lib/plugins/textinsert/lang/fr/settings.php
@@ -0,0 +1,10 @@
+<?php
+
+/**
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ * @author Ner0lph <forums@ner0lph.tk>
+ * @author ubibene <services.m@benard.info>
+ */
+$lang['stdreplace'] = 'Accepter des remplacements standards de macros en tant que modèles de catégorie';
+$lang['farm'] = 'Si ceci était une ferme et que vous souhaitez que chaque animal ait sa propre base de macros, sélectionnez cette option; sinon tous les animaux partageront la même base de données (c\'est à dire celle du fermier).';
diff --git a/platform/www/lib/plugins/textinsert/lang/ja/intro.txt b/platform/www/lib/plugins/textinsert/lang/ja/intro.txt
new file mode 100644
index 0000000..ef806fd
--- /dev/null
+++ b/platform/www/lib/plugins/textinsert/lang/ja/intro.txt
@@ -0,0 +1,16 @@
+この画面では、マクロと代替テキストの追加と削除、保存後の編集ができます。
+マクロ名は、文字・数字・アンダースコア・ハイフン・ピリオドを使用します。
+例:''Macro_one.txt''
+代替テキストは HTML も許容しますし、任意長です。
+
+一度に 6 マクロを追加できます。
+テキスト欄は拡張テキストを入力できます。
+**マクロ**欄にマクロ名、**置換**欄に代替テキストを入力します。
+
+削除に制限はありません。
+削除するマクロの隣にあるチェックを外し、画面下の**削除**ボタンをクリックします。
+編集は編集画面で行います。
+編集画面には登録されたマクロをすべて表示されています。
+複数のマクロのテキスト編集も可能です。
+
+画面下にあるボタンをクリックして編集・追加・削除を実行するまでマクロ一覧は更新されません。 \ No newline at end of file
diff --git a/platform/www/lib/plugins/textinsert/lang/ja/lang.php b/platform/www/lib/plugins/textinsert/lang/ja/lang.php
new file mode 100644
index 0000000..f9b95ba
--- /dev/null
+++ b/platform/www/lib/plugins/textinsert/lang/ja/lang.php
@@ -0,0 +1,23 @@
+<?php
+
+/**
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ * @author Hideaki SAWADA <chuno@live.jp>
+ */
+$lang['menu'] = 'TextInsert マクロ置換プラグイン';
+$lang['btn_add'] = '追加';
+$lang['btn_del'] = '削除';
+$lang['btn_edit'] = '編集の実行';
+$lang['label_list'] = 'マクロ一覧';
+$lang['label_add'] = 'マクロ追加';
+$lang['label_del'] = 'マクロ削除';
+$lang['label_edit'] = 'マクロ編集';
+$lang['col_subst'] = 'マクロ置換';
+$lang['add_macros'] = 'マクロ追加';
+$lang['delete_macros'] = 'マクロ削除';
+$lang['edit_macros'] = 'マクロ編集';
+$lang['view_macros'] = 'マクロ一覧を表示';
+$lang['hide_macros'] = 'マクロ一覧を非表示';
+$lang['hide_info'] = '解説を非表示';
+$lang['show_info'] = '解説を表示';
diff --git a/platform/www/lib/plugins/textinsert/lang/ja/settings.php b/platform/www/lib/plugins/textinsert/lang/ja/settings.php
new file mode 100644
index 0000000..c5ef2c1
--- /dev/null
+++ b/platform/www/lib/plugins/textinsert/lang/ja/settings.php
@@ -0,0 +1,9 @@
+<?php
+
+/**
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ * @author Hideaki SAWADA <chuno@live.jp>
+ */
+$lang['stdreplace'] = '名前空間テンプレートで使用されている標準マクロ置換を許容する';
+$lang['farm'] = 'この Wiki が牧場で各動物に独自のマクロデータベースを持たせたい場合、このオプションを選択してください。選択しない場合、全ての動物は同一の(つまり牧場主の)データベースを共有することになります。';
diff --git a/platform/www/lib/plugins/textinsert/lang/nl/intro.txt b/platform/www/lib/plugins/textinsert/lang/nl/intro.txt
new file mode 100644
index 0000000..8e35a1e
--- /dev/null
+++ b/platform/www/lib/plugins/textinsert/lang/nl/intro.txt
@@ -0,0 +1,11 @@
+Met dit luik kan je macro's en hun vervangingsteksten onderhouden: aanmaken, schrappen en bewerken nadat ze opgeslagen zijn. Een macro naam kan letters, cijfers, onderlijningsstreepjes, koppeltekens en punten bevatten. Voorbeeld: ''Macro_one.txt''. De vervangtekst kan in HTML zijn en gelijk welke lengte.
+
+Zes macro's tegelijk kan je toevoegen. De zesde is de tekstzone waar je uitgebreide teksten kan invoeren.
+Geef een macro naam in de **Macro** kolom en de tekst die hier aan gekoppeld is in de **Vervangteksten** kolom.
+
+Schrappen kan je onbeperkt uitvoeren. Vink het vakje of de vakjes aan naast de macro's die geschrapt moeten
+worden. Klik daarna op de **Schrappen** knop onderaan het scherm. Bewerken doe je via het bewerkingsscherm waar
+je alle macro's ziet staan. Je bewerkt teksten van zoveel macro's als je maar wil.
+
+De macro lijst wordt niet hernieuwd zolang de wijzigingen, toevoegingen, schrappingen niet doorgevoerd zijn
+door te klikken op de voorziene knop onderaan het scherm. \ No newline at end of file
diff --git a/platform/www/lib/plugins/textinsert/lang/nl/lang.php b/platform/www/lib/plugins/textinsert/lang/nl/lang.php
new file mode 100644
index 0000000..ce1b255
--- /dev/null
+++ b/platform/www/lib/plugins/textinsert/lang/nl/lang.php
@@ -0,0 +1,24 @@
+<?php
+
+/**
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ * @author Myron turner <turnermm02@shaw.ca>
+ * @author hugo smet <hugo.smet@scarlet.be>
+ */
+$lang['menu'] = 'TextInsert Macro Substitutie Plugin';
+$lang['btn_add'] = 'Toevoegen';
+$lang['btn_del'] = 'Schrappen';
+$lang['btn_edit'] = 'Wijzigingen doorvoeren';
+$lang['label_list'] = 'Macro Lijst';
+$lang['label_add'] = 'Macro\'s Toevoegen';
+$lang['label_del'] = 'Macro\'s Schrappen';
+$lang['label_edit'] = 'Macro\'s Bewerken';
+$lang['col_subst'] = 'Macro Vervangteksten';
+$lang['add_macros'] = 'Toevoegen';
+$lang['delete_macros'] = 'Schrappen';
+$lang['edit_macros'] = 'Bewerken';
+$lang['view_macros'] = 'Lijst Tonen';
+$lang['hide_macros'] = 'Lijst Verbergen';
+$lang['hide_info'] = 'Info Sluiten';
+$lang['show_info'] = 'Info Tonen';
diff --git a/platform/www/lib/plugins/textinsert/lang/nl/settings.php b/platform/www/lib/plugins/textinsert/lang/nl/settings.php
new file mode 100644
index 0000000..afb2901
--- /dev/null
+++ b/platform/www/lib/plugins/textinsert/lang/nl/settings.php
@@ -0,0 +1,8 @@
+<?php
+
+/**
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ * @author Hugo Smet <hugo.smet@scarlet.be>
+ */
+$lang['stdreplace'] = 'Standaard macro substituties aanvaarden zoals deze in naamruimte sjablonen gebruikt worden';
diff --git a/platform/www/lib/plugins/textinsert/lang/pt-br/intro.txt b/platform/www/lib/plugins/textinsert/lang/pt-br/intro.txt
new file mode 100644
index 0000000..974e84c
--- /dev/null
+++ b/platform/www/lib/plugins/textinsert/lang/pt-br/intro.txt
@@ -0,0 +1,13 @@
+Esse painel permite adicionar e excluir macros e seus textos de substituição e editá-los depois de salvos. Um nome de macro pode conter letras, números, sublinhados, hífens e pontos. Por exemplo: '' Macro_one.txt ''. Os textos de substituição podem aceitar HTML e podem ser de qualquer tamanho.
+
+Você pode adicionar até seis macros por vez. A sexta é uma área de texto que permitirá textos estendidos.
+Insira o nome da macro na coluna ** Macro ** e os textos que eles representam na
+** Substituição ** coluna.
+
+Deleções são ilimitadas; marque a caixa ao lado da(s) macro(s) a ser (em) excluída(s) e clique
+no botão ** Excluir ** na parte inferior da tela. A edição é feita através da tela Editar, onde você é apresentado
+com todas as suas macros. Você pode editar os textos de qualquer número de macros.
+
+
+A lista de macros não será atualizada até que você tenha enviado suas edições, adições ou exclusões
+clicando no botão apropriado na parte inferior da tela. \ No newline at end of file
diff --git a/platform/www/lib/plugins/textinsert/lang/pt-br/lang.php b/platform/www/lib/plugins/textinsert/lang/pt-br/lang.php
new file mode 100644
index 0000000..874bb9d
--- /dev/null
+++ b/platform/www/lib/plugins/textinsert/lang/pt-br/lang.php
@@ -0,0 +1,23 @@
+<?php
+
+/**
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ * @author Alexandre Belchior <alexbelchior@gmail.com>
+ */
+$lang['menu'] = 'Plugin de Substituição de Macro TextInsert';
+$lang['btn_add'] = 'Adicionar';
+$lang['btn_del'] = 'Delete';
+$lang['btn_edit'] = 'Enviar edições';
+$lang['label_list'] = 'Lista de Macro';
+$lang['label_add'] = 'Adicionar macros';
+$lang['label_del'] = 'Deletar macros';
+$lang['label_edit'] = 'Editar macros';
+$lang['col_subst'] = 'Substituição de macro';
+$lang['add_macros'] = 'Adicionar macros';
+$lang['delete_macros'] = 'Deletar macros';
+$lang['edit_macros'] = 'Editar macros';
+$lang['view_macros'] = 'Visualizar lista de macro';
+$lang['hide_macros'] = 'Ocultar lista de macro';
+$lang['hide_info'] = 'Fechar informação';
+$lang['show_info'] = 'Mostrar informação';
diff --git a/platform/www/lib/plugins/textinsert/lang/pt-br/settings.php b/platform/www/lib/plugins/textinsert/lang/pt-br/settings.php
new file mode 100644
index 0000000..db02fd6
--- /dev/null
+++ b/platform/www/lib/plugins/textinsert/lang/pt-br/settings.php
@@ -0,0 +1,9 @@
+<?php
+
+/**
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ * @author Alexandre Belchior <alexbelchior@gmail.com>
+ */
+$lang['stdreplace'] = 'Aceite substituições de macro padrão como usadas em modelos de namespace';
+$lang['farm'] = 'Se isto é uma fazenda e você e gostaria que cada animal tivesse seu próprio banco de dados de macros, selecione essa opção; caso contrário, todos os animais compartilharão o mesmo banco de dados (ou seja, do fazendeiro).';
diff --git a/platform/www/lib/plugins/textinsert/lang/ru/intro.txt b/platform/www/lib/plugins/textinsert/lang/ru/intro.txt
new file mode 100644
index 0000000..e432c6b
--- /dev/null
+++ b/platform/www/lib/plugins/textinsert/lang/ru/intro.txt
@@ -0,0 +1,10 @@
+Панель позволяет добавлять, редактировать и удалять макросы и их подстановки. Имена макросов могут содержать буквы, цифры, символ подчёркивания, дефис и точку. Пример: ''Macro_one.txt''. Подстановки неограничены в длине и могут содержать HTML-разметку.
+
+В режиме //добавления// можно создать до шести макросов. Шестое текстовое поле предназначено для длинных текстов. В колонке **Macro** введите имя нового макроса, а в колонке **Подстановки** --- замещающий текст.
+
+В режиме //удаления// появляются флажки для выбора удаляемых макросов. Разовое количество удалений не ограничено.
+
+В режиме //редактирования// все поля подстановок открываются для исправления. Разовое количество исправлений не ограничено.
+
+Список макросов обновится только после нажатия соответствующей функциональной кнопки внизу списка.
+
diff --git a/platform/www/lib/plugins/textinsert/lang/ru/lang.php b/platform/www/lib/plugins/textinsert/lang/ru/lang.php
new file mode 100644
index 0000000..25e2fe7
--- /dev/null
+++ b/platform/www/lib/plugins/textinsert/lang/ru/lang.php
@@ -0,0 +1,25 @@
+<?php
+
+/**
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ * @author Aleksandr Selivanov <alexgearbox@yandex.ru>
+ * @author RainbowSpike <1@2.ru>
+ */
+$lang['menu'] = 'Плагин макрозамен TextInsert';
+$lang['btn_add'] = 'Добавить';
+$lang['btn_del'] = 'Удалить';
+$lang['btn_edit'] = 'Подтвердить изменения';
+$lang['label_list'] = 'Список макросов';
+$lang['label_add'] = 'Добавить макросы';
+$lang['label_del'] = 'Удалить макросы';
+$lang['label_edit'] = 'Изменить макросы';
+$lang['col_subst'] = 'Подстановки';
+$lang['add_macros'] = 'Добавить макросы';
+$lang['delete_macros'] = 'Удалить макросы';
+$lang['edit_macros'] = 'Изменить макросы';
+$lang['view_macros'] = 'Показать список макросов';
+$lang['hide_macros'] = 'Скрыть список макросов';
+$lang['hide_info'] = 'Скрыть справку';
+$lang['show_info'] = 'Показать справку';
+$lang['not_found'] = 'Макрос не найден в перечне макросов';
diff --git a/platform/www/lib/plugins/textinsert/lang/ru/settings.php b/platform/www/lib/plugins/textinsert/lang/ru/settings.php
new file mode 100644
index 0000000..07a8850
--- /dev/null
+++ b/platform/www/lib/plugins/textinsert/lang/ru/settings.php
@@ -0,0 +1,9 @@
+<?php
+
+/**
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ * @author RainbowSpike <1@2.ru>
+ * @author Aleksandr Selivanov <alexgearbox@yandex.ru>
+ */
+$lang['stdreplace'] = 'Допускать стандартные переменные, используемые для шаблонов пространств имён';
diff --git a/platform/www/lib/plugins/textinsert/lang/zh/intro.txt b/platform/www/lib/plugins/textinsert/lang/zh/intro.txt
new file mode 100644
index 0000000..8846119
--- /dev/null
+++ b/platform/www/lib/plugins/textinsert/lang/zh/intro.txt
@@ -0,0 +1,11 @@
+通过此面板,您可以添加、删除宏及其替换文本,保存后也可以在次对其进行编辑。 宏名称可以包含字母,数字,下划线,连字符和句号。 例如:''Macro_one.txt''。替换文本支持 HTML,且不限制长度。
+
+您一次最多可以添加六个宏。 第六个是允许扩展文本的文本区域。
+在**宏**列表中输入宏的名称,并在另一边中输入它们的**替换**文本。
+
+
+选中要删除的宏旁边的框,然后单击屏幕底部的**删除**按钮,即可删除宏。
+编辑是通过编辑界面完成的,这个界面会显示你所有的宏。您可以编辑任意数量的宏的内容。
+
+
+在您通过屏幕底部的相应按钮提交编辑、添加或删除之后,宏列表才会刷新。 \ No newline at end of file
diff --git a/platform/www/lib/plugins/textinsert/lang/zh/lang.php b/platform/www/lib/plugins/textinsert/lang/zh/lang.php
new file mode 100644
index 0000000..09d578a
--- /dev/null
+++ b/platform/www/lib/plugins/textinsert/lang/zh/lang.php
@@ -0,0 +1,23 @@
+<?php
+
+/**
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ * @author RainSlide <RainSlide@outlook.com>
+ */
+$lang['menu'] = 'TextInsert 宏插件';
+$lang['btn_add'] = '添加';
+$lang['btn_del'] = '删除';
+$lang['btn_edit'] = '提交编辑';
+$lang['label_list'] = '宏列表';
+$lang['label_add'] = '添加宏';
+$lang['label_del'] = '删除宏';
+$lang['label_edit'] = '编辑宏';
+$lang['col_subst'] = '替换文本';
+$lang['add_macros'] = '添加宏';
+$lang['delete_macros'] = '删除宏';
+$lang['edit_macros'] = '编辑宏';
+$lang['view_macros'] = '显示宏列表';
+$lang['hide_macros'] = '隐藏宏列表';
+$lang['hide_info'] = '关闭信息';
+$lang['show_info'] = '显示信息';
diff --git a/platform/www/lib/plugins/textinsert/manager.dat b/platform/www/lib/plugins/textinsert/manager.dat
new file mode 100644
index 0000000..a12c912
--- /dev/null
+++ b/platform/www/lib/plugins/textinsert/manager.dat
@@ -0,0 +1,2 @@
+downloadurl=https://github.com/turnermm/TextInsert/zipball/master
+installed=Thu, 29 Apr 2021 06:46:29 -0300
diff --git a/platform/www/lib/plugins/textinsert/plugin.info.txt b/platform/www/lib/plugins/textinsert/plugin.info.txt
new file mode 100644
index 0000000..ae031d0
--- /dev/null
+++ b/platform/www/lib/plugins/textinsert/plugin.info.txt
@@ -0,0 +1,9 @@
+base textinsert
+author Myron Turner
+email turnermm02@shaw.ca
+date 2019-10-03
+name textinsert Plugin
+desc replace macros with text subsitutions
+url https://www.dokuwiki.org/plugin:textinsert
+
+
diff --git a/platform/www/lib/plugins/textinsert/syntax.php b/platform/www/lib/plugins/textinsert/syntax.php
new file mode 100644
index 0000000..82a60b6
--- /dev/null
+++ b/platform/www/lib/plugins/textinsert/syntax.php
@@ -0,0 +1,261 @@
+<?php
+/**
+ *
+ *
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ * @author Myron Turner <turnermm02@shaw.ca>
+ *
+ */
+// must be run within Dokuwiki
+if(!defined('DOKU_INC')) die();
+
+if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/');
+require_once(DOKU_PLUGIN.'syntax.php');
+define('REPLACE_DIR', DOKU_INC . 'data/meta/macros/');
+define('MACROS_FILE', REPLACE_DIR . 'macros.ser');
+
+
+/**
+ * All DokuWiki plugins to extend the parser/rendering mechanism
+ * need to inherit from this class
+ */
+class syntax_plugin_textinsert extends DokuWiki_Syntax_Plugin {
+ var $macros;
+ var $translations;
+ var $ns;
+ /**
+ * return some info
+ */
+
+ /**
+ * What kind of syntax are we?
+ */
+ function getType(){
+ return 'substition';
+ }
+
+ /**
+ * Where to sort in?
+ */
+ function getSort(){
+ return 155;
+ }
+
+
+ /**
+ * Connect pattern to lexer
+ */
+ function connectTo($mode) {
+ $this->Lexer->addSpecialPattern('#@\!?[\w\-\._]+\!?@#',$mode,'plugin_textinsert');
+ $this->Lexer->addSpecialPattern('#@\!\![\w\-\._]+@#',$mode,'plugin_textinsert');
+ $this->Lexer->addSpecialPattern('#@[\w\-\._]+~.*?~@#',$mode,'plugin_textinsert');
+ $this->Lexer->addSpecialPattern('#@[\w\-\._]+[\r\n]+~[^\r\n]+~@#',$mode,'plugin_textinsert');
+ }
+
+
+ /**
+ * Handle the match
+ */
+ function handle($match, $state, $pos, Doku_Handler $handler){
+
+ $html=false;
+ $translation = false;
+ $match = substr($match,2,-2);
+ $match = trim($match);
+ if(strpos($match, 'HTML')) $html=true;
+ if(strpos($match, 'LANG_') !== false) {
+ $translation=true;
+ list($prefix,$trans) = explode('_',$match,2);
+ }
+
+ global $ID;
+ list($ns,$rest) = explode(':',$ID,2);
+ if(@file_exists($filename = DOKU_PLUGIN . "textinsert/lang/$ns/lang.php")) {
+ include $filename;
+ $this->translations = $lang;
+
+ }
+
+ if(@file_exists($filename = DOKU_PLUGIN . "textinsert/lang/$ns/macros.php")) {
+ include $filename;
+ $ar = 'lang_' .$ns;
+ $tr = $$ar;
+ if($this->translations) {
+ $this->translations = array_merge($lang,$tr);
+ }
+ else $this->translations = $tr;
+ }
+
+ if(!empty($ns)) {
+ $this->ns = $ns;
+ }
+ $this->macros = $this->get_macros();
+
+
+
+ while(preg_match('#(\*\*|//|__|\'\').*?\1#m',$match )) {
+ $match = preg_replace_callback(
+ '#(\*\*|//|__|\'\')(.*?)(\1)#',
+ function($matches) {
+ $matches[1] = str_replace(array('**','//','__','\'\'',),array('<b>','<em>','<u>','<code>'),$matches[1]);
+ $matches[3] = str_replace(array('**','//','__','\'\''),array('</b>','</em>','</u>','</code>'),$matches[3]);
+ return $matches[1] . $matches[2] . $matches[3];
+ },$match );
+ }
+
+ if(preg_match('/(.*?)~([\s\S]+)~$/',$match,$subtitution)) {
+ $match=$subtitution[1];
+ $subtitution[2] = str_replace('\\,','&#44;',$subtitution[2]);
+ $substitutions=explode(',',$subtitution[2]);
+ $substitutions = preg_replace('#\/\/.+#',"",$substitutions);
+ $substitutions = preg_replace('#\\\n#',"<br />",$substitutions);
+ }
+
+ if(!array_key_exists($match, $this->macros) ) {
+ $err = $this->getLang('not_found');
+ msg("$match $err", -1);
+ $match = "";
+ }
+ else {
+ if($translation && isset($this->translations[$trans])){
+ $match = $this->translations[$trans];
+ }
+ else {
+ $match =$this->macros[$match];
+ }
+ }
+
+ if(!is_array($substitutions)) $substitutions = array();
+ for($i=0; $i<count($substitutions); $i++) {
+ $search = '%' . ($i+1);
+ $match = str_replace ($search , trim($substitutions[$i]), $match);
+ }
+
+ $match = $this->get_inserts($match,$translation);
+
+ if($html) {
+ $match = str_replace('&lt;','<',$match);
+ $match = str_replace('&gt;','>',$match);
+ }
+
+ return array($state,$match);
+ }
+
+ /**
+ * Create output
+ */
+ function render($mode, Doku_Renderer $renderer, $data) {
+ global $INFO;
+ if($mode == 'xhtml'){
+ list($state, $word) = $data;
+ If(strpos($word,'_ID_') !== false ) {
+ $word = str_replace('_ID_',$INFO['id'], $word);
+ }
+ $renderer->doc .= $word;
+ return true;
+ }
+ return false;
+ }
+
+ function get_macros() {
+ $a = array();
+ if(file_exists(MACROS_FILE)) {
+ $a = unserialize(file_get_contents(MACROS_FILE));
+ }
+ else if($this->getConf('farm')) {
+ $a = unserialize(file_get_contents(metaFN('macros','.ser')));
+ }
+ $r = $this->get_std_replacements() ;
+ $result = array_merge($r,$a);
+ return array_merge($r,$a);
+ }
+
+ function get_inserts($match,$translation) {
+ $inserts = array();
+
+ // replace embedded macros
+ if(preg_match_all('/#@(.*?)@#/',$match,$inserts)) {
+ $keys = $inserts[1];
+ $pats = $inserts[0];
+
+ for($i=0; $i<count($keys); $i++) {
+ $insert = $this->macros[$keys[$i]];
+ if($translation ||strpos($keys[$i], 'LANG_') !== false) {
+ list($prefix,$trans) = explode('_',$keys[$i],2);
+ $_insert = $this->translations[$trans];
+ if($_insert) $insert =$_insert;
+ }
+ $match = str_replace($pats[$i],$insert,$match);
+ }
+
+ } // end replace embedded macros
+
+
+ $entities = getEntities();
+ $e_keys = array_keys($entities);
+ $e_values = array_values($entities);
+ $match = str_replace($e_keys,$e_values,$match);
+
+ return $match;
+ }
+
+ function get_std_replacements() {
+ if(!$this->getConf('stdreplace')) return array();
+ global $conf;
+ global $INFO;
+ global $ID;
+
+ $file = noNS($ID);
+ $page = cleanID($file) ;
+
+ $names =array(
+ 'ID',
+ 'NS',
+ 'FILE',
+ '!FILE',
+ '!FILE!',
+ 'PAGE',
+ '!PAGE',
+ '!!PAGE',
+ '!PAGE!',
+ 'USER',
+ 'DATE',
+ '_ID_'
+ );
+
+ $values = array(
+ $ID,
+ getNS($ID),
+ $file,
+ utf8_ucfirst($file),
+ utf8_strtoupper($file),
+ $page,
+ utf8_ucfirst($page),
+ utf8_ucwords($page),
+ utf8_strtoupper($page),
+ $_SERVER['REMOTE_USER'],
+ strftime($conf['dformat'], time()),
+ '_ID_'
+ );
+ $std_replacements = array();
+ for($i=0; $i<count($names) ; $i++) {
+ $std_replacements[$names[$i]] = $values[$i];
+ }
+
+ return $std_replacements;
+}
+
+ function write_debug($what, $screen = false) {
+ return;
+ $what=print_r($what,true);
+ if($screen) {
+ msg('<pre>' . $what . '</pre>');
+ return;
+ }
+ $handle=fopen("textinsert.txt",'a');
+ fwrite($handle,"$what\n");
+ fclose($handle);
+ }
+}
+
+
diff --git a/platform/www/lib/plugins/textinsert/version b/platform/www/lib/plugins/textinsert/version
new file mode 100644
index 0000000..4fb8186
--- /dev/null
+++ b/platform/www/lib/plugins/textinsert/version
@@ -0,0 +1,2 @@
+19-Oct_03-20_19
+
diff --git a/platform/www/lib/plugins/translation/.travis.yml b/platform/www/lib/plugins/translation/.travis.yml
new file mode 100644
index 0000000..c7eb5a6
--- /dev/null
+++ b/platform/www/lib/plugins/translation/.travis.yml
@@ -0,0 +1,13 @@
+# Config file for travis-ci.org
+
+language: php
+php:
+ - "7.1"
+ - "7.0"
+ - "5.6"
+env:
+ - DOKUWIKI=master
+ - DOKUWIKI=stable
+before_install: wget https://raw.github.com/splitbrain/dokuwiki-travis/master/travis.sh
+install: sh travis.sh
+script: cd _test && phpunit --stderr --group plugin_translation
diff --git a/platform/www/lib/plugins/translation/README b/platform/www/lib/plugins/translation/README
new file mode 100644
index 0000000..1a5b5ef
--- /dev/null
+++ b/platform/www/lib/plugins/translation/README
@@ -0,0 +1,25 @@
+translation Plugin for DokuWiki
+
+All documentation for this plugin can be found at
+http://www.dokuwiki.org/plugin:translation
+
+If you install this plugin manually, make sure it is installed in
+lib/plugins/translation/ - if the folder is called different it
+will not work!
+
+Please refer to http://www.dokuwiki.org/plugins for additional info
+on how to install plugins in DokuWiki.
+
+----
+Copyright (C) Andreas Gohr <andi@splitbrain.org>
+
+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; version 2 of the License
+
+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.
+
+See the COPYING file in your DokuWiki folder for details
diff --git a/platform/www/lib/plugins/translation/_test/basic.test.php b/platform/www/lib/plugins/translation/_test/basic.test.php
new file mode 100644
index 0000000..870c7a7
--- /dev/null
+++ b/platform/www/lib/plugins/translation/_test/basic.test.php
@@ -0,0 +1,113 @@
+<?php
+
+/**
+ * General tests for the translation plugin
+ *
+ * @group plugin_translation
+ * @group plugins
+ */
+class basic_plugin_translation_test extends DokuWikiTest {
+
+ protected $pluginsEnabled = array('translation');
+
+ public static function buildTransID_testdata() {
+ return array(
+ array(
+ 'en',
+ 'ns:page',
+ 'de es',
+ array(':ns:page', 'en'),
+ ),
+ array(
+ '',
+ 'ns:page',
+ 'de es',
+ array(':ns:page', 'en'),
+ ),
+ array(
+ 'de',
+ 'ns:page',
+ 'de es',
+ array(':de:ns:page', 'de'),
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider buildTransID_testdata
+ *
+ * @param $inputLang
+ * @param $inputID
+ * @param $translationsOption
+ * @param $expected
+ */
+ public function test_buildTransID($inputLang, $inputID, $translationsOption, $expected) {
+ global $conf;
+ $conf['plugin']['translation']['translations'] = $translationsOption;
+ /** @var helper_plugin_translation $helper */
+ $helper = plugin_load('helper', 'translation', true);
+
+ $actual_result = $helper->buildTransID($inputLang, $inputID);
+
+ $this->assertEquals($expected, $actual_result);
+ }
+
+
+ public static function redirectStart_testdata() {
+ return array(
+ array(
+ 'start',
+ 'de es',
+ 'de,en-US;q=0.8,en;q=0.5,fr;q=0.3',
+ ':de:start',
+ 'redirect to translated page',
+ ),
+ array(
+ 'start',
+ 'de es',
+ 'en-US,de;q=0.8,en;q=0.5,fr;q=0.3',
+ array(),
+ 'do not redirect if basic namespace is correct lang',
+ ),
+ array(
+ 'de:start',
+ 'en de es',
+ 'en-US,en;q=0.8,fr;q=0.5',
+ array(),
+ 'do not redirect anything other than exactly $conf[\'start\']',
+ ),
+ );
+ }
+
+
+ /**
+ * @dataProvider redirectStart_testdata
+ *
+ * @param $input
+ * @param $translationsOption
+ * @param $httpAcceptHeader
+ * @param $expected
+ */
+ public function test_redirectStart($input, $translationsOption, $httpAcceptHeader, $expected, $msg) {
+ global $conf;
+ $conf['plugin']['translation']['translations'] = $translationsOption;
+ $conf['plugin']['translation']['redirectstart'] = 1;
+
+ /** @var helper_plugin_translation $helper */
+ $helper = plugin_load('helper', 'translation');
+ $helper->loadTranslationNamespaces();
+
+ $request = new TestRequest();
+ $request->setServer('HTTP_ACCEPT_LANGUAGE', $httpAcceptHeader);
+
+ $response = $request->get(array('id' => $input));
+ $actual = $response->getHeader('Location');
+
+ if (is_string($actual)) {
+ list(, $actual) = explode('doku.php?id=', $actual);
+ }
+
+ $this->assertEquals($expected, $actual, $msg);
+ }
+
+}
diff --git a/platform/www/lib/plugins/translation/_test/general.test.php b/platform/www/lib/plugins/translation/_test/general.test.php
new file mode 100644
index 0000000..ec1675c
--- /dev/null
+++ b/platform/www/lib/plugins/translation/_test/general.test.php
@@ -0,0 +1,61 @@
+<?php
+/**
+ * General tests for the translation plugin
+ *
+ * @group plugin_translation
+ * @group plugins
+ */
+class general_plugin_translation_test extends DokuWikiTest {
+
+ /**
+ * Simple test to make sure the plugin.info.txt is in correct format
+ */
+ public function test_plugininfo() {
+ $file = __DIR__.'/../plugin.info.txt';
+ $this->assertFileExists($file);
+
+ $info = confToHash($file);
+
+ $this->assertArrayHasKey('base', $info);
+ $this->assertArrayHasKey('author', $info);
+ $this->assertArrayHasKey('email', $info);
+ $this->assertArrayHasKey('date', $info);
+ $this->assertArrayHasKey('name', $info);
+ $this->assertArrayHasKey('desc', $info);
+ $this->assertArrayHasKey('url', $info);
+
+ $this->assertEquals('translation', $info['base']);
+ $this->assertRegExp('/^https?:\/\//', $info['url']);
+ $this->assertTrue(mail_isvalid($info['email']));
+ $this->assertRegExp('/^\d\d\d\d-\d\d-\d\d$/', $info['date']);
+ $this->assertTrue(false !== strtotime($info['date']));
+ }
+
+ /**
+ * Test to ensure that every conf['...'] entry in conf/default.php has a corresponding meta['...'] entry in
+ * conf/metadata.php.
+ */
+ public function test_plugin_conf() {
+ $conf_file = __DIR__.'/../conf/default.php';
+ if (file_exists($conf_file)){
+ include($conf_file);
+ }
+ $meta_file = __DIR__.'/../conf/metadata.php';
+ if (file_exists($meta_file)) {
+ include($meta_file);
+ }
+
+ $this->assertEquals(gettype($conf), gettype($meta),'Both ' . DOKU_PLUGIN . 'translation/conf/default.php and ' . DOKU_PLUGIN . 'translation/conf/metadata.php have to exist and contain the same keys.');
+
+ if (gettype($conf) != 'NULL' && gettype($meta) != 'NULL') {
+ foreach($conf as $key => $value) {
+ $this->assertArrayHasKey($key, $meta, 'Key $meta[\'' . $key . '\'] missing in ' . DOKU_PLUGIN . 'translation/conf/metadata.php');
+ }
+
+ foreach($meta as $key => $value) {
+ $this->assertArrayHasKey($key, $conf, 'Key $conf[\'' . $key . '\'] missing in ' . DOKU_PLUGIN . 'translation/conf/default.php');
+ }
+ }
+
+ }
+}
diff --git a/platform/www/lib/plugins/translation/action.php b/platform/www/lib/plugins/translation/action.php
new file mode 100644
index 0000000..6c79a70
--- /dev/null
+++ b/platform/www/lib/plugins/translation/action.php
@@ -0,0 +1,302 @@
+<?php
+/**
+ * Translation Plugin: Simple multilanguage plugin
+ *
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @author Guy Brand <gb@isis.u-strasbg.fr>
+ */
+
+// must be run within Dokuwiki
+if(!defined('DOKU_INC')) die();
+
+/**
+ * Class action_plugin_translation
+ */
+class action_plugin_translation extends DokuWiki_Action_Plugin {
+
+ /**
+ * For the helper plugin
+ * @var helper_plugin_translation
+ */
+ var $helper = null;
+
+ var $locale;
+
+ /**
+ * Constructor. Load helper plugin
+ */
+ function __construct() {
+ $this->helper = plugin_load('helper', 'translation');
+ }
+
+ /**
+ * Registers a callback function for a given event
+ *
+ * @param Doku_Event_Handler $controller
+ */
+ function register(Doku_Event_Handler $controller) {
+ $scriptName = basename($_SERVER['PHP_SELF']);
+
+ // should the lang be applied to UI?
+ if($this->getConf('translateui')) {
+ switch($scriptName) {
+ case 'js.php':
+ $controller->register_hook('INIT_LANG_LOAD', 'BEFORE', $this, 'translation_js');
+ $controller->register_hook('JS_CACHE_USE', 'BEFORE', $this, 'translation_jscache');
+ break;
+
+ case 'ajax.php':
+ $controller->register_hook('INIT_LANG_LOAD', 'BEFORE', $this, 'translate_media_manager');
+ break;
+
+ case 'mediamanager.php':
+ $controller->register_hook('TPL_METAHEADER_OUTPUT', 'BEFORE', $this, 'setJsCacheKey');
+ break;
+
+ default:
+ $controller->register_hook('TPL_METAHEADER_OUTPUT', 'BEFORE', $this, 'setJsCacheKey');
+ }
+ }
+
+ if($scriptName !== 'js.php' && $scriptName !== 'ajax.php') {
+ $controller->register_hook('DOKUWIKI_STARTED', 'BEFORE', $this, 'translation_hook');
+ $controller->register_hook('DETAIL_STARTED', 'BEFORE', $this, 'translation_hook');
+ $controller->register_hook('MEDIAMANAGER_STARTED', 'BEFORE', $this, 'translation_hook');
+ }
+
+ $controller->register_hook('SEARCH_QUERY_PAGELOOKUP', 'AFTER', $this, 'translation_search');
+ $controller->register_hook('COMMON_PAGETPL_LOAD', 'AFTER', $this, 'page_template_replacement');
+ }
+
+ /**
+ * Hook Callback. Make current language available as page template placeholder and handle
+ * original language copying
+ *
+ * @param Doku_Event $event
+ * @param $args
+ */
+ function page_template_replacement(Doku_Event $event, $args) {
+ global $ID;
+
+ // load orginal content as template?
+ if($this->getConf('copytrans') && $this->helper->istranslatable($ID, false)) {
+ // look for existing translations
+ $translations = $this->helper->getAvailableTranslations($ID);
+ if($translations) {
+ // find original language (might've been provided via parameter or use first translation)
+ $orig = (string) $_REQUEST['fromlang'];
+ if(!$orig) $orig = array_shift(array_keys($translations));
+
+ // load file
+ $origfile = $translations[$orig];
+ $event->data['tpl'] = io_readFile(wikiFN($origfile));
+
+ // prefix with warning
+ $warn = io_readFile($this->localFN('totranslate'));
+ if($warn) $warn .= "\n\n";
+ $event->data['tpl'] = $warn . $event->data['tpl'];
+
+ // show user a choice of translations if any
+ if(count($translations) > 1) {
+ $links = array();
+ foreach($translations as $t => $l) {
+ $links[] = '<a href="' . wl($ID, array('do' => 'edit', 'fromlang' => $t)) . '">' . $this->helper->getLocalName($t) . '</a>';
+ }
+
+ msg(
+ sprintf(
+ $this->getLang('transloaded'),
+ $this->helper->getLocalName($orig),
+ join(', ', $links)
+ )
+ );
+ }
+
+ }
+ }
+
+ // apply placeholders
+ $event->data['tpl'] = str_replace('@LANG@', $this->helper->realLC(''), $event->data['tpl']);
+ $event->data['tpl'] = str_replace('@TRANS@', $this->helper->getLangPart($ID), $event->data['tpl']);
+ }
+
+ /**
+ * Hook Callback. Load correct translation when loading JavaScript
+ *
+ * @param Doku_Event $event
+ * @param $args
+ */
+ function translation_js(Doku_Event $event, $args) {
+ global $conf;
+ if(!isset($_GET['lang'])) return;
+ if(!in_array($_GET['lang'], $this->helper->translations)) return;
+ $lang = $_GET['lang'];
+ $event->data = $lang;
+ $conf['lang'] = $lang;
+ }
+
+ /**
+ * Hook Callback. Pass language code to JavaScript dispatcher
+ *
+ * @param Doku_Event $event
+ * @param $args
+ * @return bool
+ */
+ function setJsCacheKey(Doku_Event $event, $args) {
+ if(!isset($this->locale)) return false;
+ $count = count($event->data['script']);
+ for($i = 0; $i < $count; $i++) {
+ if(strpos($event->data['script'][$i]['src'], '/lib/exe/js.php') !== false) {
+ $event->data['script'][$i]['src'] .= '&lang=' . hsc($this->locale);
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Hook Callback. Make sure the JavaScript is translation dependent
+ *
+ * @param Doku_Event $event
+ * @param $args
+ */
+ function translation_jscache(Doku_Event $event, $args) {
+ if(!isset($_GET['lang'])) return;
+ if(!in_array($_GET['lang'], $this->helper->translations)) return;
+
+ $lang = $_GET['lang'];
+ // reuse the constructor to reinitialize the cache key
+ if(method_exists($event->data, '__construct')) {
+ // New PHP 5 style constructor
+ $event->data->__construct(
+ $event->data->key . $lang,
+ $event->data->ext
+ );
+ } else {
+ // Old PHP 4 style constructor - deprecated
+ $event->data->cache(
+ $event->data->key . $lang,
+ $event->data->ext
+ );
+ }
+ }
+
+ /**
+ * Hook Callback. Translate the AJAX loaded media manager
+ *
+ * @param Doku_Event $event
+ * @param $args
+ */
+ function translate_media_manager(Doku_Event $event, $args) {
+ global $conf;
+ if(isset($_REQUEST['ID'])) {
+ $id = getID();
+ $lc = $this->helper->getLangPart($id);
+ } elseif(isset($_SESSION[DOKU_COOKIE]['translationlc'])) {
+ $lc = $_SESSION[DOKU_COOKIE]['translationlc'];
+ } else {
+ return;
+ }
+ if(!$lc) return;
+
+ $conf['lang'] = $lc;
+ $event->data = $lc;
+ }
+
+ /**
+ * Hook Callback. Change the UI language in foreign language namespaces
+ *
+ * @param Doku_Event $event
+ * @param $args
+ * @return bool
+ */
+ function translation_hook(Doku_Event $event, $args) {
+ global $ID;
+ /** @noinspection PhpUnusedLocalVariableInspection we include the language file later on */
+ global $lang;
+ global $conf;
+ global $ACT;
+ // redirect away from start page?
+ if($this->getConf('redirectstart') && $ID == $conf['start'] && $ACT == 'show') {
+ $lc = $this->helper->getBrowserLang();
+
+ list($translatedStartpage,) = $this->helper->buildTransID($lc, $conf['start']);
+ if (cleanID($translatedStartpage) !== cleanID($ID)) {
+ send_redirect(wl($translatedStartpage, '', true));
+ }
+ }
+
+ // check if we are in a foreign language namespace
+ $lc = $this->helper->getLangPart($ID);
+
+ // store language in session (for page related views only)
+ if(in_array($ACT, array('show', 'recent', 'diff', 'edit', 'preview', 'source', 'subscribe'))) {
+ $_SESSION[DOKU_COOKIE]['translationlc'] = $lc;
+ }
+ if(!$lc) $lc = $_SESSION[DOKU_COOKIE]['translationlc'];
+ if(!$lc) return false;
+ $this->locale = $lc;
+
+ if(!$this->getConf('translateui')) {
+ return true;
+ }
+
+ if(file_exists(DOKU_INC . 'inc/lang/' . $lc . '/lang.php')) {
+ require(DOKU_INC . 'inc/lang/' . $lc . '/lang.php');
+ }
+ $conf['lang_before_translation'] = $conf['lang']; //store for later access in syntax plugin
+ $conf['lang'] = $lc;
+
+ return true;
+ }
+
+ /**
+ * Hook Callback. Resort page match results so that results are ordered by translation, having the
+ * default language first
+ *
+ * @param Doku_Event $event
+ * @param $args
+ */
+ function translation_search(Doku_Event $event, $args) {
+
+ if($event->data['has_titles']) {
+ // sort into translation slots
+ $res = array();
+ foreach($event->result as $r => $t) {
+ $tr = $this->helper->getLangPart($r);
+ if(!is_array($res["x$tr"])) $res["x$tr"] = array();
+ $res["x$tr"][] = array($r, $t);
+ }
+ // sort by translations
+ ksort($res);
+ // combine
+ $event->result = array();
+ foreach($res as $r) {
+ foreach($r as $l) {
+ $event->result[$l[0]] = $l[1];
+ }
+ }
+ } else {
+ # legacy support for old DokuWiki hooks
+
+ // sort into translation slots
+ $res = array();
+ foreach($event->result as $r) {
+ $tr = $this->helper->getLangPart($r);
+ if(!is_array($res["x$tr"])) $res["x$tr"] = array();
+ $res["x$tr"][] = $r;
+ }
+ // sort by translations
+ ksort($res);
+ // combine
+ $event->result = array();
+ foreach($res as $r) {
+ $event->result = array_merge($event->result, $r);
+ }
+ }
+ }
+
+}
+
+//Setup VIM: ex: et ts=4 :
diff --git a/platform/www/lib/plugins/translation/admin.php b/platform/www/lib/plugins/translation/admin.php
new file mode 100644
index 0000000..f8d95a1
--- /dev/null
+++ b/platform/www/lib/plugins/translation/admin.php
@@ -0,0 +1,101 @@
+<?php
+
+// must be run within Dokuwiki
+if(!defined('DOKU_INC')) die();
+
+class admin_plugin_translation extends DokuWiki_Admin_Plugin {
+ function forAdminOnly() {
+ return false;
+ }
+
+ function handle() {
+ }
+
+ function html() {
+
+ /** @var helper_plugin_translation $helper */
+ $helper = plugin_load('helper', "translation");
+ $default_language = $helper->defaultlang;
+
+ /** @var Doku_Renderer_xhtml $xhtml_renderer */
+ $xhtml_renderer = p_get_renderer('xhtml');
+
+ echo "<h1>" . $this->getLang("menu") . "</h1>";
+ echo "<table id='outdated_translations' class=\"inline\">";
+ echo "<tr><th>default: $default_language</th>";
+ if ($this->getConf('show_path')) {
+ echo "<th>" . $this->getLang('path') . "</th>";
+ }
+ foreach ($helper->translations as $t) {
+ if($t === $default_language) {
+ continue;
+ }
+ echo "<th>$t</th>";
+ }
+ echo "</tr>";
+
+ $pages = $this->getAllPages();
+ foreach ($pages as $page) {
+ if (!$helper->getLangPart($page["id"]) === $default_language ||
+ !$helper->istranslatable($page["id"], false) ||
+ !page_exists($page["id"])
+ ) {
+ continue;
+ }
+ // We have an existing and translatable page in the default language
+ $showRow = false;
+ $row = "<tr><td>" . $xhtml_renderer->internallink($page['id'],null,null,true) . "</td>";
+ if ($this->getConf('show_path')) {
+ $row .= "<td>" . $xhtml_renderer->internallink($page['id'],$page['id'],null,true) . "</td>";
+ }
+
+ list($lc, $idpart) = $helper->getTransParts($page["id"]);
+
+ foreach ($helper->translations as $t) {
+ if ($t === $default_language) {
+ continue;
+ }
+
+ list($translID, $name) = $helper->buildTransID($t, $idpart);
+
+
+ $difflink = '';
+ if(!page_exists($translID)) {
+ $class = "missing";
+ $title = $this->getLang("missing");
+ $showRow = true;
+ } else {
+ $translfn = wikiFN($translID);
+ if($page['mtime'] > filemtime($translfn)) {
+ $class = "outdated";
+ $difflink = " <a href='";
+ $difflink .= $helper->getOldDiffLink($page["id"], $page['mtime']);
+ $difflink .= "'>(diff)</a>";
+ $title = $this->getLang('old');
+ $showRow = true;
+ } else {
+ $class = "current";
+ $title = $this->getLang('current');
+ }
+ }
+ $row .= "<td class='$class'>" . $xhtml_renderer->internallink($translID,$title,null,true) . $difflink . "</td>";
+ }
+ $row .= "</tr>";
+
+ if ($showRow) {
+ echo $row;
+ }
+
+ }
+ echo "</table>";
+
+ }
+
+ function getAllPages() {
+ $namespace = $this->getConf("translationns");
+ $dir = dirname(wikiFN("$namespace:foo"));
+ $pages = array();
+ search($pages, $dir, 'search_allpages',array());
+ return $pages;
+ }
+}
diff --git a/platform/www/lib/plugins/translation/admin.svg b/platform/www/lib/plugins/translation/admin.svg
new file mode 100644
index 0000000..c3d51ee
--- /dev/null
+++ b/platform/www/lib/plugins/translation/admin.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M12.87 15.07l-2.54-2.51.03-.03A17.52 17.52 0 0 0 14.07 6H17V4h-7V2H8v2H1v2h11.17C11.5 7.92 10.44 9.75 9 11.35 8.07 10.32 7.3 9.19 6.69 8h-2c.73 1.63 1.73 3.17 2.98 4.56l-5.09 5.02L4 19l5-5 3.11 3.11.76-2.04M18.5 10h-2L12 22h2l1.12-3h4.75L21 22h2l-4.5-12m-2.62 7l1.62-4.33L19.12 17h-3.24z"/></svg> \ No newline at end of file
diff --git a/platform/www/lib/plugins/translation/conf/default.php b/platform/www/lib/plugins/translation/conf/default.php
new file mode 100644
index 0000000..3a20131
--- /dev/null
+++ b/platform/www/lib/plugins/translation/conf/default.php
@@ -0,0 +1,19 @@
+<?php
+/**
+ * Default options for the translation plugin
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+
+$conf['translations'] = '';
+$conf['translationns'] = '';
+$conf['skiptrans'] = '';
+$conf['dropdown'] = 0;
+$conf['translateui'] = 0;
+$conf['redirectstart'] = 0;
+$conf['checkage'] = 0;
+$conf['about'] = '';
+$conf['localabout'] = 0;
+$conf['display'] = 'langcode,title';
+$conf['copytrans'] = 0;
+$conf['show_path'] = 1;
diff --git a/platform/www/lib/plugins/translation/conf/metadata.php b/platform/www/lib/plugins/translation/conf/metadata.php
new file mode 100644
index 0000000..d9c4d22
--- /dev/null
+++ b/platform/www/lib/plugins/translation/conf/metadata.php
@@ -0,0 +1,21 @@
+<?php
+/**
+ * Options for the translation plugin
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+
+$meta['translations'] = array('string','_pattern' => '/^(|[a-zA-Z\- ,]+)$/');
+$meta['translationns'] = array('string','_pattern' => '/^(|[\w:\-]+)$/');
+$meta['skiptrans'] = array('string');
+$meta['dropdown'] = array('onoff');
+$meta['display'] = array('multicheckbox',
+ '_choices' => array('langcode','name','flag','title','twolines'));
+$meta['translateui'] = array('onoff');
+$meta['redirectstart'] = array('onoff');
+$meta['checkage'] = array('onoff');
+$meta['about'] = array('string','_pattern' => '/^(|[\w:\-]+)$/');
+$meta['localabout'] = array('onoff');
+$meta['copytrans'] = array('onoff');
+$meta['show_path'] = array('onoff');
+
diff --git a/platform/www/lib/plugins/translation/flags/af.gif b/platform/www/lib/plugins/translation/flags/af.gif
new file mode 100644
index 0000000..9889408
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/af.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/ar.gif b/platform/www/lib/plugins/translation/flags/ar.gif
new file mode 100644
index 0000000..179961b
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/ar.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/da.gif b/platform/www/lib/plugins/translation/flags/da.gif
new file mode 100644
index 0000000..03e75bd
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/da.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/de.gif b/platform/www/lib/plugins/translation/flags/de.gif
new file mode 100644
index 0000000..75728dd
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/de.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/el.gif b/platform/www/lib/plugins/translation/flags/el.gif
new file mode 100644
index 0000000..b4c8c04
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/el.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/en.gif b/platform/www/lib/plugins/translation/flags/en.gif
new file mode 100644
index 0000000..3c6bce1
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/en.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/es.gif b/platform/www/lib/plugins/translation/flags/es.gif
new file mode 100644
index 0000000..c27d65e
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/es.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/et.gif b/platform/www/lib/plugins/translation/flags/et.gif
new file mode 100644
index 0000000..9397a2d
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/et.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/fa.gif b/platform/www/lib/plugins/translation/flags/fa.gif
new file mode 100644
index 0000000..156040f
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/fa.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/fr.gif b/platform/www/lib/plugins/translation/flags/fr.gif
new file mode 100644
index 0000000..43d0b80
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/fr.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/ga.gif b/platform/www/lib/plugins/translation/flags/ga.gif
new file mode 100644
index 0000000..506ad28
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/ga.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/he.gif b/platform/www/lib/plugins/translation/flags/he.gif
new file mode 100644
index 0000000..c8483ae
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/he.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/hu.gif b/platform/www/lib/plugins/translation/flags/hu.gif
new file mode 100644
index 0000000..6142d86
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/hu.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/it.gif b/platform/www/lib/plugins/translation/flags/it.gif
new file mode 100644
index 0000000..d79e90e
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/it.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/ja.gif b/platform/www/lib/plugins/translation/flags/ja.gif
new file mode 100644
index 0000000..444c1d0
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/ja.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/ko.gif b/platform/www/lib/plugins/translation/flags/ko.gif
new file mode 100644
index 0000000..1cddbe7
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/ko.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/ad.gif b/platform/www/lib/plugins/translation/flags/more/ad.gif
new file mode 100644
index 0000000..57b4997
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/ad.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/ae.gif b/platform/www/lib/plugins/translation/flags/more/ae.gif
new file mode 100644
index 0000000..78d15b6
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/ae.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/ag.gif b/platform/www/lib/plugins/translation/flags/more/ag.gif
new file mode 100644
index 0000000..48f8e7b
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/ag.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/ai.gif b/platform/www/lib/plugins/translation/flags/more/ai.gif
new file mode 100644
index 0000000..1cbc579
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/ai.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/al.gif b/platform/www/lib/plugins/translation/flags/more/al.gif
new file mode 100644
index 0000000..c44fe0a
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/al.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/am.gif b/platform/www/lib/plugins/translation/flags/more/am.gif
new file mode 100644
index 0000000..2915e30
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/am.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/an.gif b/platform/www/lib/plugins/translation/flags/more/an.gif
new file mode 100644
index 0000000..cb570c6
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/an.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/ao.gif b/platform/www/lib/plugins/translation/flags/more/ao.gif
new file mode 100644
index 0000000..8c854fa
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/ao.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/ar.gif b/platform/www/lib/plugins/translation/flags/more/ar.gif
new file mode 100644
index 0000000..a9f71f7
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/ar.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/as.gif b/platform/www/lib/plugins/translation/flags/more/as.gif
new file mode 100644
index 0000000..d776ec2
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/as.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/at.gif b/platform/www/lib/plugins/translation/flags/more/at.gif
new file mode 100644
index 0000000..87e1217
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/at.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/au.gif b/platform/www/lib/plugins/translation/flags/more/au.gif
new file mode 100644
index 0000000..5269c6a
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/au.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/aw.gif b/platform/www/lib/plugins/translation/flags/more/aw.gif
new file mode 100644
index 0000000..27fdb4d
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/aw.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/ax.gif b/platform/www/lib/plugins/translation/flags/more/ax.gif
new file mode 100644
index 0000000..0ceb684
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/ax.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/az.gif b/platform/www/lib/plugins/translation/flags/more/az.gif
new file mode 100644
index 0000000..d771618
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/az.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/ba.gif b/platform/www/lib/plugins/translation/flags/more/ba.gif
new file mode 100644
index 0000000..9bf5f0a
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/ba.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/bb.gif b/platform/www/lib/plugins/translation/flags/more/bb.gif
new file mode 100644
index 0000000..b7d08e5
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/bb.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/bd.gif b/platform/www/lib/plugins/translation/flags/more/bd.gif
new file mode 100644
index 0000000..0fd27ec
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/bd.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/be.gif b/platform/www/lib/plugins/translation/flags/more/be.gif
new file mode 100644
index 0000000..ae09bfb
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/be.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/bf.gif b/platform/www/lib/plugins/translation/flags/more/bf.gif
new file mode 100644
index 0000000..9d6772c
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/bf.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/bg.gif b/platform/www/lib/plugins/translation/flags/more/bg.gif
new file mode 100644
index 0000000..11cf8ff
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/bg.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/bh.gif b/platform/www/lib/plugins/translation/flags/more/bh.gif
new file mode 100644
index 0000000..56aa72b
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/bh.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/bi.gif b/platform/www/lib/plugins/translation/flags/more/bi.gif
new file mode 100644
index 0000000..6e2cbe1
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/bi.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/bj.gif b/platform/www/lib/plugins/translation/flags/more/bj.gif
new file mode 100644
index 0000000..e676116
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/bj.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/blankflag.gif b/platform/www/lib/plugins/translation/flags/more/blankflag.gif
new file mode 100644
index 0000000..9935f82
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/blankflag.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/bm.gif b/platform/www/lib/plugins/translation/flags/more/bm.gif
new file mode 100644
index 0000000..9feb87b
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/bm.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/bn.gif b/platform/www/lib/plugins/translation/flags/more/bn.gif
new file mode 100644
index 0000000..b7b6b0f
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/bn.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/bo.gif b/platform/www/lib/plugins/translation/flags/more/bo.gif
new file mode 100644
index 0000000..4844f85
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/bo.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/bs.gif b/platform/www/lib/plugins/translation/flags/more/bs.gif
new file mode 100644
index 0000000..c0a741e
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/bs.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/bt.gif b/platform/www/lib/plugins/translation/flags/more/bt.gif
new file mode 100644
index 0000000..abe2f3c
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/bt.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/bv.gif b/platform/www/lib/plugins/translation/flags/more/bv.gif
new file mode 100644
index 0000000..6202d1f
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/bv.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/bw.gif b/platform/www/lib/plugins/translation/flags/more/bw.gif
new file mode 100644
index 0000000..986ab63
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/bw.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/by.gif b/platform/www/lib/plugins/translation/flags/more/by.gif
new file mode 100644
index 0000000..43ffcd4
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/by.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/bz.gif b/platform/www/lib/plugins/translation/flags/more/bz.gif
new file mode 100644
index 0000000..791737f
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/bz.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/ca.gif b/platform/www/lib/plugins/translation/flags/more/ca.gif
new file mode 100644
index 0000000..457d966
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/ca.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/catalonia.gif b/platform/www/lib/plugins/translation/flags/more/catalonia.gif
new file mode 100644
index 0000000..73df9a0
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/catalonia.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/cc.gif b/platform/www/lib/plugins/translation/flags/more/cc.gif
new file mode 100644
index 0000000..3f78327
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/cc.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/cd.gif b/platform/www/lib/plugins/translation/flags/more/cd.gif
new file mode 100644
index 0000000..1df717a
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/cd.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/cf.gif b/platform/www/lib/plugins/translation/flags/more/cf.gif
new file mode 100644
index 0000000..35787ca
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/cf.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/cg.gif b/platform/www/lib/plugins/translation/flags/more/cg.gif
new file mode 100644
index 0000000..e0a62a5
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/cg.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/ch.gif b/platform/www/lib/plugins/translation/flags/more/ch.gif
new file mode 100644
index 0000000..d5c0e5b
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/ch.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/ci.gif b/platform/www/lib/plugins/translation/flags/more/ci.gif
new file mode 100644
index 0000000..844120a
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/ci.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/ck.gif b/platform/www/lib/plugins/translation/flags/more/ck.gif
new file mode 100644
index 0000000..2edb739
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/ck.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/cl.gif b/platform/www/lib/plugins/translation/flags/more/cl.gif
new file mode 100644
index 0000000..cbc370e
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/cl.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/cm.gif b/platform/www/lib/plugins/translation/flags/more/cm.gif
new file mode 100644
index 0000000..1fb102b
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/cm.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/co.gif b/platform/www/lib/plugins/translation/flags/more/co.gif
new file mode 100644
index 0000000..d0e15ca
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/co.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/cr.gif b/platform/www/lib/plugins/translation/flags/more/cr.gif
new file mode 100644
index 0000000..0728dd6
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/cr.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/cs.gif b/platform/www/lib/plugins/translation/flags/more/cs.gif
new file mode 100644
index 0000000..101db64
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/cs.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/cu.gif b/platform/www/lib/plugins/translation/flags/more/cu.gif
new file mode 100644
index 0000000..291255c
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/cu.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/cv.gif b/platform/www/lib/plugins/translation/flags/more/cv.gif
new file mode 100644
index 0000000..43c6c6c
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/cv.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/cx.gif b/platform/www/lib/plugins/translation/flags/more/cx.gif
new file mode 100644
index 0000000..a5b4308
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/cx.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/cy.gif b/platform/www/lib/plugins/translation/flags/more/cy.gif
new file mode 100644
index 0000000..35c661e
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/cy.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/cz.gif b/platform/www/lib/plugins/translation/flags/more/cz.gif
new file mode 100644
index 0000000..0a605e5
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/cz.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/dj.gif b/platform/www/lib/plugins/translation/flags/more/dj.gif
new file mode 100644
index 0000000..212406d
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/dj.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/dm.gif b/platform/www/lib/plugins/translation/flags/more/dm.gif
new file mode 100644
index 0000000..2f87f3c
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/dm.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/do.gif b/platform/www/lib/plugins/translation/flags/more/do.gif
new file mode 100644
index 0000000..f7d0bad
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/do.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/dz.gif b/platform/www/lib/plugins/translation/flags/more/dz.gif
new file mode 100644
index 0000000..ed580a7
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/dz.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/ec.gif b/platform/www/lib/plugins/translation/flags/more/ec.gif
new file mode 100644
index 0000000..9e41e0e
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/ec.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/eg.gif b/platform/www/lib/plugins/translation/flags/more/eg.gif
new file mode 100644
index 0000000..6857c7d
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/eg.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/eh.gif b/platform/www/lib/plugins/translation/flags/more/eh.gif
new file mode 100644
index 0000000..dd0391c
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/eh.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/england.gif b/platform/www/lib/plugins/translation/flags/more/england.gif
new file mode 100644
index 0000000..933a4f0
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/england.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/er.gif b/platform/www/lib/plugins/translation/flags/more/er.gif
new file mode 100644
index 0000000..3d4d612
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/er.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/et.gif b/platform/www/lib/plugins/translation/flags/more/et.gif
new file mode 100644
index 0000000..f77995d
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/et.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/europeanunion.gif b/platform/www/lib/plugins/translation/flags/more/europeanunion.gif
new file mode 100644
index 0000000..28a762a
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/europeanunion.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/fam.gif b/platform/www/lib/plugins/translation/flags/more/fam.gif
new file mode 100644
index 0000000..7d52885
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/fam.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/fi.gif b/platform/www/lib/plugins/translation/flags/more/fi.gif
new file mode 100644
index 0000000..8d3a191
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/fi.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/fj.gif b/platform/www/lib/plugins/translation/flags/more/fj.gif
new file mode 100644
index 0000000..486151c
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/fj.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/fk.gif b/platform/www/lib/plugins/translation/flags/more/fk.gif
new file mode 100644
index 0000000..37b5ecf
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/fk.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/fm.gif b/platform/www/lib/plugins/translation/flags/more/fm.gif
new file mode 100644
index 0000000..7f8723b
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/fm.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/fo.gif b/platform/www/lib/plugins/translation/flags/more/fo.gif
new file mode 100644
index 0000000..4a90fc0
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/fo.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/ga.gif b/platform/www/lib/plugins/translation/flags/more/ga.gif
new file mode 100644
index 0000000..23fd5f0
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/ga.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/gd.gif b/platform/www/lib/plugins/translation/flags/more/gd.gif
new file mode 100644
index 0000000..25ea312
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/gd.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/ge.gif b/platform/www/lib/plugins/translation/flags/more/ge.gif
new file mode 100644
index 0000000..faa7f12
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/ge.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/gf.gif b/platform/www/lib/plugins/translation/flags/more/gf.gif
new file mode 100644
index 0000000..43d0b80
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/gf.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/gh.gif b/platform/www/lib/plugins/translation/flags/more/gh.gif
new file mode 100644
index 0000000..273fb7d
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/gh.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/gi.gif b/platform/www/lib/plugins/translation/flags/more/gi.gif
new file mode 100644
index 0000000..7b1984b
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/gi.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/gl.gif b/platform/www/lib/plugins/translation/flags/more/gl.gif
new file mode 100644
index 0000000..ef445be
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/gl.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/gm.gif b/platform/www/lib/plugins/translation/flags/more/gm.gif
new file mode 100644
index 0000000..6847c5a
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/gm.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/gn.gif b/platform/www/lib/plugins/translation/flags/more/gn.gif
new file mode 100644
index 0000000..a982ac6
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/gn.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/gp.gif b/platform/www/lib/plugins/translation/flags/more/gp.gif
new file mode 100644
index 0000000..31166db
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/gp.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/gq.gif b/platform/www/lib/plugins/translation/flags/more/gq.gif
new file mode 100644
index 0000000..8b4e0cc
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/gq.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/gs.gif b/platform/www/lib/plugins/translation/flags/more/gs.gif
new file mode 100644
index 0000000..ccc96ec
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/gs.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/gt.gif b/platform/www/lib/plugins/translation/flags/more/gt.gif
new file mode 100644
index 0000000..7e94d1d
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/gt.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/gu.gif b/platform/www/lib/plugins/translation/flags/more/gu.gif
new file mode 100644
index 0000000..eafef68
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/gu.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/gw.gif b/platform/www/lib/plugins/translation/flags/more/gw.gif
new file mode 100644
index 0000000..55f7571
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/gw.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/gy.gif b/platform/www/lib/plugins/translation/flags/more/gy.gif
new file mode 100644
index 0000000..1cb4cd7
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/gy.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/hk.gif b/platform/www/lib/plugins/translation/flags/more/hk.gif
new file mode 100644
index 0000000..798af96
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/hk.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/hm.gif b/platform/www/lib/plugins/translation/flags/more/hm.gif
new file mode 100644
index 0000000..5269c6a
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/hm.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/hn.gif b/platform/www/lib/plugins/translation/flags/more/hn.gif
new file mode 100644
index 0000000..6c4ffe8
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/hn.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/hr.gif b/platform/www/lib/plugins/translation/flags/more/hr.gif
new file mode 100644
index 0000000..557c660
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/hr.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/ht.gif b/platform/www/lib/plugins/translation/flags/more/ht.gif
new file mode 100644
index 0000000..059604a
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/ht.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/id.gif b/platform/www/lib/plugins/translation/flags/more/id.gif
new file mode 100644
index 0000000..865161b
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/id.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/in.gif b/platform/www/lib/plugins/translation/flags/more/in.gif
new file mode 100644
index 0000000..1cd8027
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/in.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/io.gif b/platform/www/lib/plugins/translation/flags/more/io.gif
new file mode 100644
index 0000000..de7e7ab
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/io.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/iq.gif b/platform/www/lib/plugins/translation/flags/more/iq.gif
new file mode 100644
index 0000000..c34fe3c
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/iq.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/is.gif b/platform/www/lib/plugins/translation/flags/more/is.gif
new file mode 100644
index 0000000..b42502d
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/is.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/ja.gif b/platform/www/lib/plugins/translation/flags/more/ja.gif
new file mode 100644
index 0000000..444c1d0
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/ja.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/jm.gif b/platform/www/lib/plugins/translation/flags/more/jm.gif
new file mode 100644
index 0000000..0bed67c
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/jm.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/jo.gif b/platform/www/lib/plugins/translation/flags/more/jo.gif
new file mode 100644
index 0000000..03daf8a
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/jo.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/ke.gif b/platform/www/lib/plugins/translation/flags/more/ke.gif
new file mode 100644
index 0000000..c2b5d45
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/ke.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/kg.gif b/platform/www/lib/plugins/translation/flags/more/kg.gif
new file mode 100644
index 0000000..72a4d41
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/kg.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/kh.gif b/platform/www/lib/plugins/translation/flags/more/kh.gif
new file mode 100644
index 0000000..30a1831
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/kh.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/ki.gif b/platform/www/lib/plugins/translation/flags/more/ki.gif
new file mode 100644
index 0000000..4a0751a
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/ki.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/km.gif b/platform/www/lib/plugins/translation/flags/more/km.gif
new file mode 100644
index 0000000..5859595
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/km.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/kn.gif b/platform/www/lib/plugins/translation/flags/more/kn.gif
new file mode 100644
index 0000000..bb9cc34
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/kn.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/ko.gif b/platform/www/lib/plugins/translation/flags/more/ko.gif
new file mode 100644
index 0000000..1cddbe7
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/ko.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/kp.gif b/platform/www/lib/plugins/translation/flags/more/kp.gif
new file mode 100644
index 0000000..6e0ca09
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/kp.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/kw.gif b/platform/www/lib/plugins/translation/flags/more/kw.gif
new file mode 100644
index 0000000..1efc734
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/kw.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/ky.gif b/platform/www/lib/plugins/translation/flags/more/ky.gif
new file mode 100644
index 0000000..d3d02ee
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/ky.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/kz.gif b/platform/www/lib/plugins/translation/flags/more/kz.gif
new file mode 100644
index 0000000..24baebe
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/kz.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/la.gif b/platform/www/lib/plugins/translation/flags/more/la.gif
new file mode 100644
index 0000000..d14cf4d
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/la.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/lb.gif b/platform/www/lib/plugins/translation/flags/more/lb.gif
new file mode 100644
index 0000000..003d83a
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/lb.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/lc.gif b/platform/www/lib/plugins/translation/flags/more/lc.gif
new file mode 100644
index 0000000..f5fe5bf
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/lc.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/li.gif b/platform/www/lib/plugins/translation/flags/more/li.gif
new file mode 100644
index 0000000..713c58e
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/li.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/lk.gif b/platform/www/lib/plugins/translation/flags/more/lk.gif
new file mode 100644
index 0000000..1b3ee7f
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/lk.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/lr.gif b/platform/www/lib/plugins/translation/flags/more/lr.gif
new file mode 100644
index 0000000..435af9e
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/lr.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/ls.gif b/platform/www/lib/plugins/translation/flags/more/ls.gif
new file mode 100644
index 0000000..427ae95
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/ls.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/lt.gif b/platform/www/lib/plugins/translation/flags/more/lt.gif
new file mode 100644
index 0000000..dee9c60
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/lt.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/lu.gif b/platform/www/lib/plugins/translation/flags/more/lu.gif
new file mode 100644
index 0000000..7d7293e
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/lu.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/lv.gif b/platform/www/lib/plugins/translation/flags/more/lv.gif
new file mode 100644
index 0000000..17e71b7
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/lv.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/ly.gif b/platform/www/lib/plugins/translation/flags/more/ly.gif
new file mode 100644
index 0000000..a654c30
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/ly.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/ma.gif b/platform/www/lib/plugins/translation/flags/more/ma.gif
new file mode 100644
index 0000000..fc78411
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/ma.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/mc.gif b/platform/www/lib/plugins/translation/flags/more/mc.gif
new file mode 100644
index 0000000..02a7c8e
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/mc.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/md.gif b/platform/www/lib/plugins/translation/flags/more/md.gif
new file mode 100644
index 0000000..e4b8a7e
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/md.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/me.gif b/platform/www/lib/plugins/translation/flags/more/me.gif
new file mode 100644
index 0000000..a260453
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/me.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/mg.gif b/platform/www/lib/plugins/translation/flags/more/mg.gif
new file mode 100644
index 0000000..a91b577
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/mg.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/mh.gif b/platform/www/lib/plugins/translation/flags/more/mh.gif
new file mode 100644
index 0000000..92f5f48
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/mh.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/mk.gif b/platform/www/lib/plugins/translation/flags/more/mk.gif
new file mode 100644
index 0000000..7aeb831
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/mk.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/ml.gif b/platform/www/lib/plugins/translation/flags/more/ml.gif
new file mode 100644
index 0000000..53d6f49
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/ml.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/mm.gif b/platform/www/lib/plugins/translation/flags/more/mm.gif
new file mode 100644
index 0000000..9e0a275
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/mm.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/mn.gif b/platform/www/lib/plugins/translation/flags/more/mn.gif
new file mode 100644
index 0000000..dff8ea5
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/mn.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/mo.gif b/platform/www/lib/plugins/translation/flags/more/mo.gif
new file mode 100644
index 0000000..66cf5b4
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/mo.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/mp.gif b/platform/www/lib/plugins/translation/flags/more/mp.gif
new file mode 100644
index 0000000..73b7147
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/mp.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/mq.gif b/platform/www/lib/plugins/translation/flags/more/mq.gif
new file mode 100644
index 0000000..570bc5d
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/mq.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/mr.gif b/platform/www/lib/plugins/translation/flags/more/mr.gif
new file mode 100644
index 0000000..f52fcf0
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/mr.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/ms.gif b/platform/www/lib/plugins/translation/flags/more/ms.gif
new file mode 100644
index 0000000..5e5a67a
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/ms.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/mt.gif b/platform/www/lib/plugins/translation/flags/more/mt.gif
new file mode 100644
index 0000000..45c709f
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/mt.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/mu.gif b/platform/www/lib/plugins/translation/flags/more/mu.gif
new file mode 100644
index 0000000..081ab45
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/mu.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/mv.gif b/platform/www/lib/plugins/translation/flags/more/mv.gif
new file mode 100644
index 0000000..46b6387
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/mv.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/mw.gif b/platform/www/lib/plugins/translation/flags/more/mw.gif
new file mode 100644
index 0000000..ad045a0
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/mw.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/mx.gif b/platform/www/lib/plugins/translation/flags/more/mx.gif
new file mode 100644
index 0000000..ddc75d0
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/mx.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/my.gif b/platform/www/lib/plugins/translation/flags/more/my.gif
new file mode 100644
index 0000000..fc7d523
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/my.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/mz.gif b/platform/www/lib/plugins/translation/flags/more/mz.gif
new file mode 100644
index 0000000..7d63508
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/mz.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/na.gif b/platform/www/lib/plugins/translation/flags/more/na.gif
new file mode 100644
index 0000000..c0babe7
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/na.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/nc.gif b/platform/www/lib/plugins/translation/flags/more/nc.gif
new file mode 100644
index 0000000..b1e91b9
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/nc.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/ne.gif b/platform/www/lib/plugins/translation/flags/more/ne.gif
new file mode 100644
index 0000000..ff4eaf0
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/ne.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/nf.gif b/platform/www/lib/plugins/translation/flags/more/nf.gif
new file mode 100644
index 0000000..c83424c
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/nf.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/ng.gif b/platform/www/lib/plugins/translation/flags/more/ng.gif
new file mode 100644
index 0000000..bdde7cb
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/ng.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/ni.gif b/platform/www/lib/plugins/translation/flags/more/ni.gif
new file mode 100644
index 0000000..d05894d
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/ni.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/np.gif b/platform/www/lib/plugins/translation/flags/more/np.gif
new file mode 100644
index 0000000..1096893
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/np.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/nr.gif b/platform/www/lib/plugins/translation/flags/more/nr.gif
new file mode 100644
index 0000000..2e4c0c5
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/nr.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/nu.gif b/platform/www/lib/plugins/translation/flags/more/nu.gif
new file mode 100644
index 0000000..618210a
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/nu.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/nz.gif b/platform/www/lib/plugins/translation/flags/more/nz.gif
new file mode 100644
index 0000000..028a5dc
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/nz.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/om.gif b/platform/www/lib/plugins/translation/flags/more/om.gif
new file mode 100644
index 0000000..2b8c775
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/om.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/pa.gif b/platform/www/lib/plugins/translation/flags/more/pa.gif
new file mode 100644
index 0000000..d518b2f
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/pa.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/pe.gif b/platform/www/lib/plugins/translation/flags/more/pe.gif
new file mode 100644
index 0000000..3bc7639
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/pe.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/pf.gif b/platform/www/lib/plugins/translation/flags/more/pf.gif
new file mode 100644
index 0000000..849297a
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/pf.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/pg.gif b/platform/www/lib/plugins/translation/flags/more/pg.gif
new file mode 100644
index 0000000..2d20b07
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/pg.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/ph.gif b/platform/www/lib/plugins/translation/flags/more/ph.gif
new file mode 100644
index 0000000..12b380a
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/ph.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/pk.gif b/platform/www/lib/plugins/translation/flags/more/pk.gif
new file mode 100644
index 0000000..f3f62c2
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/pk.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/pl.gif b/platform/www/lib/plugins/translation/flags/more/pl.gif
new file mode 100644
index 0000000..bf10646
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/pl.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/pm.gif b/platform/www/lib/plugins/translation/flags/more/pm.gif
new file mode 100644
index 0000000..99bf6fd
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/pm.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/pn.gif b/platform/www/lib/plugins/translation/flags/more/pn.gif
new file mode 100644
index 0000000..4bc86a1
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/pn.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/pr.gif b/platform/www/lib/plugins/translation/flags/more/pr.gif
new file mode 100644
index 0000000..6d5d589
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/pr.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/ps.gif b/platform/www/lib/plugins/translation/flags/more/ps.gif
new file mode 100644
index 0000000..6afa3b7
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/ps.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/pw.gif b/platform/www/lib/plugins/translation/flags/more/pw.gif
new file mode 100644
index 0000000..5854510
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/pw.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/py.gif b/platform/www/lib/plugins/translation/flags/more/py.gif
new file mode 100644
index 0000000..f2e66af
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/py.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/qa.gif b/platform/www/lib/plugins/translation/flags/more/qa.gif
new file mode 100644
index 0000000..2e843ff
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/qa.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/re.gif b/platform/www/lib/plugins/translation/flags/more/re.gif
new file mode 100644
index 0000000..43d0b80
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/re.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/rs.gif b/platform/www/lib/plugins/translation/flags/more/rs.gif
new file mode 100644
index 0000000..3bd1fb2
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/rs.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/rw.gif b/platform/www/lib/plugins/translation/flags/more/rw.gif
new file mode 100644
index 0000000..0d095f7
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/rw.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/sb.gif b/platform/www/lib/plugins/translation/flags/more/sb.gif
new file mode 100644
index 0000000..8f5ff83
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/sb.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/sc.gif b/platform/www/lib/plugins/translation/flags/more/sc.gif
new file mode 100644
index 0000000..31b4767
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/sc.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/scotland.gif b/platform/www/lib/plugins/translation/flags/more/scotland.gif
new file mode 100644
index 0000000..03f3f1d
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/scotland.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/sd.gif b/platform/www/lib/plugins/translation/flags/more/sd.gif
new file mode 100644
index 0000000..53ae214
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/sd.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/sg.gif b/platform/www/lib/plugins/translation/flags/more/sg.gif
new file mode 100644
index 0000000..5663d39
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/sg.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/sh.gif b/platform/www/lib/plugins/translation/flags/more/sh.gif
new file mode 100644
index 0000000..dcc7f3b
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/sh.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/si.gif b/platform/www/lib/plugins/translation/flags/more/si.gif
new file mode 100644
index 0000000..23852b5
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/si.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/sj.gif b/platform/www/lib/plugins/translation/flags/more/sj.gif
new file mode 100644
index 0000000..6202d1f
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/sj.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/sk.gif b/platform/www/lib/plugins/translation/flags/more/sk.gif
new file mode 100644
index 0000000..1b3f22b
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/sk.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/sl.gif b/platform/www/lib/plugins/translation/flags/more/sl.gif
new file mode 100644
index 0000000..f0f3492
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/sl.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/sm.gif b/platform/www/lib/plugins/translation/flags/more/sm.gif
new file mode 100644
index 0000000..04d98de
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/sm.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/sn.gif b/platform/www/lib/plugins/translation/flags/more/sn.gif
new file mode 100644
index 0000000..6dac870
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/sn.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/so.gif b/platform/www/lib/plugins/translation/flags/more/so.gif
new file mode 100644
index 0000000..f196169
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/so.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/sr.gif b/platform/www/lib/plugins/translation/flags/more/sr.gif
new file mode 100644
index 0000000..0f7499a
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/sr.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/st.gif b/platform/www/lib/plugins/translation/flags/more/st.gif
new file mode 100644
index 0000000..4f1e6e0
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/st.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/sv.gif b/platform/www/lib/plugins/translation/flags/more/sv.gif
new file mode 100644
index 0000000..2d7b159
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/sv.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/sy.gif b/platform/www/lib/plugins/translation/flags/more/sy.gif
new file mode 100644
index 0000000..dc8bd50
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/sy.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/sz.gif b/platform/www/lib/plugins/translation/flags/more/sz.gif
new file mode 100644
index 0000000..f37aaf8
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/sz.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/tc.gif b/platform/www/lib/plugins/translation/flags/more/tc.gif
new file mode 100644
index 0000000..11a8c23
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/tc.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/td.gif b/platform/www/lib/plugins/translation/flags/more/td.gif
new file mode 100644
index 0000000..7aa8a10
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/td.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/tf.gif b/platform/www/lib/plugins/translation/flags/more/tf.gif
new file mode 100644
index 0000000..51a4325
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/tf.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/tg.gif b/platform/www/lib/plugins/translation/flags/more/tg.gif
new file mode 100644
index 0000000..ca6b4e7
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/tg.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/tj.gif b/platform/www/lib/plugins/translation/flags/more/tj.gif
new file mode 100644
index 0000000..2fe38d4
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/tj.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/tk.gif b/platform/www/lib/plugins/translation/flags/more/tk.gif
new file mode 100644
index 0000000..3d3a727
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/tk.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/tl.gif b/platform/www/lib/plugins/translation/flags/more/tl.gif
new file mode 100644
index 0000000..df22d58
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/tl.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/tm.gif b/platform/www/lib/plugins/translation/flags/more/tm.gif
new file mode 100644
index 0000000..36d0994
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/tm.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/tn.gif b/platform/www/lib/plugins/translation/flags/more/tn.gif
new file mode 100644
index 0000000..917d428
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/tn.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/to.gif b/platform/www/lib/plugins/translation/flags/more/to.gif
new file mode 100644
index 0000000..d7ed4d1
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/to.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/tt.gif b/platform/www/lib/plugins/translation/flags/more/tt.gif
new file mode 100644
index 0000000..47d3b80
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/tt.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/tv.gif b/platform/www/lib/plugins/translation/flags/more/tv.gif
new file mode 100644
index 0000000..3c33827
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/tv.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/tw.gif b/platform/www/lib/plugins/translation/flags/more/tw.gif
new file mode 100644
index 0000000..cacfd9b
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/tw.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/tz.gif b/platform/www/lib/plugins/translation/flags/more/tz.gif
new file mode 100644
index 0000000..82b52ca
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/tz.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/ua.gif b/platform/www/lib/plugins/translation/flags/more/ua.gif
new file mode 100644
index 0000000..5d6cd83
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/ua.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/ug.gif b/platform/www/lib/plugins/translation/flags/more/ug.gif
new file mode 100644
index 0000000..58b731a
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/ug.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/um.gif b/platform/www/lib/plugins/translation/flags/more/um.gif
new file mode 100644
index 0000000..3b4c848
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/um.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/us.gif b/platform/www/lib/plugins/translation/flags/more/us.gif
new file mode 100644
index 0000000..8f198f7
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/us.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/uy.gif b/platform/www/lib/plugins/translation/flags/more/uy.gif
new file mode 100644
index 0000000..12848c7
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/uy.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/uz.gif b/platform/www/lib/plugins/translation/flags/more/uz.gif
new file mode 100644
index 0000000..dc9daec
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/uz.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/va.gif b/platform/www/lib/plugins/translation/flags/more/va.gif
new file mode 100644
index 0000000..2bd7446
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/va.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/vc.gif b/platform/www/lib/plugins/translation/flags/more/vc.gif
new file mode 100644
index 0000000..4821381
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/vc.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/ve.gif b/platform/www/lib/plugins/translation/flags/more/ve.gif
new file mode 100644
index 0000000..19ce6c1
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/ve.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/vg.gif b/platform/www/lib/plugins/translation/flags/more/vg.gif
new file mode 100644
index 0000000..1fc0f96
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/vg.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/vi.gif b/platform/www/lib/plugins/translation/flags/more/vi.gif
new file mode 100644
index 0000000..66f9e74
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/vi.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/vu.gif b/platform/www/lib/plugins/translation/flags/more/vu.gif
new file mode 100644
index 0000000..8a8b2b0
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/vu.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/wales.gif b/platform/www/lib/plugins/translation/flags/more/wales.gif
new file mode 100644
index 0000000..901d175
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/wales.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/wf.gif b/platform/www/lib/plugins/translation/flags/more/wf.gif
new file mode 100644
index 0000000..eaa954b
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/wf.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/ws.gif b/platform/www/lib/plugins/translation/flags/more/ws.gif
new file mode 100644
index 0000000..a51f939
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/ws.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/ye.gif b/platform/www/lib/plugins/translation/flags/more/ye.gif
new file mode 100644
index 0000000..7b0183d
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/ye.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/yt.gif b/platform/www/lib/plugins/translation/flags/more/yt.gif
new file mode 100644
index 0000000..a2267c0
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/yt.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/za.gif b/platform/www/lib/plugins/translation/flags/more/za.gif
new file mode 100644
index 0000000..ede5258
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/za.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/zm.gif b/platform/www/lib/plugins/translation/flags/more/zm.gif
new file mode 100644
index 0000000..b2851d2
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/zm.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/more/zw.gif b/platform/www/lib/plugins/translation/flags/more/zw.gif
new file mode 100644
index 0000000..02901f6
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/more/zw.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/nl.gif b/platform/www/lib/plugins/translation/flags/nl.gif
new file mode 100644
index 0000000..c1c8f46
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/nl.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/no.gif b/platform/www/lib/plugins/translation/flags/no.gif
new file mode 100644
index 0000000..6202d1f
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/no.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/pt-br.gif b/platform/www/lib/plugins/translation/flags/pt-br.gif
new file mode 100644
index 0000000..8c86616
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/pt-br.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/pt.gif b/platform/www/lib/plugins/translation/flags/pt.gif
new file mode 100644
index 0000000..e735f74
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/pt.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/ro.gif b/platform/www/lib/plugins/translation/flags/ro.gif
new file mode 100644
index 0000000..f5d5f12
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/ro.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/ru.gif b/platform/www/lib/plugins/translation/flags/ru.gif
new file mode 100644
index 0000000..b525c46
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/ru.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/sv.gif b/platform/www/lib/plugins/translation/flags/sv.gif
new file mode 100644
index 0000000..80f6285
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/sv.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/th.gif b/platform/www/lib/plugins/translation/flags/th.gif
new file mode 100644
index 0000000..0130792
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/th.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/tr.gif b/platform/www/lib/plugins/translation/flags/tr.gif
new file mode 100644
index 0000000..e407d55
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/tr.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/vi.gif b/platform/www/lib/plugins/translation/flags/vi.gif
new file mode 100644
index 0000000..f1e20c9
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/vi.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/flags/zh.gif b/platform/www/lib/plugins/translation/flags/zh.gif
new file mode 100644
index 0000000..b052530
--- /dev/null
+++ b/platform/www/lib/plugins/translation/flags/zh.gif
Binary files differ
diff --git a/platform/www/lib/plugins/translation/helper.php b/platform/www/lib/plugins/translation/helper.php
new file mode 100644
index 0000000..e6bc3b5
--- /dev/null
+++ b/platform/www/lib/plugins/translation/helper.php
@@ -0,0 +1,446 @@
+<?php
+/**
+ * Translation Plugin: Simple multilanguage plugin
+ *
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+
+// must be run within Dokuwiki
+if(!defined('DOKU_INC')) die();
+
+/**
+ * Class helper_plugin_translation
+ */
+class helper_plugin_translation extends DokuWiki_Plugin {
+ var $translations = array();
+ var $translationNs = '';
+ var $defaultlang = '';
+ var $LN = array(); // hold native names
+ var $opts = array(); // display options
+
+ /**
+ * Initialize
+ */
+ function __construct() {
+ global $conf;
+ require_once(DOKU_INC . 'inc/pageutils.php');
+ require_once(DOKU_INC . 'inc/utf8.php');
+
+ $this->loadTranslationNamespaces();
+
+ // load language names
+ $this->LN = confToHash(dirname(__FILE__) . '/lang/langnames.txt');
+
+ // display options
+ $this->opts = $this->getConf('display');
+ $this->opts = explode(',', $this->opts);
+ $this->opts = array_map('trim', $this->opts);
+ $this->opts = array_fill_keys($this->opts, true);
+
+ // get default translation
+ if(!empty($conf['lang_before_translation'])) {
+ $dfl = $conf['lang'];
+ } else {
+ $dfl = $conf['lang_before_translation'];
+ }
+ if(in_array($dfl, $this->translations)) {
+ $this->defaultlang = $dfl;
+ } else {
+ $this->defaultlang = '';
+ array_unshift($this->translations, '');
+ }
+
+ $this->translationNs = cleanID($this->getConf('translationns'));
+ if($this->translationNs) $this->translationNs .= ':';
+ }
+
+ /**
+ * Parse 'translations'-setting into $this->translations
+ */
+ public function loadTranslationNamespaces() {
+ // load wanted translation into array
+ $this->translations = strtolower(str_replace(',', ' ', $this->getConf('translations')));
+ $this->translations = array_unique(array_filter(explode(' ', $this->translations)));
+ sort($this->translations);
+ }
+
+ /**
+ * Check if the given ID is a translation and return the language code.
+ *
+ * @param string $id
+ * @return string
+ */
+ function getLangPart($id) {
+ list($lng) = $this->getTransParts($id);
+ return $lng;
+ }
+
+ /**
+ * Check if the given ID is a translation and return the language code and
+ * the id part.
+ *
+ * @param string $id
+ * @return array
+ */
+ function getTransParts($id) {
+ $rx = '/^' . $this->translationNs . '(' . join('|', $this->translations) . '):(.*)/';
+ if(preg_match($rx, $id, $match)) {
+ return array($match[1], $match[2]);
+ }
+ return array('', $id);
+ }
+
+ /**
+ * Returns the browser language if it matches with one of the configured
+ * languages
+ */
+ function getBrowserLang() {
+ global $conf;
+ $langs = $this->translations;
+ if (!in_array($conf['lang'], $langs)) {
+ $langs[] = $conf['lang'];
+ }
+ $rx = '/(^|,|:|;|-)(' . join('|', $langs) . ')($|,|:|;|-)/i';
+ if(preg_match($rx, $_SERVER['HTTP_ACCEPT_LANGUAGE'], $match)) {
+ return strtolower($match[2]);
+ }
+ return false;
+ }
+
+ /**
+ * Returns the ID and name to the wanted translation, empty
+ * $lng is default lang
+ *
+ * @param string $lng
+ * @param string $idpart
+ * @return array
+ */
+ function buildTransID($lng, $idpart) {
+ if($lng && in_array($lng, $this->translations)) {
+ $link = ':' . $this->translationNs . $lng . ':' . $idpart;
+ $name = $lng;
+ } else {
+ $link = ':' . $this->translationNs . $idpart;
+ $name = $this->realLC('');
+ }
+ return array($link, $name);
+ }
+
+ /**
+ * Returns the real language code, even when an empty one is given
+ * (eg. resolves th default language)
+ *
+ * @param string $lc
+ * @return string
+ */
+ function realLC($lc) {
+ global $conf;
+ if($lc) {
+ return $lc;
+ } elseif(!$conf['lang_before_translation']) {
+ return $conf['lang'];
+ } else {
+ return $conf['lang_before_translation'];
+ }
+ }
+
+ /**
+ * Check if current ID should be translated and any GUI
+ * should be shown
+ *
+ * @param string $id
+ * @param bool $checkact
+ * @return bool
+ */
+ function istranslatable($id, $checkact = true) {
+ global $ACT;
+
+ if($checkact && $ACT != 'show') return false;
+ if($this->translationNs && strpos($id, $this->translationNs) !== 0) return false;
+ $skiptrans = trim($this->getConf('skiptrans'));
+ if($skiptrans && preg_match('/' . $skiptrans . '/ui', ':' . $id)) return false;
+ $meta = p_get_metadata($id);
+ if(!empty($meta['plugin']['translation']['notrans'])) return false;
+
+ return true;
+ }
+
+ /**
+ * Return the (localized) about link
+ */
+ function showAbout() {
+ global $ID;
+
+ $curlc = $this->getLangPart($ID);
+
+ $about = $this->getConf('about');
+ if($this->getConf('localabout')) {
+ list(/* $lc */, $idpart) = $this->getTransParts($about);
+ list($about, /* $name */) = $this->buildTransID($curlc, $idpart);
+ $about = cleanID($about);
+ }
+
+ $out = '';
+ $out .= '<sup>';
+ $out .= html_wikilink($about, '?');
+ $out .= '</sup>';
+
+ return $out;
+ }
+
+ /**
+ * Returns a list of (lc => link) for all existing translations of a page
+ *
+ * @param $id
+ * @return array
+ */
+ function getAvailableTranslations($id) {
+ $result = array();
+
+ list($lc, $idpart) = $this->getTransParts($id);
+
+ foreach($this->translations as $t) {
+ if($t == $lc) continue; //skip self
+ list($link, $name) = $this->buildTransID($t, $idpart);
+ if(page_exists($link)) {
+ $result[$name] = $link;
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * Creates an UI for linking to the available and configured translations
+ *
+ * Can be called from the template or via the ~~TRANS~~ syntax component.
+ */
+ public function showTranslations() {
+ global $conf;
+ global $INFO;
+
+ if(!$this->istranslatable($INFO['id'])) return '';
+ $this->checkage();
+
+ list($lc, $idpart) = $this->getTransParts($INFO['id']);
+ $lang = $this->realLC($lc);
+
+ $out = '<div class="plugin_translation">';
+
+ //show title and about
+ if(isset($this->opts['title'])) {
+ $out .= '<span>' . $this->getLang('translations');
+ if($this->getConf('about')) $out .= $this->showAbout();
+ $out .= ':</span> ';
+ if(isset($this->opts['twolines'])) $out .= '<br />';
+ }
+
+ // open wrapper
+ if($this->getConf('dropdown')) {
+ // select needs its own styling
+ if($INFO['exists']) {
+ $class = 'wikilink1';
+ } else {
+ $class = 'wikilink2';
+ }
+ if(isset($this->opts['flag'])) {
+ $flag = DOKU_BASE . 'lib/plugins/translation/flags/' . hsc($lang) . '.gif';
+ }else{
+ $flag = '';
+ }
+
+ if($conf['userewrite']) {
+ $action = wl();
+ } else {
+ $action = script();
+ }
+
+ $out .= '<form action="' . $action . '" id="translation__dropdown">';
+ if($flag) $out .= '<img src="' . $flag . '" alt="' . hsc($lang) . '" height="11" class="' . $class . '" /> ';
+ $out .= '<select name="id" class="' . $class . '">';
+ } else {
+ $out .= '<ul>';
+ }
+
+ // insert items
+
+ array_shift($this->translations);
+ foreach($this->translations as $t) {
+ $out .= $this->getTransItem($t, $idpart);
+ }
+
+ // close wrapper
+ if($this->getConf('dropdown')) {
+ $out .= '</select>';
+ $out .= '<input name="go" type="submit" value="&rarr;" />';
+ $out .= '</form>';
+ } else {
+ $out .= '</ul>';
+ }
+
+ // show about if not already shown
+ if(!isset($this->opts['title']) && $this->getConf('about')) {
+ $out .= '&nbsp';
+ $out .= $this->showAbout();
+ }
+
+ $out .= '</div>';
+
+ return $out;
+ }
+
+ /**
+ * Return the local name
+ *
+ * @param $lang
+ * @return string
+ */
+ function getLocalName($lang) {
+ if($this->LN[$lang]) {
+ return $this->LN[$lang];
+ }
+ return $lang;
+ }
+
+ /**
+ * Create the link or option for a single translation
+ *
+ * @param $lc string The language code
+ * @param $idpart string The ID of the translated page
+ * @returns string The item
+ */
+ function getTransItem($lc, $idpart) {
+ global $ID;
+ global $conf;
+
+ list($link, $lang) = $this->buildTransID($lc, $idpart);
+ $link = cleanID($link);
+
+ // class
+ if(page_exists($link, '', false)) {
+ $class = 'wikilink1';
+ } else {
+ $class = 'wikilink2';
+ }
+
+ // local language name
+ $localname = $this->getLocalName($lang);
+
+ $divClass = 'li';
+ // current?
+ if($ID == $link) {
+ $sel = ' selected="selected"';
+ $class .= ' cur';
+ $divClass .= ' cur';
+ } else {
+ $sel = '';
+ }
+
+ // flag
+ $flag = false;
+ $style = '';
+ if(isset($this->opts['flag'])) {
+ $flag = DOKU_BASE . 'lib/plugins/translation/flags/' . hsc($lang) . '.gif';
+ $style = ' style="background-image: url(\'' . $flag . '\')"';
+ $class .= ' flag';
+ }
+
+ // what to display as name
+ if(isset($this->opts['name'])) {
+ $display = hsc($localname);
+ if(isset($this->opts['langcode'])) $display .= ' (' . hsc($lang) . ')';
+ } elseif(isset($this->opts['langcode'])) {
+ $display = hsc($lang);
+ } else {
+ $display = '&nbsp;';
+ }
+
+ // prepare output
+ $out = '';
+ if($this->getConf('dropdown')) {
+ if($conf['useslash']) $link = str_replace(':', '/', $link);
+
+ $out .= '<option class="' . $class . '" title="' . hsc($localname) . '" value="' . $link . '"' . $sel . $style . '>';
+ $out .= $display;
+ $out .= '</option>';
+ } else {
+ $out .= "<li><div class='$divClass'>";
+ $out .= '<a href="' . wl($link) . '" class="' . $class . '" title="' . hsc($localname) . '">';
+ if($flag) $out .= '<img src="' . $flag . '" alt="' . hsc($lang) . '" height="11" />';
+ $out .= $display;
+ $out .= '</a>';
+ $out .= '</div></li>';
+ }
+
+ return $out;
+ }
+
+ /**
+ * Checks if the current page is a translation of a page
+ * in the default language. Displays a notice when it is
+ * older than the original page. Tries to link to a diff
+ * with changes on the original since the translation
+ */
+ function checkage() {
+ global $ID;
+ global $INFO;
+ if(!$this->getConf('checkage')) return;
+ if(!$INFO['exists']) return;
+ $lng = $this->getLangPart($ID);
+ if($lng == $this->defaultlang) return;
+
+ $rx = '/^' . $this->translationNs . '((' . join('|', $this->translations) . '):)?/';
+ $idpart = preg_replace($rx, '', $ID);
+
+ // compare modification times
+ list($orig, /* $name */) = $this->buildTransID($this->defaultlang, $idpart);
+ $origfn = wikiFN($orig);
+ if($INFO['lastmod'] >= @filemtime($origfn)) return;
+
+ // get revision from before translation
+ $orev = 0;
+
+ $changelog = new PageChangelog($orig);
+ $revs = $changelog->getRevisions(0, 100);
+ foreach($revs as $rev) {
+ if($rev < $INFO['lastmod']) {
+ $orev = $rev;
+ break;
+ }
+ }
+
+ // see if the found revision still exists
+ if($orev && !page_exists($orig, $orev)) $orev = 0;
+
+ // build the message and display it
+ $orig = cleanID($orig);
+ $msg = sprintf($this->getLang('outdated'), wl($orig));
+
+ $difflink = $this->getOldDiffLink($orig, $INFO['lastmod']);
+ if ($difflink) {
+ $msg .= sprintf(' ' . $this->getLang('diff'), $difflink);
+ }
+
+ echo '<div class="notify">' . $msg . '</div>';
+ }
+
+ function getOldDiffLink($id, $lastmod) {
+ // get revision from before translation
+ $orev = false;
+ $changelog = new PageChangelog($id);
+ $revs = $changelog->getRevisions(0, 100);
+ foreach($revs as $rev) {
+ if($rev < $lastmod) {
+ $orev = $rev;
+ break;
+ }
+ }
+ if($orev && !page_exists($id, $orev)) {
+ return false;
+ }
+ $id = cleanID($id);
+ return wl($id, array('do' => 'diff', 'rev' => $orev));
+
+ }
+}
diff --git a/platform/www/lib/plugins/translation/lang/be/lang.php b/platform/www/lib/plugins/translation/lang/be/lang.php
new file mode 100644
index 0000000..dd9d9ae
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/be/lang.php
@@ -0,0 +1,19 @@
+<?php
+
+/**
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ * Belarusian language file
+ *
+ * @author Rainbow-Spike <rainbow_spike@derpy.ru>
+ */
+
+$lang['translations'] = 'Пераклад гэтай старонкі';
+$lang['outdated'] = 'Гэты пераклад старэй, чым <a href="%s" class="wikilink1">арыгінальная старонка</a> і можа быць не актуальным.';
+$lang['diff'] = 'Глядзіце <a href="%s" class="wikilink1">было зменена</a>.';
+$lang['transloaded'] = 'Сэнс перакладу гэтай старонкі %s быў папярэдне загружаны для зручнасці перакладу. <br /> Таксама можаце выкарыстоўваць наступныя пераклады: %s.';
+$lang['menu'] = 'састарэлыя і адсутныя пераклады';
+$lang['missing'] = 'Адсутнічае!';
+$lang['old'] = 'застарэлы';
+$lang['current'] = 'бягучы';
+$lang['path'] = 'Шлях';
diff --git a/platform/www/lib/plugins/translation/lang/be/settings.php b/platform/www/lib/plugins/translation/lang/be/settings.php
new file mode 100644
index 0000000..9627847
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/be/settings.php
@@ -0,0 +1,23 @@
+<?php
+
+/**
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ * Belarusian language file
+ *
+ * @author Rainbow-Spike <rainbow_spike@derpy.ru>
+ */
+
+$lang['translations'] = 'Спіс падтрымоўваных моў перакладу (двохсимвольні коды ISO). Падзяляйце значэння коскамі або прабеламі.';
+$lang['translationns'] = 'Калі вы хочаце перавесці толькі пэўны Прастору імёнаў, тады ўпішыце тут яго імя.';
+$lang['skiptrans'] = 'Калі імя старонкі адпавядае гэтаму рэгулярнаму выразу, тады не адлюстроўваць меню перакладаў.';
+$lang['dropdown'] = 'Выкарыстаць выпадальны спіс для адлюстравання даступных перакладаў (рэкамендуецца, калі больш за 5 перакладаў)';
+$lang['translateui'] = 'Павінна мова інтэрфейсу карыстальніка таксама перамыкацца мовы Прасторы імёнаў?';
+$lang['redirectstart'] = 'Павінна стартавая старонка аўтаматычна перенаправлятись на Прастору імёнаў мовы, выкарыстоўваючы детектекцію мовы аглядальніка?';
+$lang['about'] = 'Калі ласка, Увядзіце імя старонкі, на якой будзе руж\'растлумачана функцыі перакладу для вашых карыстальнікаў. Яна будзе звязана з выбарам мовы.';
+$lang['localabout'] = 'Выкарыстаць лакалізаваную версію старонкі руж\'тлумачэнняў (замест адной глабальнай старонкі руж\'тлумачэнняў).';
+$lang['checkage'] = 'Адлюстроўваць папярэджанне аб магчымай не актуальнасць перакладу старонак?';
+$lang['display'] = 'Абярыце што б вы хацелі адлюстроўваць у перамыкачы моў. Заўвага: выкарыстоўваць сцяг краіны для пераключальніка моў не рэкамендуецца экспертамі па выгодзе выкарыстання інтэрфейсу.';
+
+$lang['copytrans'] = 'Скапіяваць тэкст на мове арыгінала да рэдактара ў пачатку новага перакладу?';
+$lang['show_path'] = 'Паказаць шлях адсутнічае на старонцы перакладу?';
diff --git a/platform/www/lib/plugins/translation/lang/be/totranslate.txt b/platform/www/lib/plugins/translation/lang/be/totranslate.txt
new file mode 100644
index 0000000..07eadbc
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/be/totranslate.txt
@@ -0,0 +1 @@
+FIXME **Гэтая старонка яшчэ не цалкам перакладзеная. Калі ласка, дапамажыце завяршыць пераклад.**\\ //(выдаліце гэты абзац пасля завяршэння перакладу)// \ No newline at end of file
diff --git a/platform/www/lib/plugins/translation/lang/bn/lang.php b/platform/www/lib/plugins/translation/lang/bn/lang.php
new file mode 100644
index 0000000..5bfc73b
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/bn/lang.php
@@ -0,0 +1,10 @@
+<?php
+
+/**
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ * @author ninetailz <ninetailz1125@gmail.com>
+ */
+$lang['translations'] = 'এই পাতার অনুবাদ';
+$lang['outdated'] = 'এই অনুবাদ <a href="%s" class="wiki link1">মূল পাতা</ a> তুলনায় পুরোনো হয় এবং পুরান হতে পারে.';
+$lang['diff'] = 'দেখুন কি <a href="%s" class="wikilink1">পরিবর্তন</ a> হয়েছে';
diff --git a/platform/www/lib/plugins/translation/lang/bn/settings.php b/platform/www/lib/plugins/translation/lang/bn/settings.php
new file mode 100644
index 0000000..d1df06b
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/bn/settings.php
@@ -0,0 +1,10 @@
+<?php
+
+/**
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ * @author ninetailz <ninetailz1125@gmail.com>
+ */
+$lang['about'] = 'এখানে একটি পৃষ্ঠায় নাম লিখুন যেখানে অনুবাদের বৈশিষ্ট্যটি ব্যবহারকারীদের জন্য ব্যাখ্যা করা আছে. এটা ভাষা নির্বাচক থেকে লিঙ্ক করা হবে.';
+$lang['checkage'] = 'সম্ভবত পুরোনো অনুবাদের বিষয়ে সাবধান.';
+$lang['copytrans'] = 'একটি নতুন অনুবাদ শুরু যখন সম্পাদক মধ্যে মূল ভাষা টেক্সট কপি করুন?';
diff --git a/platform/www/lib/plugins/translation/lang/bn/totranslate.txt b/platform/www/lib/plugins/translation/lang/bn/totranslate.txt
new file mode 100644
index 0000000..46e5f1c
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/bn/totranslate.txt
@@ -0,0 +1 @@
+FixMe ** এই পাতা সম্পূর্ণরূপে এখনো অনুবাদ করা হয়নি. অনুবাদ সমাপ্তির সাহায্য করুন. ** \ \ / / (অনুবাদ সমাপ্ত হয় একবার এই অনুচ্ছেদ মুছে ফেলুন) / / \ No newline at end of file
diff --git a/platform/www/lib/plugins/translation/lang/ca/lang.php b/platform/www/lib/plugins/translation/lang/ca/lang.php
new file mode 100644
index 0000000..c74c568
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/ca/lang.php
@@ -0,0 +1,16 @@
+<?php
+
+/**
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ * @author Jordi Robert Sancho <jrobertsancho@gmail.com>
+ */
+$lang['translations'] = 'Traduccions d\'aquesta pàgina';
+$lang['outdated'] = 'Aquesta traducció és més antiga que la <a href="%s" class="wikilink1">pàgina original</a> i pot estar desactualitzada.';
+$lang['diff'] = 'Veure que ha <a href="%s" class="wikilink1">canviat</a>.';
+$lang['transloaded'] = 'Els continguts de la traducció d\'aquesta pàgina a %s han sigut pre-carregats per facilitar la traducció.<br>Però pots basar la teva traducció en les següents traduccions: %s';
+$lang['menu'] = 'traduccions desactualitzades i que falten';
+$lang['missing'] = 'Falta!';
+$lang['old'] = 'desactualitzat';
+$lang['current'] = 'actualitzat';
+$lang['path'] = 'Ruta';
diff --git a/platform/www/lib/plugins/translation/lang/ca/settings.php b/platform/www/lib/plugins/translation/lang/ca/settings.php
new file mode 100644
index 0000000..4a356cc
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/ca/settings.php
@@ -0,0 +1,19 @@
+<?php
+
+/**
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ * @author Jordi Robert Sancho <jrobertsancho@gmail.com>
+ */
+$lang['translations'] = 'Llista separada per espais d\'idiomes de traducció';
+$lang['translationns'] = 'Si només vols traduccions sota un cert espai de noms, posa\'l aquí.';
+$lang['skiptrans'] = 'Quan el nom de la pàgina coincideix amb aquesta expressió regular, no mostris el menú de traducció.';
+$lang['dropdown'] = 'Utilitzar una llista desplegable per mostrar les traduccions (recomanat per a més de 5 idiomes).';
+$lang['translateui'] = 'L\'idioma de la interfície d\'usuari s\'hauria de canviar també en els espais de noms en llengües estrangeres?';
+$lang['redirectstart'] = 'La pàgina d\'inici hauria de redirigir a un espai de noms d\'idioma mitjançant la detecció d\'idioma del navegador?';
+$lang['about'] = 'Introdueix un nom de pàgina aquí, on la funció de traducció s\'explica als usuaris. Estarà connectat des del selector d\'idioma.';
+$lang['localabout'] = 'Utilitzar versions localitzades de la pàgina \'quant a\' (en lloc d\'un \'quant a\' global).';
+$lang['checkage'] = 'Advertir sobre possibles traduccions obsoletes.';
+$lang['display'] = 'Selecciona el que vulguis que es mostri al seleccionador d\'idioma. Recorda que els experts en usabilitat no recomanen fer servir banderes de país.';
+$lang['copytrans'] = 'Copiar el text en l\'idioma original en l\'editor quan s\'inicia una nova traducció?';
+$lang['show_path'] = 'Mostrar la ruta a la pàgina de traducció que falta?';
diff --git a/platform/www/lib/plugins/translation/lang/ca/totranslate.txt b/platform/www/lib/plugins/translation/lang/ca/totranslate.txt
new file mode 100644
index 0000000..96820e8
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/ca/totranslate.txt
@@ -0,0 +1 @@
+ARREGLA'M **Aquesta pàgina encara no està totalment traduïda. Si us plau, ajuda completant la traducció.**\\//(treu aquest paràgraf en acabar la traducció)// \ No newline at end of file
diff --git a/platform/www/lib/plugins/translation/lang/cs/lang.php b/platform/www/lib/plugins/translation/lang/cs/lang.php
new file mode 100644
index 0000000..faca1ae
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/cs/lang.php
@@ -0,0 +1,16 @@
+<?php
+
+/**
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ * @author Jaroslav Lichtblau <jlichtblau@seznam.cz>
+ */
+$lang['translations'] = 'Překlady této stránky';
+$lang['outdated'] = 'Tento překlad je starší než <a href="%s" class="wikilink1">originální stránka</a> a nejspíše i zastaralý.';
+$lang['diff'] = 'Zobrazit <a href="%s" class="wikilink1">změny</a>.';
+$lang['transloaded'] = 'Text pro překlad této stránky do %s byl pro ulehčení překládání automaticky načten.<br />Můžete ale použít předešlé dostupné překlady: %s.';
+$lang['menu'] = 'zastaralé a chybějící překlady';
+$lang['missing'] = 'Chybí!';
+$lang['old'] = 'zastaralý';
+$lang['current'] = 'aktuální';
+$lang['path'] = 'Cesta';
diff --git a/platform/www/lib/plugins/translation/lang/cs/settings.php b/platform/www/lib/plugins/translation/lang/cs/settings.php
new file mode 100644
index 0000000..f24ab34
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/cs/settings.php
@@ -0,0 +1,19 @@
+<?php
+
+/**
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ * @author Jaroslav Lichtblau <jlichtblau@seznam.cz>
+ */
+$lang['translations'] = 'Seznam přeložených jazyků (ISO kódů) oddělený mezerami. Nezahrnujte defaultní jazyk!';
+$lang['translationns'] = 'Chcete-li udržovat překlad jen pro konkrétní jmenný prostor, vložte jeho jméno sem.';
+$lang['skiptrans'] = 'Pokud jméno stránky obsahuje tento regulární výraz, nezobrazovat překladové menu.';
+$lang['dropdown'] = 'Použít rozbalovací seznam dostupných překladů (doporučeno pro 5 a více jazyků).';
+$lang['translateui'] = 'Mělo by se přeložit i uživatelské rozhraní při změně překladu stránky?';
+$lang['redirectstart'] = 'Má hlavní stránka automaticky přesměrovávat na dostupnou jazykovou verzi jmenného prostoru dle nastavení jazyka prohlížeče?';
+$lang['about'] = 'Vložte jméno stránky s nápovědou ohledně možnosti překládat stránky na DokuWiki s pomoci Translation pluginu. Tento odkaz bude k dispozici z výběru přeložených jazyků.';
+$lang['localabout'] = 'Použít přeložené verze stran o aplikaci (namísto té globální).';
+$lang['checkage'] = 'Upozorňovat na možné zastaralé překlady.';
+$lang['display'] = 'Vybrat co se má zobrazovat v menu pro výběr jazyka. Experti na použitelnost webu nedoporučují zobrazování obrázků vlajek zemí pro výběr jazyka.';
+$lang['copytrans'] = 'Kopírovat výchozí jazykovou verzi do editoru pro nový překlad?';
+$lang['show_path'] = 'Zobrazit cestu na chybějící stránku překladu?';
diff --git a/platform/www/lib/plugins/translation/lang/cs/totranslate.txt b/platform/www/lib/plugins/translation/lang/cs/totranslate.txt
new file mode 100644
index 0000000..5cdeee6
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/cs/totranslate.txt
@@ -0,0 +1 @@
+FIXME **Tato stránka ještě není plně přeložena. Pomozte s dokončením překladu.**\\ //(odstraňte tento odstavec, jakmile je překlad dokončen)// \ No newline at end of file
diff --git a/platform/www/lib/plugins/translation/lang/cy/lang.php b/platform/www/lib/plugins/translation/lang/cy/lang.php
new file mode 100644
index 0000000..18e12e7
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/cy/lang.php
@@ -0,0 +1,11 @@
+<?php
+
+/**
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ * @author Alan Davies <ben.brynsadler@gmail.com>
+ */
+$lang['translations'] = 'Cyfieithiadau\'r dudalen hon';
+$lang['outdated'] = 'Mae\'r cyfieithiad hwn yn hŷn na\'r <a href="%s" class="wikilink1">dudalen wreiddiol</a> a gall fod wedi dyddio.';
+$lang['diff'] = 'Gweld beth sydd wedi <a href="%s" class="wikilink1">newid</a>.';
+$lang['transloaded'] = 'Cafodd cynnwys y dudalen hon mewn %s ei raglwytho er mwyn hwyluso\'r cyfieithu.<br />Er gallwch chi seilio\'ch cyfieithiad ar y cyfieithiadau canlynol sy\'n bodoli\'n barod: %s';
diff --git a/platform/www/lib/plugins/translation/lang/cy/settings.php b/platform/www/lib/plugins/translation/lang/cy/settings.php
new file mode 100644
index 0000000..21d5315
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/cy/settings.php
@@ -0,0 +1,18 @@
+<?php
+
+/**
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ * @author Alan Davies <ben.brynsadler@gmail.com>
+ */
+$lang['translations'] = 'Rhestr gwahanwyd gan goma o iaith gyfieithu (codau ISO)';
+$lang['translationns'] = 'Os ydych chi am osod y cyfieithiadau o dan namespace penodol yn unig, rhowch e yma.';
+$lang['skiptrans'] = 'Pan fydd enw\'r dudalen yn bodloni\'r mynegiad rheolaidd, paid dangos y dewislen cyfieithu.';
+$lang['dropdown'] = 'Defnyddio cwymprestr i ddangos y cyfieithiadau (awgrymir am fwy na 5 iaith).';
+$lang['translateui'] = 'A ddylai iaith rhyngwyneb y defnyddiwr gael ei newid mewn namespaces ieithoedd estron hefyd?';
+$lang['redirectstart'] = 'A ddylai\'r dudalen gychwyn ailgyfeirio yn awtomatig i mewn i namespace iaith gan ddefnyddio datgeliad iaith y porwr?';
+$lang['about'] = 'Rhowch enw tudalen yma lle caiff y nodwedd cyfieithu ei esbonio ar gyfer eich defnyddwyr. Caiff ei gysylltu o\'r dewisydd iaith.';
+$lang['localabout'] = 'Defnyddio fersiynau lleoledig o\'r dudalen \'ynghylch\' (yn hytrach nag un dudalen \'ynghylch\' gyffredinol).';
+$lang['checkage'] = 'Rhybuddio ynghylch cyfieithiadau sydd efallai wedi dyddio.';
+$lang['display'] = 'Dewiswch yr hyn hoffech chi weld yn y dewisydd iaith. \'Dyw defnyddio baneri gwlad ddim i\'w awgrymu yn ôl arbenigwyr.';
+$lang['copytrans'] = 'Copïo testun y iaith wreiddiol i\'r golygydd wrth ddechrau cyfieithiad newydd?';
diff --git a/platform/www/lib/plugins/translation/lang/cy/totranslate.txt b/platform/www/lib/plugins/translation/lang/cy/totranslate.txt
new file mode 100644
index 0000000..da8bfaa
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/cy/totranslate.txt
@@ -0,0 +1 @@
+FIXME **'Dyw'r dudalen heb ei chyfieithu'n llawn eto. Cynorthwywch gan gyflawni'r cyfieithiad.**\\ //(tynnych y paragraff hwn unwaith i chi orffen y cyfieithu)// \ No newline at end of file
diff --git a/platform/www/lib/plugins/translation/lang/da/lang.php b/platform/www/lib/plugins/translation/lang/da/lang.php
new file mode 100644
index 0000000..af5f450
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/da/lang.php
@@ -0,0 +1,12 @@
+<?php
+
+/**
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ * @author Markus Petersen <markus@mdev.dk>
+ * @author Soren Birk <soer9648@eucl.dk>
+ */
+$lang['translations'] = 'Oversættelser af denne side';
+$lang['outdated'] = 'Denne oversættelse er ældre end den <a href="%s" class="wikilink1">originale side</a> og er muligvis forældet.';
+$lang['diff'] = 'Se hvad der er <a href="%s" class="wikilink1">ændret</a>.';
+$lang['transloaded'] = 'Indholdet af denne sides oversættelse i %s er blevet præ-indlæst for lettere oversættelse. <br />Du kan basere din oversættelse på følgende nuværende oversættelser: %s.';
diff --git a/platform/www/lib/plugins/translation/lang/da/settings.php b/platform/www/lib/plugins/translation/lang/da/settings.php
new file mode 100644
index 0000000..7b43734
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/da/settings.php
@@ -0,0 +1,20 @@
+<?php
+
+/**
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ * @author Markus Petersen <markus@mdev.dk>
+ * @author Soren Birk <soer9648@eucl.dk>
+ * @author Jacob Palm <mail@jacobpalm.dk>
+ */
+$lang['translations'] = 'Mellemrums-separeret liste a oversættelsessprog (ISO koder). Lad være med at inkludere standardsproget.';
+$lang['translationns'] = 'Hvis du kun vil have oversættelser under et bestemt navnerum, indsæt det her.';
+$lang['skiptrans'] = 'Hvis navnet på siden matcher dette regulære udtryk, så lad være med at vise oversættelsesmenuen.';
+$lang['dropdown'] = 'Benyt en rulleliste til at vise oversættelserne (anbefales til 5 sprog eller mere).';
+$lang['translateui'] = 'Skal brugerfladens sprog også skiftes i fremmedsprogets navnerum?';
+$lang['redirectstart'] = 'Skal startsiden automatisk henvise til et sprog-navnerum vha browserens sprog-genkendelse?';
+$lang['about'] = 'Skriv et sidenavn her hvor oversættelsesfunktionen er forklaret for dine brugere. Siden vil blive linket til fra sprogvælgeren.';
+$lang['localabout'] = 'Anvend lokaliserede versions af "Om" siden (i stedet for en global "Om" side)';
+$lang['checkage'] = 'Advar om mulige forældede oversættelser.';
+$lang['display'] = 'Angiv hvad du ønsker der skal vises menuen til valg af sprog. Bemærk venligst, at det frarådes at benytte landeflag til sprogvalg.';
+$lang['copytrans'] = 'Kopier tekst fra originalt sporg ind i editorern når en ny oversættelse påbegyndes?';
diff --git a/platform/www/lib/plugins/translation/lang/da/totranslate.txt b/platform/www/lib/plugins/translation/lang/da/totranslate.txt
new file mode 100644
index 0000000..3109105
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/da/totranslate.txt
@@ -0,0 +1 @@
+FIXME **Denne side er endnu ikke fuldt oversat. Måske kan du hjælpe med at færdiggøre oversættelsen?**\\ //(fjern dette afsnit når siden er oversat)// \ No newline at end of file
diff --git a/platform/www/lib/plugins/translation/lang/de-informal/lang.php b/platform/www/lib/plugins/translation/lang/de-informal/lang.php
new file mode 100644
index 0000000..83c73aa
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/de-informal/lang.php
@@ -0,0 +1,16 @@
+<?php
+
+/**
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ * @author F. Mueller-Donath <j.felix@mueller-donath.de>
+ */
+$lang['translations'] = 'Übersetzungen dieser Seite';
+$lang['outdated'] = 'Diese Übersetzung ist älter als das <a href="%s" class="wikilink1">Original</a> und ist eventuell veraltet.';
+$lang['diff'] = '<a href="%s" class="wikilink1">Änderungen</a> zeigen.';
+$lang['transloaded'] = 'Der Inhalt dieser Seite auf %s wurde in den Editor geladen um die Übersetzung zu erleichtern.<br />Du kannst deine Arbeit auch mit einer der folgenden vorhandenen Übersetzungen beginnen: %s.';
+$lang['menu'] = 'veraltete und fehlende Übersetzungen';
+$lang['missing'] = 'Fehlt!';
+$lang['old'] = 'veraltet';
+$lang['current'] = 'aktuell';
+$lang['path'] = 'Pfad';
diff --git a/platform/www/lib/plugins/translation/lang/de-informal/settings.php b/platform/www/lib/plugins/translation/lang/de-informal/settings.php
new file mode 100644
index 0000000..a0db7a2
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/de-informal/settings.php
@@ -0,0 +1,19 @@
+<?php
+
+/**
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ * @author F. Mueller-Donath <j.felix@mueller-donath.de>
+ */
+$lang['translations'] = 'Liste der Sprachen (ISO codes), mittels Leerzeichen separiert. Die Default-Sprache nicht angeben.';
+$lang['translationns'] = 'Wenn die Übersetzung nur unterhalb eines Namensraumes gelten soll, diesen hier angeben.';
+$lang['skiptrans'] = 'Wenn der Seitennamen dem regulären Ausdruck entspricht, dann den Sprachumschalter nicht anzeigen.';
+$lang['dropdown'] = 'Eine Auswahlliste benutzen, um die Übersetzungen anzuzeigen (zu bevorzugen bei mehr als fünf Sprachen).';
+$lang['translateui'] = 'Soll die Sprache der Benutzerschnittstelle auch in die jeweilige Fremdspache umgeschaltet werden?';
+$lang['redirectstart'] = 'Anhand des Browsers des Benutzers erkennen, welche Sprache angezeigt werden soll. (Startseite leitet in den passenden Namensraum um).';
+$lang['about'] = 'Gebe hier eine Seite an, welche den Mechanismus der Übersetzung erklärt. Sie wird vom Sprachumschalter verlinkt.';
+$lang['localabout'] = 'Sprachspezifische Versionen der oben angegebenen Seite (anstelle einer globalen) nutzen.';
+$lang['checkage'] = 'Warnungen von möglicherweise veralteten Übersetzungen anzeigen.';
+$lang['display'] = 'Gib hier an welches/r Symbol/Text im Sprachumschalter angezeigt werden soll. (Die Nutzung von länderspezifischen Flaggen wird aus Gründen der Benutzbarkeit nicht empfohlen.)';
+$lang['copytrans'] = 'Original Sprachversion in den Editor kopieren wenn eine neue Übersetzung begonnen wird?';
+$lang['show_path'] = 'Seitenpfad in der Übersicht der fehlenden Übersetzungen anzeigen?';
diff --git a/platform/www/lib/plugins/translation/lang/de-informal/totranslate.txt b/platform/www/lib/plugins/translation/lang/de-informal/totranslate.txt
new file mode 100644
index 0000000..934a71e
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/de-informal/totranslate.txt
@@ -0,0 +1 @@
+FIXME **Diese Seite wurde noch nicht vollständig übersetzt. Bitte hilf bei der Übersetzung.**\\ //(diesen Absatz entfernen, wenn die Übersetzung abgeschlossen wurde)// \ No newline at end of file
diff --git a/platform/www/lib/plugins/translation/lang/de/lang.php b/platform/www/lib/plugins/translation/lang/de/lang.php
new file mode 100644
index 0000000..750b6c6
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/de/lang.php
@@ -0,0 +1,16 @@
+<?php
+
+/**
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+$lang['translations'] = 'Übersetzungen dieser Seite';
+$lang['outdated'] = 'Diese Übersetzung ist älter als das <a href="%s" class="wikilink1">Original</a> und ist eventuell veraltet.';
+$lang['diff'] = '<a href="%s" class="wikilink1">Änderungen</a> zeigen.';
+$lang['transloaded'] = 'Der Inhalt dieser Seite auf %s wurde in den Editor geladen um die Übersetzung zu erleichtern.<br />Sie können Ihre Arbeit auch mit einer der folgenden vorhandenen Übersetzungen beginnen: %s.';
+$lang['menu'] = "veraltete und fehlende Übersetzungen";
+$lang['missing'] = 'Fehlt!';
+$lang['old'] = 'veraltet';
+$lang['current'] = 'aktuell';
+$lang['path'] = 'Pfad';
diff --git a/platform/www/lib/plugins/translation/lang/de/settings.php b/platform/www/lib/plugins/translation/lang/de/settings.php
new file mode 100644
index 0000000..ce7fbda
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/de/settings.php
@@ -0,0 +1,19 @@
+<?php
+
+/**
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+$lang['translations'] = 'Liste der Sprachen (ISO codes), mittels Leerzeichen separiert. Die Default-Sprache nicht angeben.';
+$lang['translationns'] = 'Wenn die Übersetzung nur unterhalb eines Namensraumes gelten soll, diesen hier angeben.';
+$lang['skiptrans'] = 'Wenn der Seitennamen dem regulären Ausdruck entspricht, dann den Sprachumschalter nicht anzeigen.';
+$lang['dropdown'] = 'Eine Auswahlliste benutzen, um die Übersetzungen anzuzeigen (zu bevorzugen bei mehr als fünf Sprachen).';
+$lang['translateui'] = 'Soll die Sprache der Benutzerschnittstelle auch in die jeweilige Fremdspache umgeschaltet werden?';
+$lang['redirectstart'] = 'Anhand des Browsers des Benutzers erkennen, welche Sprache angezeigt werden soll. (Startseite leitet in den passenden Namensraum um).';
+$lang['about'] = 'Geben Sie hier eine Seite an, welche den Mechanismus der Übersetzung erklärt. Sie wird vom Sprachumschalter verlinkt.';
+$lang['localabout'] = 'Sprachspezifische Versionen der oben angegebenen Seite (anstelle einer globalen) nutzen.';
+$lang['checkage'] = 'Warnungen von möglicherweise veralteten Übersetzungen anzeigen.';
+$lang['display'] = 'Geben Sie an welches/r Symbol/Text im Sprachumschalter angezeigt werden soll. (Die Nutzung von länderspezifischen Flaggen wird aus Gründen der Benutzbarkeit nicht empfohlen.)';
+$lang['copytrans'] = 'Original Sprachversion in den Editor kopieren wenn eine neue Übersetzung begonnen wird?';
+$lang['show_path'] = 'Seitenpfad in der Übersicht der fehlenden Übersetzungen anzeigen?';
diff --git a/platform/www/lib/plugins/translation/lang/de/totranslate.txt b/platform/www/lib/plugins/translation/lang/de/totranslate.txt
new file mode 100644
index 0000000..37d03ae
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/de/totranslate.txt
@@ -0,0 +1 @@
+FIXME **Diese Seite wurde noch nicht vollständig übersetzt. Bitte helfen Sie bei der Übersetzung.**\\ //(diesen Absatz entfernen, wenn die Übersetzung abgeschlossen wurde)// \ No newline at end of file
diff --git a/platform/www/lib/plugins/translation/lang/en/lang.php b/platform/www/lib/plugins/translation/lang/en/lang.php
new file mode 100644
index 0000000..304d298
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/en/lang.php
@@ -0,0 +1,11 @@
+<?php
+
+$lang['translations'] = 'Translations of this page';
+$lang['outdated'] = 'This translation is older than the <a href="%s" class="wikilink1">original page</a> and might be outdated.';
+$lang['diff'] = 'See what has <a href="%s" class="wikilink1">changed</a>.';
+$lang['transloaded'] = 'The contents of this page\'s translation in %s have been pre-loaded for easy translation.<br />But you can base your translation on the following existing translations: %s.';
+$lang['menu'] = "outdated and missing translations";
+$lang['missing'] = 'Missing!';
+$lang['old'] = 'outdated';
+$lang['current'] = 'up-to-date';
+$lang['path'] = 'Path';
diff --git a/platform/www/lib/plugins/translation/lang/en/settings.php b/platform/www/lib/plugins/translation/lang/en/settings.php
new file mode 100644
index 0000000..bda50a6
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/en/settings.php
@@ -0,0 +1,20 @@
+<?php
+/**
+ * English language file
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+
+$lang['translations'] = 'Space separated list of translation languages (ISO codes).';
+$lang['translationns'] = 'If you only want translations below a certain namespace, put it here.';
+$lang['skiptrans'] = 'When the pagename matches this regular expression, don\'t show the translation menu.';
+$lang['dropdown'] = 'Use a dropdown list to display the translations (recommended for more than 5 languages).';
+$lang['translateui'] = 'Should the language of the user interface be switched in foreign language namespaces, too?';
+$lang['redirectstart'] = 'Should the start page automatically redirect into a language namespace using browser language detection?';
+$lang['about'] = 'Enter a pagename here where the translation feature is explained for your users. It will be linked from the language selector.';
+$lang['localabout'] = 'Use localized versions of about page (instead of one global about page).';
+$lang['checkage'] = 'Warn about possibly outdated translations.';
+$lang['display'] = 'Select what you\'d like to have shown in the language selector. Note that using country flags for language selection is not recommended by usability experts.';
+
+$lang['copytrans'] = 'Copy original language text into the editor when starting a new translation?';
+$lang['show_path'] = 'Show path on the missing translation page?'; \ No newline at end of file
diff --git a/platform/www/lib/plugins/translation/lang/en/totranslate.txt b/platform/www/lib/plugins/translation/lang/en/totranslate.txt
new file mode 100644
index 0000000..ab42d5f
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/en/totranslate.txt
@@ -0,0 +1 @@
+FIXME **This page is not fully translated, yet. Please help completing the translation.**\\ //(remove this paragraph once the translation is finished)// \ No newline at end of file
diff --git a/platform/www/lib/plugins/translation/lang/eo/lang.php b/platform/www/lib/plugins/translation/lang/eo/lang.php
new file mode 100644
index 0000000..3b325da
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/eo/lang.php
@@ -0,0 +1,11 @@
+<?php
+
+/**
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ * @author Robert Bogenschneider <bogi@uea.org>
+ */
+$lang['translations'] = 'Tradukoj de tiu paĝo';
+$lang['outdated'] = 'Tiu traduko estas pli malnova ol la <a href="%s" class="wikilink1">origina paĝo</a> kaj povus esti malaktuala.';
+$lang['diff'] = 'Vidi kio <a href="%s" class="wikilink1">ŝanĝiĝis</a>.';
+$lang['transloaded'] = 'La enhavo de la paĝtraduko en %s disponeblas por facila tradukado.<br />Sed vi povas bazi vian tradukon sur la sekvaj tradukoj: %s.';
diff --git a/platform/www/lib/plugins/translation/lang/eo/settings.php b/platform/www/lib/plugins/translation/lang/eo/settings.php
new file mode 100644
index 0000000..6edf642
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/eo/settings.php
@@ -0,0 +1,18 @@
+<?php
+
+/**
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ * @author Robert Bogenschneider <bogi@uea.org>
+ */
+$lang['translations'] = 'Spaco-disigita listo de tradukaj lingvoj (ISO-kodoj).';
+$lang['translationns'] = 'Se vi volas traduki nur ene de certa nomspaco, indiku ĝin.';
+$lang['skiptrans'] = 'Ne montri la tradukmenuon, kiam la paĝnomo kongruas al tiu regula esprimo.';
+$lang['dropdown'] = 'Uzi falmenuon por montri la tradukojn (rekomendata por pli ol 5 lingvoj).';
+$lang['translateui'] = 'Ĉu ankaŭ ŝanĝi la lingvon de la uzanto-interfaco en alilingvaj nomspacoj?';
+$lang['redirectstart'] = 'Ĉu la startpaĝo aŭtomate redirektiĝu al lingva nomspaco laŭ foliumila rekonado?';
+$lang['about'] = 'Paĝnomo, kie klariĝas la tradukad-funkcio al uzantoj. La lingvo-selektilo ligos tien.';
+$lang['localabout'] = 'Uzi lokajn versiojn de la pri-paĝo (anstataŭ unu ĝenerala pri-paĝo).';
+$lang['checkage'] = 'Averti pri eble malaktualaj tradukoj.';
+$lang['display'] = 'Kion montri en la lingvo-selektilo. Notu ke uzeblec-fakuloj ne rekomendas uzi landajn flagetojn por lingvo-elekto.';
+$lang['copytrans'] = 'Ĉu kopii la originlingvan tekston en la redaktokampon por komenci novan tradukon?';
diff --git a/platform/www/lib/plugins/translation/lang/eo/totranslate.txt b/platform/www/lib/plugins/translation/lang/eo/totranslate.txt
new file mode 100644
index 0000000..1987959
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/eo/totranslate.txt
@@ -0,0 +1 @@
+FIXME **Tiu paĝo ankoraŭ ne plene tradukiĝis. Bv. helpi kompletigi la tradukon.**\\ //(forigu tiun alineon post fintraduko)// \ No newline at end of file
diff --git a/platform/www/lib/plugins/translation/lang/es/lang.php b/platform/www/lib/plugins/translation/lang/es/lang.php
new file mode 100644
index 0000000..f4786c2
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/es/lang.php
@@ -0,0 +1,18 @@
+<?php
+
+/**
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ * @author Domingo Redal <docxml@gmail.com>
+ * @author Camilo Sampedro <sampedro1903@gmail.com>
+ * @author carlos <carloscyb@gmail.com>
+ */
+$lang['translations'] = 'Traducciones de esta página';
+$lang['outdated'] = 'Esta traducción es más antigua que la <a href="%s" class="wikilink1">página original</a> y podría estar obsoleta.';
+$lang['diff'] = 'Ver lo que <a href="%s" class="wikilink1">ha cambiado</a>.';
+$lang['transloaded'] = 'Los contenidos de la traducción de esta página en %s han sido precargados para facilitar la traducción.<br />Pero puedes basar tu traducción en las siguientes traducciones existentes: %s.';
+$lang['menu'] = 'traducciones obsoletas y ausentes';
+$lang['missing'] = '¡Ausente!';
+$lang['old'] = 'obsoleta';
+$lang['current'] = 'actualizado';
+$lang['path'] = 'ruta';
diff --git a/platform/www/lib/plugins/translation/lang/es/settings.php b/platform/www/lib/plugins/translation/lang/es/settings.php
new file mode 100644
index 0000000..723fb12
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/es/settings.php
@@ -0,0 +1,22 @@
+<?php
+
+/**
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ * @author Domingo Redal <docxml@gmail.com>
+ * @author Cristina Amor <princesa.7@gmail.com>
+ * @author Camilo Sampedro <sampedro1903@gmail.com>
+ * @author carlos <carloscyb@gmail.com>
+ */
+$lang['translations'] = 'Lista de lenguajes para traducción (Códigos ISO), separados por espacios. No incluir el lenguaje por defecto.';
+$lang['translationns'] = 'Si sólo quieres traducciones en determinados espacios de nombre, indícalos aquí.';
+$lang['skiptrans'] = 'Cuando el nombre de la página concuerda con esta expresión regular, no mostrar el menú de traducción.';
+$lang['dropdown'] = 'Utiliza una lista desplegable para mostrar las traducciones (recomendado para más de 5 idiomas).';
+$lang['translateui'] = '¿También debería el lenguaje del interfaz de usuario cambiarse en los espacios de nombre foráneos?';
+$lang['redirectstart'] = '¿Debería la página principal redireccionar automáticamente a una página de un idioma según sea detectado por el navegador?';
+$lang['about'] = 'Introduce aquí un nombre de página donde se explique a tus usuarios la funcionalidad de traducción. Se enlazará desde el selector de lenguaje.';
+$lang['localabout'] = 'Utiliza versiones localizadas de la página \'acerca de\' (en lugar de una página \'acerca de\' global)';
+$lang['checkage'] = 'Alertar sobre posibles traducciones obsoletas.';
+$lang['display'] = 'Selecciona lo que quieras que sea mostrado en el selector de idioma. Ten en cuenta que el uso de parámetros de país para la selección de idioma no está recomendada por los expertos en usabilidad.';
+$lang['copytrans'] = '¿Mostrar el texto en el idioma original en el editor cuando se comienza una nueva traducción?';
+$lang['show_path'] = '¿Muestra la ruta en la página de traducción ausente?';
diff --git a/platform/www/lib/plugins/translation/lang/es/totranslate.txt b/platform/www/lib/plugins/translation/lang/es/totranslate.txt
new file mode 100644
index 0000000..6dc2803
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/es/totranslate.txt
@@ -0,0 +1 @@
+FIXME **Esta página no está completamente traducida, aún. Por favor, contribuye a su traducción.**\\ //(Elimina este párrafo una vez la traducción esté completa)// \ No newline at end of file
diff --git a/platform/www/lib/plugins/translation/lang/fa/lang.php b/platform/www/lib/plugins/translation/lang/fa/lang.php
new file mode 100644
index 0000000..2abecd7
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/fa/lang.php
@@ -0,0 +1,16 @@
+<?php
+
+/**
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ * @author Sam01 <m.sajad079@gmail.com>
+ */
+$lang['translations'] = 'ترجمه‌های این صفحه';
+$lang['outdated'] = 'این ترجمه از a href="%s" class="wikilink1">صفحه‌ی اصلی</a> قدیمی‌تر است و ممکن است منسوخ شده باشد.';
+$lang['diff'] = 'ببینید چه چیزی <a href="%s" class="wikilink1">تغییر کرده</a>.';
+$lang['transloaded'] = 'محتویات این ترجمه‌ی صفحه در %s برای ترجمه‌ی آسان از قبل پر شده‌است. <br />اما شما می‌توانید پایه‌ی ترجمه‌هایتان را در ترجمه‌های موجود زیر ببینید: %s.';
+$lang['menu'] = 'ترجمه‌های منسوخ‌ شده و پیدا نشده';
+$lang['missing'] = 'پیدا نشده!';
+$lang['old'] = 'منسوخ شده';
+$lang['current'] = 'به روز';
+$lang['path'] = 'مسیر';
diff --git a/platform/www/lib/plugins/translation/lang/fa/settings.php b/platform/www/lib/plugins/translation/lang/fa/settings.php
new file mode 100644
index 0000000..2331783
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/fa/settings.php
@@ -0,0 +1,19 @@
+<?php
+
+/**
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ * @author Sam01 <m.sajad079@gmail.com>
+ */
+$lang['translations'] = 'فضای لیست جداشده‌ی زبان‌های ترجمه شده (کدهای آی‌اس‌او)';
+$lang['translationns'] = 'اگر شما فقط می‌خواهید ترجمه‌ها زیر یک فضای‌نام خاص باشند، اینجا قرار دهید.';
+$lang['skiptrans'] = 'وقتی نام‌صفحه با عبارات منظم هم‌خوانی داشت، منوی ترجمه را نشان نده.';
+$lang['dropdown'] = 'استفاده از یک لیست کشویی برای نمایش ترجمه (توصیه شده برای بیشتر از ۵ زبان)';
+$lang['translateui'] = 'باید زبان رابط کاربر در زبان‌های خارجی فضای‌نام تغییر یابد، همچنین؟';
+$lang['redirectstart'] = 'باید صفحه‌ی آغازین به‌طور خودکار به زبانی که فضای‌نام توسط مرورگر کشف شده، تغییرمسیر کند؟';
+$lang['about'] = 'وارد کردن یک نام‌صفحه جایی که ';
+$lang['localabout'] = 'استفاده از نسخه‌های متمرکز شده‌ی درباره صفحه (به جای یک جهانی درباره صفحه)';
+$lang['checkage'] = 'هشدار درمورد ترجمه‌های احتمالا منسوخ شده.';
+$lang['display'] = 'انتخاب این‌که شما چه چیزی را می‌پسندید تا در انتخابگر زبان نمایش داده شود. توجه داشته‌باشید که استفاده از پرچم کشورها برای انتخابگر زبان توسط کارشناسان توصیه نمی‌شود.';
+$lang['copytrans'] = 'کپی‌کردن زبان اصلی متن داخل ویرایشگر وقتی که یک ترجمه جدید آغار می‌شود؟';
+$lang['show_path'] = 'نمایش مسیر در ترجمه‌ی پیدانشده‌ی صفحه‌‌ها؟';
diff --git a/platform/www/lib/plugins/translation/lang/fa/totranslate.txt b/platform/www/lib/plugins/translation/lang/fa/totranslate.txt
new file mode 100644
index 0000000..665eb5b
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/fa/totranslate.txt
@@ -0,0 +1 @@
+تعمیرم کن **این صفحه کامل ترجمه نشده، اکنون. لطفا برای کامل‌شدنش کمک کنید.**\\ //(بعد از پایان ترجمه این بند را از ترجمه حذف کنید)// \ No newline at end of file
diff --git a/platform/www/lib/plugins/translation/lang/fr/lang.php b/platform/www/lib/plugins/translation/lang/fr/lang.php
new file mode 100644
index 0000000..d8a8b9d
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/fr/lang.php
@@ -0,0 +1,19 @@
+<?php
+
+/**
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ * @author Vincent Feltz <psycho@feltzv.fr>
+ * @author NicolasFriedli <nicolas@theologique.ch>
+ * @author Gilles-Philippe Morin <gilles.philippe.morin@gmail.com>
+ * @author Schplurtz le Déboulonné <schplurtz@laposte.net>
+ */
+$lang['translations'] = 'Traductions de cette page';
+$lang['outdated'] = 'Cette traduction est plus ancienne que <a href="%s" class="wikilink1">la page originale</a> et est peut-être dépassée.';
+$lang['old'] = 'dépassée';
+$lang['diff'] = 'Voir ce qui a <a href="%s" class="wikilink1">changé</a>.';
+$lang['transloaded'] = 'Le contenu de cette page en %s a été pré-chargé pour faciliter la traduction.<br/> Mais vous pouvez baser votre traduction sur les traductions existantes: %s';
+$lang['menu'] = 'traductions dépassées et manquantes';
+$lang['missing'] = 'Manquante!';
+$lang['current'] = 'à jour';
+$lang['path'] = 'Chemin';
diff --git a/platform/www/lib/plugins/translation/lang/fr/settings.php b/platform/www/lib/plugins/translation/lang/fr/settings.php
new file mode 100644
index 0000000..74869ce
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/fr/settings.php
@@ -0,0 +1,22 @@
+<?php
+
+/**
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ * @author Guy Brand <gb@isis.u-strasbg.fr>
+ * @author Vincent Feltz <psycho@feltzv.fr>
+ * @author NicolasFriedli <nicolas@theologique.ch>
+ * @author Schplurtz le Déboulonné <schplurtz@laposte.net>
+ */
+$lang['translations'] = 'Liste des langues disponibles séparées par des espaces (codes ISO).';
+$lang['translationns'] = 'Si vous souhaitez ne traduire qu\'une certaine catégorie, indiquez-la ici.';
+$lang['skiptrans'] = 'Quand le nom de la page correspond à cette expression régulière, ne pas montrer le menu de traduction.';
+$lang['dropdown'] = 'Utiliser un menu déroulant pour afficher les traductions (recommandé pour plus de 5 langues).';
+$lang['translateui'] = 'Faut-il changer la langue de l\'interface utilisateur dans les catégories traduites ?';
+$lang['redirectstart'] = 'La page de départ devrait-elle rediriger vers une catégorie traduite en utilisant la détection de langue du navigateur ?';
+$lang['about'] = 'Entrez ici un nom de page où la fonctionnalité de traduction est expliquée aux utilisateurs. Elle sera accessible depuis le sélecteur de langue.';
+$lang['localabout'] = 'Utiliser des versions traduites de la page à propos (au lieu d\'une page à propos globale).';
+$lang['checkage'] = 'Avertir de la possibilité de traductions dépassées.';
+$lang['display'] = 'Sélectionnez ce que vous voudriez afficher dans le sélecteur de langue. Notez qu\'utiliser les drapeaux de pays pour la sélection de langue n\'est pas recommandé par les experts en ergonomie.';
+$lang['copytrans'] = 'Copier le texte en langue source dans l\'éditeur quand une nouvelle traduction est lancée ?';
+$lang['show_path'] = 'Montrer les chemins sur la page des traductions manquantes ?';
diff --git a/platform/www/lib/plugins/translation/lang/fr/totranslate.txt b/platform/www/lib/plugins/translation/lang/fr/totranslate.txt
new file mode 100644
index 0000000..3603d4e
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/fr/totranslate.txt
@@ -0,0 +1 @@
+FIXME **Cette page n'est pas encore traduite entièrement. Merci de terminer la traduction**\\ //(supprimez ce paragraphe une fois la traduction terminée)// \ No newline at end of file
diff --git a/platform/www/lib/plugins/translation/lang/hr/lang.php b/platform/www/lib/plugins/translation/lang/hr/lang.php
new file mode 100644
index 0000000..80e9399
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/hr/lang.php
@@ -0,0 +1,16 @@
+<?php
+
+/**
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ * @author Davor Turkalj <turki.bsc@gmail.com>
+ */
+$lang['translations'] = 'Prijevodi ove stranice';
+$lang['outdated'] = 'Prijevod ove stranice je stariji od <a href="%s" class="wikilink1">originalne stranice</a> i može biti zastario.';
+$lang['diff'] = 'Pogledajte što je <a href="%s" class="wikilink1">izmijenjeno</a>.';
+$lang['transloaded'] = 'Sadržaj ove stranice u jeziku %s je napunjeno radi lakšeg prevođenja.<br />Ali možete bazirati Vaš prijevod i prema slijedećim raspoloživim prijevodima: %s.';
+$lang['menu'] = 'zastarjeli i nedostajući prijevodi';
+$lang['missing'] = 'Nedostaje!';
+$lang['old'] = 'zastarjelo';
+$lang['current'] = 'ažuran';
+$lang['path'] = 'Staza';
diff --git a/platform/www/lib/plugins/translation/lang/hr/settings.php b/platform/www/lib/plugins/translation/lang/hr/settings.php
new file mode 100644
index 0000000..d1769cc
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/hr/settings.php
@@ -0,0 +1,19 @@
+<?php
+
+/**
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ * @author Davor Turkalj <turki.bsc@gmail.com>
+ */
+$lang['translations'] = 'Razmacima odvojena lista podržanih jezika (ISO oznake).';
+$lang['translationns'] = 'Ako želite prijevode samo ispod određenog imenskog prostora, navedite ga ovdje.';
+$lang['skiptrans'] = 'Kada ime stranice odgovara ovom regularnom izrazu, ne prikazujte meni za prijevode.';
+$lang['dropdown'] = 'Koristi padajuću listu za prikaz prijevoda (preporučeno kada ima više od 5 jezika).';
+$lang['translateui'] = 'Da li da jezik korisničkog sučelja također bude prebačen u jezik stranog imenskog prostora ?';
+$lang['redirectstart'] = 'Da li da se početna strana automatski preusmjeri na imenski prostor koristeći detektirani jezik preglednika?';
+$lang['about'] = 'Unesi naziv stranice gdje je korisnicima pojašnjene mogućnosti prevođenja. Ona će biti povezana na izbornik jezika.';
+$lang['localabout'] = 'Koristi lokaliziranu inačicu "about" stranice (umjesto jedinstvene globalne)';
+$lang['checkage'] = 'Upozori o mogućem zastarjelom prijevodu.';
+$lang['display'] = 'Odaberite što želite da bude prikazano u izborniku jezika. Budite svjesni da korištenje zastava za odabir jezika nije preporučeno od strane eksperata.';
+$lang['copytrans'] = 'Kopirati originalni tekst u editor kada otvorite novi prijevod ?';
+$lang['show_path'] = 'Prikaži stazu do nedostajuće stranice s prijevodom?';
diff --git a/platform/www/lib/plugins/translation/lang/hr/totranslate.txt b/platform/www/lib/plugins/translation/lang/hr/totranslate.txt
new file mode 100644
index 0000000..b49e869
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/hr/totranslate.txt
@@ -0,0 +1 @@
+FIXME **Ova stranica još nije prevedena u cijelosti. Molimo pomognite u njenom prijevodu.**\\ //(uklonite ovaj paragraf jednom kada je prevođenje završeno)// \ No newline at end of file
diff --git a/platform/www/lib/plugins/translation/lang/hu/lang.php b/platform/www/lib/plugins/translation/lang/hu/lang.php
new file mode 100644
index 0000000..6a576ff
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/hu/lang.php
@@ -0,0 +1,11 @@
+<?php
+
+/**
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ * @author Marina Vladi <deldadam@gmail.com>
+ */
+$lang['translations'] = 'Oldal fordításai';
+$lang['outdated'] = 'A fordítás régebbi, mint az <a href="%s" class="wikilink1">eredeti oldal</a>, ezért lehet, hogy már elavult.';
+$lang['diff'] = 'Módosítások <a href="%s" class="wikilink1">megtekintése</a>.';
+$lang['transloaded'] = 'Az oldal tartalmának %s nyelvi fordítását előre betöltöttem a könnyebb módosítás érdekében.<br />Ugyanakkor a fordítást elvégezhetjük a már létező %s fordítás alapján is.';
diff --git a/platform/www/lib/plugins/translation/lang/hu/settings.php b/platform/www/lib/plugins/translation/lang/hu/settings.php
new file mode 100644
index 0000000..5a27108
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/hu/settings.php
@@ -0,0 +1,18 @@
+<?php
+
+/**
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ * @author Marina Vladi <deldadam@gmail.com>
+ */
+$lang['translations'] = 'Szóközzel elválasztott lista a nyelvi fordításokról (ISO-kódokkal).';
+$lang['translationns'] = 'Ha csak egy bizonyos névtér alatt lévő fordítást szeretnénk, tegyük ide.';
+$lang['skiptrans'] = 'Ha az oldal neve illeszkedik ehhez a reguláris kifejezéshez, ne jelenjen meg a fordítások menüje.';
+$lang['dropdown'] = 'Legördülő lista használata a fordításokhoz (5 nyelvnél több esetén javasolt).';
+$lang['translateui'] = 'Módosuljon a felhasználói felület nyelve is idegen nyelvi névterek alatt?';
+$lang['redirectstart'] = 'Átirányítsuk automatikusan a kezdőoldalt abba a nyelvi névtérbe, amely nyelv a böngészőben van beállítva?';
+$lang['about'] = 'Itt adhatjuk meg annak az oldalnak a nevét, amelyen a fordítási lehetőségeket ismertetjük a felhasználókkal. Erre fog hivatkozni a nyelvkiválasztó képernyőelem.';
+$lang['localabout'] = 'A névjegy oldal fordított változátanak használata (a globális névjegy oldal helyett).';
+$lang['checkage'] = 'Figyelmeztetés az esetlegesen elavult fordításokra.';
+$lang['display'] = 'Válasszuk ki, mi jelenjen meg a nyelvi kiválasztó képernyőelemében. Jegyezzük meg: az országzászlókat nem javasolják a használhatósági szakértők.';
+$lang['copytrans'] = 'Átmásoljuk az eredeti nyelvi szöveget a szövegszerkesztőbe új fordítás indításakor?';
diff --git a/platform/www/lib/plugins/translation/lang/hu/totranslate.txt b/platform/www/lib/plugins/translation/lang/hu/totranslate.txt
new file mode 100644
index 0000000..e072d8a
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/hu/totranslate.txt
@@ -0,0 +1 @@
+JAVÍTANDÓ **Az oldal még nincs teljesen lefordítva. Kérjük, segítsen a befejezésében!**\\ //(Töröljük ezt a bekezdést a fordítás elkészültekor.)// \ No newline at end of file
diff --git a/platform/www/lib/plugins/translation/lang/it/lang.php b/platform/www/lib/plugins/translation/lang/it/lang.php
new file mode 100644
index 0000000..23d3bd6
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/it/lang.php
@@ -0,0 +1,10 @@
+<?php
+
+/**
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ * @author Torpedo <dgtorpedo@gmail.com>
+ */
+$lang['translations'] = 'Traduzioni di questa pagina';
+$lang['outdated'] = 'Questa traduzione è più vecchia di quella della <a href="%s" class="wikilink1">pagina originale</a> è potrebbe essere superata.';
+$lang['diff'] = 'Vedi cosa è <a href="%s" class="wikilink1">cambiato</a>.';
diff --git a/platform/www/lib/plugins/translation/lang/it/settings.php b/platform/www/lib/plugins/translation/lang/it/settings.php
new file mode 100644
index 0000000..775fbf7
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/it/settings.php
@@ -0,0 +1,20 @@
+<?php
+
+/**
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @author Diego Pierotto <ita.translations@tiscali.it>
+ * @author Sebastiano Pistore <olatusrooc@virgilio.it>
+ * @author Sebastiano Pistore <sebastiano.pistore.info@aol.com>
+ * @author OlatusRooc <olatusrooc@virgilio.it>
+ */
+$lang['translations'] = 'Elenco delle lingue di traduzione separati da spazi (codici ISO). Non includere la lingua predefinita';
+$lang['translationns'] = 'Scrivi qui solo se vuoi le traduzioni all\'interno di una certa categoria.';
+$lang['skiptrans'] = 'Quando i nomi delle pagine corrispondono a questa espressione regolare non mostrare il menu di traduzione.';
+$lang['dropdown'] = 'Utilizza un menu a tendina per visualizzare le traduzioni (consigliato quando si lavora con più di cinque lingue).';
+$lang['translateui'] = 'Vuoi che anche la lingua dell\'interfaccia utente sia modificata in categorie della stessa lingua?';
+$lang['about'] = 'Inserisci qui una pagina dove la funzione di traduzione viene spiegata agli utenti. Sarà collegata al selettore lingua.';
+$lang['localabout'] = 'Mostra le versioni localizzate della pagina About.';
+$lang['checkage'] = 'Avvisa della possibile presenza di traduzioni obsolete.';
+$lang['copytrans'] = 'Copia nell\'editor il testo in lingua originale quando viene iniziata una nuova traduzione?';
diff --git a/platform/www/lib/plugins/translation/lang/it/totranslate.txt b/platform/www/lib/plugins/translation/lang/it/totranslate.txt
new file mode 100644
index 0000000..e83c52a
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/it/totranslate.txt
@@ -0,0 +1 @@
+FIXME ** Questa pagina non è ancora completamente tradotta. Chi può potrebbe aiutarne il completamento. ** \\ // (Rimuovere questo paragrafo a lavoro completato) // \ No newline at end of file
diff --git a/platform/www/lib/plugins/translation/lang/ja/lang.php b/platform/www/lib/plugins/translation/lang/ja/lang.php
new file mode 100644
index 0000000..b438bc6
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/ja/lang.php
@@ -0,0 +1,16 @@
+<?php
+
+/**
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ * @author Hideaki SAWADA <chuno@live.jp>
+ */
+$lang['translations'] = 'このページの翻訳';
+$lang['outdated'] = 'この翻訳は<a href="%s" class="wikilink1">元のページ</a>よりも更新日が古く、内容が古い可能性があります。';
+$lang['diff'] = '<a href="%s" class="wikilink1">変更点</a>を参照して下さい。';
+$lang['transloaded'] = '翻訳し易くするために %s にあるこのページの翻訳内容を事前に読み込みました。<br />以下の既存の翻訳を翻訳の基にすることができます:%s。';
+$lang['menu'] = '古い翻訳と欠落している翻訳';
+$lang['missing'] = '欠落';
+$lang['old'] = '内容が古い';
+$lang['current'] = '最新';
+$lang['path'] = 'パス';
diff --git a/platform/www/lib/plugins/translation/lang/ja/settings.php b/platform/www/lib/plugins/translation/lang/ja/settings.php
new file mode 100644
index 0000000..b8c8899
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/ja/settings.php
@@ -0,0 +1,19 @@
+<?php
+
+/**
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ * @author Hideaki SAWADA <chuno@live.jp>
+ */
+$lang['translations'] = '翻訳言語(ISOコード)のスペース区切り一覧';
+$lang['translationns'] = '特定の名前空間以下のみを翻訳したい場合、名前空間を記入する。';
+$lang['skiptrans'] = 'ページ名がこの正規表現と一致すると、翻訳メニューが表示されません。';
+$lang['dropdown'] = '翻訳を表示するためにドロップダウン一覧を使用する(5言語以上の場合推奨)。';
+$lang['translateui'] = 'ユーザーインターフェイスの言語も、名前空間の言語に切り替えるか?';
+$lang['redirectstart'] = 'ブラウザーの言語設定を利用して、スタートページを各言語の名前空間に自動的にリダイレクトするか?';
+$lang['about'] = '翻訳機能をユーザーに説明するページ名を入力して下さい。言語セレクタからリンクされます。';
+$lang['localabout'] = '(包括的な概要ページの代わりに)翻訳版の概要ページを使用する。';
+$lang['checkage'] = '古い翻訳について警告する。';
+$lang['display'] = '言語セレクタに何を表示するかを選択する。言語選択に国旗を使用することをユーザビリティ専門家は奨励しないので注意してください。';
+$lang['copytrans'] = '新しく翻訳を開始する時、エディタに元の言語の文章をコピーしますか?';
+$lang['show_path'] = '欠落している翻訳ページのパスを表示します。';
diff --git a/platform/www/lib/plugins/translation/lang/ja/totranslate.txt b/platform/www/lib/plugins/translation/lang/ja/totranslate.txt
new file mode 100644
index 0000000..05ac184
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/ja/totranslate.txt
@@ -0,0 +1 @@
+FIXME **このページはまだ完全には、翻訳されません。翻訳の完了を支援して下さい。**\\ //(翻訳が完了したらこの段落を削除して下さい)// \ No newline at end of file
diff --git a/platform/www/lib/plugins/translation/lang/ko/lang.php b/platform/www/lib/plugins/translation/lang/ko/lang.php
new file mode 100644
index 0000000..d99d64d
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/ko/lang.php
@@ -0,0 +1,16 @@
+<?php
+
+/**
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ * @author Myeongjin <aranet100@gmail.com>
+ */
+$lang['translations'] = '이 문서의 번역';
+$lang['outdated'] = '이 번역은 <a href="%s" class="wikilink1">원래 문서</a>보다 오래되었고 오래된 번역일 수 있습니다.';
+$lang['diff'] = '무엇이 <a href="%s" class="wikilink1">바뀌었는지</a> 보세요.';
+$lang['transloaded'] = '%s에 있는 이 문서의 번역의 내용을 쉽게 번역하기 위해 미리 불러왔습니다.<br />하지만 다음 기존 번역에 당신의 번역을 바탕으로 할 수 있습니다: %s.';
+$lang['menu'] = '오래되었고 없는 번역';
+$lang['missing'] = '없음!';
+$lang['old'] = '오래됨';
+$lang['current'] = '최신';
+$lang['path'] = '경로';
diff --git a/platform/www/lib/plugins/translation/lang/ko/settings.php b/platform/www/lib/plugins/translation/lang/ko/settings.php
new file mode 100644
index 0000000..ccf6d1c
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/ko/settings.php
@@ -0,0 +1,19 @@
+<?php
+
+/**
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ * @author Myeongjin <aranet100@gmail.com>
+ */
+$lang['translations'] = '번역 언어의 공백으로 구분한 목록 (ISO 코드).';
+$lang['translationns'] = '특정 이름공간에 따라 번역을 원하면, 여기에 넣으세요.';
+$lang['skiptrans'] = '문서 이름이 정규 표현식과 일치하면, 번역 메뉴를 보여주지 마세요.';
+$lang['dropdown'] = '번역을 표시할 드롭다운 목록을 사용합니다. (5개 이상의 언어에 권장)';
+$lang['translateui'] = '사용자 인터페이스의 언어도 외국어 이름공간으로 전환해야 합니까?';
+$lang['redirectstart'] = '시작 문서가 자동으로 브라우저 언어 감지를 사용해 언어 이름공간으로 넘겨줘야 합니까?';
+$lang['about'] = '사용자에게 설명할 번역 기능이 어디에 있는지 여기에 문서 이름을 입력하세요.';
+$lang['localabout'] = '(하나의 전역 소개 문서 대신) 소개 문서의 지역화된 버전을 사용합니다.';
+$lang['checkage'] = '가능하면 오래된 번역에 대해 경고합니다.';
+$lang['display'] = '언어 선택기에 보여주고 싶은 것을 선택하세요. 언어 선택에 국기를 사용하는 것은 사용성 전문가에게 권장하지 않음을 참고하세요.';
+$lang['copytrans'] = '새 번역을 시작할 때 편집기에 원래 언어 문장을 복사하겠습니까?';
+$lang['show_path'] = '없는 번역 문서에서의 경로를 보여줄까요?';
diff --git a/platform/www/lib/plugins/translation/lang/ko/totranslate.txt b/platform/www/lib/plugins/translation/lang/ko/totranslate.txt
new file mode 100644
index 0000000..9a19833
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/ko/totranslate.txt
@@ -0,0 +1 @@
+FIXME **이 문서는 아직 완전히 번역되지 않았습니다. 번역을 완료하는 데 도와주세요.**\\ //(번역을 마치면 이 단락을 지우세요)// \ No newline at end of file
diff --git a/platform/www/lib/plugins/translation/lang/langnames.txt b/platform/www/lib/plugins/translation/lang/langnames.txt
new file mode 100644
index 0000000..90992ee
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/langnames.txt
@@ -0,0 +1,188 @@
+# Native language names
+# extracted from http://en.wikipedia.org/wiki/List_of_ISO_639-1_codes
+
+aa Afaraf
+ab Аҧсуа
+ae Avesta
+af Afrikaans
+ak Akan
+am አማርኛ
+an Aragonés
+ar |العربية
+as অসমীয়া
+av Авар мацӀ
+ay Aymar aru
+az Azərbaycan dili
+ba Башҡорт теле
+be Беларуская
+bg Български език
+bh भोजपुरी
+bi Bislama
+bm Bamanankan
+bn বাংলা
+bo བོད་ཡིག
+br Brezhoneg
+bs Bosanski Jezik
+ca Català
+ce Нохчийн Мотт
+ch Chamoru
+co Corsu
+cr ᓀᐦᐃᔭᐍᐏᐣ
+cs Česky
+cu Ѩзыкъ Словѣньскъ
+cv Чӑваш Чӗлхи
+cy Cymraeg
+da Dansk
+de Deutsch
+dv ދިވެހި
+dz རྫོང་ཁ
+ee Eʋegbe
+el Ελληνικά
+en English
+eo Esperanto
+es Español
+et Eesti
+eu Euskara
+fa فارسی
+ff Fulfulde
+fi Suomi
+fj Vosa Vakaviti
+fo Føroyskt
+fr Français
+fy Frysk
+ga Gaeilge
+gd Gaelic
+gl Galego
+gn Avañe'ẽ
+gu ગુજરાતી
+gv Gaelg, Gailck
+ha هَوُسَ
+he עברית
+hi हिन्दी, हिंदी
+ho Hiri Motu
+hr Hrvatski
+ht Kreyòl Ayisyen
+hu Magyar
+hy Հայերեն
+hz Otjiherero
+ia Interlingua
+id Bahasa Indonesia
+ie Interlingue
+ig Igbo
+ii ꆇꉙ
+ik Iñupiaq
+io Ido
+is Íslenska
+it Italiano
+iu ᐃᓄᒃᑎᑐᑦ
+ja 日本語
+jv Basa Jawa
+ka ქართული
+kg KiKongo
+ki Gĩkũyũ
+kj Kuanyama
+kk Қазақ тілі
+kl kalaallisut
+km ភាសាខ្មែរ
+kn ಕನ್ನಡ
+ko 한국어
+kr Kanuri
+ks कश्मीरी}}
+ku Kurdî
+kv Коми Кыв
+kw Kernewek
+ky Кыргыз Тили
+la Latine
+lb Lëtzebuergesch
+lg Luganda
+li Limburgs
+ln Lingála
+lo ພາສາລາວ
+lt Lietuvių Kalba
+lv Latviešu Valoda
+mg Malagasy Fiteny
+mh Kajin M̧ajeļ
+mi Te Reo Māori
+mk Македонски Јазик
+ml മലയാളം
+mn Монгол
+mr मराठी
+ms بهاس ملايو
+mt Malti
+my ဗမာစာ
+na Ekakairũ Naoero
+nb Norsk bokmål
+nd isiNdebele
+ne नेपाली
+ng Owambo
+nl Nederlands
+nn Norsk nynorsk
+no Norsk
+nr IsiNdebele
+nv Diné bizaad
+ny ChiCheŵa
+oc Occitan
+oj ᐊᓂᔑᓈᐯᒧᐎᓐ
+om Afaan Oromoo
+or ଓଡ଼ିଆ
+os Ирон æвзаг
+pa ਪੰਜਾਬੀ,
+pi पाऴि
+pl Polski
+ps پښتو
+pt Português
+pt-br Português
+qu Runa Simi
+rm Rumantsch Grischun
+rn KiRundi
+ro Română
+ru Русский
+rw Ikinyarwanda
+sa संस्कृतम्
+sc Sardu
+sd सिन्धी}}
+se Davvisámegiella
+sg Yângâ Tî Sängö
+si සිංහල
+sk Slovenčina
+sl Slovenščina
+sm Gagana fa'a Samoa
+sn ChiShona
+so Soomaaliga
+sq Shqip
+sr Српски Језик
+ss SiSwati
+st Sesotho
+su Basa Sunda
+sv Svenska
+sw Kiswahili
+ta தமிழ்
+te తెలుగు
+tg Тоҷикӣ
+th ไทย
+ti ትግርኛ
+tk Türkmen
+tl Wikang Tagalog
+tn Setswana
+to Faka Tonga
+tr Türkçe
+ts Xitsonga
+tt Татарча
+tw Twi
+ty Reo Mā`ohi
+ug Uyƣurqə
+uk Українська
+ur اردو
+uz O'zbek
+ve Tshivenḓa
+vi Tiếng Việt
+vo Volapük
+wa Walon
+wo Wollof
+xh IsiXhosa
+yi ייִדיש
+yo Yorùbá
+za Saɯ cueŋƅ
+zh 中文
+zh-tw 繁體中文
+zu IsiZulu
diff --git a/platform/www/lib/plugins/translation/lang/lv/lang.php b/platform/www/lib/plugins/translation/lang/lv/lang.php
new file mode 100644
index 0000000..af85894
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/lv/lang.php
@@ -0,0 +1,11 @@
+<?php
+
+/**
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ * @author Aivars Miška <allefm@gmail.com>
+ */
+$lang['translations'] = 'Citās valodās';
+$lang['outdated'] = 'Šis tulkojums ir vecāks par <a href="%s" class="wikilink1">oriģinālo lapu</a> un varbūt ir novecojis.';
+$lang['diff'] = 'Redzēt, ka ir <a href="%s" class="wikilink1">mainījies</a>.';
+$lang['transloaded'] = 'Vieglākai tulkošanai ir ielādēts lapas saturs no %s .<br />Bet varat balstīties arī uz šādiem tulkojumiem: %s.';
diff --git a/platform/www/lib/plugins/translation/lang/lv/settings.php b/platform/www/lib/plugins/translation/lang/lv/settings.php
new file mode 100644
index 0000000..1b61b46
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/lv/settings.php
@@ -0,0 +1,19 @@
+<?php
+
+/**
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ * @author Aivars Miška <allefm@gmail.com>
+ * @author Aivars Miška <allefm@gmail.com>
+ */
+$lang['translations'] = 'Ar atstarpēm atdalīts tulkojumu valodu saraksts (ISO kodi). Izņemot noklusēto valodu.';
+$lang['translationns'] = 'Ja tulkojumus vajag tikai noteiktā nodaļā, ieraksti to šeit.';
+$lang['skiptrans'] = 'Ja lapas nosaukums atbilst regulārajai izteiksmei, tulkošanas izvēlni nerādīt.';
+$lang['dropdown'] = 'Lietot izkrītošo izvēlni tulkojumu parādīšanai (ieteikt, ja ir vairāk par 5 valodām). ';
+$lang['translateui'] = 'Vai svešvalodu nodaļās jāpārslēdz arī lietotāja sakarnes valoda?';
+$lang['redirectstart'] = 'Vai sākuma lapai automātiski jāpārslēdzas atkarībā no pārlūkprogrammas noteiktās valodas?';
+$lang['about'] = 'Ieraksti šeit lapu, kurā lietotājiem izskaidrotas tulkošas iespējas. Tā tiks piesaistīta valodu izvēlei.';
+$lang['localabout'] = 'Lietot "par" lapas lokalizēto versiju, nevis globālo "par" lapu.';
+$lang['checkage'] = 'Brīdināt pa varbūt novecojušiem tulkojumiem. ';
+$lang['display'] = 'Norādiet, ko lietot valodas izvēlei. Ņemiet vērā, ka valodām izmantot valstu karogus neiesaka.';
+$lang['copytrans'] = 'Sākot tulkojumu, iekopēt redaktorā oriģināltekstu?';
diff --git a/platform/www/lib/plugins/translation/lang/lv/totranslate.txt b/platform/www/lib/plugins/translation/lang/lv/totranslate.txt
new file mode 100644
index 0000000..c46c14e
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/lv/totranslate.txt
@@ -0,0 +1 @@
+IZLABO **Lapa nav pilnībā pārtulkota. Lūdzu palīdzi pabeigt tulkojumu!** \\ //(Izdzēs šo rindkopu, kad tulkojums pabeigts!)// \ No newline at end of file
diff --git a/platform/www/lib/plugins/translation/lang/nl/lang.php b/platform/www/lib/plugins/translation/lang/nl/lang.php
new file mode 100644
index 0000000..1864f07
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/nl/lang.php
@@ -0,0 +1,17 @@
+<?php
+
+/**
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ * @author Gerrit Uitslag <klapinklapin@gmail.com>
+ * @author Marcel Bachus <marcel.bachus@ziggo.nl>
+ */
+$lang['translations'] = 'Vertaling van deze pagina';
+$lang['outdated'] = 'Deze vertaling is ouder dan de <a href="%s" class="wikilink1">originele pagina</a> en kan verouderd zijn.';
+$lang['diff'] = 'Kijk wat er is <a href="%s" class="wikilink1">veranderd</a>.';
+$lang['transloaded'] = 'De inhoud van vertaling van deze pagina in %s is al geladen om vertalen makkelijker te maken.<br />Maar je kunt je vertaling ook baseren op één van de volgende bestaande vertalingen: %s.';
+$lang['menu'] = 'verouderde of missende vertaling';
+$lang['missing'] = 'Niet gevonden!';
+$lang['old'] = 'verouderd';
+$lang['current'] = 'laatste stand van zaken';
+$lang['path'] = 'Pad';
diff --git a/platform/www/lib/plugins/translation/lang/nl/settings.php b/platform/www/lib/plugins/translation/lang/nl/settings.php
new file mode 100644
index 0000000..88dbe98
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/nl/settings.php
@@ -0,0 +1,21 @@
+<?php
+
+/**
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ * @author Gerrit <klapinklapin@gmail.com>
+ * @author Gerrit Uitslag <klapinklapin@gmail.com>
+ * @author Marcel Bachus <marcel.bachus@ziggo.nl>
+ */
+$lang['translations'] = 'Spatiegescheiden lijst van vertalingen (ISO codes).';
+$lang['translationns'] = 'Als je alleen vertalingen in een bepaalde namespace wenst, plaatst die dan hier.';
+$lang['skiptrans'] = 'Wanneer een paginanaam overeenstemt met deze reguliere expressie, wordt het vertaalmenu niet getoond.';
+$lang['dropdown'] = 'Gebruik een dropdownlijst om vertalingen weer te geven (aanbevolen bij meer dan 5 talen).';
+$lang['translateui'] = 'Moet de taal van de gebruikersinterface ook veranderen naar de taal van vertaalde namespace?';
+$lang['redirectstart'] = 'Moet de startpagina automatisch doorverwijzen naar de namespace van de taal die de taaldetectie van de browser doorgeeft?';
+$lang['about'] = 'Geef een paginanaam waar de vertaalfunctie wordt uitgelegd voor je gebruikers. Het zal worden gelinkt vanuit de talenkiezer.';
+$lang['localabout'] = 'Gebruik vertaalde versies van bovengenoemde vertalingsuitlegpagina (in plaats van één globale uitlegpagina).';
+$lang['checkage'] = 'Waarschuw voor mogelijk gedateerde vertalingen.';
+$lang['display'] = 'Selecteer wat je wil zien in de talenkiezer. Let op dat het gebruik van landenvlaggen in de talenkiezer niet altijd gebruiksvriendelijkheid is.';
+$lang['copytrans'] = 'De tekst in de oorspronkelijke taal naar het bewerkvenster kopiëren als er een nieuwe vertaling wordt begonnen.';
+$lang['show_path'] = 'Toon het pad naar de missende vertalings pagina?';
diff --git a/platform/www/lib/plugins/translation/lang/nl/totranslate.txt b/platform/www/lib/plugins/translation/lang/nl/totranslate.txt
new file mode 100644
index 0000000..d5f8cee
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/nl/totranslate.txt
@@ -0,0 +1 @@
+FIXME **Deze pagina is nog niet volledig vertaald. Help alsjeblieft de vertaling compleet te maken.**\\ //(verwijder deze paragraaf als de vertaling is voltooid)// \ No newline at end of file
diff --git a/platform/www/lib/plugins/translation/lang/pt-br/lang.php b/platform/www/lib/plugins/translation/lang/pt-br/lang.php
new file mode 100644
index 0000000..c6fa3f7
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/pt-br/lang.php
@@ -0,0 +1,17 @@
+<?php
+
+/**
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ * @author Felipe Castro <fefcas@gmail.com>
+ * @author Edney Rossi <edneyrossi@gmail.com>
+ */
+$lang['translations'] = 'Traduções desta página';
+$lang['outdated'] = 'Esta tradução é mais antiga que a <a href="%s" class="wikilink1"> página original </a> e pode estar desatualizada.';
+$lang['diff'] = 'Veja o que foi <a href="%s" class="wikilink1">mudado</a>.';
+$lang['transloaded'] = 'O conteúdo da tradução desta página em %s foi pré-carregado para facilitar o trabalho.< br/>Mas você pode basear sua tradução nas seguintes traduções existentes: %s.';
+$lang['menu'] = 'traduções desatualizadas e inexistentes';
+$lang['missing'] = 'Inexistente!';
+$lang['old'] = 'desatualizado';
+$lang['current'] = 'atualizada';
+$lang['path'] = 'Caminho';
diff --git a/platform/www/lib/plugins/translation/lang/pt-br/settings.php b/platform/www/lib/plugins/translation/lang/pt-br/settings.php
new file mode 100644
index 0000000..e053e13
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/pt-br/settings.php
@@ -0,0 +1,21 @@
+<?php
+
+/**
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ * @author Paulino Michelazzo <paulino@michelazzo.com.br>
+ * @author Felipe Castro <fefcas@gmail.com>
+ * @author Edney Rossi <edneyrossi@gmail.com>
+ */
+$lang['translations'] = 'Lista de idiomas separados por espaço (códigos ISO).';
+$lang['translationns'] = 'Se você deseja traduções apenas para certo idioma, coloque-o aqui.';
+$lang['skiptrans'] = 'Quando o nome-de-página estiver de acordo com esta expressão regular, não mostre o menu de tradução.';
+$lang['dropdown'] = 'Usar listagem desdobrada para mostrar as traduções (recomendado para mais que 5 línguas).';
+$lang['translateui'] = 'A interface do usuário deve ser trocada para o idioma, também?';
+$lang['redirectstart'] = 'A página inicial deve redirecionar automaticamente para o "namespace" da língua usando a detecção de idiomas no navegador?';
+$lang['about'] = 'Digite um nome de página aqui onde o recurso de tradução é explicado para seus usuários. Ele será vinculado a partir do seletor de idioma.';
+$lang['localabout'] = 'Usar versões localizadas da página "a respeito de" (em vez de uma página global "a respeito de").';
+$lang['checkage'] = 'Avisar sobre possíveis traduções desatualizadas.';
+$lang['display'] = 'Selecionar o que você gostaria de mostrar no seletor de línguas. Note que usar bandeirinhas de países para selecionar línguas não é recomendado por especialistas em usabilidade.';
+$lang['copytrans'] = 'Copiar o texto da língua original no editor quando começar uma nova tradução?';
+$lang['show_path'] = 'Mostrar o caminho na página com tradução inexistente?';
diff --git a/platform/www/lib/plugins/translation/lang/pt-br/totranslate.txt b/platform/www/lib/plugins/translation/lang/pt-br/totranslate.txt
new file mode 100644
index 0000000..5329cda
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/pt-br/totranslate.txt
@@ -0,0 +1 @@
+FIXME ** Esta página não está completamente traduzida ainda. Por favor ajude a completar sua tradução.**\\ //(remova este parágrafo assim que a tradução tenha terminado)// \ No newline at end of file
diff --git a/platform/www/lib/plugins/translation/lang/pt/lang.php b/platform/www/lib/plugins/translation/lang/pt/lang.php
new file mode 100644
index 0000000..a17ab35
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/pt/lang.php
@@ -0,0 +1,11 @@
+<?php
+
+/**
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ * @author André Neves <drakferion@gmail.com>
+ * @author Alfredo Silva <alfredo.silva@sky.com>
+ */
+$lang['translations'] = 'Traduções para esta página';
+$lang['outdated'] = 'Esta tradução é mais antiga do que a <a href="%s" class="wikilink1">página original</a> e poderá estar desatualizada.';
+$lang['diff'] = 'Veja o que foi <a href="%s" class="wikilink1">alterado</a>.';
diff --git a/platform/www/lib/plugins/translation/lang/pt/settings.php b/platform/www/lib/plugins/translation/lang/pt/settings.php
new file mode 100644
index 0000000..4cdd114
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/pt/settings.php
@@ -0,0 +1,19 @@
+<?php
+
+/**
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ * @author André Neves <drakferion@gmail.com>
+ * @author Alfredo Silva <alfredo.silva@sky.com>
+ */
+$lang['translations'] = 'Lista de idiomas de tradução (códigos ISO) separada por espaço.';
+$lang['translationns'] = 'Se pretender apenas as traduções abaixo de um determinado espaço de nome, coloque-as aqui.';
+$lang['skiptrans'] = 'Quando o nome da página corresponder com esta expressão regular, não mostrar o menu de tradução.';
+$lang['dropdown'] = 'Utilizar uma lista de menu para exibir as traduções (recomendado para mais de 5 idiomas).';
+$lang['translateui'] = 'O idioma da interface do utilizador também deverá ser alterado nos espaços de nome do idioma estrangeiro?';
+$lang['redirectstart'] = 'A página inicial deve redirecionar automaticamente para um espaço de nome do idioma utilizando a deteção de idioma do navegador?';
+$lang['about'] = 'Insira aqui um nome de página onde a funcionalidade de tradução é explicada aos seus utilizadores. O seletor de língua terá uma ligação para lá.';
+$lang['localabout'] = 'Utilizar versões localizadas da página sobre (em vez de uma página global sobre).';
+$lang['checkage'] = 'Avisar sobre as possíveis traduções desatualizadas.';
+$lang['display'] = 'Selecione o que gostaria de ver mostrado no seletor de linguagem. Note que usar bandeiras de países para seleção de linguagem não é recomendado por peritos de usabilidade.';
+$lang['copytrans'] = 'Copiar o texto do idioma original no editor quando iniciar uma nova tradução?';
diff --git a/platform/www/lib/plugins/translation/lang/ru/lang.php b/platform/www/lib/plugins/translation/lang/ru/lang.php
new file mode 100644
index 0000000..dc85abc
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/ru/lang.php
@@ -0,0 +1,18 @@
+<?php
+
+/**
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ * @author Aleksandr Selivanov <alexgearbox@gmail.com>
+ * @author Vasilyy Balyasnyy <v.balyasnyy@gmail.com>
+ * @author Anotheroneuser <w20151222@ya.ru>
+ */
+$lang['translations'] = 'Перевод этой страницы';
+$lang['outdated'] = 'Этот перевод старее, чем <a href="%s" class="wikilink1">оригинальная страница</a>, и может быть неактуальным.';
+$lang['diff'] = 'Смотрите, что <a href="%s" class="wikilink1">было изменено</a>.';
+$lang['transloaded'] = 'Содержание перевода этой страницы в %s было предварительно загружено для упрощения перевода.<br />Но вы можете переводить на основе следующего существующего перевода: %s.';
+$lang['menu'] = 'Устаревшие или отсутствующие переводы';
+$lang['missing'] = 'Отсутствует! ';
+$lang['old'] = 'устарело';
+$lang['current'] = 'обновить (привести в актуальное состояние)';
+$lang['path'] = 'Путь';
diff --git a/platform/www/lib/plugins/translation/lang/ru/settings.php b/platform/www/lib/plugins/translation/lang/ru/settings.php
new file mode 100644
index 0000000..a974b68
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/ru/settings.php
@@ -0,0 +1,21 @@
+<?php
+
+/**
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ * @author Oleksiy Zagorskyi <zalex_ua@i.ua>
+ * @author Aleksandr Selivanov <alexgearbox@gmail.com>
+ * @author Anotheroneuser <w20151222@ya.ru>
+ */
+$lang['translations'] = 'Список поддерживаемых языков перевода (двухсимвольные коды ISO). Разделите значения пробелами.';
+$lang['translationns'] = 'Если вы хотите перевести только определённое пространство имён, тогда впишите здесь его имя.';
+$lang['skiptrans'] = 'Если имя страницы соответствует этому регулярному выражению, тогда не отображать меню перевода.';
+$lang['dropdown'] = 'Использовать выпадающий список для отображения доступных переводов (рекомендуется, если более 5 переводов)';
+$lang['translateui'] = 'Должен ли язык интерфейса пользователя также переключаться согласно языку пространства имён?';
+$lang['redirectstart'] = 'Должна ли стартовая страница автоматически перенаправляться на пространство имён языка, используя автоопределение языка браузера?';
+$lang['about'] = 'Введите здесь имя страницы, на которой будут разъяснены функции перевода для ваших пользователей. Она будет связана с выбором языка.';
+$lang['localabout'] = 'Использовать локализованную версию страницы разъяснений (вместо одной глобальной страницы разъяснений).';
+$lang['checkage'] = 'Отображать предупреждение о возможной неактуальности перевода?';
+$lang['display'] = 'Выберите, что бы вы хотели видеть в поле выбора языков. Имейте в виду, что использование изображения государственного флага в поле выбора языков не было рекомендовано экспертами в области потребительского удобства. ';
+$lang['copytrans'] = 'Копировать текст оригинала в окно редактирования при создании нового перевода?';
+$lang['show_path'] = 'Показывать путь на непереведённых страницах? ';
diff --git a/platform/www/lib/plugins/translation/lang/ru/totranslate.txt b/platform/www/lib/plugins/translation/lang/ru/totranslate.txt
new file mode 100644
index 0000000..b34588e
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/ru/totranslate.txt
@@ -0,0 +1 @@
+FIXME **Эта страница пока что не переведена полностью. Пожалуйста, помогите завершить перевод.**\\ //(Сотрите это сообщение по окончании перевода.)// \ No newline at end of file
diff --git a/platform/www/lib/plugins/translation/lang/sl/lang.php b/platform/www/lib/plugins/translation/lang/sl/lang.php
new file mode 100644
index 0000000..be8a195
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/sl/lang.php
@@ -0,0 +1,9 @@
+<?php
+
+/**
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ */
+$lang['translations'] = 'Prevod trenutne strani';
+$lang['outdated'] = 'Prevod je starejši od <a href="%s" class="wikilink1">izvorne strani</a> in je zato lahko zastarel.';
+$lang['diff'] = 'Oglejte si <a href="%s" class="wikilink1">spremembe</a>.';
diff --git a/platform/www/lib/plugins/translation/lang/sl/settings.php b/platform/www/lib/plugins/translation/lang/sl/settings.php
new file mode 100644
index 0000000..6bdcff4
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/sl/settings.php
@@ -0,0 +1,18 @@
+<?php
+
+/**
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @author Matej Urbančič <mateju@svn.gnome.org>
+ */
+$lang['translations'] = 'Space separated list of translation languages (ISO codes).';
+$lang['translationns'] = 'If you only want translations below a certain namespace, put it here.';
+$lang['skiptrans'] = 'When the pagename matches this regular expression, don\'t show the translation menu.';
+$lang['dropdown'] = 'Use a dropdown list to display the translations (recommended for more than 5 languages).';
+$lang['translateui'] = 'Should the language of the user interface be switched in foreign language namespaces, too?';
+$lang['redirectstart'] = 'Should the start page automatically redirect into a language namespace using browser language detection?';
+$lang['about'] = 'Enter a pagename here where the translation feature is explained for your users. It will be linked from the language selector.';
+$lang['localabout'] = 'Uporabi prevedeno različico strani o vstavku (namesto splošne strani).';
+$lang['checkage'] = 'Opozori o zastarelem prevodu.';
+$lang['display'] = 'Izbor možnosti za prikaz jezika v izbirniku jezika. Izbor zastave jezika v izbiri ni priporočen.';
diff --git a/platform/www/lib/plugins/translation/lang/sv/lang.php b/platform/www/lib/plugins/translation/lang/sv/lang.php
new file mode 100644
index 0000000..6f40f58
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/sv/lang.php
@@ -0,0 +1,16 @@
+<?php
+
+/**
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ * @author Tor Härnqvist <tor@harnqvist.se>
+ */
+$lang['translations'] = 'Översättningar av denna sida';
+$lang['outdated'] = 'Denna översättning är äldre än <a href="%s" class="wikilink1">orginalsidan</a> och kan vara daterad.';
+$lang['diff'] = 'Se vad som har <a href="%s" class="wikilink1">ändrats</a>.';
+$lang['transloaded'] = 'Innehållet på denna sidas översättning på %s har blivit förinläst för enklare översättning.<br />Du kan dock basera din översättning på följande redan existerande översättningar: %s.';
+$lang['menu'] = 'daterade och saknade översättningar';
+$lang['missing'] = 'Saknas!';
+$lang['old'] = 'daterad';
+$lang['current'] = 'uppdaterad';
+$lang['path'] = 'Sökväg';
diff --git a/platform/www/lib/plugins/translation/lang/sv/settings.php b/platform/www/lib/plugins/translation/lang/sv/settings.php
new file mode 100644
index 0000000..4cd67f4
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/sv/settings.php
@@ -0,0 +1,19 @@
+<?php
+
+/**
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ * @author Tor Härnqvist <tor@harnqvist.se>
+ */
+$lang['translations'] = 'Blankstegsseparerad lista över översatta språk (ISO-koder).';
+$lang['translationns'] = 'Om du bara önskar översättningar under en specifik namnrymd, placera den här.';
+$lang['skiptrans'] = 'När sidnamnet matchar detta reguljära uttryck, visa ej översättningsmenyn.';
+$lang['dropdown'] = 'Använd en rullista för att visa översättningarna (rekommenderat till fem eller fler språk).';
+$lang['translateui'] = 'Skall språket för användargränssnittet växlas även för namnrymder på främmande språk?';
+$lang['redirectstart'] = 'Skall startsidan automatiskt omdirigeras till en språkspecifik namnrymd baserat på webbläsarens språkdetektion?';
+$lang['about'] = 'Fyll i ett sidnamn här där översättningsfunktionen förklaras för dina användare. Sidan kommer att länkas från språkväljaren.';
+$lang['localabout'] = 'Använd översatt version av "Om"-sidan (istället för en global "Om"-sida).';
+$lang['checkage'] = 'Varna för möjligt daterade översättningar.';
+$lang['display'] = 'Ange vad du önskar visas i menyn för språkval. Vänligen notera att nationsfanor för språkval ej är rekommenderat.';
+$lang['copytrans'] = 'Kopiera texten på originalspråket till textredigeraren när en ny översättning inleds?';
+$lang['show_path'] = 'Visa sökväg på sidan för den saknade översättningen?';
diff --git a/platform/www/lib/plugins/translation/lang/sv/totranslate.txt b/platform/www/lib/plugins/translation/lang/sv/totranslate.txt
new file mode 100644
index 0000000..d4f7a53
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/sv/totranslate.txt
@@ -0,0 +1 @@
+FIXME **Denna sida är inte helt översatt än. Var god hjälp till att avsluta översättningen.**\\ //(ta bort detta stycke när översättningen är färdig)// \ No newline at end of file
diff --git a/platform/www/lib/plugins/translation/lang/tr/lang.php b/platform/www/lib/plugins/translation/lang/tr/lang.php
new file mode 100644
index 0000000..6f8f617
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/tr/lang.php
@@ -0,0 +1,10 @@
+<?php
+
+/**
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ * @author İlker R. Kapaç <irifat@gmail.com>
+ */
+$lang['translations'] = 'Bu sayfanın çevirileri';
+$lang['outdated'] = 'Bu çeviri <a href="%s" class="wikilink1">orjinal sayfadan</a> daha eski tarihli. Dolayısıyla güncel olmayabilir.';
+$lang['diff'] = 'Nelerin değiştiğini görmek için <a href="%s" class="wikilink1">tıklayın</a>.';
diff --git a/platform/www/lib/plugins/translation/lang/tr/settings.php b/platform/www/lib/plugins/translation/lang/tr/settings.php
new file mode 100644
index 0000000..bad9dbc
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/tr/settings.php
@@ -0,0 +1,15 @@
+<?php
+
+/**
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ * @author İlker R. Kapaç <irifat@gmail.com>
+ */
+$lang['translations'] = 'Tercüme dillerinin listesi. (boşluk ile ayrılmış, ISO kodları)';
+$lang['translationns'] = 'Eğer tercümelerin bir isim alanın (namespace) altında olmasını istiyorsanız, buraya yazın.';
+$lang['skiptrans'] = 'İsim alanı (Namespace) buradaki tanıma uyduğunda, tercüme arayüzünü gösterme.';
+$lang['dropdown'] = 'Dilleri listelemek için açılır arayüz kullan. (5\'ten fazla dil olduğunda kullanılması önerilir)';
+$lang['localabout'] = 'Bir tane genel "hakkında" sayfası kullanmak yerine, yerelleştirilmiş "hakkında" sayfaları kullan. ';
+$lang['checkage'] = 'Eski tarihli tercümeler hakkında uyarı göster.';
+$lang['display'] = 'Dil seçiminde görünmesini istediklerinizi seçin. Lütfen unutmayın, dil seçiminde ülke bayrağı kullanmak, erişilebilirlik uzmanları tarafından tavsiye edilmez.';
+$lang['copytrans'] = 'Yeni tercümeye başlarken orjinal dildeki metin, düzenleme ekranına kopyalansın mı?';
diff --git a/platform/www/lib/plugins/translation/lang/tr/totranslate.txt b/platform/www/lib/plugins/translation/lang/tr/totranslate.txt
new file mode 100644
index 0000000..e281874
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/tr/totranslate.txt
@@ -0,0 +1 @@
+FIXME **Bu sayfanın çevirisi henüz tamamlanmadı. Lütfen çevirinin tamamlanmasına yardımcı olun.**\\ //(Çeviri tamamlandığında bu paragrafı silin)// \ No newline at end of file
diff --git a/platform/www/lib/plugins/translation/lang/uk/lang.php b/platform/www/lib/plugins/translation/lang/uk/lang.php
new file mode 100644
index 0000000..8be6648
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/uk/lang.php
@@ -0,0 +1,17 @@
+<?php
+
+/**
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ * @author Олексій <alexey.furashev@gmail.com>
+ * @author Vitaly <vitaly.balashov@smuzzy.com.ua>
+ */
+$lang['translations'] = 'Переклад цієї сторінки';
+$lang['outdated'] = 'Цей переклад старіший ніж <a href="%s" class="wikilink1">оригінальна сторінка</a> і може бути не актуальним.';
+$lang['diff'] = 'Дивіться що <a href="%s" class="wikilink1">було змінено</a>.';
+$lang['transloaded'] = 'Зміст перекладу цієї сторінки %s був попередньо завантажений для зручності перекладу. <br /> Також можете використовувати наступні переклади: %s.';
+$lang['menu'] = 'застарілі та відсутні переклади';
+$lang['missing'] = 'Відсутній!';
+$lang['old'] = 'застарілий';
+$lang['current'] = 'поточний';
+$lang['path'] = 'Шлях';
diff --git a/platform/www/lib/plugins/translation/lang/uk/settings.php b/platform/www/lib/plugins/translation/lang/uk/settings.php
new file mode 100644
index 0000000..51ec6d4
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/uk/settings.php
@@ -0,0 +1,23 @@
+<?php
+
+/**
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ * Ukrainian language file
+ *
+ * @author Олексій <alexey.furashev@gmail.com>
+ * @author Vitaly <vitaly.balashov@smuzzy.com.ua>
+ * @author Oleksiy Zagorskyi <zalex_ua@i.ua>
+ */
+$lang['translations'] = 'Список підтримуваних мов перекладу (двохсимвольні коди ISO). Розділіть значення комами або пробілами.';
+$lang['translationns'] = 'Якщо ви хочете перекласти тільки визначений Простір імен, тоді впишіть тут його ім\'я.';
+$lang['skiptrans'] = 'Якщо ім\'я сторінки відповідає цьому регулярному виразу, тоді не відображувати меню перекладів.';
+$lang['dropdown'] = 'Використовувати випадаючий список для відображення доступних перекладів (рекомендується, якщо більше 5 перекладів)';
+$lang['translateui'] = 'Чи повинна мова інтерфейсу користувача також перемикатись відповідно до мови Простору імен?';
+$lang['redirectstart'] = 'Чи повинна стартова сторінка автоматично перенаправлятись на Простір імен мови, використовуючи детектекцію мови оглядача?';
+$lang['about'] = 'Введіть тут ім\'я сторінки, на якій буде роз\'яснено функції перекладу для ваших користувачів. Вона буде пов\'язана з вибором мови.';
+$lang['localabout'] = 'Використовувати локалізовану версію сторінки роз\'яснень (замість однієї глобальної сторінки роз\'яснень).';
+$lang['checkage'] = 'Відображувати попередження про можливу не актуальність перекладу сторінок?';
+$lang['display'] = 'Оберіть що б ви хотіли відображувати в перемикачі мов. Примітка: використовувати прапор країни для перемикача мов не рекомендується експертами по зручності використання інтерфейсу.';
+$lang['copytrans'] = 'Зкопіювати текст мовою оригіналу до редактора на початку нового перекладу?';
+$lang['show_path'] = 'Показати шлях на відсутній сторінці перекладу?';
diff --git a/platform/www/lib/plugins/translation/lang/uk/totranslate.txt b/platform/www/lib/plugins/translation/lang/uk/totranslate.txt
new file mode 100644
index 0000000..ee2d980
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/uk/totranslate.txt
@@ -0,0 +1 @@
+FIXME ** Ця сторінка ще не повністю переведена. Будь-ласка, допоможіть завершити переклад. ** \\ //(видаліть цей абзац після завершення перекладу)// \ No newline at end of file
diff --git a/platform/www/lib/plugins/translation/lang/zh-tw/lang.php b/platform/www/lib/plugins/translation/lang/zh-tw/lang.php
new file mode 100644
index 0000000..7b9f694
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/zh-tw/lang.php
@@ -0,0 +1,6 @@
+<?php
+
+$lang['translations'] = '本頁之翻譯';
+$lang['outdated'] = '這份翻譯較<a href="%s" class="wikilink1">原始頁面</a>舊,可能已過時。';
+$lang['diff'] = '檢視<a href="%s" class="wikilink1">變更</a>。';
+
diff --git a/platform/www/lib/plugins/translation/lang/zh-tw/settings.php b/platform/www/lib/plugins/translation/lang/zh-tw/settings.php
new file mode 100644
index 0000000..7cc76dc
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/zh-tw/settings.php
@@ -0,0 +1,16 @@
+<?php
+/**
+ * Traditional Chinese language file
+ */
+
+$lang['translations'] = '空白分隔的翻譯語言列表 (ISO 碼)。不填預設語言將使用根命名空間。';
+$lang['translationns'] = '如果你只想翻譯某些命名空間,請寫在這裡。';
+$lang['skiptrans'] = '頁面名稱若符合此正規式就不顯示翻譯選單。';
+$lang['dropdown'] = '用下拉式選單顯示 (多於 5 個語言時建議使用)。';
+$lang['translateui'] = '在外語的命名空間時,也轉換使用者介面嗎?';
+$lang['redirectstart'] = '要偵測覽器語言,把開始頁面重新導向到語言的命名空間嗎?';
+$lang['about'] = '用於解釋翻譯機制的頁面名稱,它的連結會出現在語言選單。';
+$lang['localabout'] = '解釋頁使用翻譯版本 (而非一個通用頁)。';
+$lang['checkage'] = '警告可能過時的翻譯。';
+$lang['display'] = '選擇你希望在語言選單中顯示的項目。注意國旗 (flag) 選項是不被易用性專家建議的。';
+
diff --git a/platform/www/lib/plugins/translation/lang/zh/lang.php b/platform/www/lib/plugins/translation/lang/zh/lang.php
new file mode 100644
index 0000000..4aaa2d3
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/zh/lang.php
@@ -0,0 +1,18 @@
+<?php
+
+/**
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ * @author hfl <huangfeilong@gmail.com>
+ * @author oott123 <ip.192.168.1.1@qq.com>
+ * @author kuma <kuma000@qq.com>
+ */
+$lang['translations'] = '本页面的其他翻译';
+$lang['outdated'] = '翻译跟<a href="%s" class="wikilink1">原始页面</a>比较起来显得有些陈旧,所以可能失效。';
+$lang['diff'] = '查看<a href="%s" class="wikilink1">更新</a>';
+$lang['transloaded'] = '此页面的 %s 已经由 easy translation 预翻译。<br />但你可以以以下现存的语言为基础翻译你的版本。%s';
+$lang['menu'] = '过时的和缺失的翻译';
+$lang['missing'] = '缺失!';
+$lang['old'] = '过时';
+$lang['current'] = '最新的';
+$lang['path'] = '路径';
diff --git a/platform/www/lib/plugins/translation/lang/zh/settings.php b/platform/www/lib/plugins/translation/lang/zh/settings.php
new file mode 100644
index 0000000..6ca6666
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/zh/settings.php
@@ -0,0 +1,21 @@
+<?php
+
+/**
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ * @author ZDYX <zhangduyixiong@gmail.com>
+ * @author oott123 <ip.192.168.1.1@qq.com>
+ * @author kuma <kuma000@qq.com>
+ */
+$lang['translations'] = '使用空格分隔的翻译语言列表(ISO 码)。请勿填入默认语言。';
+$lang['translationns'] = '如果您只希望本插件作用于某个特定的名称空间,请在这里写上其名称。';
+$lang['skiptrans'] = '当页面名称与此正则匹配时,不要显示翻译菜单。';
+$lang['dropdown'] = '使用下拉列表显示翻译语言(5+语言时建议启用)';
+$lang['translateui'] = '整个用户界面也跟随某个页面的翻译语言而改变吗?';
+$lang['redirectstart'] = '首页是否根据浏览器语言自动切换到相应语言?';
+$lang['about'] = '请在此输入向用户解释翻译功能的页面的名称空间。它的链接将出现在语言选择器上。';
+$lang['localabout'] = '使用本地化的关于页面(而不是一个全局关于页面)。';
+$lang['checkage'] = '警告:可能过时了的翻译。';
+$lang['display'] = '选择你想在选择器中显示什么。注意可用性专家并不推荐使用国旗选择语言。';
+$lang['copytrans'] = '开始新翻译的时候在编辑器中复制原始语言版本?';
+$lang['show_path'] = '缺失的翻译页面上显示路径?';
diff --git a/platform/www/lib/plugins/translation/lang/zh/totranslate.txt b/platform/www/lib/plugins/translation/lang/zh/totranslate.txt
new file mode 100644
index 0000000..aaa32f9
--- /dev/null
+++ b/platform/www/lib/plugins/translation/lang/zh/totranslate.txt
@@ -0,0 +1 @@
+等待修复 **此页面没有被翻译完全。请帮助翻译本页。**\\ //(当全文翻译完时请移除这个段落。)// \ No newline at end of file
diff --git a/platform/www/lib/plugins/translation/manager.dat b/platform/www/lib/plugins/translation/manager.dat
new file mode 100644
index 0000000..29afe7c
--- /dev/null
+++ b/platform/www/lib/plugins/translation/manager.dat
@@ -0,0 +1,2 @@
+downloadurl=https://github.com/splitbrain/dokuwiki-plugin-translation/zipball/master
+installed=Wed, 23 Jan 2019 14:38:38 -0300
diff --git a/platform/www/lib/plugins/translation/plugin.info.txt b/platform/www/lib/plugins/translation/plugin.info.txt
new file mode 100644
index 0000000..9424ec9
--- /dev/null
+++ b/platform/www/lib/plugins/translation/plugin.info.txt
@@ -0,0 +1,8 @@
+# General Plugin Info do not edit
+base translation
+author Andreas Gohr
+email andi@splitbrain.org
+date 2018-08-17
+name Translation Plugin
+desc Supports the easy setup of a multi-language wiki.
+url http://www.dokuwiki.org/plugin:translation
diff --git a/platform/www/lib/plugins/translation/print.css b/platform/www/lib/plugins/translation/print.css
new file mode 100644
index 0000000..c2fd328
--- /dev/null
+++ b/platform/www/lib/plugins/translation/print.css
@@ -0,0 +1 @@
+.dokuwiki div.plugin_translation { display: none }
diff --git a/platform/www/lib/plugins/translation/script.js b/platform/www/lib/plugins/translation/script.js
new file mode 100644
index 0000000..819b80e
--- /dev/null
+++ b/platform/www/lib/plugins/translation/script.js
@@ -0,0 +1,20 @@
+/**
+ * Remove go button from translation dropdown
+ */
+jQuery(function(){
+ var $frm = jQuery('#translation__dropdown');
+ if(!$frm.length) return;
+ $frm.find('input[name=go]').hide();
+ $frm.find('select[name=id]').change(function(){
+ var id = jQuery(this).val();
+ // this should hopefully detect rewriting good enough:
+ var action = $frm.attr('action');
+ if(action.substr(action.length-1) == '/'){
+ var link = action + id;
+ }else{
+ var link = action + '?id=' + id;
+ }
+
+ window.location.href= link;
+ });
+});
diff --git a/platform/www/lib/plugins/translation/style.css b/platform/www/lib/plugins/translation/style.css
new file mode 100644
index 0000000..4ace94d
--- /dev/null
+++ b/platform/www/lib/plugins/translation/style.css
@@ -0,0 +1,114 @@
+.dokuwiki div.plugin_translation {
+ font-size: 95%;
+ padding-right: 0.1em;
+ margin : 0.0em 0 0.3em 0;
+}
+
+/* List */
+
+.dokuwiki div.plugin_translation ul {
+ padding: 0;
+ margin: 0;
+ display: inline-block;
+}
+.dokuwiki div.plugin_translation ul li {
+ float: left;
+ list-style-type: none;
+ padding: 0;
+ margin: 0.2em 0 0 0;
+}
+.dokuwiki div.plugin_translation ul li img {
+ margin: -0.1em 0.2em;
+}
+
+#dokuwiki__footer .plugin_translation ul li a.wikilink1:link,
+#dokuwiki__footer .plugin_translation ul li a.wikilink1:active,
+#dokuwiki__footer .plugin_translation ul li a.wikilink1:visited {
+ background-color: #fff;
+ color: #505050;
+ text-decoration:none;
+ padding: 0.1em 0.4em;
+ margin: 0.1em 0.2em;
+ border: none !important;
+}
+
+#dokuwiki__footer .plugin_translation ul li a.wikilink1:hover {
+ background-color: #eee;
+}
+
+
+#dokuwiki__footer .plugin_translation ul li a.wikilink2:link,
+#dokuwiki__footer .plugin_translation ul li a.wikilink2:hover,
+#dokuwiki__footer .plugin_translation ul li a.wikilink2:active,
+#dokuwiki__footer .plugin_translation ul li a.wikilink2:visited {
+ background-color: #fff;
+ color: #ccc;
+ text-decoration:none;
+ padding: 0.1em 0.4em;
+ margin: 0.1em 0.2em;
+ border: none !important;
+}
+
+#dokuwiki__footer .plugin_translation ul li a.wikilink1.cur:link,
+#dokuwiki__footer .plugin_translation ul li a.wikilink1.cur:hover,
+#dokuwiki__footer .plugin_translation ul li a.wikilink1.cur:active,
+#dokuwiki__footer .plugin_translation ul li a.wikilink1.cur:visited,
+#dokuwiki__footer .plugin_translation ul li a.wikilink2.cur:link,
+#dokuwiki__footer .plugin_translation ul li a.wikilink2.cur:hover,
+#dokuwiki__footer .plugin_translation ul li a.wikilink2.cur:active,
+#dokuwiki__footer .plugin_translation ul li a.wikilink2.cur:visited {
+ background-color: #505050 !important;
+ color: #fff !important;
+ border-radius: 3px;
+
+}
+
+
+/* Dropdown */
+
+.dokuwiki div.plugin_translation select,
+.dokuwiki div.plugin_translation input {
+ border: none;
+ background-color: #ccc;
+}
+
+.dokuwiki div.plugin_translation option.flag {
+ padding-left: 18px;
+ background-repeat: no-repeat;
+ background-position: left center;
+}
+
+.dokuwiki div.plugin_translation select.wikilink1,
+.dokuwiki div.plugin_translation option.wikilink1 {
+ color: #000080;
+ text-align: center;
+}
+
+.dokuwiki div.plugin_translation select.wikilink2,
+.dokuwiki div.plugin_translation option.wikilink2 {
+ color: #808080;
+ text-align: center;
+}
+
+/* flags for non-existing pages */
+.dokuwiki div.plugin_translation img.wikilink2,
+.dokuwiki div.plugin_translation .wikilink2 img {
+ opacity: 0.5;
+}
+
+table#outdated_translations td {
+ padding-left: 3px;
+ padding-right: 3px;
+}
+
+table#outdated_translations td.missing {
+ background-color: #ff6666;
+}
+
+table#outdated_translations td.outdated {
+ background-color: #ffff66;
+}
+
+table#outdated_translations td.current {
+ background-color: #00CC00;
+}
diff --git a/platform/www/lib/plugins/translation/syntax/notrans.php b/platform/www/lib/plugins/translation/syntax/notrans.php
new file mode 100644
index 0000000..0d04671
--- /dev/null
+++ b/platform/www/lib/plugins/translation/syntax/notrans.php
@@ -0,0 +1,92 @@
+<?php
+/**
+ * Translation Plugin: Simple multilanguage plugin
+ *
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+// must be run within Dokuwiki
+if(!defined('DOKU_INC')) die();
+
+/**
+ * Class syntax_plugin_translation_notrans
+ */
+class syntax_plugin_translation_notrans extends DokuWiki_Syntax_Plugin {
+
+ /**
+ * for th helper plugin
+ * @var helper_plugin_translation
+ */
+ var $hlp = null;
+
+ /**
+ * Constructor. Load helper plugin
+ */
+ function __construct(){
+ $this->hlp = plugin_load('helper', 'translation');
+ }
+
+ /**
+ * What kind of syntax are we?
+ */
+ function getType(){
+ return 'substition';
+ }
+
+ /**
+ * Where to sort in?
+ */
+ function getSort(){
+ return 155;
+ }
+
+ /**
+ * Connect pattern to lexer
+ *
+ * @param string $mode
+ */
+ function connectTo($mode) {
+ $this->Lexer->addSpecialPattern('~~NOTRANS~~',$mode,'plugin_translation_notrans');
+ }
+
+ /**
+ * Handler to prepare matched data for the rendering process
+ *
+ * @param string $match The text matched by the patterns
+ * @param int $state The lexer state for the match
+ * @param int $pos The character position of the matched text
+ * @param Doku_Handler $handler The Doku_Handler object
+ * @return bool|array Return an array with all data you want to use in render, false don't add an instruction
+ */
+ function handle($match, $state, $pos, Doku_Handler $handler){
+ return array('notrans');
+ }
+
+ /**
+ * Create output
+ *
+ * @param string $format
+ * @param Doku_Renderer $renderer
+ * @param array $data
+ * @return bool
+ */
+ function render($format, Doku_Renderer $renderer, $data) {
+ // store info in metadata
+ if($format == 'metadata'){
+ /** @var Doku_Renderer_metadata $renderer */
+ $renderer->meta['plugin']['translation']['notrans'] = true;
+ }
+ return false;
+ }
+
+ // for backward compatibility
+ /**
+ * @return string
+ */
+ function _showTranslations(){
+ return $this->hlp->showTranslations();
+ }
+
+}
+
+//Setup VIM: ex: et ts=4 enc=utf-8 :
diff --git a/platform/www/lib/plugins/translation/syntax/trans.php b/platform/www/lib/plugins/translation/syntax/trans.php
new file mode 100644
index 0000000..152276e
--- /dev/null
+++ b/platform/www/lib/plugins/translation/syntax/trans.php
@@ -0,0 +1,72 @@
+<?php
+/**
+ * Translation Plugin: Simple multilanguage plugin
+ *
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+// must be run within Dokuwiki
+if(!defined('DOKU_INC')) die();
+
+/**
+ * Class syntax_plugin_translation_trans
+ */
+class syntax_plugin_translation_trans extends DokuWiki_Syntax_Plugin {
+ /**
+ * What kind of syntax are we?
+ */
+ function getType() {
+ return 'substition';
+ }
+
+ /**
+ * Where to sort in?
+ */
+ function getSort() {
+ return 155;
+ }
+
+ /**
+ * Connect pattern to lexer
+ *
+ * @param string $mode
+ */
+ function connectTo($mode) {
+ $this->Lexer->addSpecialPattern('~~TRANS~~', $mode, 'plugin_translation_trans');
+ }
+
+ /**
+ * Handler to prepare matched data for the rendering process
+ *
+ * @param string $match The text matched by the patterns
+ * @param int $state The lexer state for the match
+ * @param int $pos The character position of the matched text
+ * @param Doku_Handler $handler The Doku_Handler object
+ * @return bool|array Return an array with all data you want to use in render, false don't add an instruction
+ */
+ function handle($match, $state, $pos, Doku_Handler $handler) {
+ return array();
+ }
+
+ /**
+ * Handles the actual output creation.
+ *
+ * @param string $format output format being rendered
+ * @param Doku_Renderer $renderer the current renderer object
+ * @param array $data data created by handler()
+ * @return boolean rendered correctly? (however, returned value is not used at the moment)
+ */
+ function render($format, Doku_Renderer $renderer, $data) {
+ if($format != 'xhtml') return false;
+ // disable caching
+ $renderer->nocache();
+
+ /** @var helper_plugin_translation $hlp */
+ $hlp = plugin_load('helper', 'translation');
+ $renderer->doc .= $hlp->showTranslations();
+ return true;
+ }
+
+}
+
+//Setup VIM: ex: et ts=4 enc=utf-8 :
diff --git a/platform/www/lib/plugins/usermanager/admin.php b/platform/www/lib/plugins/usermanager/admin.php
new file mode 100644
index 0000000..4234671
--- /dev/null
+++ b/platform/www/lib/plugins/usermanager/admin.php
@@ -0,0 +1,1235 @@
+<?php
+/*
+ * User Manager
+ *
+ * Dokuwiki Admin Plugin
+ *
+ * This version of the user manager has been modified to only work with
+ * objectified version of auth system
+ *
+ * @author neolao <neolao@neolao.com>
+ * @author Chris Smith <chris@jalakai.co.uk>
+ */
+
+/**
+ * All DokuWiki plugins to extend the admin function
+ * need to inherit from this class
+ */
+class admin_plugin_usermanager extends DokuWiki_Admin_Plugin
+{
+ const IMAGE_DIR = DOKU_BASE.'lib/plugins/usermanager/images/';
+
+ protected $auth = null; // auth object
+ protected $users_total = 0; // number of registered users
+ protected $filter = array(); // user selection filter(s)
+ protected $start = 0; // index of first user to be displayed
+ protected $last = 0; // index of the last user to be displayed
+ protected $pagesize = 20; // number of users to list on one page
+ protected $edit_user = ''; // set to user selected for editing
+ protected $edit_userdata = array();
+ protected $disabled = ''; // if disabled set to explanatory string
+ protected $import_failures = array();
+ protected $lastdisabled = false; // set to true if last user is unknown and last button is hence buggy
+
+ /**
+ * Constructor
+ */
+ public function __construct()
+ {
+ /** @var DokuWiki_Auth_Plugin $auth */
+ global $auth;
+
+ $this->setupLocale();
+
+ if (!isset($auth)) {
+ $this->disabled = $this->lang['noauth'];
+ } elseif (!$auth->canDo('getUsers')) {
+ $this->disabled = $this->lang['nosupport'];
+ } else {
+ // we're good to go
+ $this->auth = & $auth;
+ }
+
+ // attempt to retrieve any import failures from the session
+ if (!empty($_SESSION['import_failures'])) {
+ $this->import_failures = $_SESSION['import_failures'];
+ }
+ }
+
+ /**
+ * Return prompt for admin menu
+ *
+ * @param string $language
+ * @return string
+ */
+ public function getMenuText($language)
+ {
+
+ if (!is_null($this->auth))
+ return parent::getMenuText($language);
+
+ return $this->getLang('menu').' '.$this->disabled;
+ }
+
+ /**
+ * return sort order for position in admin menu
+ *
+ * @return int
+ */
+ public function getMenuSort()
+ {
+ return 2;
+ }
+
+ /**
+ * @return int current start value for pageination
+ */
+ public function getStart()
+ {
+ return $this->start;
+ }
+
+ /**
+ * @return int number of users per page
+ */
+ public function getPagesize()
+ {
+ return $this->pagesize;
+ }
+
+ /**
+ * @param boolean $lastdisabled
+ */
+ public function setLastdisabled($lastdisabled)
+ {
+ $this->lastdisabled = $lastdisabled;
+ }
+
+ /**
+ * Handle user request
+ *
+ * @return bool
+ */
+ public function handle()
+ {
+ global $INPUT;
+ if (is_null($this->auth)) return false;
+
+ // extract the command and any specific parameters
+ // submit button name is of the form - fn[cmd][param(s)]
+ $fn = $INPUT->param('fn');
+
+ if (is_array($fn)) {
+ $cmd = key($fn);
+ $param = is_array($fn[$cmd]) ? key($fn[$cmd]) : null;
+ } else {
+ $cmd = $fn;
+ $param = null;
+ }
+
+ if ($cmd != "search") {
+ $this->start = $INPUT->int('start', 0);
+ $this->filter = $this->retrieveFilter();
+ }
+
+ switch ($cmd) {
+ case "add":
+ $this->addUser();
+ break;
+ case "delete":
+ $this->deleteUser();
+ break;
+ case "modify":
+ $this->modifyUser();
+ break;
+ case "edit":
+ $this->editUser($param);
+ break;
+ case "search":
+ $this->setFilter($param);
+ $this->start = 0;
+ break;
+ case "export":
+ $this->exportCSV();
+ break;
+ case "import":
+ $this->importCSV();
+ break;
+ case "importfails":
+ $this->downloadImportFailures();
+ break;
+ }
+
+ $this->users_total = $this->auth->canDo('getUserCount') ? $this->auth->getUserCount($this->filter) : -1;
+
+ // page handling
+ switch ($cmd) {
+ case 'start':
+ $this->start = 0;
+ break;
+ case 'prev':
+ $this->start -= $this->pagesize;
+ break;
+ case 'next':
+ $this->start += $this->pagesize;
+ break;
+ case 'last':
+ $this->start = $this->users_total;
+ break;
+ }
+ $this->validatePagination();
+ return true;
+ }
+
+ /**
+ * Output appropriate html
+ *
+ * @return bool
+ */
+ public function html()
+ {
+ global $ID;
+
+ if (is_null($this->auth)) {
+ print $this->lang['badauth'];
+ return false;
+ }
+
+ $user_list = $this->auth->retrieveUsers($this->start, $this->pagesize, $this->filter);
+
+ $page_buttons = $this->pagination();
+ $delete_disable = $this->auth->canDo('delUser') ? '' : 'disabled="disabled"';
+
+ $editable = $this->auth->canDo('UserMod');
+ $export_label = empty($this->filter) ? $this->lang['export_all'] : $this->lang['export_filtered'];
+
+ print $this->locale_xhtml('intro');
+ print $this->locale_xhtml('list');
+
+ ptln("<div id=\"user__manager\">");
+ ptln("<div class=\"level2\">");
+
+ if ($this->users_total > 0) {
+ ptln(
+ "<p>" . sprintf(
+ $this->lang['summary'],
+ $this->start + 1,
+ $this->last,
+ $this->users_total,
+ $this->auth->getUserCount()
+ ) . "</p>"
+ );
+ } else {
+ if ($this->users_total < 0) {
+ $allUserTotal = 0;
+ } else {
+ $allUserTotal = $this->auth->getUserCount();
+ }
+ ptln("<p>".sprintf($this->lang['nonefound'], $allUserTotal)."</p>");
+ }
+ ptln("<form action=\"".wl($ID)."\" method=\"post\">");
+ formSecurityToken();
+ ptln(" <div class=\"table\">");
+ ptln(" <table class=\"inline\">");
+ ptln(" <thead>");
+ ptln(" <tr>");
+ ptln(" <th>&#160;</th>
+ <th>".$this->lang["user_id"]."</th>
+ <th>".$this->lang["user_name"]."</th>
+ <th>".$this->lang["user_mail"]."</th>
+ <th>".$this->lang["user_groups"]."</th>");
+ ptln(" </tr>");
+
+ ptln(" <tr>");
+ ptln(" <td class=\"rightalign\"><input type=\"image\" src=\"".
+ self::IMAGE_DIR."search.png\" name=\"fn[search][new]\" title=\"".
+ $this->lang['search_prompt']."\" alt=\"".$this->lang['search']."\" class=\"button\" /></td>");
+ ptln(" <td><input type=\"text\" name=\"userid\" class=\"edit\" value=\"".
+ $this->htmlFilter('user')."\" /></td>");
+ ptln(" <td><input type=\"text\" name=\"username\" class=\"edit\" value=\"".
+ $this->htmlFilter('name')."\" /></td>");
+ ptln(" <td><input type=\"text\" name=\"usermail\" class=\"edit\" value=\"".
+ $this->htmlFilter('mail')."\" /></td>");
+ ptln(" <td><input type=\"text\" name=\"usergroups\" class=\"edit\" value=\"".
+ $this->htmlFilter('grps')."\" /></td>");
+ ptln(" </tr>");
+ ptln(" </thead>");
+
+ if ($this->users_total) {
+ ptln(" <tbody>");
+ foreach ($user_list as $user => $userinfo) {
+ extract($userinfo);
+ /**
+ * @var string $name
+ * @var string $pass
+ * @var string $mail
+ * @var array $grps
+ */
+ $groups = join(', ', $grps);
+ ptln(" <tr class=\"user_info\">");
+ ptln(" <td class=\"centeralign\"><input type=\"checkbox\" name=\"delete[".hsc($user).
+ "]\" ".$delete_disable." /></td>");
+ if ($editable) {
+ ptln(" <td><a href=\"".wl($ID, array('fn[edit]['.$user.']' => 1,
+ 'do' => 'admin',
+ 'page' => 'usermanager',
+ 'sectok' => getSecurityToken())).
+ "\" title=\"".$this->lang['edit_prompt']."\">".hsc($user)."</a></td>");
+ } else {
+ ptln(" <td>".hsc($user)."</td>");
+ }
+ ptln(" <td>".hsc($name)."</td><td>".hsc($mail)."</td><td>".hsc($groups)."</td>");
+ ptln(" </tr>");
+ }
+ ptln(" </tbody>");
+ }
+
+ ptln(" <tbody>");
+ ptln(" <tr><td colspan=\"5\" class=\"centeralign\">");
+ ptln(" <span class=\"medialeft\">");
+ ptln(" <button type=\"submit\" name=\"fn[delete]\" id=\"usrmgr__del\" ".$delete_disable.">".
+ $this->lang['delete_selected']."</button>");
+ ptln(" </span>");
+ ptln(" <span class=\"mediaright\">");
+ ptln(" <button type=\"submit\" name=\"fn[start]\" ".$page_buttons['start'].">".
+ $this->lang['start']."</button>");
+ ptln(" <button type=\"submit\" name=\"fn[prev]\" ".$page_buttons['prev'].">".
+ $this->lang['prev']."</button>");
+ ptln(" <button type=\"submit\" name=\"fn[next]\" ".$page_buttons['next'].">".
+ $this->lang['next']."</button>");
+ ptln(" <button type=\"submit\" name=\"fn[last]\" ".$page_buttons['last'].">".
+ $this->lang['last']."</button>");
+ ptln(" </span>");
+ if (!empty($this->filter)) {
+ ptln(" <button type=\"submit\" name=\"fn[search][clear]\">".$this->lang['clear']."</button>");
+ }
+ ptln(" <button type=\"submit\" name=\"fn[export]\">".$export_label."</button>");
+ ptln(" <input type=\"hidden\" name=\"do\" value=\"admin\" />");
+ ptln(" <input type=\"hidden\" name=\"page\" value=\"usermanager\" />");
+
+ $this->htmlFilterSettings(2);
+
+ ptln(" </td></tr>");
+ ptln(" </tbody>");
+ ptln(" </table>");
+ ptln(" </div>");
+
+ ptln("</form>");
+ ptln("</div>");
+
+ $style = $this->edit_user ? " class=\"edit_user\"" : "";
+
+ if ($this->auth->canDo('addUser')) {
+ ptln("<div".$style.">");
+ print $this->locale_xhtml('add');
+ ptln(" <div class=\"level2\">");
+
+ $this->htmlUserForm('add', null, array(), 4);
+
+ ptln(" </div>");
+ ptln("</div>");
+ }
+
+ if ($this->edit_user && $this->auth->canDo('UserMod')) {
+ ptln("<div".$style." id=\"scroll__here\">");
+ print $this->locale_xhtml('edit');
+ ptln(" <div class=\"level2\">");
+
+ $this->htmlUserForm('modify', $this->edit_user, $this->edit_userdata, 4);
+
+ ptln(" </div>");
+ ptln("</div>");
+ }
+
+ if ($this->auth->canDo('addUser')) {
+ $this->htmlImportForm();
+ }
+ ptln("</div>");
+ return true;
+ }
+
+ /**
+ * User Manager is only available if the auth backend supports it
+ *
+ * @inheritdoc
+ * @return bool
+ */
+ public function isAccessibleByCurrentUser()
+ {
+ /** @var DokuWiki_Auth_Plugin $auth */
+ global $auth;
+ if(!$auth || !$auth->canDo('getUsers') ) {
+ return false;
+ }
+
+ return parent::isAccessibleByCurrentUser();
+ }
+
+
+ /**
+ * Display form to add or modify a user
+ *
+ * @param string $cmd 'add' or 'modify'
+ * @param string $user id of user
+ * @param array $userdata array with name, mail, pass and grps
+ * @param int $indent
+ */
+ protected function htmlUserForm($cmd, $user = '', $userdata = array(), $indent = 0)
+ {
+ global $conf;
+ global $ID;
+ global $lang;
+
+ $name = $mail = $groups = '';
+ $notes = array();
+
+ if ($user) {
+ extract($userdata);
+ if (!empty($grps)) $groups = join(',', $grps);
+ } else {
+ $notes[] = sprintf($this->lang['note_group'], $conf['defaultgroup']);
+ }
+
+ ptln("<form action=\"".wl($ID)."\" method=\"post\">", $indent);
+ formSecurityToken();
+ ptln(" <div class=\"table\">", $indent);
+ ptln(" <table class=\"inline\">", $indent);
+ ptln(" <thead>", $indent);
+ ptln(" <tr><th>".$this->lang["field"]."</th><th>".$this->lang["value"]."</th></tr>", $indent);
+ ptln(" </thead>", $indent);
+ ptln(" <tbody>", $indent);
+
+ $this->htmlInputField(
+ $cmd . "_userid",
+ "userid",
+ $this->lang["user_id"],
+ $user,
+ $this->auth->canDo("modLogin"),
+ true,
+ $indent + 6
+ );
+ $this->htmlInputField(
+ $cmd . "_userpass",
+ "userpass",
+ $this->lang["user_pass"],
+ "",
+ $this->auth->canDo("modPass"),
+ false,
+ $indent + 6
+ );
+ $this->htmlInputField(
+ $cmd . "_userpass2",
+ "userpass2",
+ $lang["passchk"],
+ "",
+ $this->auth->canDo("modPass"),
+ false,
+ $indent + 6
+ );
+ $this->htmlInputField(
+ $cmd . "_username",
+ "username",
+ $this->lang["user_name"],
+ $name,
+ $this->auth->canDo("modName"),
+ true,
+ $indent + 6
+ );
+ $this->htmlInputField(
+ $cmd . "_usermail",
+ "usermail",
+ $this->lang["user_mail"],
+ $mail,
+ $this->auth->canDo("modMail"),
+ true,
+ $indent + 6
+ );
+ $this->htmlInputField(
+ $cmd . "_usergroups",
+ "usergroups",
+ $this->lang["user_groups"],
+ $groups,
+ $this->auth->canDo("modGroups"),
+ false,
+ $indent + 6
+ );
+
+ if ($this->auth->canDo("modPass")) {
+ if ($cmd == 'add') {
+ $notes[] = $this->lang['note_pass'];
+ }
+ if ($user) {
+ $notes[] = $this->lang['note_notify'];
+ }
+
+ ptln("<tr><td><label for=\"".$cmd."_usernotify\" >".
+ $this->lang["user_notify"].": </label></td>
+ <td><input type=\"checkbox\" id=\"".$cmd."_usernotify\" name=\"usernotify\" value=\"1\" />
+ </td></tr>", $indent);
+ }
+
+ ptln(" </tbody>", $indent);
+ ptln(" <tbody>", $indent);
+ ptln(" <tr>", $indent);
+ ptln(" <td colspan=\"2\">", $indent);
+ ptln(" <input type=\"hidden\" name=\"do\" value=\"admin\" />", $indent);
+ ptln(" <input type=\"hidden\" name=\"page\" value=\"usermanager\" />", $indent);
+
+ // save current $user, we need this to access details if the name is changed
+ if ($user)
+ ptln(" <input type=\"hidden\" name=\"userid_old\" value=\"".hsc($user)."\" />", $indent);
+
+ $this->htmlFilterSettings($indent+10);
+
+ ptln(" <button type=\"submit\" name=\"fn[".$cmd."]\">".$this->lang[$cmd]."</button>", $indent);
+ ptln(" </td>", $indent);
+ ptln(" </tr>", $indent);
+ ptln(" </tbody>", $indent);
+ ptln(" </table>", $indent);
+
+ if ($notes) {
+ ptln(" <ul class=\"notes\">");
+ foreach ($notes as $note) {
+ ptln(" <li><span class=\"li\">".$note."</li>", $indent);
+ }
+ ptln(" </ul>");
+ }
+ ptln(" </div>", $indent);
+ ptln("</form>", $indent);
+ }
+
+ /**
+ * Prints a inputfield
+ *
+ * @param string $id
+ * @param string $name
+ * @param string $label
+ * @param string $value
+ * @param bool $cando whether auth backend is capable to do this action
+ * @param bool $required is this field required?
+ * @param int $indent
+ */
+ protected function htmlInputField($id, $name, $label, $value, $cando, $required, $indent = 0)
+ {
+ $class = $cando ? '' : ' class="disabled"';
+ echo str_pad('', $indent);
+
+ if ($name == 'userpass' || $name == 'userpass2') {
+ $fieldtype = 'password';
+ $autocomp = 'autocomplete="off"';
+ } elseif ($name == 'usermail') {
+ $fieldtype = 'email';
+ $autocomp = '';
+ } else {
+ $fieldtype = 'text';
+ $autocomp = '';
+ }
+ $value = hsc($value);
+
+ echo "<tr $class>";
+ echo "<td><label for=\"$id\" >$label: </label></td>";
+ echo "<td>";
+ if ($cando) {
+ $req = '';
+ if ($required) $req = 'required="required"';
+ echo "<input type=\"$fieldtype\" id=\"$id\" name=\"$name\"
+ value=\"$value\" class=\"edit\" $autocomp $req />";
+ } else {
+ echo "<input type=\"hidden\" name=\"$name\" value=\"$value\" />";
+ echo "<input type=\"$fieldtype\" id=\"$id\" name=\"$name\"
+ value=\"$value\" class=\"edit disabled\" disabled=\"disabled\" />";
+ }
+ echo "</td>";
+ echo "</tr>";
+ }
+
+ /**
+ * Returns htmlescaped filter value
+ *
+ * @param string $key name of search field
+ * @return string html escaped value
+ */
+ protected function htmlFilter($key)
+ {
+ if (empty($this->filter)) return '';
+ return (isset($this->filter[$key]) ? hsc($this->filter[$key]) : '');
+ }
+
+ /**
+ * Print hidden inputs with the current filter values
+ *
+ * @param int $indent
+ */
+ protected function htmlFilterSettings($indent = 0)
+ {
+
+ ptln("<input type=\"hidden\" name=\"start\" value=\"".$this->start."\" />", $indent);
+
+ foreach ($this->filter as $key => $filter) {
+ ptln("<input type=\"hidden\" name=\"filter[".$key."]\" value=\"".hsc($filter)."\" />", $indent);
+ }
+ }
+
+ /**
+ * Print import form and summary of previous import
+ *
+ * @param int $indent
+ */
+ protected function htmlImportForm($indent = 0)
+ {
+ global $ID;
+
+ $failure_download_link = wl($ID, array('do'=>'admin','page'=>'usermanager','fn[importfails]'=>1));
+
+ ptln('<div class="level2 import_users">', $indent);
+ print $this->locale_xhtml('import');
+ ptln(' <form action="'.wl($ID).'" method="post" enctype="multipart/form-data">', $indent);
+ formSecurityToken();
+ ptln(' <label>'.$this->lang['import_userlistcsv'].'<input type="file" name="import" /></label>', $indent);
+ ptln(' <button type="submit" name="fn[import]">'.$this->lang['import'].'</button>', $indent);
+ ptln(' <input type="hidden" name="do" value="admin" />', $indent);
+ ptln(' <input type="hidden" name="page" value="usermanager" />', $indent);
+
+ $this->htmlFilterSettings($indent+4);
+ ptln(' </form>', $indent);
+ ptln('</div>');
+
+ // list failures from the previous import
+ if ($this->import_failures) {
+ $digits = strlen(count($this->import_failures));
+ ptln('<div class="level3 import_failures">', $indent);
+ ptln(' <h3>'.$this->lang['import_header'].'</h3>');
+ ptln(' <table class="import_failures">', $indent);
+ ptln(' <thead>', $indent);
+ ptln(' <tr>', $indent);
+ ptln(' <th class="line">'.$this->lang['line'].'</th>', $indent);
+ ptln(' <th class="error">'.$this->lang['error'].'</th>', $indent);
+ ptln(' <th class="userid">'.$this->lang['user_id'].'</th>', $indent);
+ ptln(' <th class="username">'.$this->lang['user_name'].'</th>', $indent);
+ ptln(' <th class="usermail">'.$this->lang['user_mail'].'</th>', $indent);
+ ptln(' <th class="usergroups">'.$this->lang['user_groups'].'</th>', $indent);
+ ptln(' </tr>', $indent);
+ ptln(' </thead>', $indent);
+ ptln(' <tbody>', $indent);
+ foreach ($this->import_failures as $line => $failure) {
+ ptln(' <tr>', $indent);
+ ptln(' <td class="lineno"> '.sprintf('%0'.$digits.'d', $line).' </td>', $indent);
+ ptln(' <td class="error">' .$failure['error'].' </td>', $indent);
+ ptln(' <td class="field userid"> '.hsc($failure['user'][0]).' </td>', $indent);
+ ptln(' <td class="field username"> '.hsc($failure['user'][2]).' </td>', $indent);
+ ptln(' <td class="field usermail"> '.hsc($failure['user'][3]).' </td>', $indent);
+ ptln(' <td class="field usergroups"> '.hsc($failure['user'][4]).' </td>', $indent);
+ ptln(' </tr>', $indent);
+ }
+ ptln(' </tbody>', $indent);
+ ptln(' </table>', $indent);
+ ptln(' <p><a href="'.$failure_download_link.'">'.$this->lang['import_downloadfailures'].'</a></p>');
+ ptln('</div>');
+ }
+ }
+
+ /**
+ * Add an user to auth backend
+ *
+ * @return bool whether succesful
+ */
+ protected function addUser()
+ {
+ global $INPUT;
+ if (!checkSecurityToken()) return false;
+ if (!$this->auth->canDo('addUser')) return false;
+
+ list($user,$pass,$name,$mail,$grps,$passconfirm) = $this->retrieveUser();
+ if (empty($user)) return false;
+
+ if ($this->auth->canDo('modPass')) {
+ if (empty($pass)) {
+ if ($INPUT->has('usernotify')) {
+ $pass = auth_pwgen($user);
+ } else {
+ msg($this->lang['add_fail'], -1);
+ msg($this->lang['addUser_error_missing_pass'], -1);
+ return false;
+ }
+ } else {
+ if (!$this->verifyPassword($pass, $passconfirm)) {
+ msg($this->lang['add_fail'], -1);
+ msg($this->lang['addUser_error_pass_not_identical'], -1);
+ return false;
+ }
+ }
+ } else {
+ if (!empty($pass)) {
+ msg($this->lang['add_fail'], -1);
+ msg($this->lang['addUser_error_modPass_disabled'], -1);
+ return false;
+ }
+ }
+
+ if ($this->auth->canDo('modName')) {
+ if (empty($name)) {
+ msg($this->lang['add_fail'], -1);
+ msg($this->lang['addUser_error_name_missing'], -1);
+ return false;
+ }
+ } else {
+ if (!empty($name)) {
+ msg($this->lang['add_fail'], -1);
+ msg($this->lang['addUser_error_modName_disabled'], -1);
+ return false;
+ }
+ }
+
+ if ($this->auth->canDo('modMail')) {
+ if (empty($mail)) {
+ msg($this->lang['add_fail'], -1);
+ msg($this->lang['addUser_error_mail_missing'], -1);
+ return false;
+ }
+ } else {
+ if (!empty($mail)) {
+ msg($this->lang['add_fail'], -1);
+ msg($this->lang['addUser_error_modMail_disabled'], -1);
+ return false;
+ }
+ }
+
+ if ($ok = $this->auth->triggerUserMod('create', array($user, $pass, $name, $mail, $grps))) {
+ msg($this->lang['add_ok'], 1);
+
+ if ($INPUT->has('usernotify') && $pass) {
+ $this->notifyUser($user, $pass);
+ }
+ } else {
+ msg($this->lang['add_fail'], -1);
+ msg($this->lang['addUser_error_create_event_failed'], -1);
+ }
+
+ return $ok;
+ }
+
+ /**
+ * Delete user from auth backend
+ *
+ * @return bool whether succesful
+ */
+ protected function deleteUser()
+ {
+ global $conf, $INPUT;
+
+ if (!checkSecurityToken()) return false;
+ if (!$this->auth->canDo('delUser')) return false;
+
+ $selected = $INPUT->arr('delete');
+ if (empty($selected)) return false;
+ $selected = array_keys($selected);
+
+ if (in_array($_SERVER['REMOTE_USER'], $selected)) {
+ msg("You can't delete yourself!", -1);
+ return false;
+ }
+
+ $count = $this->auth->triggerUserMod('delete', array($selected));
+ if ($count == count($selected)) {
+ $text = str_replace('%d', $count, $this->lang['delete_ok']);
+ msg("$text.", 1);
+ } else {
+ $part1 = str_replace('%d', $count, $this->lang['delete_ok']);
+ $part2 = str_replace('%d', (count($selected)-$count), $this->lang['delete_fail']);
+ msg("$part1, $part2", -1);
+ }
+
+ // invalidate all sessions
+ io_saveFile($conf['cachedir'].'/sessionpurge', time());
+
+ return true;
+ }
+
+ /**
+ * Edit user (a user has been selected for editing)
+ *
+ * @param string $param id of the user
+ * @return bool whether succesful
+ */
+ protected function editUser($param)
+ {
+ if (!checkSecurityToken()) return false;
+ if (!$this->auth->canDo('UserMod')) return false;
+ $user = $this->auth->cleanUser(preg_replace('/.*[:\/]/', '', $param));
+ $userdata = $this->auth->getUserData($user);
+
+ // no user found?
+ if (!$userdata) {
+ msg($this->lang['edit_usermissing'], -1);
+ return false;
+ }
+
+ $this->edit_user = $user;
+ $this->edit_userdata = $userdata;
+
+ return true;
+ }
+
+ /**
+ * Modify user in the auth backend (modified user data has been recieved)
+ *
+ * @return bool whether succesful
+ */
+ protected function modifyUser()
+ {
+ global $conf, $INPUT;
+
+ if (!checkSecurityToken()) return false;
+ if (!$this->auth->canDo('UserMod')) return false;
+
+ // get currently valid user data
+ $olduser = $this->auth->cleanUser(preg_replace('/.*[:\/]/', '', $INPUT->str('userid_old')));
+ $oldinfo = $this->auth->getUserData($olduser);
+
+ // get new user data subject to change
+ list($newuser,$newpass,$newname,$newmail,$newgrps,$passconfirm) = $this->retrieveUser();
+ if (empty($newuser)) return false;
+
+ $changes = array();
+ if ($newuser != $olduser) {
+ if (!$this->auth->canDo('modLogin')) { // sanity check, shouldn't be possible
+ msg($this->lang['update_fail'], -1);
+ return false;
+ }
+
+ // check if $newuser already exists
+ if ($this->auth->getUserData($newuser)) {
+ msg(sprintf($this->lang['update_exists'], $newuser), -1);
+ $re_edit = true;
+ } else {
+ $changes['user'] = $newuser;
+ }
+ }
+ if ($this->auth->canDo('modPass')) {
+ if ($newpass || $passconfirm) {
+ if ($this->verifyPassword($newpass, $passconfirm)) {
+ $changes['pass'] = $newpass;
+ } else {
+ return false;
+ }
+ } else {
+ // no new password supplied, check if we need to generate one (or it stays unchanged)
+ if ($INPUT->has('usernotify')) {
+ $changes['pass'] = auth_pwgen($olduser);
+ }
+ }
+ }
+
+ if (!empty($newname) && $this->auth->canDo('modName') && $newname != $oldinfo['name']) {
+ $changes['name'] = $newname;
+ }
+ if (!empty($newmail) && $this->auth->canDo('modMail') && $newmail != $oldinfo['mail']) {
+ $changes['mail'] = $newmail;
+ }
+ if (!empty($newgrps) && $this->auth->canDo('modGroups') && $newgrps != $oldinfo['grps']) {
+ $changes['grps'] = $newgrps;
+ }
+
+ if ($ok = $this->auth->triggerUserMod('modify', array($olduser, $changes))) {
+ msg($this->lang['update_ok'], 1);
+
+ if ($INPUT->has('usernotify') && !empty($changes['pass'])) {
+ $notify = empty($changes['user']) ? $olduser : $newuser;
+ $this->notifyUser($notify, $changes['pass']);
+ }
+
+ // invalidate all sessions
+ io_saveFile($conf['cachedir'].'/sessionpurge', time());
+ } else {
+ msg($this->lang['update_fail'], -1);
+ }
+
+ if (!empty($re_edit)) {
+ $this->editUser($olduser);
+ }
+
+ return $ok;
+ }
+
+ /**
+ * Send password change notification email
+ *
+ * @param string $user id of user
+ * @param string $password plain text
+ * @param bool $status_alert whether status alert should be shown
+ * @return bool whether succesful
+ */
+ protected function notifyUser($user, $password, $status_alert = true)
+ {
+
+ if ($sent = auth_sendPassword($user, $password)) {
+ if ($status_alert) {
+ msg($this->lang['notify_ok'], 1);
+ }
+ } else {
+ if ($status_alert) {
+ msg($this->lang['notify_fail'], -1);
+ }
+ }
+
+ return $sent;
+ }
+
+ /**
+ * Verify password meets minimum requirements
+ * :TODO: extend to support password strength
+ *
+ * @param string $password candidate string for new password
+ * @param string $confirm repeated password for confirmation
+ * @return bool true if meets requirements, false otherwise
+ */
+ protected function verifyPassword($password, $confirm)
+ {
+ global $lang;
+
+ if (empty($password) && empty($confirm)) {
+ return false;
+ }
+
+ if ($password !== $confirm) {
+ msg($lang['regbadpass'], -1);
+ return false;
+ }
+
+ // :TODO: test password for required strength
+
+ // if we make it this far the password is good
+ return true;
+ }
+
+ /**
+ * Retrieve & clean user data from the form
+ *
+ * @param bool $clean whether the cleanUser method of the authentication backend is applied
+ * @return array (user, password, full name, email, array(groups))
+ */
+ protected function retrieveUser($clean = true)
+ {
+ /** @var DokuWiki_Auth_Plugin $auth */
+ global $auth;
+ global $INPUT;
+
+ $user = array();
+ $user[0] = ($clean) ? $auth->cleanUser($INPUT->str('userid')) : $INPUT->str('userid');
+ $user[1] = $INPUT->str('userpass');
+ $user[2] = $INPUT->str('username');
+ $user[3] = $INPUT->str('usermail');
+ $user[4] = explode(',', $INPUT->str('usergroups'));
+ $user[5] = $INPUT->str('userpass2'); // repeated password for confirmation
+
+ $user[4] = array_map('trim', $user[4]);
+ if ($clean) $user[4] = array_map(array($auth,'cleanGroup'), $user[4]);
+ $user[4] = array_filter($user[4]);
+ $user[4] = array_unique($user[4]);
+ if (!count($user[4])) $user[4] = null;
+
+ return $user;
+ }
+
+ /**
+ * Set the filter with the current search terms or clear the filter
+ *
+ * @param string $op 'new' or 'clear'
+ */
+ protected function setFilter($op)
+ {
+
+ $this->filter = array();
+
+ if ($op == 'new') {
+ list($user,/* $pass */,$name,$mail,$grps) = $this->retrieveUser(false);
+
+ if (!empty($user)) $this->filter['user'] = $user;
+ if (!empty($name)) $this->filter['name'] = $name;
+ if (!empty($mail)) $this->filter['mail'] = $mail;
+ if (!empty($grps)) $this->filter['grps'] = join('|', $grps);
+ }
+ }
+
+ /**
+ * Get the current search terms
+ *
+ * @return array
+ */
+ protected function retrieveFilter()
+ {
+ global $INPUT;
+
+ $t_filter = $INPUT->arr('filter');
+
+ // messy, but this way we ensure we aren't getting any additional crap from malicious users
+ $filter = array();
+
+ if (isset($t_filter['user'])) $filter['user'] = $t_filter['user'];
+ if (isset($t_filter['name'])) $filter['name'] = $t_filter['name'];
+ if (isset($t_filter['mail'])) $filter['mail'] = $t_filter['mail'];
+ if (isset($t_filter['grps'])) $filter['grps'] = $t_filter['grps'];
+
+ return $filter;
+ }
+
+ /**
+ * Validate and improve the pagination values
+ */
+ protected function validatePagination()
+ {
+
+ if ($this->start >= $this->users_total) {
+ $this->start = $this->users_total - $this->pagesize;
+ }
+ if ($this->start < 0) $this->start = 0;
+
+ $this->last = min($this->users_total, $this->start + $this->pagesize);
+ }
+
+ /**
+ * Return an array of strings to enable/disable pagination buttons
+ *
+ * @return array with enable/disable attributes
+ */
+ protected function pagination()
+ {
+
+ $disabled = 'disabled="disabled"';
+
+ $buttons = array();
+ $buttons['start'] = $buttons['prev'] = ($this->start == 0) ? $disabled : '';
+
+ if ($this->users_total == -1) {
+ $buttons['last'] = $disabled;
+ $buttons['next'] = '';
+ } else {
+ $buttons['last'] = $buttons['next'] =
+ (($this->start + $this->pagesize) >= $this->users_total) ? $disabled : '';
+ }
+
+ if ($this->lastdisabled) {
+ $buttons['last'] = $disabled;
+ }
+
+ return $buttons;
+ }
+
+ /**
+ * Export a list of users in csv format using the current filter criteria
+ */
+ protected function exportCSV()
+ {
+ // list of users for export - based on current filter criteria
+ $user_list = $this->auth->retrieveUsers(0, 0, $this->filter);
+ $column_headings = array(
+ $this->lang["user_id"],
+ $this->lang["user_name"],
+ $this->lang["user_mail"],
+ $this->lang["user_groups"]
+ );
+
+ // ==============================================================================================
+ // GENERATE OUTPUT
+ // normal headers for downloading...
+ header('Content-type: text/csv;charset=utf-8');
+ header('Content-Disposition: attachment; filename="wikiusers.csv"');
+# // for debugging assistance, send as text plain to the browser
+# header('Content-type: text/plain;charset=utf-8');
+
+ // output the csv
+ $fd = fopen('php://output', 'w');
+ fputcsv($fd, $column_headings);
+ foreach ($user_list as $user => $info) {
+ $line = array($user, $info['name'], $info['mail'], join(',', $info['grps']));
+ fputcsv($fd, $line);
+ }
+ fclose($fd);
+ if (defined('DOKU_UNITTEST')) {
+ return;
+ }
+
+ die;
+ }
+
+ /**
+ * Import a file of users in csv format
+ *
+ * csv file should have 4 columns, user_id, full name, email, groups (comma separated)
+ *
+ * @return bool whether successful
+ */
+ protected function importCSV()
+ {
+ // check we are allowed to add users
+ if (!checkSecurityToken()) return false;
+ if (!$this->auth->canDo('addUser')) return false;
+
+ // check file uploaded ok.
+ if (empty($_FILES['import']['size']) ||
+ !empty($_FILES['import']['error']) && $this->isUploadedFile($_FILES['import']['tmp_name'])
+ ) {
+ msg($this->lang['import_error_upload'], -1);
+ return false;
+ }
+ // retrieve users from the file
+ $this->import_failures = array();
+ $import_success_count = 0;
+ $import_fail_count = 0;
+ $line = 0;
+ $fd = fopen($_FILES['import']['tmp_name'], 'r');
+ if ($fd) {
+ while ($csv = fgets($fd)) {
+ if (!\dokuwiki\Utf8\Clean::isUtf8($csv)) {
+ $csv = utf8_encode($csv);
+ }
+ $raw = str_getcsv($csv);
+ $error = ''; // clean out any errors from the previous line
+ // data checks...
+ if (1 == ++$line) {
+ if ($raw[0] == 'user_id' || $raw[0] == $this->lang['user_id']) continue; // skip headers
+ }
+ if (count($raw) < 4) { // need at least four fields
+ $import_fail_count++;
+ $error = sprintf($this->lang['import_error_fields'], count($raw));
+ $this->import_failures[$line] = array('error' => $error, 'user' => $raw, 'orig' => $csv);
+ continue;
+ }
+ array_splice($raw, 1, 0, auth_pwgen()); // splice in a generated password
+ $clean = $this->cleanImportUser($raw, $error);
+ if ($clean && $this->importUser($clean, $error)) {
+ $sent = $this->notifyUser($clean[0], $clean[1], false);
+ if (!$sent) {
+ msg(sprintf($this->lang['import_notify_fail'], $clean[0], $clean[3]), -1);
+ }
+ $import_success_count++;
+ } else {
+ $import_fail_count++;
+ array_splice($raw, 1, 1); // remove the spliced in password
+ $this->import_failures[$line] = array('error' => $error, 'user' => $raw, 'orig' => $csv);
+ }
+ }
+ msg(
+ sprintf(
+ $this->lang['import_success_count'],
+ ($import_success_count + $import_fail_count),
+ $import_success_count
+ ),
+ ($import_success_count ? 1 : -1)
+ );
+ if ($import_fail_count) {
+ msg(sprintf($this->lang['import_failure_count'], $import_fail_count), -1);
+ }
+ } else {
+ msg($this->lang['import_error_readfail'], -1);
+ }
+
+ // save import failures into the session
+ if (!headers_sent()) {
+ session_start();
+ $_SESSION['import_failures'] = $this->import_failures;
+ session_write_close();
+ }
+ return true;
+ }
+
+ /**
+ * Returns cleaned user data
+ *
+ * @param array $candidate raw values of line from input file
+ * @param string $error
+ * @return array|false cleaned data or false
+ */
+ protected function cleanImportUser($candidate, & $error)
+ {
+ global $INPUT;
+
+ // FIXME kludgy ....
+ $INPUT->set('userid', $candidate[0]);
+ $INPUT->set('userpass', $candidate[1]);
+ $INPUT->set('username', $candidate[2]);
+ $INPUT->set('usermail', $candidate[3]);
+ $INPUT->set('usergroups', $candidate[4]);
+
+ $cleaned = $this->retrieveUser();
+ list($user,/* $pass */,$name,$mail,/* $grps */) = $cleaned;
+ if (empty($user)) {
+ $error = $this->lang['import_error_baduserid'];
+ return false;
+ }
+
+ // no need to check password, handled elsewhere
+
+ if (!($this->auth->canDo('modName') xor empty($name))) {
+ $error = $this->lang['import_error_badname'];
+ return false;
+ }
+
+ if ($this->auth->canDo('modMail')) {
+ if (empty($mail) || !mail_isvalid($mail)) {
+ $error = $this->lang['import_error_badmail'];
+ return false;
+ }
+ } else {
+ if (!empty($mail)) {
+ $error = $this->lang['import_error_badmail'];
+ return false;
+ }
+ }
+
+ return $cleaned;
+ }
+
+ /**
+ * Adds imported user to auth backend
+ *
+ * Required a check of canDo('addUser') before
+ *
+ * @param array $user data of user
+ * @param string &$error reference catched error message
+ * @return bool whether successful
+ */
+ protected function importUser($user, &$error)
+ {
+ if (!$this->auth->triggerUserMod('create', $user)) {
+ $error = $this->lang['import_error_create'];
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Downloads failures as csv file
+ */
+ protected function downloadImportFailures()
+ {
+
+ // ==============================================================================================
+ // GENERATE OUTPUT
+ // normal headers for downloading...
+ header('Content-type: text/csv;charset=utf-8');
+ header('Content-Disposition: attachment; filename="importfails.csv"');
+# // for debugging assistance, send as text plain to the browser
+# header('Content-type: text/plain;charset=utf-8');
+
+ // output the csv
+ $fd = fopen('php://output', 'w');
+ foreach ($this->import_failures as $fail) {
+ fputs($fd, $fail['orig']);
+ }
+ fclose($fd);
+ die;
+ }
+
+ /**
+ * wrapper for is_uploaded_file to facilitate overriding by test suite
+ *
+ * @param string $file filename
+ * @return bool
+ */
+ protected function isUploadedFile($file)
+ {
+ return is_uploaded_file($file);
+ }
+}
diff --git a/platform/www/lib/plugins/usermanager/admin.svg b/platform/www/lib/plugins/usermanager/admin.svg
new file mode 100644
index 0000000..74a72c0
--- /dev/null
+++ b/platform/www/lib/plugins/usermanager/admin.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M16 13c-.29 0-.62 0-.97.05C16.19 13.89 17 15 17 16.5V19h6v-2.5c0-2.33-4.67-3.5-7-3.5m-8 0c-2.33 0-7 1.17-7 3.5V19h14v-2.5c0-2.33-4.67-3.5-7-3.5m0-2a3 3 0 0 0 3-3 3 3 0 0 0-3-3 3 3 0 0 0-3 3 3 3 0 0 0 3 3m8 0a3 3 0 0 0 3-3 3 3 0 0 0-3-3 3 3 0 0 0-3 3 3 3 0 0 0 3 3z"/></svg> \ No newline at end of file
diff --git a/platform/www/lib/plugins/usermanager/images/search.png b/platform/www/lib/plugins/usermanager/images/search.png
new file mode 100644
index 0000000..3f2a0b5
--- /dev/null
+++ b/platform/www/lib/plugins/usermanager/images/search.png
Binary files differ
diff --git a/platform/www/lib/plugins/usermanager/lang/en/add.txt b/platform/www/lib/plugins/usermanager/lang/en/add.txt
new file mode 100644
index 0000000..9afecb5
--- /dev/null
+++ b/platform/www/lib/plugins/usermanager/lang/en/add.txt
@@ -0,0 +1 @@
+===== Add user =====
diff --git a/platform/www/lib/plugins/usermanager/lang/en/delete.txt b/platform/www/lib/plugins/usermanager/lang/en/delete.txt
new file mode 100644
index 0000000..c3ca90d
--- /dev/null
+++ b/platform/www/lib/plugins/usermanager/lang/en/delete.txt
@@ -0,0 +1 @@
+===== Delete user =====
diff --git a/platform/www/lib/plugins/usermanager/lang/en/edit.txt b/platform/www/lib/plugins/usermanager/lang/en/edit.txt
new file mode 100644
index 0000000..4d02dfd
--- /dev/null
+++ b/platform/www/lib/plugins/usermanager/lang/en/edit.txt
@@ -0,0 +1 @@
+===== Edit user =====
diff --git a/platform/www/lib/plugins/usermanager/lang/en/import.txt b/platform/www/lib/plugins/usermanager/lang/en/import.txt
new file mode 100644
index 0000000..3a1cf99
--- /dev/null
+++ b/platform/www/lib/plugins/usermanager/lang/en/import.txt
@@ -0,0 +1,9 @@
+===== Bulk User Import =====
+
+Requires a CSV file of users with at least four columns.
+The columns must contain, in order: user-id, full name, email address and groups.
+The CSV fields should be separated by commas (,) and strings delimited by quotation marks (%%""%%). Backslash (\) can be used for escaping.
+For an example of a suitable file, try the "Export Users" function above.
+Duplicate user-ids will be ignored.
+
+A password will be generated and emailed to each successfully imported user.
diff --git a/platform/www/lib/plugins/usermanager/lang/en/intro.txt b/platform/www/lib/plugins/usermanager/lang/en/intro.txt
new file mode 100644
index 0000000..73bf556
--- /dev/null
+++ b/platform/www/lib/plugins/usermanager/lang/en/intro.txt
@@ -0,0 +1 @@
+====== User Manager ======
diff --git a/platform/www/lib/plugins/usermanager/lang/en/lang.php b/platform/www/lib/plugins/usermanager/lang/en/lang.php
new file mode 100644
index 0000000..5f47673
--- /dev/null
+++ b/platform/www/lib/plugins/usermanager/lang/en/lang.php
@@ -0,0 +1,86 @@
+<?php
+/**
+ * English language file
+ *
+ * @author Chris Smith <chris@jalakai.co.uk>
+ */
+
+$lang['menu'] = 'User Manager';
+
+// custom language strings for the plugin
+$lang['noauth'] = '(user authentication not available)';
+$lang['nosupport'] = '(user management not supported)';
+
+$lang['badauth'] = 'invalid auth mechanism'; // should never be displayed!
+
+$lang['user_id'] = 'User';
+$lang['user_pass'] = 'Password';
+$lang['user_name'] = 'Real Name';
+$lang['user_mail'] = 'Email';
+$lang['user_groups'] = 'Groups';
+
+$lang['field'] = 'Field';
+$lang['value'] = 'Value';
+$lang['add'] = 'Add';
+$lang['delete'] = 'Delete';
+$lang['delete_selected'] = 'Delete Selected';
+$lang['edit'] = 'Edit';
+$lang['edit_prompt'] = 'Edit this user';
+$lang['modify'] = 'Save Changes';
+$lang['search'] = 'Search';
+$lang['search_prompt'] = 'Perform search';
+$lang['clear'] = 'Reset Search Filter';
+$lang['filter'] = 'Filter';
+$lang['export_all'] = 'Export All Users (CSV)';
+$lang['export_filtered'] = 'Export Filtered User list (CSV)';
+$lang['import'] = 'Import New Users';
+$lang['line'] = 'Line no.';
+$lang['error'] = 'Error message';
+
+$lang['summary'] = 'Displaying users %1$d-%2$d of %3$d found. %4$d users total.';
+$lang['nonefound'] = 'No users found. %d users total.';
+$lang['delete_ok'] = '%d users deleted';
+$lang['delete_fail'] = '%d failed deleting.';
+$lang['update_ok'] = 'User updated successfully';
+$lang['update_fail'] = 'User update failed';
+$lang['update_exists'] = 'User name change failed, the specified user name (%s) already exists (any other changes will be applied).';
+
+$lang['start'] = 'start';
+$lang['prev'] = 'previous';
+$lang['next'] = 'next';
+$lang['last'] = 'last';
+
+// added after 2006-03-09 release
+$lang['edit_usermissing'] = 'Selected user not found, the specified user name may have been deleted or changed elsewhere.';
+$lang['user_notify'] = 'Notify user';
+$lang['note_notify'] = 'Notification emails are only sent if the user is given a new password.';
+$lang['note_group'] = 'New users will be added to the default group (%s) if no group is specified.';
+$lang['note_pass'] = 'The password will be autogenerated if the field is left empty and notification of the user is enabled.';
+$lang['add_ok'] = 'User added successfully';
+$lang['add_fail'] = 'User addition failed';
+$lang['notify_ok'] = 'Notification email sent';
+$lang['notify_fail'] = 'Notification email could not be sent';
+
+// import & errors
+$lang['import_userlistcsv'] = 'User list file (CSV): ';
+$lang['import_header'] = 'Most Recent Import - Failures';
+$lang['import_success_count'] = 'User Import: %d users found, %d imported successfully.';
+$lang['import_failure_count'] = 'User Import: %d failed. Failures are listed below.';
+$lang['import_error_fields'] = "Insufficient fields, found %d, require 4.";
+$lang['import_error_baduserid'] = "User-id missing";
+$lang['import_error_badname'] = 'Bad name';
+$lang['import_error_badmail'] = 'Bad email address';
+$lang['import_error_upload'] = 'Import Failed. The csv file could not be uploaded or is empty.';
+$lang['import_error_readfail'] = 'Import Failed. Unable to read uploaded file.';
+$lang['import_error_create'] = 'Unable to create the user';
+$lang['import_notify_fail'] = 'Notification message could not be sent for imported user, %s with email %s.';
+$lang['import_downloadfailures'] = 'Download Failures as CSV for correction';
+
+$lang['addUser_error_missing_pass'] = 'Please either set a password or activate user notification to enable password generation.';
+$lang['addUser_error_pass_not_identical'] = 'The entered passwords were not identical.';
+$lang['addUser_error_modPass_disabled'] = 'Modifing passwords is currently disabled';
+$lang['addUser_error_name_missing'] = 'Please enter a name for the new user.';
+$lang['addUser_error_modName_disabled'] = 'Modifing names is currently disabled.';
+$lang['addUser_error_mail_missing'] = 'Please enter an Email-Adress for the new user.';
+$lang['addUser_error_modMail_disabled'] = 'Modifing Email-Adresses is currently disabled.';
+$lang['addUser_error_create_event_failed'] = 'A plugin prevented the new user being added. Review possible other messages for more information.';
diff --git a/platform/www/lib/plugins/usermanager/lang/en/list.txt b/platform/www/lib/plugins/usermanager/lang/en/list.txt
new file mode 100644
index 0000000..54c45ca
--- /dev/null
+++ b/platform/www/lib/plugins/usermanager/lang/en/list.txt
@@ -0,0 +1 @@
+===== User List =====
diff --git a/platform/www/lib/plugins/usermanager/plugin.info.txt b/platform/www/lib/plugins/usermanager/plugin.info.txt
new file mode 100644
index 0000000..607eca7
--- /dev/null
+++ b/platform/www/lib/plugins/usermanager/plugin.info.txt
@@ -0,0 +1,7 @@
+base usermanager
+author Chris Smith
+email chris@jalakai.co.uk
+date 2015-07-15
+name User Manager
+desc Manage DokuWiki user accounts
+url http://dokuwiki.org/plugin:usermanager
diff --git a/platform/www/lib/plugins/usermanager/script.js b/platform/www/lib/plugins/usermanager/script.js
new file mode 100644
index 0000000..3b7ad09
--- /dev/null
+++ b/platform/www/lib/plugins/usermanager/script.js
@@ -0,0 +1,8 @@
+/**
+ * Add JavaScript confirmation to the User Delete button
+ */
+jQuery(function(){
+ jQuery('#usrmgr__del').on('click', function(){
+ return confirm(LANG.del_confirm);
+ });
+});
diff --git a/platform/www/lib/plugins/usermanager/style.css b/platform/www/lib/plugins/usermanager/style.css
new file mode 100644
index 0000000..9028fed
--- /dev/null
+++ b/platform/www/lib/plugins/usermanager/style.css
@@ -0,0 +1,33 @@
+/* User Manager specific styles */
+#user__manager tr.disabled {
+ color: #6f6f6f;
+ background: #e4e4e4;
+}
+#user__manager tr.user_info {
+ vertical-align: top;
+}
+#user__manager div.edit_user {
+ width: 46%;
+ float: left;
+}
+#user__manager table {
+ margin-bottom: 1em;
+}
+#user__manager ul.notes {
+ padding-left: 0;
+ padding-right: 1.4em;
+}
+#user__manager button[disabled] {
+ color: #ccc!important;
+ border-color: #ccc!important;
+}
+#user__manager .import_users {
+ margin-top: 1.4em;
+}
+#user__manager .import_failures {
+ margin-top: 1.4em;
+}
+#user__manager .import_failures td.lineno {
+ text-align: center;
+}
+/* IE won't understand but doesn't require it */