Import admintools and devtools from CVS HEAD
authorMichael M Slusarz <slusarz@curecanti.org>
Wed, 23 Dec 2009 01:20:45 +0000 (18:20 -0700)
committerMichael M Slusarz <slusarz@curecanti.org>
Wed, 23 Dec 2009 01:20:45 +0000 (18:20 -0700)
18 files changed:
framework/admintools/README [new file with mode: 0644]
framework/admintools/horde-base.php [new file with mode: 0644]
framework/admintools/horde-create-sequence.php [new file with mode: 0644]
framework/admintools/horde-db-dumpdata.php [new file with mode: 0755]
framework/admintools/horde-db-dumpschema.php [new file with mode: 0755]
framework/admintools/horde-db-updatedata.php [new file with mode: 0755]
framework/admintools/horde-db-updateschema.php [new file with mode: 0755]
framework/admintools/horde-remove-pref.php [new file with mode: 0755]
framework/admintools/horde-sql-shell.php [new file with mode: 0755]
framework/admintools/horde-update.sh [new file with mode: 0755]
framework/admintools/package.xml [new file with mode: 0644]
framework/devtools/README [new file with mode: 0644]
framework/devtools/horde-check-themes.php [new file with mode: 0755]
framework/devtools/horde-fw-symlinks.php [new file with mode: 0755]
framework/devtools/horde-highlight.php [new file with mode: 0755]
framework/devtools/horde-merge.php [new file with mode: 0755]
framework/devtools/horde-rev-cmp.sh [new file with mode: 0755]
framework/devtools/package.xml [new file with mode: 0644]

