diff --git a/doc/syncmonotone.mdtext b/doc/syncmonotone.mdtext index 7355810..c21a325 100644 --- a/doc/syncmonotone.mdtext +++ b/doc/syncmonotone.mdtext @@ -149,7 +149,6 @@ look like this: $cfg['mtn_repositories'] = '/var/lib/usher/projects/%s/'; $cfg['mtn_remote_url'] = 'mtn://my.server.com/%s'; $cfg['mtn_db_access'] = 'remote'; - $cfg['mtn_remote_auth'] = true; $cfg['mtn_usher_conf'] = '/var/lib/usher/usher.conf'; ... @@ -188,8 +187,10 @@ Remote commands can be helpful for a user or a 3rd party tool (like contents remotely without having to pull everything in first instance. Private projects on the other hand can only be synced by team members -or additional invited people. Also noo remote command execution is enabled -by default. +or additional invited people. Remote command execution is still enabled +by default - if you want to disable that, simply remove the symlink to +the file `indefero_authorize_remote_automate.conf` in your project's `hooks.d` +directory or copy the file from the original location and adapt it. ## Notifications @@ -204,8 +205,54 @@ in a directory called `hooks.d` right under the project's base directory (configured via $cfg['mtn_repositories']) and this is the ideal place to put or link these additional lua sources. +## Custom project configurations and templates + +If a new project is created in IDF, the SyncMonotone plugin creates a new +configuration tree for the project into the project's configuration directory, +determined by `$cfg['mtn_repositories']`. IDF ships with the minimum set of +files for this configuration tree and sets up everything automatically for you. + +Even more, most of the configuration files from the newly created tree are only +symlinked to the original configuration directory which is configurable via +`$cfg['mtn_confdir']` and defaults to `src/IDF/Plugin/SyncMonotone/`. This has +the advantage that your standard IDF setup automatically receives updates to +existing (symlinked) configuration files as soon as you update to a newer +version. + +You could, however, also choose to place the directory tree somewhere else +and adapt the contents of the individual files yourself, so these changes get +automatically applied to all new projects you create. You could even go so far +and add new files to the tree and let them be processed automatically just +as the basic files! All you need to do is to copy your files and / or directories +underknees your `$cfg['mtn_confdir']` and add their relative paths to +`$cfg['mtn_confdir_extra']`. + +By convention, all entries which end with a slash are considered directories, +so mkdir(1) is issued for these entries, all files which do not end up with +".in" are considered to be static script files which are just symlinked from +the basic configuration dir and all entries ending on ".in" are considered +configuration files or templates, which are copied over to the project's +configuration tree and which get some basic project-specific values replaced. + +The following placeholders are currently recognized and replaced for these files: + + * %%PROJECT%% - the name of the created project + * %%MTNPOSTPUSH%% - the absolute path to the `mtn-post-push` script + * %%MTNCLIENTKEY%% - the public key hash of the key which is used by IDF + to authenticate remote stdio access + +Thats it - I hope you find it useful :) + ## Q&A +### After I created a new project, IDF throws an exception and tells me that it couldn't save the membership data with a cryptic error message. Whats wrong? + +Multiple issues could cause that. If you've set up usher, make sure the usher +can fork your database at all and look out for specific errors in the log file +of your project. If you stumble upon permission issues, ensure that the user +who runs the usher can access all files in your project's configuration directory, +including symlinked files. + ### I pushed a branch to my server, but it does not show up in IDF. Whats wrong? Check if the heads of your branch are not suspended, i.e. do not carry a diff --git a/logo/indefero-logo-lite.svg b/logo/indefero-logo-lite.svg new file mode 100644 index 0000000..ac50cfe --- /dev/null +++ b/logo/indefero-logo-lite.svg @@ -0,0 +1,86 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/scripts/svnpostcommit.php b/scripts/svnpostcommit.php index d3430b9..c38c796 100644 --- a/scripts/svnpostcommit.php +++ b/scripts/svnpostcommit.php @@ -55,6 +55,7 @@ Pluf_Dispatcher::loadControllers(Pluf::f('idf_views')); $params = array('repo_dir' => $argv[1], 'revision' => $argv[2], 'env' => array_merge($_ENV, $_SERVER)); +Pluf_Log::event(array('svnpostcommit.php', 'Send run signal.', $params)); Pluf_Signal::send('svnpostcommit.php::run', 'svnpostcommit.php', $params); diff --git a/src/IDF/Form/UserAccount.php b/src/IDF/Form/UserAccount.php index 7b6a5cb..d09417d 100644 --- a/src/IDF/Form/UserAccount.php +++ b/src/IDF/Form/UserAccount.php @@ -295,7 +295,7 @@ class IDF_Form_UserAccount extends Pluf_Form return ''; } - if (preg_match('#^ssh\-[a-z]{3}\s\S+==(\s\S+)?$#', $key)) { + if (preg_match('#^ssh\-[a-z]{3}\s\S+(\s\S+)?$#', $key)) { $key = str_replace(array("\n", "\r"), '', $key); if (Pluf::f('idf_strong_key_check', false)) { diff --git a/src/IDF/Plugin/SyncGit/Serve.php b/src/IDF/Plugin/SyncGit/Serve.php index 499a1ba..842797b 100644 --- a/src/IDF/Plugin/SyncGit/Serve.php +++ b/src/IDF/Plugin/SyncGit/Serve.php @@ -230,6 +230,24 @@ class IDF_Plugin_SyncGit_Serve } Pluf_Log::debug(array('IDF_Plugin_Git_Serve::initRepository', 'Added post-update hook.', $fullpath)); + // Configure the core.quotepath option + $quotepath = (Pluf::f('git_core_quotepath', true) == true) ? 'true' : 'false'; + $out = array(); + $res = 0; + exec(sprintf(Pluf::f('idf_exec_cmd_prefix', ''). + Pluf::f('git_path', 'git').' config -f %s/config --add core.quotepath %s', + escapeshellarg($fullpath), + escapeshellarg($quotepath) + ), + $out, $res); + if ($res != 0) { + Pluf_Log::warn(array('IDF_Plugin_Git_Serve::initRepository', + 'core.quotepath configuration error.', + $quotepath)); + return; + } + Pluf_Log::debug(array('IDF_Plugin_Git_Serve::initRepository', + 'core.quotepath configured.', $quotepath)); } /** diff --git a/src/IDF/Plugin/SyncMonotone.php b/src/IDF/Plugin/SyncMonotone.php index 2da68cd..f75f530 100644 --- a/src/IDF/Plugin/SyncMonotone.php +++ b/src/IDF/Plugin/SyncMonotone.php @@ -62,7 +62,7 @@ class IDF_Plugin_SyncMonotone * 'mtn_repositories' * 2) create a new server key in the same directory * 3) create a new client key for IDF and store it in the project conf - * 4) write monotonerc + * 4) setup the configuration * 5) add the database as new local server in the usher configuration * 6) reload the running usher instance so it acknowledges the new server * @@ -101,6 +101,36 @@ class IDF_Plugin_SyncMonotone )); } + // check some static configuration files + $confdir = Pluf::f('mtn_confdir', false); + if ($confdir === false) { + $confdir = dirname(__FILE__).'/SyncMonotone/'; + } + $confdir_contents = array( + 'monotonerc.in', + 'remote-automate-permissions.in', + 'hooks.d/', + // this is linked and not copied to be able to update + // the list of read-only commands on upgrades + 'hooks.d/indefero_authorize_remote_automate.conf', + 'hooks.d/indefero_authorize_remote_automate.lua', + 'hooks.d/indefero_post_push.conf.in', + 'hooks.d/indefero_post_push.lua', + ); + // check whether we should handle additional files in the config directory + $confdir_extra_contents = Pluf::f('mtn_confdir_extra', false); + if ($confdir_extra_contents !== false) { + $confdir_contents = + array_merge($confdir_contents, $confdir_extra_contents); + } + foreach ($confdir_contents as $content) { + if (!file_exists($confdir.$content)) { + throw new IDF_Scm_Exception(sprintf( + __('The configuration file %s is missing.'), $content + )); + } + } + $shortname = $project->shortname; $projectpath = sprintf($projecttempl, $shortname); if (file_exists($projectpath)) { @@ -144,79 +174,97 @@ class IDF_Plugin_SyncMonotone // // step 3) create a client key, and save it in IDF // - $clientkey_hash = ''; - $monotonerc_tpl = 'monotonerc-noauth.tpl'; - - if (Pluf::f('mtn_remote_auth', true)) { - $monotonerc_tpl = 'monotonerc-auth.tpl'; - $keydir = Pluf::f('tmp_folder').'/mtn-client-keys'; - if (!file_exists($keydir)) { - if (!mkdir($keydir)) { - throw new IDF_Scm_Exception(sprintf( - __('The key directory %s could not be created.'), $keydir - )); - } - } - - $clientkey_name = $shortname.'-client@'.$server; - $cmd = sprintf('au generate_key --keydir=%s %s ""', - escapeshellarg($keydir), - escapeshellarg($clientkey_name) - ); - $keyinfo = self::_mtn_exec($cmd); - - $parsed_keyinfo = array(); - try { - $parsed_keyinfo = IDF_Scm_Monotone_BasicIO::parse($keyinfo); - } - catch (Exception $e) { + $keydir = Pluf::f('tmp_folder').'/mtn-client-keys'; + if (!file_exists($keydir)) { + if (!mkdir($keydir)) { throw new IDF_Scm_Exception(sprintf( - __('Could not parse key information: %s'), $e->getMessage() + __('The key directory %s could not be created.'), $keydir )); } - - $clientkey_hash = $parsed_keyinfo[0][1]['hash']; - $clientkey_file = $keydir . '/' . $clientkey_name . '.' . $clientkey_hash; - $clientkey_data = file_get_contents($clientkey_file); - - $project->getConf()->setVal('mtn_client_key_name', $clientkey_name); - $project->getConf()->setVal('mtn_client_key_hash', $clientkey_hash); - $project->getConf()->setVal('mtn_client_key_data', $clientkey_data); - - // add the public client key to the server - $cmd = sprintf('au get_public_key --keydir=%s %s', - escapeshellarg($keydir), - escapeshellarg($clientkey_hash) - ); - $clientkey_pubdata = self::_mtn_exec($cmd); - - $cmd = sprintf('au put_public_key --db=%s %s', - escapeshellarg($dbfile), - escapeshellarg($clientkey_pubdata) - ); - self::_mtn_exec($cmd); } - // - // step 4) write monotonerc - // - $monotonerc = file_get_contents( - dirname(__FILE__).'/SyncMonotone/'.$monotonerc_tpl - ); - $monotonerc = str_replace( - array('%%MTNPOSTPUSH%%', '%%PROJECT%%', '%%MTNCLIENTKEY%%'), - array($mtnpostpush, $shortname, $clientkey_hash), - $monotonerc + $clientkey_name = $shortname.'-client@'.$server; + $cmd = sprintf('au generate_key --keydir=%s %s ""', + escapeshellarg($keydir), + escapeshellarg($clientkey_name) ); + $keyinfo = self::_mtn_exec($cmd); - $rcfile = $projectpath.'/monotonerc'; - - if (file_put_contents($rcfile, $monotonerc, LOCK_EX) === false) { + $parsed_keyinfo = array(); + try { + $parsed_keyinfo = IDF_Scm_Monotone_BasicIO::parse($keyinfo); + } + catch (Exception $e) { throw new IDF_Scm_Exception(sprintf( - __('Could not write mtn configuration file "%s"'), $rcfile + __('Could not parse key information: %s'), $e->getMessage() )); } + $clientkey_hash = $parsed_keyinfo[0][1]['hash']; + $clientkey_file = $keydir . '/' . $clientkey_name . '.' . $clientkey_hash; + $clientkey_data = file_get_contents($clientkey_file); + + $project->getConf()->setVal('mtn_client_key_name', $clientkey_name); + $project->getConf()->setVal('mtn_client_key_hash', $clientkey_hash); + $project->getConf()->setVal('mtn_client_key_data', $clientkey_data); + + // add the public client key to the server + $cmd = sprintf('au get_public_key --keydir=%s %s', + escapeshellarg($keydir), + escapeshellarg($clientkey_hash) + ); + $clientkey_pubdata = self::_mtn_exec($cmd); + + $cmd = sprintf('au put_public_key --db=%s %s', + escapeshellarg($dbfile), + escapeshellarg($clientkey_pubdata) + ); + self::_mtn_exec($cmd); + + // + // step 4) setup the configuration + // + + // we assume that all confdir entries ending with a slash mean a + // directory that has to be created, that all files ending on ".in" + // have to be processed and copied in place and that all other files + // just need to be symlinked from the original location + foreach ($confdir_contents as $content) { + $filepath = $projectpath.'/'.$content; + if (substr($content, -1) == '/') { + if (!mkdir($filepath)) { + throw new IDF_Scm_Exception(sprintf( + __('Could not create configuration directory "%s"'), $filepath + )); + } + continue; + } + + if (substr($content, -3) != '.in') { + if (!symlink($confdir.$content, $filepath)) { + IDF_Scm_Exception(sprintf( + __('Could not create symlink "%s"'), $filepath + )); + } + continue; + } + + $filecontents = file_get_contents($confdir.'/'.$content); + $filecontents = str_replace( + array('%%MTNPOSTPUSH%%', '%%PROJECT%%', '%%MTNCLIENTKEY%%'), + array($mtnpostpush, $shortname, $clientkey_hash), + $filecontents + ); + + // remove the .in + $filepath = substr($filepath, 0, -3); + if (file_put_contents($filepath, $filecontents, LOCK_EX) === false) { + throw new IDF_Scm_Exception(sprintf( + __('Could not write configuration file "%s"'), $filepath + )); + } + } + // // step 5) read in and append the usher config with the new server // @@ -252,7 +300,7 @@ class IDF_Plugin_SyncMonotone '--confdir', $projectpath, '-d', $dbfile, '--timestamps', - '--ticker=dot' + '--ticker=dot' )), ); @@ -378,17 +426,15 @@ class IDF_Plugin_SyncMonotone } } - if (Pluf::f('mtn_remote_auth', true)) { - $keydir = Pluf::f('tmp_folder').'/mtn-client-keys'; - $keyname = $project->getConf()->getVal('mtn_client_key_name', false); - $keyhash = $project->getConf()->getVal('mtn_client_key_hash', false); - if ($keyname && $keyhash && - file_exists($keydir .'/'. $keyname . '.' . $keyhash)) { - if (!@unlink($keydir .'/'. $keyname . '.' . $keyhash)) { - throw new IDF_Scm_Exception(sprintf( - __('Could not delete client private key %s'), $keyname - )); - } + $keydir = Pluf::f('tmp_folder').'/mtn-client-keys'; + $keyname = $project->getConf()->getVal('mtn_client_key_name', false); + $keyhash = $project->getConf()->getVal('mtn_client_key_hash', false); + if ($keyname && $keyhash && + file_exists($keydir .'/'. $keyname . '.' . $keyhash)) { + if (!@unlink($keydir .'/'. $keyname . '.' . $keyhash)) { + throw new IDF_Scm_Exception(sprintf( + __('Could not delete client private key %s'), $keyname + )); } } @@ -721,7 +767,7 @@ class IDF_Plugin_SyncMonotone private static function _delete_recursive($path) { - if (is_file($path)) { + if (is_file($path) || is_link($path)) { return @unlink($path); } diff --git a/src/IDF/Plugin/SyncMonotone/hooks.d/indefero_authorize_remote_automate.conf b/src/IDF/Plugin/SyncMonotone/hooks.d/indefero_authorize_remote_automate.conf new file mode 100644 index 0000000..c38789f --- /dev/null +++ b/src/IDF/Plugin/SyncMonotone/hooks.d/indefero_authorize_remote_automate.conf @@ -0,0 +1,10 @@ +ARA_safe_commands = { + "get_corresponding_path", "get_content_changed", "tags", "branches", + "common_ancestors", "packet_for_fdelta", "packet_for_fdata", + "packets_for_certs", "packet_for_rdata", "get_manifest_of", + "get_revision", "select", "graph", "children", "parents", "roots", + "leaves", "ancestry_difference", "toposort", "erase_ancestors", + "descendents", "ancestors", "heads", "get_file_of", "get_file", + "interface_version", "get_attributes", "content_diff", + "file_merge", "show_conflicts", "certs", "keys", "get_extended_manifest_of" +} diff --git a/src/IDF/Plugin/SyncMonotone/hooks.d/indefero_authorize_remote_automate.lua b/src/IDF/Plugin/SyncMonotone/hooks.d/indefero_authorize_remote_automate.lua new file mode 100644 index 0000000..e66a64b --- /dev/null +++ b/src/IDF/Plugin/SyncMonotone/hooks.d/indefero_authorize_remote_automate.lua @@ -0,0 +1,88 @@ +-- ***** BEGIN LICENSE BLOCK ***** +-- This file is part of InDefero, an open source project management application. +-- Copyright (C) 2011 Céondo Ltd and contributors. +-- Copyright (C) 2010 Thomas Keller +-- Richard Levitte +-- +-- InDefero is free software; you can redistribute it and/or modify +-- it under the terms of the GNU General Public License as published by +-- the Free Software Foundation; either version 2 of the License, or +-- (at your option) any later version. +-- +-- InDefero is distributed in the hope that it will be useful, +-- but WITHOUT ANY WARRANTY; without even the implied warranty of +-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +-- GNU General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License +-- along with this program; if not, write to the Free Software +-- Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +-- +-- ***** END LICENSE BLOCK ***** + +-- +-- This script reads key identities from a file "remote-automate-permissions" +-- in the configuration directory and permits those authenticating with one +-- of those keys to perform dangerous (read/write) remote automate operations. +-- The format of the file is very simple, one key identity on every line. +-- Lines starting with # are ignore, as well as empty lines. +-- +-- It's possible to configure this script to allow the performance of some +-- remote automate commands anonymously, through the variable +-- ARA_safe_commands, which has to be a table of commands as strings. +-- One example configuration, taken from the setup at code.monotone.ca, could +-- be this: +-- +-- ARA_safe_commands = { +-- "get_corresponding_path", "get_content_changed", "tags", "branches", +-- "common_ancestors", "packet_for_fdelta", "packet_for_fdata", +-- "packets_for_certs", "packet_for_rdata", "get_manifest_of", +-- "get_revision", "select", "graph", "children", "parents", "roots", +-- "leaves", "ancestry_difference", "toposort", "erase_ancestors", +-- "descendents", "ancestors", "heads", "get_file_of", "get_file", +-- "interface_version", "get_attributes", "content_diff", +-- "file_merge", "show_conflicts", "certs", "keys", "get_extended_manifest_of" +-- } +-- +do + local _safe_commands = {} + if ARA_safe_commands then + _safe_commands = ARA_safe_commands + end + + local _save_get_remote_automate_permitted = get_remote_automate_permitted + function get_remote_automate_permitted(key_identity, command, options) + local permfile = + io.open(get_confdir() .. "/remote-automate-permissions", "r") + if (permfile == nil) then + return false + end + + -- See if the incoming key matches any of the key identities or + -- patterns found in the permissions file. + local matches = false + local line = permfile:read() + while (not matches and line ~= nil) do + if not globish_match("#*", line) then + local _, _, ln = string.find(line, "%s*([^%s]*)%s*") + if ln == "*" then matches = true end + if ln == key_identity.id then matches = true end + if globish_match(ln, key_identity.name) then matches = true end + line = permfile:read() + end + end + io.close(permfile) + if matches then return true end + + -- No matching key found, let's see if the command matches one the + -- admin allowed to be performed anonymously + for _,v in ipairs(_safe_commands) do + if (v == command[1]) then + return true + end + end + + -- No matches found anywhere, then don't permit this operation + return false + end +end diff --git a/src/IDF/Plugin/SyncMonotone/hooks.d/indefero_post_push.conf.in b/src/IDF/Plugin/SyncMonotone/hooks.d/indefero_post_push.conf.in new file mode 100644 index 0000000..efeeaad --- /dev/null +++ b/src/IDF/Plugin/SyncMonotone/hooks.d/indefero_post_push.conf.in @@ -0,0 +1,2 @@ +IDF_project = "%%PROJECT%%" +IDF_push_script = "%%MTNPOSTPUSH%%" diff --git a/src/IDF/Plugin/SyncMonotone/hooks.d/indefero_post_push.lua b/src/IDF/Plugin/SyncMonotone/hooks.d/indefero_post_push.lua new file mode 100644 index 0000000..3038037 --- /dev/null +++ b/src/IDF/Plugin/SyncMonotone/hooks.d/indefero_post_push.lua @@ -0,0 +1,58 @@ +-- ***** BEGIN LICENSE BLOCK ***** +-- This file is part of InDefero, an open source project management application. +-- Copyright (C) 2011 Céondo Ltd and contributors. +-- +-- InDefero is free software; you can redistribute it and/or modify +-- it under the terms of the GNU General Public License as published by +-- the Free Software Foundation; either version 2 of the License, or +-- (at your option) any later version. +-- +-- InDefero is distributed in the hope that it will be useful, +-- but WITHOUT ANY WARRANTY; without even the implied warranty of +-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +-- GNU General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License +-- along with this program; if not, write to the Free Software +-- Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +-- +-- ***** END LICENSE BLOCK ***** + +-- +-- let IDF know of new arriving revisions to fill its timeline +-- +_idf_revs = {} +push_hook_functions( + { + start = + function (session_id) + _idf_revs[session_id] = {} + return "continue",nil + end, + revision_received = + function (new_id, revision, certs, session_id) + table.insert(_idf_revs[session_id], new_id) + return "continue",nil + end, + ["end"] = + function (session_id, ...) + if table.getn(_idf_revs[session_id]) == 0 then + return "continue",nil + end + + local pin,pout,pid = spawn_pipe(IDF_push_script, IDF_project); + if pid == -1 then + print("could not execute " .. IDF_push_script) + return "continue",nil + end + + for _,r in ipairs(_idf_revs[session_id]) do + pin:write(r .. "\n") + end + pin:close() + + wait(pid) + return "continue",nil + end + }) + diff --git a/src/IDF/Plugin/SyncMonotone/monotonerc-auth.tpl b/src/IDF/Plugin/SyncMonotone/monotonerc-auth.tpl deleted file mode 100644 index c59d58f..0000000 --- a/src/IDF/Plugin/SyncMonotone/monotonerc-auth.tpl +++ /dev/null @@ -1,79 +0,0 @@ --- ***** BEGIN LICENSE BLOCK ***** --- This file is part of InDefero, an open source project management application. --- Copyright (C) 2010 Céondo Ltd and contributors. --- --- InDefero is free software; you can redistribute it and/or modify --- it under the terms of the GNU General Public License as published by --- the Free Software Foundation; either version 2 of the License, or --- (at your option) any later version. --- --- InDefero is distributed in the hope that it will be useful, --- but WITHOUT ANY WARRANTY; without even the implied warranty of --- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the --- GNU General Public License for more details. --- --- You should have received a copy of the GNU General Public License --- along with this program; if not, write to the Free Software --- Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA --- --- ***** END LICENSE BLOCK ***** - --- --- controls the access rights for remote_stdio which is used by IDFs frontend --- and other interested parties --- -function get_remote_automate_permitted(key_identity, command, options) - if (key_identity.id == "%%MTNCLIENTKEY%%") then - return true - end - - return false -end - --- --- let IDF know of new arriving revisions to fill its timeline --- -_idf_revs = {} -push_hook_functions({ - ["start"] = function (session_id) - _idf_revs[session_id] = {} - return "continue",nil - end, - ["revision_received"] = function (new_id, revision, certs, session_id) - table.insert(_idf_revs[session_id], new_id) - return "continue",nil - end, - ["end"] = function (session_id, ...) - if table.getn(_idf_revs[session_id]) == 0 then - return "continue",nil - end - - local pin,pout,pid = spawn_pipe("%%MTNPOSTPUSH%%", "%%PROJECT%%"); - if pid == -1 then - print("could not execute %%MTNPOSTPUSH%%") - return - end - - for _,r in ipairs(_idf_revs[session_id]) do - pin:write(r .. "\n") - end - pin:close() - - wait(pid) - return "continue",nil - end -}) - --- --- Load local hooks if they exist. --- --- The way this is supposed to work is that hooks.d can contain symbolic --- links to lua scripts. These links MUST have the extension .lua --- If the script needs some configuration, a corresponding file with --- the extension .conf is the right spot. --- --- First load the configuration of the hooks, if applicable -includedirpattern(get_confdir() .. "/hooks.d/", "*.conf") --- Then load the hooks themselves -includedirpattern(get_confdir() .. "/hooks.d/", "*.lua") - diff --git a/src/IDF/Plugin/SyncMonotone/monotonerc-noauth.tpl b/src/IDF/Plugin/SyncMonotone/monotonerc-noauth.tpl deleted file mode 100644 index 3ad79ba..0000000 --- a/src/IDF/Plugin/SyncMonotone/monotonerc-noauth.tpl +++ /dev/null @@ -1,92 +0,0 @@ --- ***** BEGIN LICENSE BLOCK ***** --- This file is part of InDefero, an open source project management application. --- Copyright (C) 2010 Céondo Ltd and contributors. --- --- InDefero is free software; you can redistribute it and/or modify --- it under the terms of the GNU General Public License as published by --- the Free Software Foundation; either version 2 of the License, or --- (at your option) any later version. --- --- InDefero is distributed in the hope that it will be useful, --- but WITHOUT ANY WARRANTY; without even the implied warranty of --- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the --- GNU General Public License for more details. --- --- You should have received a copy of the GNU General Public License --- along with this program; if not, write to the Free Software --- Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA --- --- ***** END LICENSE BLOCK ***** - --- --- controls the access rights for remote_stdio which is used by IDFs frontend --- and other interested parties --- -function get_remote_automate_permitted(key_identity, command, options) - local read_only_commands = { - "get_corresponding_path", "get_content_changed", "tags", "branches", - "common_ancestors", "packet_for_fdelta", "packet_for_fdata", - "packets_for_certs", "packet_for_rdata", "get_manifest_of", - "get_revision", "select", "graph", "children", "parents", "roots", - "leaves", "ancestry_difference", "toposort", "erase_ancestors", - "descendents", "ancestors", "heads", "get_file_of", "get_file", - "interface_version", "get_attributes", "content_diff", - "file_merge", "show_conflicts", "certs", "keys", "get_file_size", - "get_extended_manifest_of" - } - - for _,v in ipairs(read_only_commands) do - if (v == command[1]) then - return true - end - end - - return false -end - --- --- let IDF know of new arriving revisions to fill its timeline --- -_idf_revs = {} -push_hook_functions({ - ["start"] = function (session_id) - _idf_revs[session_id] = {} - return "continue",nil - end, - ["revision_received"] = function (new_id, revision, certs, session_id) - table.insert(_idf_revs[session_id], new_id) - return "continue",nil - end, - ["end"] = function (session_id, ...) - if table.getn(_idf_revs[session_id]) == 0 then - return "continue",nil - end - - local pin,pout,pid = spawn_pipe("%%MTNPOSTPUSH%%", "%%PROJECT%%"); - if pid == -1 then - print("could not execute %%MTNPOSTPUSH%%") - return - end - - for _,r in ipairs(_idf_revs[session_id]) do - pin:write(r .. "\n") - end - pin:close() - - wait(pid) - return "continue",nil - end -}) - --- --- Load local hooks if they exist. --- --- The way this is supposed to work is that hooks.d can contain symbolic --- links to lua scripts. These links MUST have the extension .lua --- If the script needs some configuration, a corresponding file with --- the extension .conf is the right spot. --- --- First load the configuration of the hooks, if applicable -includedirpattern(get_confdir() .. "/hooks.d/", "*.conf") --- Then load the hooks themselves -includedirpattern(get_confdir() .. "/hooks.d/", "*.lua") diff --git a/src/IDF/Plugin/SyncMonotone/monotonerc.in b/src/IDF/Plugin/SyncMonotone/monotonerc.in new file mode 100644 index 0000000..985ef49 --- /dev/null +++ b/src/IDF/Plugin/SyncMonotone/monotonerc.in @@ -0,0 +1,30 @@ +-- ***** BEGIN LICENSE BLOCK ***** +-- This file is part of InDefero, an open source project management application. +-- Copyright (C) 2011 Céondo Ltd and contributors. +-- +-- InDefero is free software; you can redistribute it and/or modify +-- it under the terms of the GNU General Public License as published by +-- the Free Software Foundation; either version 2 of the License, or +-- (at your option) any later version. +-- +-- InDefero is distributed in the hope that it will be useful, +-- but WITHOUT ANY WARRANTY; without even the implied warranty of +-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +-- GNU General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License +-- along with this program; if not, write to the Free Software +-- Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +-- +-- ***** END LICENSE BLOCK ***** + +---- Load local hooks if they exist. +-- The way this is supposed to work is that hooks.d can contain symbolic +-- links to lua scripts. These links MUST have the extension .lua +-- If the script needs some configuration, a corresponding file with +-- the extension .conf is the right spot. +---- +-- First load the configuration of the hooks, if applicable +includedirpattern(get_confdir() .. "/hooks.d/","*.conf") +-- Then load the hooks themselves +includedirpattern(get_confdir() .. "/hooks.d/","*.lua") diff --git a/src/IDF/Plugin/SyncMonotone/remote-automate-permissions.in b/src/IDF/Plugin/SyncMonotone/remote-automate-permissions.in new file mode 100644 index 0000000..164a44c --- /dev/null +++ b/src/IDF/Plugin/SyncMonotone/remote-automate-permissions.in @@ -0,0 +1 @@ +%%MTNCLIENTKEY%% diff --git a/src/IDF/Scm/Git.php b/src/IDF/Scm/Git.php index ca05429..edc1aab 100644 --- a/src/IDF/Scm/Git.php +++ b/src/IDF/Scm/Git.php @@ -280,6 +280,14 @@ class IDF_Scm_Git extends IDF_Scm public static function getAuthAccessUrl($project, $user, $commit=null) { + // if the user haven't registred a public ssh key, + // he can't use the write url which use the SSH authentification + if ($user != null) { + $keys = $user->get_idf_key_list(); + if (count ($keys) == 0) + return self::getAnonymousAccessUrl($project); + } + return sprintf(Pluf::f('git_write_remote_url'), $project->shortname); } diff --git a/src/IDF/Scm/Monotone/Stdio.php b/src/IDF/Scm/Monotone/Stdio.php index fa243d8..86c4b50 100644 --- a/src/IDF/Scm/Monotone/Stdio.php +++ b/src/IDF/Scm/Monotone/Stdio.php @@ -70,11 +70,6 @@ class IDF_Scm_Monotone_Stdio */ public function _getAuthOptions() { - // no remote authentication - the simple case - if (!Pluf::f('mtn_remote_auth', true)) { - return '--key= '; - } - $prjconf = $this->project->getConf(); $name = $prjconf->getVal('mtn_client_key_name', false); $hash = $prjconf->getVal('mtn_client_key_hash', false); diff --git a/src/IDF/Template/MarkdownPrefilter.php b/src/IDF/Template/MarkdownPrefilter.php index 1cabf43..ecf2b1f 100644 --- a/src/IDF/Template/MarkdownPrefilter.php +++ b/src/IDF/Template/MarkdownPrefilter.php @@ -168,6 +168,7 @@ class IDF_Template_MarkdownPrefilter extends Pluf_Text_HTML_Filter 'https', 'ftp', 'mailto', + 'irc' ); // tags which should be removed if they contain no content // (e.g. "" or "") diff --git a/src/IDF/Views/Issue.php b/src/IDF/Views/Issue.php index 7dbafee..2f43f6a 100644 --- a/src/IDF/Views/Issue.php +++ b/src/IDF/Views/Issue.php @@ -78,6 +78,164 @@ class IDF_Views_Issue $params, $request); } + /** + * View the issues watch list of a given user. + * Limited to a specified project + */ + public $watchList_precond = array('IDF_Precondition::accessIssues', + 'Pluf_Precondition::loginRequired'); + public function watchList($request, $match) + { + $prj = $request->project; + $otags = $prj->getTagIdsByStatus('open'); + $ctags = $prj->getTagIdsByStatus('closed'); + if (count($otags) == 0) $otags[] = 0; + if (count($ctags) == 0) $ctags[] = 0; + + // Get the id list of issue in the user watch list (for all projects !) + $db =& Pluf::db(); + $sql_results = $db->select('SELECT idf_issue_id as id FROM '.Pluf::f('db_table_prefix', '').'idf_issue_pluf_user_assoc WHERE pluf_user_id='.$request->user->id); + $issue_ids = array(0); + foreach ($sql_results as $id) { + $issue_ids[] = $id['id']; + } + $issue_ids = implode (',', $issue_ids); + + // Count open and close issues + $sql = new Pluf_SQL('project=%s AND id IN ('.$issue_ids.') AND status IN ('.implode(', ', $otags).')', array($prj->id)); + $nb_open = Pluf::factory('IDF_Issue')->getCount(array('filter'=>$sql->gen())); + $sql = new Pluf_SQL('project=%s AND id IN ('.$issue_ids.') AND status IN ('.implode(', ', $ctags).')', array($prj->id)); + $nb_closed = Pluf::factory('IDF_Issue')->getCount(array('filter'=>$sql->gen())); + + // Generate a filter for the paginator + switch ($match[2]) { + case 'closed': + $title = sprintf(__('Watch List: Closed Issues for %s'), (string) $prj); + $summary = __('This table shows the closed issues in your watch list for %s project.', (string) $prj); + $f_sql = new Pluf_SQL('project=%s AND id IN ('.$issue_ids.') AND status IN ('.implode(', ', $ctags).')', array($prj->id)); + break; + case 'open': + default: + $title = sprintf(__('Watch List: Open Issues for %s'), (string) $prj); + $summary = __('This table shows the open issues in your watch list for %s project.', (string) $prj); + $f_sql = new Pluf_SQL('project=%s AND id IN ('.$issue_ids.') AND status IN ('.implode(', ', $otags).')', array($prj->id)); + break; + } + + // Paginator to paginate the issues + $pag = new Pluf_Paginator(new IDF_Issue()); + $pag->class = 'recent-issues'; + $pag->item_extra_props = array('project_m' => $prj, + 'shortname' => $prj->shortname, + 'current_user' => $request->user); + $pag->summary = $summary; + $pag->forced_where = $f_sql; + $pag->action = array('IDF_Views_Issue::watchList', array($prj->shortname, $match[1])); + $pag->sort_order = array('modif_dtime', 'ASC'); // will be reverted + $pag->sort_reverse_order = array('modif_dtime'); + $pag->sort_link_title = true; + $pag->extra_classes = array('a-c', '', 'a-c', ''); + $list_display = array( + 'id' => __('Id'), + array('summary', 'IDF_Views_Issue_SummaryAndLabels', __('Summary')), + array('status', 'IDF_Views_Issue_ShowStatus', __('Status')), + array('modif_dtime', 'Pluf_Paginator_DateAgo', __('Last Updated')), + ); + $pag->configure($list_display, array(), array('id', 'status', 'modif_dtime')); + $pag->items_per_page = 10; + $pag->no_results_text = __('No issues were found.'); + $pag->setFromRequest($request); + return Pluf_Shortcuts_RenderToResponse('idf/issues/project-watchlist.html', + array('project' => $prj, + 'page_title' => $title, + 'open' => $nb_open, + 'closed' => $nb_closed, + 'issues' => $pag, + ), + $request); + } + + /** + * View the issues watch list of a given user. + * For all projects + */ + public $forgeWatchList_precond = array('Pluf_Precondition::loginRequired'); + public function forgeWatchList($request, $match) + { + $otags = array(); + $ctags = array(); + // Note that this approach does not scale, we will need to add + // a table to cache the meaning of the tags for large forges. + foreach (IDF_Views::getProjects($request->user) as $project) { + $otags = array_merge($otags, $project->getTagIdsByStatus('open')); + } + foreach (IDF_Views::getProjects($request->user) as $project) { + $ctags = array_merge($ctags, $project->getTagIdsByStatus('closed')); + } + if (count($otags) == 0) $otags[] = 0; + if (count($ctags) == 0) $ctags[] = 0; + + // Get the id list of issue in the user watch list (for all projects !) + $db =& Pluf::db(); + $sql_results = $db->select('SELECT idf_issue_id as id FROM '.Pluf::f('db_table_prefix', '').'idf_issue_pluf_user_assoc WHERE pluf_user_id='.$request->user->id); + $issue_ids = array(0); + foreach ($sql_results as $id) { + $issue_ids[] = $id['id']; + } + $issue_ids = implode (',', $issue_ids); + + // Count open and close issues + $sql = new Pluf_SQL('id IN ('.$issue_ids.') AND status IN ('.implode(', ', $otags).')', array()); + $nb_open = Pluf::factory('IDF_Issue')->getCount(array('filter'=>$sql->gen())); + $sql = new Pluf_SQL('id IN ('.$issue_ids.') AND status IN ('.implode(', ', $ctags).')', array()); + $nb_closed = Pluf::factory('IDF_Issue')->getCount(array('filter'=>$sql->gen())); + + // Generate a filter for the paginator + switch ($match[1]) { + case 'closed': + $title = sprintf(__('Watch List: Closed Issues')); + $summary = __('This table shows the closed issues in your watch list.'); + $f_sql = new Pluf_SQL('id IN ('.$issue_ids.') AND status IN ('.implode(', ', $ctags).')', array()); + break; + case 'open': + default: + $title = sprintf(__('Watch List: Open Issues')); + $summary = __('This table shows the open issues in your watch list.'); + $f_sql = new Pluf_SQL('id IN ('.$issue_ids.') AND status IN ('.implode(', ', $otags).')', array()); + break; + } + + // Paginator to paginate the issues + $pag = new Pluf_Paginator(new IDF_Issue()); + $pag->class = 'recent-issues'; + $pag->item_extra_props = array('current_user' => $request->user); + $pag->summary = $summary; + $pag->forced_where = $f_sql; + $pag->action = array('IDF_Views_Issue::forgeWatchList', array($match[1])); + $pag->sort_order = array('modif_dtime', 'ASC'); // will be reverted + $pag->sort_reverse_order = array('modif_dtime'); + $pag->sort_link_title = true; + $pag->extra_classes = array('a-c', '', 'a-c', 'a-c', 'a-c'); + $list_display = array( + 'id' => __('Id'), + array('summary', 'IDF_Views_Issue_SummaryAndLabelsUnknownProject', __('Summary')), + array('project', 'Pluf_Paginator_FkToString', __('Project')), + array('status', 'IDF_Views_Issue_ShowStatus', __('Status')), + array('modif_dtime', 'Pluf_Paginator_DateAgo', __('Last Updated')), + ); + $pag->configure($list_display, array(), array('id', 'project', 'status', 'modif_dtime')); + $pag->items_per_page = 10; + $pag->no_results_text = __('No issues were found.'); + $pag->setFromRequest($request); + return Pluf_Shortcuts_RenderToResponse('idf/issues/forge-watchlist.html', + array('page_title' => $title, + 'open' => $nb_open, + 'closed' => $nb_closed, + 'issues' => $pag, + ), + $request); + } + /** * View the issues of a given user. * @@ -280,6 +438,26 @@ class IDF_Views_Issue } } + // Search previous and next issue id + $octags = $prj->getTagIdsByStatus(($closed) ? 'closed' : 'open'); + if (count($octags) == 0) $octags[] = 0; + $sql_previous = new Pluf_SQL('project=%s AND status IN ('.implode(', ', $octags).') AND id<%s', + array($prj->id, $match[2]) + ); + $sql_next = new Pluf_SQL('project=%s AND status IN ('.implode(', ', $octags).') AND id>%s', + array($prj->id, $match[2]) + ); + $previous_issue = Pluf::factory('IDF_Issue')->getList(array('filter' => $sql_previous->gen(), + 'order' => 'id DESC', + 'nb' => 1 + )); + $next_issue = Pluf::factory('IDF_Issue')->getList(array('filter' => $sql_next->gen(), + 'order' => 'id ASC', + 'nb' => 1 + )); + $previous_issue_id = (isset($previous_issue[0])) ? $previous_issue[0]->id : 0; + $next_issue_id = (isset($next_issue[0])) ? $next_issue[0]->id : 0; + $arrays = self::autoCompleteArrays($prj); return Pluf_Shortcuts_RenderToResponse('idf/issues/view.html', array_merge( @@ -292,6 +470,8 @@ class IDF_Views_Issue 'closed' => $closed, 'preview' => $preview, 'interested' => $interested->count(), + 'previous_issue_id' => $previous_issue_id, + 'next_issue_id' => $next_issue_id ), $arrays), $request); @@ -540,6 +720,17 @@ class IDF_Views_Issue } } +/** + * When you access to your forge watch list, issue don't known + * the project shortname. + */ +function IDF_Views_Issue_SummaryAndLabelsUnknownProject($field, $issue, $extra='') +{ + $shortname = $issue->get_project()->shortname; + $issue->__set('shortname', $shortname); + return IDF_Views_Issue_SummaryAndLabels ($field, $issue, $extra); +} + /** * Display the summary of an issue, then on a new line, display the * list of labels with a link to a view "by label only". @@ -576,3 +767,5 @@ function IDF_Views_Issue_ShowStatus($field, $issue, $extra='') { return Pluf_esc($issue->get_status()->name); } + + diff --git a/src/IDF/conf/idf.php-dist b/src/IDF/conf/idf.php-dist index e1dbd4a..9e0ed8a 100644 --- a/src/IDF/conf/idf.php-dist +++ b/src/IDF/conf/idf.php-dist @@ -98,17 +98,16 @@ $cfg['mtn_remote_url'] = 'mtn://my-host.biz/%s'; # choosed for manual setups and / or ssh access. $cfg['mtn_db_access'] = 'local'; -# If true, each access to the database is authenticated with an auto-generated -# project key which is stored in the IDF project configuration -# ('mtn_client_key_*') and written out to $cfg['tmp_folder']/mtn-client-keys -# for its actual use. This key is then configured on the server to have -# full read / write access to all functions, while anonymous access can be -# completely disabled. -# If false, IDF tries to connect anonymously, without authentication, to -# the remote monotone server instance. In this case no project-specific -# keys are generated and the server must be configured to allow at least -# anonymous read access to the main functions. -#$cfg['mtn_remote_auth'] = true; +# Full path to the directory tree which contains default configuration files +# that are automatically created for new projects. This is only needed +# if $cfg['mtn_db_access'] is set to remote, i.e. in case the SyncMonotone +# plugin should be used. If unset, it defaults to the tree underknees +# src/IDF/Plugin/SyncMonotone/. Don't forget the trailing slash! +#$cfg['mtn_confdir'] = '/path/to/dir/tree/'; + +# Additional configuration files you want to create / copy for new setups. +# All these file paths have to be relative to $cfg['mtn_confdir']. +#$cfg['mtn_confdir_extra'] = array('hooks.d/something.lua') # Needs to be configured for remote / usher usage. # This allows basic control of a running usher process via the forge @@ -256,11 +255,17 @@ $cfg['allowed_scm'] = array('git' => 'IDF_Scm_Git', 'mtn' => 'IDF_Scm_Monotone', ); +# Specific git config +# The core.quotepath is configured on new repository +# True -> All characters upper than 0x80 will be escape (default) +# False -> Characters is print directly, enable accented character in a UTF-8 shell +# $cfg['git_core_quotepath'] = false; + # Set to true when uploaded public keys should not only be validated # syntactically, but also by the specific backend. For SSH public # keys, ssh-keygen(3) must be available and usable in PATH, for # monotone public keys, the monotone binary (as configured above) -# is used. +# is used. # $cfg['idf_strong_key_check'] = false; # If you want to use another memtypes database diff --git a/src/IDF/conf/urls.php b/src/IDF/conf/urls.php index 1afe134..b341580 100644 --- a/src/IDF/conf/urls.php +++ b/src/IDF/conf/urls.php @@ -141,6 +141,16 @@ $ctl[] = array('regex' => '#^/p/([\-\w]+)/issues/view/attachment/(\d+)/(.*)$#', 'model' => 'IDF_Views_Issue', 'method' => 'viewAttachment'); +$ctl[] = array('regex' => '#^/p/([\-\w]+)/issues/watchlist/(\w+)$#', + 'base' => $base, + 'model' => 'IDF_Views_Issue', + 'method' => 'watchList'); + +$ctl[] = array('regex' => '#^/watchlist/(\w+)$#', + 'base' => $base, + 'model' => 'IDF_Views_Issue', + 'method' => 'forgeWatchList'); + // ---------- SCM ---------------------------------------- $ctl[] = array('regex' => '#^/p/([\-\w]+)/source/help/$#', diff --git a/src/IDF/templates/idf/base-full.html b/src/IDF/templates/idf/base-full.html index e0bb337..5f505d8 100644 --- a/src/IDF/templates/idf/base-full.html +++ b/src/IDF/templates/idf/base-full.html @@ -25,6 +25,7 @@ + diff --git a/src/IDF/templates/idf/base-simple.html b/src/IDF/templates/idf/base-simple.html index 27e6a5c..b5168c8 100644 --- a/src/IDF/templates/idf/base-simple.html +++ b/src/IDF/templates/idf/base-simple.html @@ -25,6 +25,7 @@ + diff --git a/src/IDF/templates/idf/base.html b/src/IDF/templates/idf/base.html index 91af76a..27fdae1 100644 --- a/src/IDF/templates/idf/base.html +++ b/src/IDF/templates/idf/base.html @@ -25,6 +25,7 @@ + diff --git a/src/IDF/templates/idf/issues/base.html b/src/IDF/templates/idf/issues/base.html index 8c142f8..779de34 100644 --- a/src/IDF/templates/idf/issues/base.html +++ b/src/IDF/templates/idf/issues/base.html @@ -3,7 +3,8 @@ {block subtabs}
{trans 'Open Issues'} -{if !$user.isAnonymous()} | {trans 'New Issue'} | {trans 'My Issues'}{/if} | +{if !$user.isAnonymous()} | {trans 'New Issue'} | {trans 'My Issues'} +| {trans 'My watch list'}{/if} |
diff --git a/src/IDF/templates/idf/issues/forge-watchlist.html b/src/IDF/templates/idf/issues/forge-watchlist.html new file mode 100644 index 0000000..b6d2884 --- /dev/null +++ b/src/IDF/templates/idf/issues/forge-watchlist.html @@ -0,0 +1,12 @@ +{extends "idf/base-simple.html"} + +{block body} +{$issues.render} +{/block} + +{block context} +{aurl 'open_url', 'IDF_Views_Issue::forgeWatchList', array('open')} +{aurl 'closed_url', 'IDF_Views_Issue::forgeWatchList', array('closed')} +{blocktrans}

Open issues: {$open}

+

Closed issues: {$closed}

{/blocktrans} +{/block} diff --git a/src/IDF/templates/idf/issues/project-watchlist.html b/src/IDF/templates/idf/issues/project-watchlist.html new file mode 100644 index 0000000..ecc9241 --- /dev/null +++ b/src/IDF/templates/idf/issues/project-watchlist.html @@ -0,0 +1,17 @@ +{extends "idf/issues/base.html"} + +{block docclass}yui-t2{assign $inWatchList = true}{/block} + +{block body} +{$issues.render} +{if !$user.isAnonymous()} +{aurl 'url', 'IDF_Views_Issue::create', array($project.shortname)} +

+ {trans 'New Issue'}

{/if} +{/block} + +{block context} +{aurl 'open_url', 'IDF_Views_Issue::watchList', array($project.shortname, 'open')} +{aurl 'closed_url', 'IDF_Views_Issue::watchList', array($project.shortname, 'closed')} +{blocktrans}

Open issues: {$open}

+

Closed issues: {$closed}

{/blocktrans} +{/block} diff --git a/src/IDF/templates/idf/issues/view.html b/src/IDF/templates/idf/issues/view.html index 4d0e1ba..4ad9e4e 100644 --- a/src/IDF/templates/idf/issues/view.html +++ b/src/IDF/templates/idf/issues/view.html @@ -1,6 +1,15 @@ {extends "idf/issues/base.html"} {block titleicon}{if $form}
{/if}{/block} {block body} +
+{if $previous_issue_id} +Previous issue +{/if} +{if $previous_issue_id and $next_issue_id} - {/if} +{if $next_issue_id} +Next issue +{/if} +
{assign $i = 0} {assign $nc = $comments.count()} {foreach $comments as $c}{ashowuser 'submitter', $c.get_submitter(), $request} diff --git a/src/IDF/templates/idf/user/dashboard.html b/src/IDF/templates/idf/user/dashboard.html index 67f9856..584d837 100644 --- a/src/IDF/templates/idf/user/dashboard.html +++ b/src/IDF/templates/idf/user/dashboard.html @@ -11,5 +11,7 @@

{blocktrans}Update your account.{/blocktrans}

{aurl 'url', 'IDF_Views_User::view', array($user.login)}

{blocktrans}See your public profile.{/blocktrans}

+{aurl 'url', 'IDF_Views_Issue::forgeWatchList', array('open')} +

{blocktrans}See your forge issue watch list.{/blocktrans}

{/block} diff --git a/src/IDF/templates/idf/wiki/view.html b/src/IDF/templates/idf/wiki/view.html index 21594cd..42fba21 100644 --- a/src/IDF/templates/idf/wiki/view.html +++ b/src/IDF/templates/idf/wiki/view.html @@ -18,6 +18,9 @@ by {$submitter}.{/blocktrans}

{/if} +
+
{trans 'Table of Content'}
+

{$page.summary}

{if !$oldrev} @@ -29,6 +32,7 @@ by {$submitter}.{/blocktrans}

{/if} {/if} +
{/block} {block context} {ashowuser 'submitter', $page.get_submitter(), $request} diff --git a/www/media/idf/css/style.css b/www/media/idf/css/style.css index ef64f05..942a1b7 100644 --- a/www/media/idf/css/style.css +++ b/www/media/idf/css/style.css @@ -225,6 +225,7 @@ div.issue-comment { div.issue-comment-first { border-top: 1px solid #d3d7cf; + clear: both; } div.issue-comment-signin { @@ -702,6 +703,14 @@ div.deprecated-page { color: #a00; } +ul > li { + list-style: disc outside none; +} + +ol > li { + list-style: decimal outside none; +} + #branding { float: right; position: relative; @@ -737,6 +746,32 @@ div.deprecated-page { margin: 0px; } +#wiki-toc { + float: right; + margin-left: 10px; +} + +#wiki-toc-content { + border: 1px solid #999999; + border-width: 1px 0; + padding: 10px 0; + padding-bottom: 25px; + background-color: #ffffff; + display: block; +} + +#wiki-toc-content a { + display: block; +} + +#wiki-toc-content a.wiki-h2 { + margin-left: 1em; +} + +#wiki-toc-content a.wiki-h3 { + margin-left: 2em; +} + /** * main menu */ diff --git a/www/media/idf/img/favicon.png b/www/media/idf/img/favicon.png new file mode 100644 index 0000000..51c91e4 Binary files /dev/null and b/www/media/idf/img/favicon.png differ diff --git a/www/media/idf/js/wiki-toc.js b/www/media/idf/js/wiki-toc.js new file mode 100644 index 0000000..e7e8e3d --- /dev/null +++ b/www/media/idf/js/wiki-toc.js @@ -0,0 +1,7 @@ +$(document).ready(function() { + $(":header", "#wiki-content").map(function (index) { + this.id = "wikititle_" + index; + $("" + this.innerText + "").addClass("wiki-" + this.tagName.toLowerCase()).appendTo('#wiki-toc-content'); + }); +}); +