diff --git a/framework/admintools/README b/framework/admintools/README
new file mode 100644 (file)
index 0000000..eca9724
--- /dev/null
@@ -0,0 +1,20 @@
+=========================
+Horde Administrator Tools
+=========================
+
+Introduction
+------------
+
+This directory contains various utility scripts for Horde administrators.
+
+
+Script Index
+------------
+
+horde-remove-prefs.php
+    Remove all entries of a preference name/scope combination from an
+    SQL prefs backend.
+
+horde-update.sh
+    Facilitates the updating and/or installation of horde and its
+    applications.
diff --git a/framework/admintools/horde-base.php b/framework/admintools/horde-base.php
new file mode 100644 (file)
index 0000000..67a5e8b
--- /dev/null
@@ -0,0 +1,11 @@
+<?php
+// Use the HORDE_BASE environment variable if it's set.
+if ((($base = getenv('HORDE_BASE')) ||
+     (!empty($_ENV['HORDE_BASE']) && $base = $_ENV['HORDE_BASE'])) &&
+    is_dir($base) && is_readable($base)) {
+    $horde_base = $base;
+} elseif (is_file(getcwd() . '/lib/core.php')) {
+    $horde_base = getcwd();
+} else {
+    $horde_base = dirname(dirname(dirname(__FILE__)));
+}
diff --git a/framework/admintools/horde-create-sequence.php b/framework/admintools/horde-create-sequence.php
new file mode 100644 (file)
index 0000000..ea13787
--- /dev/null
@@ -0,0 +1,88 @@
+#!@php_bin@
+<?php
+/**
+ * $Horde: framework/admintools/horde-create-sequence.php,v 1.9 2009/07/22 06:40:30 slusarz Exp $
+ *
+ * Copyright 2007-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @package admintools
+ * @author Chuck Hagenbuch <chuck@horde.org>
+ */
+
+// Do CLI checks and environment setup first.
+require_once dirname(__FILE__) . '/horde-base.php';
+require_once $horde_base . '/lib/core.php';
+
+// Make sure no one runs this from the web.
+if (!Horde_Cli::runningFromCLI()) {
+    exit("Must be run from the command line\n");
+}
+
+// Load the CLI environment - make sure there's no time limit, init
+// some variables, etc.
+$cli = Horde_Cli::singleton();
+$cli->init();
+
+// Include needed libraries.
+$horde_authentication = 'none';
+require_once HORDE_BASE . '/lib/base.php';
+
+$db_lib = 'DB';
+$sequence = null;
+if (isset($_SERVER['argv']) && count($_SERVER['argv']) >= 2) {
+    array_shift($_SERVER['argv']);
+    while ($arg = array_shift($_SERVER['argv'])) {
+        if ($arg == '--mdb2') {
+            $db_lib = 'MDB2';
+        } else {
+            $sequence = $arg;
+        }
+    }
+}
+if (is_null($sequence)) {
+    $sequence = $cli->prompt(_("What sequence do you want to create (_seq will be added automatically)?"));
+}
+
+switch ($db_lib) {
+case 'DB':
+    require_once 'DB.php';
+    $dbh = DB::connect($conf['sql']);
+    break;
+
+case 'MDB2':
+    require_once 'MDB2.php';
+    $params = $conf['sql'];
+    unset($params['charset']);
+    $dbh = MDB2::factory($params);
+    break;
+
+default:
+    throw new Horde_Exception('Unknown database abstraction library');
+}
+if (is_a($dbh, 'PEAR_Error')) {
+    throw new Horde_Exception($dbh);
+}
+
+if (!preg_match('/^\w+$/', $sequence)) {
+    $cli->fatal('Invalid sequence name');
+}
+
+switch ($db_lib) {
+case 'DB':
+    $result = $dbh->createSequence($sequence);
+    break;
+
+case 'MDB2':
+    $dbh->loadModule('Manager', null, true);
+    $result = $dbh->manager->createSequence($sequence);
+    break;
+}
+if (is_a($result, 'PEAR_Error')) {
+    $cli->fatal($result->getMessage());
+}
+
+$cli->green('Sequence created.');
+exit(0);
diff --git a/framework/admintools/horde-db-dumpdata.php b/framework/admintools/horde-db-dumpdata.php
new file mode 100755 (executable)
index 0000000..7b69846
--- /dev/null
@@ -0,0 +1,46 @@
+#!@php_bin@
+<?php
+/**
+ * Dump the requested tables (or all) from the Horde database to XML data
+ * format.
+ *
+ * Copyright 2008-2009 The Horde Project (http://www.horde.org/)
+ *
+ * @author   Chuck Hagenbuch <chuck@horde.org>
+ * @category Horde
+ * @package  admintools
+ */
+
+// Do CLI checks and environment setup first.
+require_once dirname(__FILE__) . '/horde-base.php';
+require_once $horde_base . '/lib/core.php';
+
+// Make sure no one runs this from the web.
+if (!Horde_Cli::runningFromCLI()) {
+    exit("Must be run from the command line\n");
+}
+
+// Load the CLI environment - make sure there's no time limit, init
+// some variables, etc.
+$cli = Horde_Cli::singleton();
+$cli->init();
+
+// Include needed libraries.
+$horde_authentication = 'none';
+require_once HORDE_BASE . '/lib/base.php';
+
+$manager = Horde_SQL_Manager::getInstance();
+if (is_a($manager, 'PEAR_Error')) {
+    $cli->fatal($manager->toString());
+}
+
+// Get rid of the script name
+array_shift($_SERVER['argv']);
+$tables = array_values($_SERVER['argv']);
+
+$result = $manager->dumpData('php://stdout', $tables);
+if (is_a($result, 'PEAR_Error')) {
+    $cli->fatal($result->toString());
+}
+
+exit(0);
diff --git a/framework/admintools/horde-db-dumpschema.php b/framework/admintools/horde-db-dumpschema.php
new file mode 100755 (executable)
index 0000000..4f31867
--- /dev/null
@@ -0,0 +1,47 @@
+#!@php_bin@
+<?php
+/**
+ * Dump the requested tables (or all) from the Horde database to XML schema
+ * format.
+ *
+ * Copyright 2008-2009 The Horde Project (http://www.horde.org/)
+ *
+ * @author   Chuck Hagenbuch <chuck@horde.org>
+ * @category Horde
+ * @package  admintools
+ */
+
+// Do CLI checks and environment setup first.
+require_once dirname(__FILE__) . '/horde-base.php';
+require_once $horde_base . '/lib/core.php';
+
+// Make sure no one runs this from the web.
+if (!Horde_Cli::runningFromCLI()) {
+    exit("Must be run from the command line\n");
+}
+
+// Load the CLI environment - make sure there's no time limit, init
+// some variables, etc.
+$cli = Horde_Cli::singleton();
+$cli->init();
+
+// Include needed libraries.
+$horde_authentication = 'none';
+require_once HORDE_BASE . '/lib/base.php';
+
+$manager = Horde_SQL_Manager::getInstance();
+if (is_a($manager, 'PEAR_Error')) {
+    $cli->fatal($manager->toString());
+}
+
+// Get rid of the script name
+array_shift($_SERVER['argv']);
+$tables = array_values($_SERVER['argv']);
+
+$xml = $manager->dumpSchema($tables);
+if (is_a($xml, 'PEAR_Error')) {
+    $cli->fatal($xml->toString());
+}
+
+echo $xml;
+exit(0);
diff --git a/framework/admintools/horde-db-updatedata.php b/framework/admintools/horde-db-updatedata.php
new file mode 100755 (executable)
index 0000000..da39bcc
--- /dev/null
@@ -0,0 +1,53 @@
+#!@php_bin@
+<?php
+/**
+ * Update database definitions from the given .xml data file.
+ *
+ * Copyright 2008-2009 The Horde Project (http://www.horde.org/)
+ *
+ * @author   Chuck Hagenbuch <chuck@horde.org>
+ * @category Horde
+ * @package  admintools
+ */
+
+// Do CLI checks and environment setup first.
+require_once dirname(__FILE__) . '/horde-base.php';
+require_once $horde_base . '/lib/core.php';
+
+// Make sure no one runs this from the web.
+if (!Horde_Cli::runningFromCLI()) {
+    exit("Must be run from the command line\n");
+}
+
+// Load the CLI environment - make sure there's no time limit, init
+// some variables, etc.
+$cli = Horde_Cli::singleton();
+$cli->init();
+
+// Include needed libraries.
+$horde_authentication = 'none';
+require_once HORDE_BASE . '/lib/base.php';
+
+$manager = Horde_SQL_Manager::getInstance();
+if (is_a($manager, 'PEAR_Error')) {
+    $cli->fatal($manager->toString());
+}
+
+// Get arguments.
+array_shift($_SERVER['argv']);
+if (!count($_SERVER['argv'])) {
+    exit("You must specify the data file to update.\n");
+}
+$file = array_shift($_SERVER['argv']);
+$debug = count($_SERVER['argv']) && array_shift($_SERVER['argv']) == 'debug';
+
+$result = $manager->updateData($file, $debug);
+if (is_a($result, 'PEAR_Error')) {
+    $cli->fatal('Failed to update database data: ' . $result->toString());
+    exit(1);
+} elseif ($debug) {
+    echo $result;
+} else {
+    $cli->message('Successfully updated the database with data from "' . $file . '".', 'cli.success');
+}
+exit(0);
diff --git a/framework/admintools/horde-db-updateschema.php b/framework/admintools/horde-db-updateschema.php
new file mode 100755 (executable)
index 0000000..68d95c2
--- /dev/null
@@ -0,0 +1,53 @@
+#!@php_bin@
+<?php
+/**
+ * Update database definitions from the given .xml schema file.
+ *
+ * Copyright 2008-2009 The Horde Project (http://www.horde.org/)
+ *
+ * @author   Chuck Hagenbuch <chuck@horde.org>
+ * @category Horde
+ * @package  admintools
+ */
+
+// Do CLI checks and environment setup first.
+require_once dirname(__FILE__) . '/horde-base.php';
+require_once $horde_base . '/lib/core.php';
+
+// Make sure no one runs this from the web.
+if (!Horde_Cli::runningFromCLI()) {
+    exit("Must be run from the command line\n");
+}
+
+// Load the CLI environment - make sure there's no time limit, init
+// some variables, etc.
+$cli = Horde_Cli::singleton();
+$cli->init();
+
+// Include needed libraries.
+$horde_authentication = 'none';
+require_once HORDE_BASE . '/lib/base.php';
+
+$manager = Horde_SQL_Manager::getInstance();
+if (is_a($manager, 'PEAR_Error')) {
+    $cli->fatal($manager->toString());
+}
+
+// Get arguments.
+array_shift($_SERVER['argv']);
+if (!count($_SERVER['argv'])) {
+    exit("You must specify the schema file to update.\n");
+}
+$file = array_shift($_SERVER['argv']);
+$debug = count($_SERVER['argv']) && array_shift($_SERVER['argv']) == 'debug';
+
+$result = $manager->updateSchema($file, $debug);
+if (is_a($result, 'PEAR_Error')) {
+    $cli->fatal('Failed to update database definitions: ' . $result->toString());
+    exit(1);
+} elseif ($debug) {
+    echo $result;
+} else {
+    $cli->message('Successfully updated the database with definitions from "' . $file . '".', 'cli.success');
+}
+exit(0);
diff --git a/framework/admintools/horde-remove-pref.php b/framework/admintools/horde-remove-pref.php
new file mode 100755 (executable)
index 0000000..6f0e025
--- /dev/null
@@ -0,0 +1,77 @@
+#!@php_bin@
+<?php
+/**
+ * $Horde: framework/admintools/horde-remove-pref.php,v 1.4 2009/07/22 06:40:30 slusarz Exp $
+ *
+ * Copyright 2007-2009 The Horde Project (http://www.horde.org/)
+ *
+ * This script removes a pref from users' settings. Helps when a setting is
+ * to be moved from locked = false, to locked = true and there have already
+ * been prefs set by the users.
+ *
+ * @package admintools
+ */
+
+/**
+ ** Set this to true if you want DB modifications done.
+ **/
+$live = false;
+
+// Do CLI checks and environment setup first.
+require_once dirname(__FILE__) . '/horde-base.php';
+require_once $horde_base . '/lib/core.php';
+
+// Make sure no one runs this from the web.
+if (!Horde_Cli::runningFromCLI()) {
+    exit("Must be run from the command line\n");
+}
+
+// Load the CLI environment - make sure there's no time limit, init
+// some variables, etc.
+$cli = Horde_Cli::singleton();
+$cli->init();
+
+$horde_authentication = 'none';
+$horde_no_compress = true;
+require_once HORDE_BASE . '/lib/base.php';
+
+$scope = $cli->prompt(_("Enter value for pref_scope:"));
+$name = $cli->prompt(_("Enter value for pref_name:"));
+
+/* Open the database. */
+$db = DB::connect($conf['sql']);
+if (is_a($db, 'PEAR_Error')) {
+   var_dump($db);
+   exit;
+}
+
+// Set DB portability options.
+switch ($db->phptype) {
+case 'mssql':
+    $db->setOption('portability', DB_PORTABILITY_LOWERCASE | DB_PORTABILITY_ERRORS | DB_PORTABILITY_RTRIM);
+    break;
+default:
+    $db->setOption('portability', DB_PORTABILITY_LOWERCASE | DB_PORTABILITY_ERRORS);
+}
+
+if ($live) {
+    $sql = 'DELETE FROM horde_prefs WHERE pref_scope = ? AND pref_name = ?';
+    $values = array($scope, $name);
+    $result = $db->getAll($sql, $values);
+    if (is_a($result, 'PEAR_Error')) {
+        var_dump($result);
+    } elseif (empty($result)) {
+        $cli->writeln(sprintf(_("No preference \"%s\" found in scope \"%s\"."), $name, $scope));
+    } else {
+        $cli->writeln(sprintf(_("Preferences \"%s\" deleted in scope \"%s\"."), $name, $scope));
+    }
+} else {
+    $sql = 'SELECT * FROM horde_prefs WHERE pref_scope = ? AND pref_name = ?';
+    $values = array($scope, $name);
+    $result = $db->getAll($sql, $values);
+    if (empty($result)) {
+        $cli->writeln(sprintf(_("No preference \"%s\" found in scope \"%s\"."), $name, $scope));
+    } else {
+        var_dump($result);
+    }
+}
diff --git a/framework/admintools/horde-sql-shell.php b/framework/admintools/horde-sql-shell.php
new file mode 100755 (executable)
index 0000000..d2cc397
--- /dev/null
@@ -0,0 +1,55 @@
+#!@php_bin@
+<?php
+/**
+ * $Horde: framework/admintools/horde-sql-shell.php,v 1.10 2009/07/22 06:40:30 slusarz Exp $
+ *
+ * Copyright 2007-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @package admintools
+ * @author Chuck Hagenbuch <chuck@horde.org>
+ */
+
+// Do CLI checks and environment setup first.
+require_once dirname(__FILE__) . '/horde-base.php';
+require_once $horde_base . '/lib/core.php';
+
+// Make sure no one runs this from the web.
+if (!Horde_Cli::runningFromCLI()) {
+    exit("Must be run from the command line\n");
+}
+
+// Load the CLI environment - make sure there's no time limit, init some
+// variables, etc.
+Horde_Cli::init();
+
+// Include needed libraries.
+$horde_authentication = 'none';
+require_once HORDE_BASE . '/lib/base.php';
+
+$dbh = DB::connect($conf['sql']);
+if (is_a($dbh, 'PEAR_Error')) {
+    throw new Horde_Exception($dbh);
+}
+$dbh->setOption('portability', DB_PORTABILITY_LOWERCASE | DB_PORTABILITY_ERRORS);
+
+// list databases command
+// $result = $dbh->getListOf('databases');
+
+// list tables command
+// $result = $dbh->getListOf('tables');
+
+// read sql file for statements to run
+$statements = new Horde_Db_StatementParser($_SERVER['argv'][1]);
+foreach ($statements as $stmt) {
+    echo "Running:\n  " . preg_replace('/\s+/', ' ', $stmt) . "\n";
+    $result = $dbh->query($stmt);
+    if (is_a($result, 'PEAR_Error')) {
+        var_dump($result);
+        exit;
+    }
+
+    echo "  ...done\n\n";
+}
diff --git a/framework/admintools/horde-update.sh b/framework/admintools/horde-update.sh
new file mode 100755 (executable)
index 0000000..d042127
--- /dev/null
@@ -0,0 +1,388 @@
+#!/bin/sh
+# $Horde: framework/admintools/horde-update.sh,v 1.1 2006/09/11 05:01:14 chuck Exp $
+#
+# update_horde.sh - Marcus I. Ryan <marcus@riboflavin.net>
+#
+# I wrote this script for me, so there isn't really much documentation.
+# I'll explain the few things that come to mind:
+#
+# Summary: This script facilitates the updating and/or installation of horde
+# and its applications.
+# * Create a temporary directory to hold a new install
+# * Make a patch file of the various *.dist files and their non-dist versions
+# * Copy the existing install or download fresh from CVS
+# * Apply the patch made earlier
+# * Make a backup of the existing install
+# * Make the new download the current install
+#
+# Options/Variables:
+# NUM_SLASHES - Since I'm too lazy to code a better way, this is the number
+#               of slashes to remove from the path to make the patch work
+#               relative to the temporary directory.
+# NUM_BACKUPS - How many backups do we want to keep?
+# WEBDIR      - The actual install directory (this is the live version)
+# BACKUPDIR   - Where do we want to keep the backups
+# TMPDIR      - Where do we build the new version to install
+# RELEASES    - A list of the various "releases" we've defined
+# <rel>_dir   - The name of the "horde" dir (usually horde) for release <rel>
+# <rel>_ver   - A list of the various CVS distributions for realease <ver>
+#               and the CVS branches to download.  See examples.
+#
+# WARNING: I do not currently have it set up to check if a patch fails 
+#          completely.  This is not usually a problem, but if a *.dist file
+#          is drastically updated it has been known to cause problems.
+#          If you use this script, please keep an eye out for this.  That
+#          said, I updated both my installs (release and head) with it
+#          over 50 times before I finally had a problem.
+
+trim () {
+  echo $@
+}
+
+######## CONFIGURATION ########
+
+### MISC. OPTIONS ###
+NUM_SLASHES=4
+NUM_BACKUPS=5
+
+### DIRECTORIES ###
+WEBDIR=/usr/local/www
+BACKUPDIR=/usr/local/src
+TMPDIR=/usr/local/src
+
+### RELEASES ###
+RELEASES="release head"
+
+# For each release, we need _dir and _ver settings
+
+#This is the "array" of applications and versions to get
+release_dir=horde
+release_ver="
+        horde-RELENG_2
+       imp-RELENG_3
+       turba-RELENG_1
+       kronolith-RELENG_1
+       nag-RELENG_1
+       mnemo-RELENG_1
+"
+release_ver=`trim ${release_ver}`
+
+head_dir=horde.head
+head_ver="
+       horde-HEAD
+       framework-HEAD
+       imp-HEAD
+       turba-HEAD
+       gollem-HEAD
+       kronolith-HEAD
+       jonah-HEAD
+       troll-HEAD
+       nag-HEAD
+       nic-HEAD
+       mnemo-HEAD
+       passwd-HEAD
+       sam-HEAD
+"
+head_ver=`trim ${head_ver}`
+
+######## MAIN CODE ########
+
+APPVER=$head_ver
+HORDEDIR=$head_dir
+
+# check to see if any command line args are specified
+for arg in $*
+do
+  name=${arg%%=*}
+  name=${name##--}
+  value=${arg##*=}
+  if [ "$name" = "release" ]; then
+    APPVER=`eval echo '$'$value'_ver'`
+    HORDEDIR=`eval echo '$'$value'_dir'`
+    if [ "$APPVER" = "" -o "$HORDEDIR" = "" ]; then
+      echo "ERROR: No settings for release $value"
+    fi
+    continue
+  fi
+  echo "Unknown option $arg ('$name' = '$value')"
+  exit 1
+done
+
+echo "Verifying distribution list"
+if [ ! "${APPVER%%[- ]*}" = "horde" ]; then
+  echo "  horde MUST be the first item in the APPVER list!"
+  exit 1
+fi
+
+echo "Determining temporary directory..."
+EXISTS=`ls -d ${TMPDIR}/${HORDEDIR}.TMP.[0-9]* | head -1`
+TMPDIR="${TMPDIR}/${HORDEDIR}.TMP.$$"
+
+if [ ! -z ${EXISTS} ]; then
+  echo "  Found an existing (aborted?) update of horde (${EXISTS})."
+  read -p "  Should I use it? [Yes]" USE_EXISTING
+  case ${USE_EXISTING} in
+  [Nn]|[Nn][Oo])
+    read -p "  Should I delete ${EXISTS}? [Yes]" DELETE_EXISTING
+    case ${DELETE_EXISTING} in
+    [Nn]|[Nn][Oo])
+      echo "    Not deleting ${EXISTS}"
+      ;;
+    *)
+      echo "    Deleting ${EXISTS}"
+      rm -rf ${EXISTS}
+      ;;
+    esac
+    ;;
+  *)
+    TMPDIR=${EXISTS}
+    ;;
+  esac
+  unset USE_EXISTING
+fi
+if [ -e ${TMPDIR} ]; then
+  echo "  Using ${TMPDIR}"
+else
+  echo "  Creating new directory ${TMPDIR}"
+  mkdir ${TMPDIR}
+  if [ ! -e ${TMPDIR} ]; then
+    echo "ERROR: Couldn't create ${TMPDIR}"
+    exit 1
+  fi
+fi
+
+echo "Creating config patch file from existing horde"
+if [ -e ${TMPDIR}/update.patch ]; then
+  read -p "  This directory includes a patch. Use it? [Yes]" USE_EXISTING
+  case ${USE_EXISTING} in
+  [Nn]|[Nn][Oo])
+    echo "  Clearing existing update.patch"
+    rm -f ${TMPDIR}/update.patch || exit 1
+    ;;
+  esac
+  unset USE_EXISTING
+fi
+if [ ! -e ${TMPDIR}/update.patch ]; then
+  if [ ! -e ${WEBDIR}/${HORDEDIR} ]; then
+    echo "  No existing horde distribution found.  Can't create patch."
+  else
+    read -p "  Do you want to create a patch from the existing install? [Yes]" MAKE_PATCH
+    case ${MAKE_PATCH} in
+    [Nn]|[Nn][Oo])
+      echo "  Not creating a patch"
+      ;;
+    *)
+      echo "  Creating patch...this could take a bit..."
+      find ${WEBDIR}/${HORDEDIR} -type f -name \*.dist -print \
+       | perl -ne 's/\.dist[\r\n]*//; print "$_.dist\n$_\n";' \
+       | xargs -n 2 diff -u > ${TMPDIR}/update.patch
+      ;;
+    esac
+    unset MAKE_PATCH
+  fi
+fi
+
+if [ -e "${WEBDIR}/${HORDEDIR}" ]; then
+  read -p "Do you want to fetch new (N) or update (U)? [U] " FETCH_NEW
+else
+  FETCH_NEW=new
+fi
+
+case ${FETCH_NEW} in
+  [Nn]|[Nn][Ee][Ww])
+    for APP in ${APPVER}
+    {
+      app=${APP%%-*}
+      rel=${APP##*-}
+      if [ ${app} = ${APP} ]; then
+        echo "  No release specified...assuming HEAD"
+        rel=HEAD
+      fi
+
+      case ${app} in
+        horde)
+          APPDIR=${TMPDIR}
+          EXISTDIR="${TMPDIR}/${HORDEDIR}"
+          ;;
+        *)
+          APPDIR=${TMPDIR}/${HORDEDIR}
+          EXISTDIR="${TMPDIR}/${HORDEDIR}/${app}"
+          ;;
+      esac
+
+      if [ -e $EXISTDIR ]; then
+        case ${REGET} in
+        [Aa]|[Aa][Ll][Ll])
+          echo "  Removing existing ${APPDIR}/${app}...";
+          rm -rf ${APPDIR}/${app}
+          echo "  Retrieving ${app} $rel..."
+          cd ${APPDIR}
+          cvs -Q -z3 -d :pserver:cvsread@anoncvs.horde.org:/repository co \
+           -r $rel ${app}
+          ;;
+        [Nn][Oo][Nn][Ee])
+          REGET="NONE"
+          echo "  Using existing ${APPDIR}/${app}"
+          ;;
+        *)
+          echo "  ${app} exists. Should I get ${app} anyway?"
+          if [ "${app}" = "horde" ]; then
+            echo "  NOTE: regetting horde does not clear out any existing files"
+          fi
+          read -p "   [Y]es/[N]o/[A]ll/None (default None): " REGET
+          case ${REGET} in
+          [Yy]|[Yy][Ee][Ss]|[Aa]|[Aa][Ll][Ll])
+            echo "  Removing existing ${APPDIR}/${app}...";
+            rm -rf ${APPDIR}/${app}
+            echo "  Retrieving ${app} $rel..."
+            cd ${APPDIR}
+            cvs -Q -z3 -d :pserver:cvsread@anoncvs.horde.org:/repository co \
+             -r $rel ${app}
+            ;;
+          [Nn]|[Nn][Oo])
+            echo "  Using existing ${APPDIR}/${app}"
+            ;;
+          *)
+            echo "  Using existing ${APPDIR}/${app}"
+            REGET=NONE
+            ;;
+          esac
+          ;;
+        esac
+      else
+        echo -n "  Retrieving ${app} $rel..."
+        cd ${APPDIR}
+        cvs -Q -z3 -d :pserver:cvsread@anoncvs.horde.org:/repository co \
+         -r $rel ${app}
+       echo "done"
+      fi
+      if [ "$app" = "horde" -a "$HORDEDIR" != "horde" ]; then
+       echo "  Moving ${TMPDIR}/horde to ${TMPDIR}/${HORDEDIR}"
+        mv ${TMPDIR}/horde ${TMPDIR}/${HORDEDIR}
+      fi
+    }
+    ;;
+  *)
+    mkdir ${TMPDIR}/${HORDEDIR}
+    cp -Rpf ${WEBDIR}/${HORDEDIR}/* ${TMPDIR}/${HORDEDIR}
+    cd ${TMPDIR}/${HORDEDIR}
+    cvs update -PdC
+    ;;
+esac
+
+echo "Putting default config files in place..."
+if [ -e ${TMPDIR}/${HORDEDIR}/config/conf.php ]; then
+  echo "  I have found some configuration files already in place."
+  echo "  If you are updating an existing installation this is normal."
+  echo "  NOTE: If some have been copied and others not, horde will be broken."
+  read -p "   Should I copy .dist files anyway? [Yes] " USE_EXISTING
+  #The phrasing of the question means USE_EXISTING from the read is backwards
+  # but it seems better to confuse the programmer than the user...
+  case ${USE_EXISTING} in
+   [Nn]|[Nn][Oo])
+    USE_EXISTING=YES
+    ;;
+  *)
+    USE_EXISTING=NO
+    ;;
+  esac
+fi
+if [ "${USE_EXISTING:=NO}" = "NO" ]; then
+  echo "  Copying *.dist files..."
+  find ${TMPDIR}/${HORDEDIR} -type f -name \*.dist -print \
+   | perl -ne 'print "$_"; s/\.dist//; print "$_"' \
+   | xargs -n 2 cp
+fi
+
+echo "Applying patch..."
+echo "  Clearing out any old reject files..."
+find ${TMPDIR} -name \*.rej -type f -exec rm {} \; -print
+
+if [ ! -e ${TMPDIR}/update.patch ]; then
+  echo "  I can't seem to find the patch file ${TMPDIR}/update.patch!"
+  read -p "  Do you want me to load all config files in $EDITOR? [No]" EDIT
+  case ${EDIT} in
+  [Yy]|[Yy][Ee][Ss])
+    find ${TMPDIR}/${HORDEDIR} -type f -name \*.dist \
+     | perl -ne 's/\.dist[\r\n]*//; print "$_\n";' \
+     | xargs -n 2 echo $EDITOR > ${TMPDIR}/edit.sh
+    sh ${TMPDIR}/edit.sh
+    rm ${TMPDIR}/edit.sh
+    ;;
+  *)
+    echo "  WARNING: You need to change the config files later!"
+    ;;
+  esac
+else
+  if [ "${USE_EXISTING}" = "YES" ]; then
+    echo "  We kept the modified configuration files."
+    read -p "  Should we still apply the patch? [No] " PATCH
+    case ${PATCH} in
+    [Yy]|[Yy][Ee][Ss])
+      PATCH=YES
+      ;;
+    *)
+      PATCH=NO
+      ;;
+    esac
+  fi
+
+  if [ "${PATCH:=YES}" = "YES" ]; then
+    echo "  running patch"
+    cd ${TMPDIR}
+    if [ `patch -f -p${NUM_SLASHES} -s < ${TMPDIR}/update.patch` ]; then
+      echo "  Patch applied successfully"
+    else
+      find ${TMPDIR}/${HORDEDIR} -type f -name \*.rej \
+       | perl -ne 's/\.rej[\r\n]*//; print "$_.rej\n$_\n"; ' \
+       | xargs -n 2 echo $EDITOR > ${TMPDIR}/edit.sh
+      sh ${TMPDIR}/edit.sh
+      rm ${TMPDIR}/edit.sh
+    fi
+  fi
+fi
+
+read -p "Are you ready to put the new CVS into production? [Yes]" PROD
+case ${PROD} in
+[Nn]|[Nn][Oo])
+  echo "${TMPDIR} has not been put in production."
+  ;;
+*)
+  if [ -e ${WEBDIR}/${HORDEDIR} ]; then
+    i=1
+    while [ ${i} != ${NUM_BACKUPS} ]
+    do
+      if [ ! -e ${BACKUPDIR}/${HORDEDIR}.${i} ]; then
+        break;
+      fi
+      i=$((${i}+1))
+    done
+
+    if [ ${i} = ${NUM_BACKUPS} ] && [ -e ${BACKUPDIR}/${HORDEDIR}.${i} ]; then
+      echo "  Removing oldest backup directory (${BACKUPDIR}/${HORDEDIR}.${i})"
+      rm -rf ${BACKUPDIR}/${HORDEDIR}.${i} || exit 1
+    fi
+
+    while [ ${i} != 1 ]
+    do
+      echo "  Moving ${BACKUPDIR}/${HORDEDIR}.$((${i}-1)) to ${BACKUPDIR}/${HORDEDIR}.${i}"
+      mv ${BACKUPDIR}/${HORDEDIR}.$((${i}-1)) ${BACKUPDIR}/${HORDEDIR}.${i} || exit 1
+      i=$((${i}-1))
+    done
+
+    echo "  Moving ${WEBDIR}/${HORDEDIR} to ${BACKUPDIR}/${HORDEDIR}.1"
+    mv ${WEBDIR}/${HORDEDIR} ${BACKUPDIR}/${HORDEDIR}.1 || exit 1
+
+    echo "  Moving ${TMPDIR}/${HORDEDIR} ${WEBDIR}/${HORDEDIR}"
+    mv ${TMPDIR}/${HORDEDIR} ${WEBDIR}/${HORDEDIR} || exit 1
+
+    echo "  Removing ${TMPDIR}"
+    rm -rf ${TMPDIR}
+
+    echo "New CVS horde is now in production!"
+  else
+    echo "${WEBDIR}/${HORDEDIR} does not exist.  Copying ${TMPDIR}/${HORDEDIR} to ${WEBDIR}/${HORDEDIR}"
+    cp ${TMPDIR}/${HORDEDIR} ${WEBDIR}/${HORDEDIR}
+  fi
+  ;;
+esac
diff --git a/framework/admintools/package.xml b/framework/admintools/package.xml
new file mode 100644 (file)
index 0000000..9e77024
--- /dev/null
@@ -0,0 +1,103 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<package packagerversion="1.4.9" version="2.0" xmlns="http://pear.php.net/dtd/package-2.0" xmlns:tasks="http://pear.php.net/dtd/tasks-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0
+http://pear.php.net/dtd/tasks-1.0.xsd
+http://pear.php.net/dtd/package-2.0
+http://pear.php.net/dtd/package-2.0.xsd">
+ <name>admintools</name>
+ <channel>pear.horde.org</channel>
+ <summary>Horde Administrator Tools</summary>
+ <description>This package contains scripts useful to people administering Horde installations.
+ </description>
+ <lead>
+  <name>Chuck Hagenbuch</name>
+  <user>chuck</user>
+  <email>chuck@horde.org</email>
+  <active>yes</active>
+ </lead>
+ <lead>
+  <name>Jan Schneider</name>
+  <user>jan</user>
+  <email>jan@horde.org</email>
+  <active>yes</active>
+ </lead>
+ <date>2009-12-22</date>
+ <version>
+  <release>0.3.0</release>
+  <api>0.3.0</api>
+ </version>
+ <stability>
+  <release>beta</release>
+  <api>beta</api>
+ </stability>
+ <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+ <notes>* Initial Horde 4 package.
+ <contents>
+  <dir name="/">
+   <file name="horde-base.php" role="script" />
+   <file name="horde-create-sequence.php" role="script">
+    <tasks:replace from="@php_bin@" to="php_bin" type="pear-config"/>
+   </file>
+   <file name="horde-db-dumpdata.php" role="script">
+    <tasks:replace from="@php_bin@" to="php_bin" type="pear-config"/>
+   </file>
+   <file name="horde-db-dumpschema.php" role="script">
+    <tasks:replace from="@php_bin@" to="php_bin" type="pear-config"/>
+   </file>
+   <file name="horde-db-updatedata.php" role="script">
+    <tasks:replace from="@php_bin@" to="php_bin" type="pear-config"/>
+   </file>
+   <file name="horde-db-updateschema.php" role="script">
+    <tasks:replace from="@php_bin@" to="php_bin" type="pear-config"/>
+   </file>
+   <file name="horde-remove-pref.php" role="script">
+    <tasks:replace from="@php_bin@" to="php_bin" type="pear-config"/>
+   </file>
+   <file name="horde-sql-shell.php" role="script">
+    <tasks:replace from="@php_bin@" to="php_bin" type="pear-config"/>
+   </file>
+   <file name="horde-update.sh" role="script" />
+  </dir> <!-- / -->
+ </contents>
+ <dependencies>
+  <required>
+   <php>
+    <min>5.2.0</min>
+   </php>
+   <pearinstaller>
+    <min>1.7.0</min>
+   </pearinstaller>
+  </required>
+ </dependencies>
+ <phprelease>
+  <filelist>
+   <install name="horde-base.php" as="horde-base.php" />
+   <install name="horde-create-sequence.php" as="horde-create-sequence.php" />
+   <install name="horde-db-dumpdata.php" as="horde-db-dumpdata.php" />
+   <install name="horde-db-dumpschema.php" as="horde-db-dumpschema.php" />
+   <install name="horde-db-updatedata.php" as="horde-db-updatedata.php" />
+   <install name="horde-db-updateschema.php" as="horde-db-updateschema.php" />
+   <install name="horde-remove-pref.php" as="horde-remove-pref.php" />
+   <install name="horde-sql-shell.php" as="horde-sql-shell.php" />
+   <install name="horde-update.sh" as="horde-update.sh" />
+  </filelist>
+ </phprelease>
+ <changelog>
+  <release>
+   <date>2008-06-24</date>
+   <version>
+    <release>0.2.0</release>
+    <api>0.2.0</api>
+   </version>
+   <stability>
+    <release>beta</release>
+    <api>beta</api>
+   </stability>
+   <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+   <notes>- Add a script for creating PEAR::DB sequences
+   - Add scripts for dumping and updating the Horde database from MDB2_Schema XML files
+   - Add a script for running a series of SQL statements from STDIN on the Horde database
+   - Add scripts for dumping and restoring data from the Horde database
+   </notes>
+  </release>
+ </changelog>
+</package>
diff --git a/framework/devtools/README b/framework/devtools/README
new file mode 100644 (file)
index 0000000..773d414
--- /dev/null
@@ -0,0 +1,29 @@
+=====================
+Horde Developer Tools
+=====================
+
+Introduction
+~~~~~~~~~~~~
+This directory contains various developer scripts for the Horde
+distribution.
+
+
+Script Index
+~~~~~~~~~~~~
+horde-check-themes.php
+    Makes sure that themes that provide their own images aren't missing any.
+
+horde-fw-symlinks.php
+    Creates symbolic links necessary for developers to work directly on the
+    files in the framework module without needing to install the changed
+    packages.
+
+horde-highlight.php
+    Highlights source files on the console.
+
+horde-merge.php
+    Merges commits from a commit message into the CVS tree of the current
+    directory.
+
+horde-rev-cmp.sh
+    Compares different CVS revisions of the same file.
diff --git a/framework/devtools/horde-check-themes.php b/framework/devtools/horde-check-themes.php
new file mode 100755 (executable)
index 0000000..0915a4e
--- /dev/null
@@ -0,0 +1,203 @@
+#!@php_bin@
+<?php
+/**
+ * This script does some checking to make sure images are synchronised
+ * across themes.
+ *
+ * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @category Horde
+ * @package  devtools
+ * @author   Marko Djukic <marko@oblo.com>
+ */
+
+/* CLI checks and environment setup first. */
+require_once 'Horde/Cli.php';
+
+/* Make sure no one runs this from the web. */
+if (!Horde_Cli::runningFromCLI()) {
+    exit("Must be run from the command line\n");
+}
+
+/* Get any options. */
+$simple = false;
+$horde_base = null;
+$ignore = array();
+if (isset($argv)) {
+    /* Get rid of the first arg which is the script name. */
+    array_shift($argv);
+    while ($arg = array_shift($argv)) {
+        if ($arg == '--help') {
+            print_usage();
+        } elseif ($arg == '-s') {
+            $simple = true;
+        } elseif (strpos($arg, '-i') === 0) {
+            list(,$ignore[]) = explode('=', $arg);
+        } elseif (file_exists($arg . '/config/registry.php')) {
+            $horde_base = $arg;
+        } else {
+            print_usage("Unrecognised option $arg");
+        }
+    }
+}
+
+if ($horde_base === null) {
+    print_usage("You must specify the base path to Horde.");
+}
+
+/* Set up CLI. */
+$cli = Horde_Cli::singleton();
+$cli->init();
+
+$horde_authentication = 'none';
+require_once $horde_base . '/lib/base.php';
+
+/* Get the apps and start doing checks. */
+$apps = $registry->listApps(array('hidden', 'notoolbar', 'active', 'admin'));
+
+/* Get a list of themes. */
+$themes = array();
+$themes_dir = $registry->get('themesfs', 'horde');
+if ($handle = opendir($themes_dir)) {
+    while ($file = readdir($handle)) {
+        if ($file == '.' || $file == '..' || $file == 'CVS' ||
+            $file == '.svn' ||
+            !file_exists("$themes_dir/$file/themed_graphics") ||
+            !file_exists("$themes_dir/$file/graphics")) {
+            continue;
+        }
+
+        /* Store the apps and their theme directories. */
+        foreach ($apps as $app) {
+            $dir = $registry->get('themesfs', $app) . '/' . $file . '/graphics';
+            if (is_dir($dir)) {
+                $themes[$app][$file] = $dir;
+            }
+        }
+    }
+}
+
+foreach ($apps as $app) {
+    /* Skip applications without icon themes. */
+    if (!isset($themes[$app])) {
+        continue;
+    }
+
+    /* Set up some dirs. */
+    $themes_dir = $registry->get('themesfs', $app);
+    $horde_icon_dir = $themes_dir . '/graphics';
+
+    /* Sanity check for the directories. */
+    if (!file_exists($horde_icon_dir)) {
+        continue;
+    }
+
+    /* Get a list of all horde images recursively. */
+    $horde_icon_list = array();
+    readDirRecursively($horde_icon_dir, $horde_icon_dir, $horde_icon_list);
+
+    /* Loop through themes that replace icons and check for differences. */
+    foreach ($themes[$app] as $theme => $theme_icon_dir) {
+        $theme_icon_list = array();
+        readDirRecursively($theme_icon_dir, $theme_icon_dir, $theme_icon_list);
+
+        /* Check for icons that are in the Horde base theme and not in the
+         * custom theme. */
+        $diff = array_diff($horde_icon_list, $theme_icon_list);
+        /* Don't bother reporting anything for themes that have all the horde
+         * base icons. */
+        if (empty($diff)) {
+            continue;
+        }
+
+        $cli->writeln($cli->red(sprintf('[%s] "%s" theme missing these icons:',
+                                strtoupper($app),
+                                $theme)));
+        sort($diff);
+        foreach ($diff as $icon) {
+            $cli->writeln($icon);
+        }
+
+        /* Check if only doing a Horde base theme to custom theme check. Skip
+         * the reverse checking if true. */
+        if ($simple) {
+            continue;
+        }
+
+        /* Check for icons that are in the Horde base theme and not in the
+         * custom theme. */
+        $diff = array_diff($theme_icon_list, $horde_icon_list);
+        /* Don't bother reporting anything for themes that don't have any icons
+         * more than the base theme. */
+        if (empty($diff)) {
+            continue;
+        }
+
+        $cli->writeln($cli->blue(sprintf('[%s] "%s" theme has these extra icons:',
+                                strtoupper($app),
+                                $theme)));
+        sort($diff);
+        foreach ($diff as $icon) {
+            $cli->writeln($icon);
+        }
+    }
+}
+
+$cli->writeln($cli->green('Done.'));
+exit;
+
+/**
+ * Loops through the directory recursively and stores the found
+ * graphics into an array.
+ */
+function readDirRecursively($path, $basepath, &$list)
+{
+    global $ignore;
+
+    if ($handle = opendir($path)) {
+        while ($file = readdir($handle)) {
+            if ($file == '.' || $file == '..' ||
+                $file == 'CVS' || $file == '.svn') {
+                continue;
+            }
+            if (is_dir("$path/$file")) {
+                readDirRecursively("$path/$file", $basepath, $list);
+            } else {
+                foreach ($ignore as $pattern) {
+                    if (preg_match($pattern, $file)) {
+                        continue 2;
+                    }
+                }
+                $list[] = substr($path, strlen($basepath)) . "/$file";
+            }
+        }
+        closedir($handle);
+    }
+
+}
+
+function print_usage($message = '')
+{
+
+    if (!empty($message)) {
+        print "themes_check.php: $message\n\n";
+    }
+
+    print <<<USAGE
+Usage: themes_check.php [OPTION] /path/to/horde
+
+Possible options:
+  -s            Do only a simple check for any Horde base theme graphics that
+                are missing from the other themes, and no check of the
+                opposite.
+  -i=PATTERN    Insert any valid regex pattern to ignore files from being
+                checked. You can enter multiple -i options to include multiple
+                patterns. For example: -i="/xcf$/ to ignore any original
+                GIMP files.
+
+USAGE;
+    exit;
+}
diff --git a/framework/devtools/horde-fw-symlinks.php b/framework/devtools/horde-fw-symlinks.php
new file mode 100755 (executable)
index 0000000..4c49f82
--- /dev/null
@@ -0,0 +1,548 @@
+#!@php_bin@
+<?php
+/**
+ * This script creates softlinks to the library files you retrieved from
+ * the CVS "framework" module. This script also works on a framework
+ * installation retrieved from git.
+ *
+ * It creates the same directory structure the packages would have if they
+ * were installed with "pear install package.xml".
+ * For creating this structure it uses the information given in the
+ * package.xml files inside each package directory.
+ *
+ * Copyright 2002 Wolfram Kriesing <wolfram@kriesing.de>
+ * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @category Horde
+ * @package  devtools
+ * @author   Wolfram Kriesing <wolfram@kriesing.de>
+ * @author   Jan Schneider <jan@horde.org>
+ */
+
+// Default values for srcDir and destDir are empty.
+$srcDir = null;
+$destDir = null;
+
+// Default to copying if this is run on Windows.
+$copy = strncasecmp(PHP_OS, 'WIN', 3) ? false : true;
+
+// All packages by default.
+$pkg = null;
+
+for ($i = 1; $i < count($argv); $i++) {
+    switch ($argv[$i]) {
+    case '--copy':
+        $copy = true;
+        break;
+
+    case '--help':
+        print_usage();
+
+    case '--src':
+        if (isset($argv[$i + 1])) {
+            if (is_dir($argv[$i + 1])) {
+                $srcDir = $argv[$i + 1];
+                $i++;
+            } else {
+                exit($argv[$i + 1] . " is not a directory");
+            }
+        }
+        break;
+
+    case '--dest':
+        if (isset($argv[$i + 1])) {
+            if (is_dir($argv[$i + 1])) {
+                $destDir = $argv[$i + 1];
+                $i++;
+            } else {
+                exit($argv[$i + 1] . " is not a directory");
+            }
+        }
+        break;
+
+    case '--pkg':
+        $pkg = $argv[$i + 1];
+        if (!is_dir($pkg) || !file_exists($pkg . '/package.xml')) {
+            exit("$pkg is not a valid package directory.\n");
+        }
+        $pkg = preg_replace('|/+$|', '', $pkg);
+        $i++;
+        break;
+
+    default:
+        print_usage("Unrecognised option $argv[$i]");
+    }
+}
+
+// Try to auto-detect the source and dest dirs.
+$cwd = getcwd();
+if ($srcDir === null && is_dir($cwd . DIRECTORY_SEPARATOR . 'framework')) {
+    $srcDir = $cwd . DIRECTORY_SEPARATOR . 'framework';
+}
+if ($destDir === null && is_dir($cwd . DIRECTORY_SEPARATOR . 'libs')) {
+    $destDir = $cwd . DIRECTORY_SEPARATOR . 'libs';
+}
+
+if ($srcDir === null || $destDir === null) {
+    print_usage('Failed to auto-detect source and destination directories,');
+}
+
+// Make $srcDir an absolute path.
+if (($srcDir[0] != '/' && !preg_match('/[A-Za-z]:/', $srcDir)) &&
+    $cwd = getcwd()) {
+    $srcDir = $cwd . '/' . $srcDir;
+}
+$srcDir = rtrim($srcDir, '/');
+
+// Make $destDir an absolute path.
+if (($destDir[0] != '/' && !preg_match('/[A-Za-z]:/', $destDir)) &&
+    $cwd = getcwd()) {
+    $destDir = $cwd . '/' . $destDir;
+}
+$destDir = rtrim($destDir, '/');
+
+// Put $destDir into include_path.
+if (strpos(ini_get('include_path'), $destDir) === false) {
+    ini_set('include_path', $destDir . PATH_SEPARATOR . ini_get('include_path'));
+}
+
+// Do CLI checks and environment setup first.
+if (!@include_once 'Horde/Cli.php') {
+    if (!@include_once $srcDir . '/Cli/lib/Horde/Cli.php') {
+        if (!@include_once $cwd . DIRECTORY_SEPARATOR . 'framework/Cli/lib/Horde/Cli.php') {
+            print_usage('Horde_Cli library is not in the include_path or in the src directory.');
+        }
+    }
+}
+
+// Make sure no one runs this from the web.
+if (!Horde_Cli::runningFromCLI()) {
+    exit;
+}
+
+// Load the CLI environment - make sure there's no time limit, init
+// some variables, etc.
+Horde_Cli::init();
+
+if (!class_exists('SimpleXMLElement', false)) {
+    include_once 'Tree/Tree.php';
+    if (!class_exists('Tree')) {
+        print_usage('You need the PEAR "Tree" package installed');
+    }
+}
+
+// Tree throws some irrelevant reference; silence them.
+error_reporting(E_ALL & ~E_NOTICE);
+
+$linker = new Linker($copy);
+if ($pkg) {
+    $linker->process(getcwd() . DIRECTORY_SEPARATOR . $pkg, $destDir);
+} elseif ($handle = opendir($srcDir)) {
+    while ($file = readdir($handle)) {
+        if ($file != '.' &&
+            $file != '..' &&
+            $file != 'CVS' &&
+            is_dir($srcDir . '/' . $file)) {
+            $linker->process($srcDir . '/' . $file, $destDir);
+        }
+    }
+    closedir($handle);
+ }
+
+echo "\n";
+
+/**
+ */
+class Linker {
+
+    var $_srcDir;
+
+    /**
+     * The base directory for the complete package.
+     *
+     * @string
+     */
+    var $_baseDir;
+
+    /**
+     * The base installation directories of the current directory or file
+     * relative to $_baseDir. The current base directory is always at array
+     * position 0.
+     *
+     * @array
+     */
+    var $_baseInstallDir = array('');
+
+    var $_fileroles = array('php');
+
+    var $_role;
+
+    var $_copy;
+
+    var $_tree;
+
+    var $_contents;
+
+    function Linker($copy = false)
+    {
+        $this->_copy = $copy;
+    }
+
+    function process($srcDir, $destDir)
+    {
+        $this->_srcDir = $srcDir;
+        $packageFile = $this->_srcDir . '/package.xml';
+        $cli = &Horde_Cli::singleton();
+
+        if (!is_file($packageFile)) {
+            $cli->message('No package.xml in ' . $this->_srcDir, 'cli.warning');
+            return false;
+        }
+
+        $this->_tree = $this->getXmlTree($packageFile);
+
+        // Read package name.
+        $packageName = trim($this->_tree->getElementContent('/package/name', 'cdata'));
+        $cli->writeln("Processing package $packageName.");
+
+        // First, look for '/package/phprelease/filelist', which
+        // overrides '/package/contents'.
+        if (($filelist = $this->_tree->getElementByPath('/package/phprelease/filelist'))) {
+            // Do this better, make the tree class work case insensitive.
+            $this->_baseDir = preg_replace('|/+|', '/', $destDir);
+            if (!is_dir($this->_baseDir)) {
+                require_once 'System.php';
+                System::mkdir('-p ' . $this->_baseDir);
+            }
+
+            $this->_handleFilelistTag($filelist);
+
+        // Look for contents in '/package/contents'.
+        } elseif (($this->_contents = $this->_tree->getElementByPath('/package/contents'))) {
+            // Do this better, make the tree class work case insensitive.
+            $this->_baseDir = preg_replace('|/+|', '/', $destDir);
+            if (!is_dir($this->_baseDir)) {
+                require_once 'System.php';
+                System::mkdir('-p ' . $this->_baseDir);
+            }
+
+            $this->_handleContentsTag($this->_contents);
+
+        // Didn't find either.
+        } else {
+            $cli->message('No filelist or contents tags found inside: ' . $packageFile, 'cli.warning');
+        }
+
+        unset($this->_tree);
+        unset($this->_contents);
+    }
+
+    function _handleFilelistTag($element, $curDir = '')
+    {
+        if (isset($element['children'])) {
+            foreach ($element['children'] as $child) {
+                switch ($child['name']) {
+                case 'install':
+                    // <install name="lib/Horde/Log/Exception.php" as="Horde/Log.php" />
+                    $this->_handleInstallTag($child, $curDir);
+                    break;
+
+                default:
+                    $cli = &Horde_Cli::singleton();
+                    $cli->message('No handler for tag: ' . $child['name'], 'cli-warning');
+                    break;
+                }
+            }
+        }
+    }
+
+    function _handleContentsTag($element, $curDir = '')
+    {
+        if (isset($element['children'])) {
+            foreach ($element['children'] as $child) {
+                switch ($child['name']) {
+                case 'file':
+                    $this->_handleFileTag($child, $curDir);
+                    break;
+
+                case 'dir':
+                    $this->_handleDirTag($child, $curDir);
+                    break;
+
+                default:
+                    $cli = &Horde_Cli::singleton();
+                    $cli->message('No handler for tag: ' . $child['name'], 'cli-warning');
+                    break;
+                }
+            }
+        }
+    }
+
+    function _handleDirTag($element, $curDir)
+    {
+        if ($element['attributes']['name'] != '/') {
+            if (substr($curDir, -1) != DIRECTORY_SEPARATOR) {
+                $curDir .= DIRECTORY_SEPARATOR;
+            }
+            $curDir .= $element['attributes']['name'];
+        }
+
+        if (!empty($element['attributes']['baseinstalldir'])) {
+            array_unshift($this->_baseInstallDir, $element['attributes']['baseinstalldir']);
+        }
+        $this->_handleContentsTag($element, $curDir);
+        if (!empty($element['attributes']['baseinstalldir'])) {
+            array_shift($this->_baseInstallDir);
+        }
+    }
+
+    function _handleFileTag($element, $curDir)
+    {
+        if (!empty($element['attributes']['role'])) {
+            $this->_role = $element['attributes']['role'];
+        }
+
+        if (!in_array($this->_role, $this->_fileroles)) {
+            return;
+        }
+
+        if (!empty($element['attributes']['name'])) {
+            $filename = $element['attributes']['name'];
+        } else {
+            $filename = $element['cdata'];
+        }
+        $filename = trim($filename);
+
+        if (!empty($element['attributes']['baseinstalldir'])) {
+            $dir = $element['attributes']['baseinstalldir'];
+        } else {
+            $dir = $this->_baseInstallDir[0];
+        }
+        if (substr($dir, -1) == '/') {
+            $dir = substr($dir, 0, -1);
+        }
+        $dir .= $curDir;
+        if (substr($dir, -1) == '/') {
+            $dir = substr($dir, 0, -1);
+        }
+
+        if (!is_dir($this->_baseDir . $dir)) {
+            require_once 'System.php';
+            System::mkdir('-p ' . $this->_baseDir . $dir);
+        }
+
+        if ($this->_copy) {
+            $cmd = "cp {$this->_srcDir}$curDir/$filename {$this->_baseDir}$dir/$filename";
+        } else {
+            $parent = $this->_findCommonParent($this->_srcDir . $curDir,
+                                               $this->_baseDir . $dir);
+            $dirs = substr_count(substr($this->_baseDir . $dir,
+                                        strlen($parent)),
+                                 '/');
+            $src = str_repeat('../', $dirs) .
+                substr($this->_srcDir . $curDir, strlen($parent) + 1);
+            $cmd = "ln -sf $src/$filename {$this->_baseDir}$dir/$filename";
+        }
+
+        exec($cmd);
+    }
+
+    function _handleInstallTag($element, $curDir)
+    {
+        if (empty($element['attributes']['name'])) {
+            // Warning?
+            return;
+        }
+        $src = trim($element['attributes']['name']);
+        $srcDir = dirname($src);
+
+        if (empty($element['attributes']['as'])) {
+            // Warning?
+            return;
+        }
+        $as = trim($element['attributes']['as']);
+        $asDir = dirname($as);
+
+        $role = $this->_findRole($src);
+        if (!in_array($role, $this->_fileroles)) {
+            return;
+        }
+
+        if (!is_dir($this->_baseDir . '/' . $asDir)) {
+            require_once 'System.php';
+            System::mkdir('-p ' . $this->_baseDir . '/' . $asDir);
+        }
+
+        if ($this->_copy) {
+            $cmd = "cp {$this->_srcDir}$curDir/$src {$this->_baseDir}/$as";
+        } else {
+            $parent = $this->_findCommonParent($this->_srcDir . $curDir,
+                                               $this->_baseDir . $asDir);
+            $dirs = substr_count(substr($this->_baseDir . $srcDir, strlen($parent)),
+                                 '/');
+            $src = str_repeat('../', $dirs) . substr($this->_srcDir . $curDir, strlen($parent) + 1) . '/' . $src;
+            $cmd = "ln -sf $src {$this->_baseDir}/$as";
+        }
+
+        exec($cmd);
+    }
+
+    function _findRole($filename)
+    {
+        if (!$this->_contents) {
+            $this->_contents = $this->_tree->getElementByPath('/package/contents');
+            if (!$this->_contents) {
+                return false;
+            }
+        }
+
+        if (!isset($this->_contents['children'])) {
+            return false;
+        }
+
+        $pieces = explode('/', $filename);
+        if (!count($pieces)) {
+            return false;
+        }
+
+        $element = $this->_contents;
+        while (true) {
+            $continue = false;
+            foreach ($element['children'] as $child) {
+                if (!in_array($child['name'], array('file', 'dir'))) {
+                    continue;
+                }
+
+                if ($child['attributes']['name'] == '/') {
+                    $continue = true;
+                    break;
+                }
+
+                if ($child['attributes']['name'] == $pieces[0]) {
+                    if (count($pieces) == 1) {
+                        if (isset($child['attributes']['role'])) {
+                            return $child['attributes']['role'];
+                        } else {
+                            return false;
+                        }
+                    }
+
+                    array_shift($pieces);
+                    if (!count($pieces)) {
+                        return false;
+                    }
+
+                    $continue = true;
+                    break;
+                }
+            }
+
+            if (!$continue) {
+                return false;
+            }
+
+            if (!isset($child['children'])) {
+                return false;
+            }
+            $element = $child;
+        }
+
+        return false;
+    }
+
+    function _findCommonParent($a, $b)
+    {
+        for ($common = '', $lastpos = 0, $pos = strpos($a, '/', 1);
+             $pos !== false && strpos($b, substr($a, 0, $pos)) === 0;
+             $pos = strpos($a, '/', $pos + 1)) {
+            $common .= substr($a, $lastpos, $pos - $lastpos);
+            $lastpos = $pos;
+        }
+        return $common;
+    }
+
+    function getXmlTree($packageFile)
+    {
+        if (class_exists('SimpleXMLElement', false)) {
+            return new Linker_Xml_Tree($packageFile);
+        } else {
+            $tree = Tree::setupMemory('XML', $packageFile);
+            $tree->setup();
+            return $tree;
+        }
+    }
+
+}
+
+class Linker_Xml_Tree
+{
+    var $_sxml;
+
+    function __construct($packageFile)
+    {
+        $this->_sxml = simplexml_load_file($packageFile);
+    }
+
+    function getElementContent($path, $field)
+    {
+        $elt = $this->getElementByPath($path);
+        return $elt[$field];
+    }
+
+    function getElementByPath($path)
+    {
+        $path = str_replace('/package', '', $path);
+
+        $node = $this->_sxml;
+        $path = preg_split('|/|', $path, -1, PREG_SPLIT_NO_EMPTY);
+        while ($path) {
+            $ptr = array_shift($path);
+            if (!$node->$ptr) return null;
+            $node = $node->$ptr;
+        }
+
+        return $this->_asArray($node);
+    }
+
+    function _asArray($sxml)
+    {
+        $element = array();
+        $element['name'] = $sxml->getName();
+        $element['cdata'] = (string)$sxml;
+        $element['attributes'] = array();
+        foreach ($sxml->attributes() as $k => $v) {
+            $element['attributes'][$k] = (string)$v;
+        }
+        $element['children'] = array();
+        foreach ($sxml->children() as $node) {
+            $element['children'][] = $this->_asArray($node);
+        }
+
+        return $element;
+    }
+
+}
+
+function print_usage($message = '')
+{
+
+    if (!empty($message)) {
+        echo "horde-fw-symlinks.php: $message\n\n";
+    }
+
+    echo <<<USAGE
+Usage: horde-fw-symlinks.php [OPTION]
+
+Possible options:
+  --copy        Do not create symbolic links, but actually copy the libraries
+                (this is done automatically on Windows).
+  --src DIR     The source directory for the framework libraries.
+  --dest DIR    The destination directory for the framework libraries.
+  --pkg DIR     Path to a single package to install.
+
+USAGE;
+    exit;
+}
diff --git a/framework/devtools/horde-highlight.php b/framework/devtools/horde-highlight.php
new file mode 100755 (executable)
index 0000000..a25a86f
--- /dev/null
@@ -0,0 +1,53 @@
+#!@php_bin@
+<?php
+/**
+ * This script highlights various source files on the console.
+ *
+ * Copyright 2004-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @category Horde
+ * @package  devtools
+ * @author   Jan Schneider <jan@horde.org>
+ */
+
+if (!isset($argv[1])) {
+    echo "Usage: highlight.php SOURCE_FILE [HIGHLIGHTER]\n";
+    exit;
+}
+
+require_once 'Text/Highlighter.php';
+require_once 'Text/Highlighter/Renderer/Console.php';
+
+/* File to highlight. */
+$file = $argv[1];
+
+/* Optional highlighter. */
+if (isset($argv[2])) {
+    $type = $argv[2];
+} else {
+    /* Try autodetecting. */
+    $map = array('cpp' => 'CPP',
+                 'css' => 'CSS',
+                 'diff' => 'DIFF', 'patch' => 'DIFF',
+                 'dtd' => 'DTD',
+                 'js' => 'JAVASCRIPT',
+                 'pl' => 'PERL',
+                 'php' => 'PHP',
+                 'py' => 'PYTHON',
+                 'sql' => 'SQL',
+                 'xml' => 'XML');
+    $ext = strtolower(substr($file, strrpos($file, '.') + 1));
+    if (isset($map[$ext])) {
+        $type = $map[$ext];
+    } else {
+        $type = 'PHP';
+    }
+}
+
+$hl = Text_Highlighter::factory($type);
+$hl->setRenderer(new Text_Highlighter_Renderer_Console());
+
+echo $hl->highlight(file_get_contents($file));
diff --git a/framework/devtools/horde-merge.php b/framework/devtools/horde-merge.php
new file mode 100755 (executable)
index 0000000..aaf8403
--- /dev/null
@@ -0,0 +1,135 @@
+#!@php_bin@
+<?php
+/**
+ * A small script that takes lines of a commit message like:
+ * <pre>
+ *   2.485     +26 -5     imp/compose.php
+ *   1.503     +2 -0      imp/docs/CHANGES
+ *   2.159     +25 -12    imp/templates/compose/compose.inc
+ *   2.55      +28 -3     imp/templates/compose/javascript.inc
+ * </pre>
+ * from the standard input and merges these commits into the appropriate files
+ * of the current directory. Mainly for merging changes in HEAD to the RELENG
+ * tree. This script should be run from Horde's root.
+ *
+ * @category Horde
+ * @package  devtools
+ */
+
+// Location of the cvs binary
+$CVS = 'cvs';
+
+@set_time_limit(0);
+ob_implicit_flush(true);
+ini_set('track_errors', true);
+ini_set('implicit_flush', true);
+ini_set('html_errors', false);
+ini_set('magic_quotes_runtime', false);
+
+$reverse = $join = $commit = $compress = false;
+$target = '.';
+while ($arg = array_shift($argv)) {
+    switch ($arg) {
+    case '-R':
+        $reverse = true;
+        break;
+    case '-j':
+        $join = true;
+        break;
+    case '-c':
+        $commit = true;
+        break;
+    case '-z':
+        $compress = true;
+        break;
+    case '-t':
+        $target = array_shift($argv);
+        break;
+    }
+}
+$target .= '/';
+
+$lines = array();
+while (!feof(STDIN)) {
+    $lines[] = fgets(STDIN);
+}
+foreach ($lines as $line) {
+    $line = trim($line);
+    if (empty($line)) {
+        continue;
+    }
+    $tok = preg_split('/\s+/', $line, -1, PREG_SPLIT_NO_EMPTY);
+    $file = str_replace(array('horde/', 'Attic/'), '', $tok[3]);
+    if (isset($tok[4]) && $tok[4] == '(dead)') {
+        $cmd = $CVS . ' remove -f ' . $file;
+        $new_version = $tok[0];
+    } elseif (isset($tok[4]) && $tok[4] == '(new)') {
+        $cmd = '';
+        $dir = dirname($file);
+        if (!is_dir($dir)) {
+            $cmd = "$CVS -f co $dir; ";
+        }
+        $cmd .= $CVS . ' up -j ' . $tok[0] . ' ' . $file;
+        $new_version = $tok[0];
+    } else {
+        if (count($tok) != 4) {
+            print "Unknown line format:\n" . $line . "\n";
+            continue;
+        }
+        $new_version = explode('.', $tok[0]);
+        $old_version = $new_version;
+        $old_version[count($old_version) - 1]--;
+        if ($old_version[count($old_version) - 1] == 0) {
+            unset($old_version[count($old_version) - 1]);
+            unset($old_version[count($old_version) - 1]);
+        }
+        $new_version = implode('.', $new_version);
+        $old_version = implode('.', $old_version);
+        if ($reverse) {
+            $tmp = $new_version;
+            $new_version = $old_version;
+            $old_version = $tmp;
+        }
+        if ($join) {
+            $cmd = sprintf($CVS . ' up -j %s -j %s -kk %s',
+                           $old_version,
+                           $new_version,
+                           $file);
+        } else {
+            $cmd = sprintf($CVS . ' diff -N -r %s -r %s -kk %s | patch %s',
+                           $old_version,
+                           $new_version,
+                           $file,
+                           $target . $file);
+        }
+    }
+    print $cmd . "\n";
+    system($cmd . ' 2>&1', $exit);
+    print "\n";
+
+    if ($exit !== 0) {
+        continue;
+    }
+
+    // Compress JS files if necessary
+    if ($compress && $old_version &&
+        preg_match('/^([^\/]+)\/(.+)\/(.+)$/', $file, $matches) &&
+        $matches[2] == 'js/src') {
+        $currdir = getcwd();
+        chdir(implode('/', array($matches[1], $matches[2])));
+        passthru('php ' . $currdir . '/framework/devtools/horde-js-compress.php --file=' . $matches[3]);
+        chdir($currdir);
+        if ($commit) {
+            print "Commit doesn't work for compressed JS files - you need to commit manually.\n";
+        }
+    }
+
+    if ($commit) {
+        $cmd = sprintf($CVS . ' ci -m "%s: %s" %s',
+                       $reverse ? 'Revert' : (substr_count($new_version, '.') > 1 ? 'MFB' : 'MFH'),
+                       $reverse ? $old_version : $new_version,
+                       $file);
+        print $cmd . "\n";
+        system($cmd . ' 2>&1');
+    }
+}
diff --git a/framework/devtools/horde-rev-cmp.sh b/framework/devtools/horde-rev-cmp.sh
new file mode 100755 (executable)
index 0000000..eae2167
--- /dev/null
@@ -0,0 +1,10 @@
+#!/bin/sh
+#
+# Script to compare revisions and ignore files that should be different, and
+# differences in revision numbers.
+# Syntax: ./compare_revisions.sh FIRST_FOLDER SECOND_FOLDER ['ADDITIONAL PARAMETERS']
+
+FIRST=$1
+SECOND=$2
+shift 2
+diff -r -I "\$Horde" -I "\$Revision" -I "\$Date" -I "\$Id" --exclude version.php --exclude CHANGES --exclude CREDITS --exclude '*.po' --exclude '*.pot' --exclude locale --exclude CVS --exclude '.#*' --exclude '*~' --exclude '*.bak' --exclude '*.orig' --exclude '*.rej' "$@" $FIRST $SECOND | grep -v "config/.*\.php "
diff --git a/framework/devtools/package.xml b/framework/devtools/package.xml
new file mode 100644 (file)
index 0000000..5c75a68
--- /dev/null
@@ -0,0 +1,83 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<package packagerversion="1.4.9" version="2.0" xmlns="http://pear.php.net/dtd/package-2.0" xmlns:tasks="http://pear.php.net/dtd/tasks-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0
+http://pear.php.net/dtd/tasks-1.0.xsd
+http://pear.php.net/dtd/package-2.0
+http://pear.php.net/dtd/package-2.0.xsd">
+ <name>devtools</name>
+ <channel>pear.horde.org</channel>
+ <summary>Horde Developer Tools</summary>
+ <description>This package contains scripts useful to people doing Horde development.
+ </description>
+ <lead>
+  <name>Chuck Hagenbuch</name>
+  <user>chuck</user>
+  <email>chuck@horde.org</email>
+  <active>yes</active>
+ </lead>
+ <lead>
+  <name>Jan Schneider</name>
+  <user>jan</user>
+  <email>jan@horde.org</email>
+  <active>yes</active>
+ </lead>
+ <date>2009-12-23</date>
+ <version>
+  <release>0.2.0</release>
+  <api>0.2.0</api>
+ </version>
+ <stability>
+  <release>beta</release>
+  <api>beta</api>
+ </stability>
+ <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+ <notes>* Initial Horde 4 release.
+ </notes>
+ <contents>
+  <dir name="/">
+   <file name="horde-fw-symlinks.php" role="script">
+    <tasks:replace from="@php_bin@" to="php_bin" type="pear-config"/>
+   </file>
+   <file name="horde-highlight.php" role="script">
+    <tasks:replace from="@php_bin@" to="php_bin" type="pear-config"/>
+   </file>
+   <file name="horde-merge.php" role="script">
+    <tasks:replace from="@php_bin@" to="php_bin" type="pear-config"/>
+   </file>
+   <file name="horde-rev-cmp.sh" role="script" />
+  </dir> <!-- / -->
+ </contents>
+ <dependencies>
+  <required>
+   <php>
+    <min>5.2.0</min>
+   </php>
+   <pearinstaller>
+    <min>1.7.0</min>
+   </pearinstaller>
+  </required>
+ </dependencies>
+ <phprelease>
+  <filelist>
+   <install name="horde-fw-symlinks.php" as="horde-fw-symlinks.php" />
+   <install name="horde-highlight.php" as="horde-highlight.php" />
+   <install name="horde-merge.php" as="horde-merge.php" />
+   <install name="horde-rev-cmp.sh" as="horde-rev-cmp.sh" />
+  </filelist>
+ </phprelease>
+ <changelog>
+  <release>
+   <date>2006-09-11</date>
+   <version>
+    <release>0.2.0</release>
+    <api>0.2.0</api>
+   </version>
+   <stability>
+    <release>beta</release>
+    <api>beta</api>
+   </stability>
+   <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+   <notes>* Initial Horde 4 release.
+   </notes>
+  </release>
+ </changelog>
+</package>