diff --git a/.scrutinizer.yml b/.scrutinizer.yml
new file mode 100644
index 000000000..dc1e74008
--- /dev/null
+++ b/.scrutinizer.yml
@@ -0,0 +1,11 @@
+filter:
+ excluded_paths:
+ - '3rdparty/*'
+
+imports:
+ - javascript
+ - php
+
+tools:
+ external_code_coverage: true
+
diff --git a/.travis.yml b/.travis.yml
index af69e92e6..6d8c7fbb5 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,34 +1,44 @@
-# see http://about.travis-ci.org/docs/user/languages/php/ for more hints
language: php
-
-# list any PHP version you want to test against
php:
- # using major version aliases
-
- # aliased to a recent 5.3.x version
- 5.3
- # aliased to a recent 5.4.x version
- 5.4
- # aliased to a recent 5.5.x version
- 5.5
-# optionally specify a list of environments, for example to test different RDBMS
-#env:
-# - DB=mysql
-# - DB=pgsql
+env:
+ global:
+ - CORE_BRANCH=master
+ matrix:
+ - DB=sqlite
+
+branches:
+ only:
+ - master
+ - stable7
+
+before_install:
+# - composer install
+ - wget https://raw.githubusercontent.com/owncloud/administration/master/travis-ci/before_install.sh
+ - bash ./before_install.sh search_lucene $CORE_BRANCH $DB
-# execute any number of scripts before the test run, custom env's are available as variables
-#before_script:
-# - if [[ "$DB" == "pgsql" ]]; then psql -c "DROP DATABASE IF EXISTS hello_world_test;" -U postgres; fi
-# - if [[ "$DB" == "pgsql" ]]; then psql -c "create database hello_world_test;" -U postgres; fi
-# - if [[ "$DB" == "mysql" ]]; then mysql -e "create database IF NOT EXISTS hello_world_test;" -uroot; fi
+script:
+ # Test lint
+ - cd ../core/apps/search_lucene
+ - sh -c "if [ '$DB' = 'sqlite' ]; then ant test; fi"
-# omitting "script:" will default to phpunit
-# use the $DB env variable to determine the phpunit.xml to use
-#script: phpunit --configuration phpunit_$DB.xml --coverage-text
-script: ant test
+ # Run phpunit tests
+ - cd tests/unit
+ - phpunit --configuration phpunit.xml
-# configure notifications (email, IRC, campfire etc)
-#notifications:
-# irc: "irc.freenode.org#travis"
+ # Create coverage report
+ - wget https://scrutinizer-ci.com/ocular.phar
+ - php ocular.phar code-coverage:upload --format=php-clover clover.xml
+matrix:
+ include:
+ - php: 5.4
+ env: DB=mysql
+ - php: 5.4
+ env: DB=pgsql
+ allow_failures:
+ - php: hhvm
+ fast_finish: true
diff --git a/3rdparty/Zend/Loader.php b/3rdparty/Zend/Loader.php
new file mode 100644
index 000000000..a2b3de1c0
--- /dev/null
+++ b/3rdparty/Zend/Loader.php
@@ -0,0 +1,343 @@
+ $dir) {
+ if ($dir == '.') {
+ $dirs[$key] = $dirPath;
+ } else {
+ $dir = rtrim($dir, '\\/');
+ $dirs[$key] = $dir . DIRECTORY_SEPARATOR . $dirPath;
+ }
+ }
+ $file = basename($file);
+ self::loadFile($file, $dirs, true);
+ } else {
+ self::loadFile($file, null, true);
+ }
+
+ if (!class_exists($class, false) && !interface_exists($class, false)) {
+ require_once 'Zend/Exception.php';
+ throw new Zend_Exception("File \"$file\" does not exist or class \"$class\" was not found in the file");
+ }
+ }
+
+ /**
+ * Loads a PHP file. This is a wrapper for PHP's include() function.
+ *
+ * $filename must be the complete filename, including any
+ * extension such as ".php". Note that a security check is performed that
+ * does not permit extended characters in the filename. This method is
+ * intended for loading Zend Framework files.
+ *
+ * If $dirs is a string or an array, it will search the directories
+ * in the order supplied, and attempt to load the first matching file.
+ *
+ * If the file was not found in the $dirs, or if no $dirs were specified,
+ * it will attempt to load it from PHP's include_path.
+ *
+ * If $once is TRUE, it will use include_once() instead of include().
+ *
+ * @param string $filename
+ * @param string|array $dirs - OPTIONAL either a path or array of paths
+ * to search.
+ * @param boolean $once
+ * @return boolean
+ * @throws Zend_Exception
+ */
+ public static function loadFile($filename, $dirs = null, $once = false)
+ {
+ self::_securityCheck($filename);
+
+ /**
+ * Search in provided directories, as well as include_path
+ */
+ $incPath = false;
+ if (!empty($dirs) && (is_array($dirs) || is_string($dirs))) {
+ if (is_array($dirs)) {
+ $dirs = implode(PATH_SEPARATOR, $dirs);
+ }
+ $incPath = get_include_path();
+ set_include_path($dirs . PATH_SEPARATOR . $incPath);
+ }
+
+ /**
+ * Try finding for the plain filename in the include_path.
+ */
+ if ($once) {
+ include_once $filename;
+ } else {
+ include $filename;
+ }
+
+ /**
+ * If searching in directories, reset include_path
+ */
+ if ($incPath) {
+ set_include_path($incPath);
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns TRUE if the $filename is readable, or FALSE otherwise.
+ * This function uses the PHP include_path, where PHP's is_readable()
+ * does not.
+ *
+ * Note from ZF-2900:
+ * If you use custom error handler, please check whether return value
+ * from error_reporting() is zero or not.
+ * At mark of fopen() can not suppress warning if the handler is used.
+ *
+ * @param string $filename
+ * @return boolean
+ */
+ public static function isReadable($filename)
+ {
+ if (is_readable($filename)) {
+ // Return early if the filename is readable without needing the
+ // include_path
+ return true;
+ }
+
+ if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN'
+ && preg_match('/^[a-z]:/i', $filename)
+ ) {
+ // If on windows, and path provided is clearly an absolute path,
+ // return false immediately
+ return false;
+ }
+
+ foreach (self::explodeIncludePath() as $path) {
+ if ($path == '.') {
+ if (is_readable($filename)) {
+ return true;
+ }
+ continue;
+ }
+ $file = $path . '/' . $filename;
+ if (is_readable($file)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Explode an include path into an array
+ *
+ * If no path provided, uses current include_path. Works around issues that
+ * occur when the path includes stream schemas.
+ *
+ * @param string|null $path
+ * @return array
+ */
+ public static function explodeIncludePath($path = null)
+ {
+ if (null === $path) {
+ $path = get_include_path();
+ }
+
+ if (PATH_SEPARATOR == ':') {
+ // On *nix systems, include_paths which include paths with a stream
+ // schema cannot be safely explode'd, so we have to be a bit more
+ // intelligent in the approach.
+ $paths = preg_split('#:(?!//)#', $path);
+ } else {
+ $paths = explode(PATH_SEPARATOR, $path);
+ }
+ return $paths;
+ }
+
+ /**
+ * spl_autoload() suitable implementation for supporting class autoloading.
+ *
+ * Attach to spl_autoload() using the following:
+ *
+ * spl_autoload_register(array('Zend_Loader', 'autoload'));
+ *
+ *
+ * @deprecated Since 1.8.0
+ * @param string $class
+ * @return string|false Class name on success; false on failure
+ */
+ public static function autoload($class)
+ {
+ trigger_error(__CLASS__ . '::' . __METHOD__ . ' is deprecated as of 1.8.0 and will be removed with 2.0.0; use Zend_Loader_Autoloader instead', E_USER_NOTICE);
+ try {
+ @self::loadClass($class);
+ return $class;
+ } catch (Exception $e) {
+ return false;
+ }
+ }
+
+ /**
+ * Register {@link autoload()} with spl_autoload()
+ *
+ * @deprecated Since 1.8.0
+ * @param string $class (optional)
+ * @param boolean $enabled (optional)
+ * @return void
+ * @throws Zend_Exception if spl_autoload() is not found
+ * or if the specified class does not have an autoload() method.
+ */
+ public static function registerAutoload($class = 'Zend_Loader', $enabled = true)
+ {
+ trigger_error(__CLASS__ . '::' . __METHOD__ . ' is deprecated as of 1.8.0 and will be removed with 2.0.0; use Zend_Loader_Autoloader instead', E_USER_NOTICE);
+ require_once 'Zend/Loader/Autoloader.php';
+ $autoloader = Zend_Loader_Autoloader::getInstance();
+ $autoloader->setFallbackAutoloader(true);
+
+ if ('Zend_Loader' != $class) {
+ self::loadClass($class);
+ $methods = get_class_methods($class);
+ if (!in_array('autoload', (array) $methods)) {
+ require_once 'Zend/Exception.php';
+ throw new Zend_Exception("The class \"$class\" does not have an autoload() method");
+ }
+
+ $callback = array($class, 'autoload');
+
+ if ($enabled) {
+ $autoloader->pushAutoloader($callback);
+ } else {
+ $autoloader->removeAutoloader($callback);
+ }
+ }
+ }
+
+ /**
+ * Ensure that filename does not contain exploits
+ *
+ * @param string $filename
+ * @return void
+ * @throws Zend_Exception
+ */
+ protected static function _securityCheck($filename)
+ {
+ /**
+ * Security check
+ */
+ if (preg_match('/[^a-z0-9\\/\\\\_.:-]/i', $filename)) {
+ require_once 'Zend/Exception.php';
+ throw new Zend_Exception('Security check: Illegal character in filename');
+ }
+ }
+
+ /**
+ * Attempt to include() the file.
+ *
+ * include() is not prefixed with the @ operator because if
+ * the file is loaded and contains a parse error, execution
+ * will halt silently and this is difficult to debug.
+ *
+ * Always set display_errors = Off on production servers!
+ *
+ * @param string $filespec
+ * @param boolean $once
+ * @return boolean
+ * @deprecated Since 1.5.0; use loadFile() instead
+ */
+ protected static function _includeFile($filespec, $once = false)
+ {
+ if ($once) {
+ return include_once $filespec;
+ } else {
+ return include $filespec ;
+ }
+ }
+
+ /**
+ * Standardise the filename.
+ *
+ * Convert the supplied filename into the namespace-aware standard,
+ * based on the Framework Interop Group reference implementation:
+ * http://groups.google.com/group/php-standards/web/psr-0-final-proposal
+ *
+ * The filename must be formatted as "$file.php".
+ *
+ * @param string $file - The file name to be loaded.
+ * @return string
+ */
+ public static function standardiseFile($file)
+ {
+ $fileName = ltrim($file, '\\');
+ $file = '';
+ $namespace = '';
+ if ($lastNsPos = strripos($fileName, '\\')) {
+ $namespace = substr($fileName, 0, $lastNsPos);
+ $fileName = substr($fileName, $lastNsPos + 1);
+ $file = str_replace('\\', DIRECTORY_SEPARATOR, $namespace) . DIRECTORY_SEPARATOR;
+ }
+ $file .= str_replace('_', DIRECTORY_SEPARATOR, $fileName) . '.php';
+ return $file;
+ }
+}
diff --git a/3rdparty/Zend/Loader/Autoloader.php b/3rdparty/Zend/Loader/Autoloader.php
new file mode 100644
index 000000000..ed5a2c43d
--- /dev/null
+++ b/3rdparty/Zend/Loader/Autoloader.php
@@ -0,0 +1,589 @@
+ true,
+ 'ZendX_' => true,
+ );
+
+ /**
+ * @var array Namespace-specific autoloaders
+ */
+ protected $_namespaceAutoloaders = array();
+
+ /**
+ * @var bool Whether or not to suppress file not found warnings
+ */
+ protected $_suppressNotFoundWarnings = false;
+
+ /**
+ * @var null|string
+ */
+ protected $_zfPath;
+
+ /**
+ * Retrieve singleton instance
+ *
+ * @return Zend_Loader_Autoloader
+ */
+ public static function getInstance()
+ {
+ if (null === self::$_instance) {
+ self::$_instance = new self();
+ }
+ return self::$_instance;
+ }
+
+ /**
+ * Reset the singleton instance
+ *
+ * @return void
+ */
+ public static function resetInstance()
+ {
+ self::$_instance = null;
+ }
+
+ /**
+ * Autoload a class
+ *
+ * @param string $class
+ * @return bool
+ */
+ public static function autoload($class)
+ {
+ $self = self::getInstance();
+
+ foreach ($self->getClassAutoloaders($class) as $autoloader) {
+ if ($autoloader instanceof Zend_Loader_Autoloader_Interface) {
+ if ($autoloader->autoload($class)) {
+ return true;
+ }
+ } elseif (is_array($autoloader)) {
+ if (call_user_func($autoloader, $class)) {
+ return true;
+ }
+ } elseif (is_string($autoloader) || is_callable($autoloader)) {
+ if ($autoloader($class)) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Set the default autoloader implementation
+ *
+ * @param string|array $callback PHP callback
+ * @return void
+ */
+ public function setDefaultAutoloader($callback)
+ {
+ if (!is_callable($callback)) {
+ throw new Zend_Loader_Exception('Invalid callback specified for default autoloader');
+ }
+
+ $this->_defaultAutoloader = $callback;
+ return $this;
+ }
+
+ /**
+ * Retrieve the default autoloader callback
+ *
+ * @return string|array PHP Callback
+ */
+ public function getDefaultAutoloader()
+ {
+ return $this->_defaultAutoloader;
+ }
+
+ /**
+ * Set several autoloader callbacks at once
+ *
+ * @param array $autoloaders Array of PHP callbacks (or Zend_Loader_Autoloader_Interface implementations) to act as autoloaders
+ * @return Zend_Loader_Autoloader
+ */
+ public function setAutoloaders(array $autoloaders)
+ {
+ $this->_autoloaders = $autoloaders;
+ return $this;
+ }
+
+ /**
+ * Get attached autoloader implementations
+ *
+ * @return array
+ */
+ public function getAutoloaders()
+ {
+ return $this->_autoloaders;
+ }
+
+ /**
+ * Return all autoloaders for a given namespace
+ *
+ * @param string $namespace
+ * @return array
+ */
+ public function getNamespaceAutoloaders($namespace)
+ {
+ $namespace = (string) $namespace;
+ if (!array_key_exists($namespace, $this->_namespaceAutoloaders)) {
+ return array();
+ }
+ return $this->_namespaceAutoloaders[$namespace];
+ }
+
+ /**
+ * Register a namespace to autoload
+ *
+ * @param string|array $namespace
+ * @return Zend_Loader_Autoloader
+ */
+ public function registerNamespace($namespace)
+ {
+ if (is_string($namespace)) {
+ $namespace = (array) $namespace;
+ } elseif (!is_array($namespace)) {
+ throw new Zend_Loader_Exception('Invalid namespace provided');
+ }
+
+ foreach ($namespace as $ns) {
+ if (!isset($this->_namespaces[$ns])) {
+ $this->_namespaces[$ns] = true;
+ }
+ }
+ return $this;
+ }
+
+ /**
+ * Unload a registered autoload namespace
+ *
+ * @param string|array $namespace
+ * @return Zend_Loader_Autoloader
+ */
+ public function unregisterNamespace($namespace)
+ {
+ if (is_string($namespace)) {
+ $namespace = (array) $namespace;
+ } elseif (!is_array($namespace)) {
+ throw new Zend_Loader_Exception('Invalid namespace provided');
+ }
+
+ foreach ($namespace as $ns) {
+ if (isset($this->_namespaces[$ns])) {
+ unset($this->_namespaces[$ns]);
+ }
+ }
+ return $this;
+ }
+
+ /**
+ * Get a list of registered autoload namespaces
+ *
+ * @return array
+ */
+ public function getRegisteredNamespaces()
+ {
+ return array_keys($this->_namespaces);
+ }
+
+ public function setZfPath($spec, $version = 'latest')
+ {
+ $path = $spec;
+ if (is_array($spec)) {
+ if (!isset($spec['path'])) {
+ throw new Zend_Loader_Exception('No path specified for ZF');
+ }
+ $path = $spec['path'];
+ if (isset($spec['version'])) {
+ $version = $spec['version'];
+ }
+ }
+
+ $this->_zfPath = $this->_getVersionPath($path, $version);
+ set_include_path(implode(PATH_SEPARATOR, array(
+ $this->_zfPath,
+ get_include_path(),
+ )));
+ return $this;
+ }
+
+ public function getZfPath()
+ {
+ return $this->_zfPath;
+ }
+
+ /**
+ * Get or set the value of the "suppress not found warnings" flag
+ *
+ * @param null|bool $flag
+ * @return bool|Zend_Loader_Autoloader Returns boolean if no argument is passed, object instance otherwise
+ */
+ public function suppressNotFoundWarnings($flag = null)
+ {
+ if (null === $flag) {
+ return $this->_suppressNotFoundWarnings;
+ }
+ $this->_suppressNotFoundWarnings = (bool) $flag;
+ return $this;
+ }
+
+ /**
+ * Indicate whether or not this autoloader should be a fallback autoloader
+ *
+ * @param bool $flag
+ * @return Zend_Loader_Autoloader
+ */
+ public function setFallbackAutoloader($flag)
+ {
+ $this->_fallbackAutoloader = (bool) $flag;
+ return $this;
+ }
+
+ /**
+ * Is this instance acting as a fallback autoloader?
+ *
+ * @return bool
+ */
+ public function isFallbackAutoloader()
+ {
+ return $this->_fallbackAutoloader;
+ }
+
+ /**
+ * Get autoloaders to use when matching class
+ *
+ * Determines if the class matches a registered namespace, and, if so,
+ * returns only the autoloaders for that namespace. Otherwise, it returns
+ * all non-namespaced autoloaders.
+ *
+ * @param string $class
+ * @return array Array of autoloaders to use
+ */
+ public function getClassAutoloaders($class)
+ {
+ $namespace = false;
+ $autoloaders = array();
+
+ // Add concrete namespaced autoloaders
+ foreach (array_keys($this->_namespaceAutoloaders) as $ns) {
+ if ('' == $ns) {
+ continue;
+ }
+ if (0 === strpos($class, $ns)) {
+ if ((false === $namespace) || (strlen($ns) > strlen($namespace))) {
+ $namespace = $ns;
+ $autoloaders = $this->getNamespaceAutoloaders($ns);
+ }
+ }
+ }
+
+ // Add internal namespaced autoloader
+ foreach ($this->getRegisteredNamespaces() as $ns) {
+ if (0 === strpos($class, $ns)) {
+ $namespace = $ns;
+ $autoloaders[] = $this->_internalAutoloader;
+ break;
+ }
+ }
+
+ // Add non-namespaced autoloaders
+ $autoloadersNonNamespace = $this->getNamespaceAutoloaders('');
+ if (count($autoloadersNonNamespace)) {
+ foreach ($autoloadersNonNamespace as $ns) {
+ $autoloaders[] = $ns;
+ }
+ unset($autoloadersNonNamespace);
+ }
+
+ // Add fallback autoloader
+ if (!$namespace && $this->isFallbackAutoloader()) {
+ $autoloaders[] = $this->_internalAutoloader;
+ }
+
+ return $autoloaders;
+ }
+
+ /**
+ * Add an autoloader to the beginning of the stack
+ *
+ * @param object|array|string $callback PHP callback or Zend_Loader_Autoloader_Interface implementation
+ * @param string|array $namespace Specific namespace(s) under which to register callback
+ * @return Zend_Loader_Autoloader
+ */
+ public function unshiftAutoloader($callback, $namespace = '')
+ {
+ $autoloaders = $this->getAutoloaders();
+ array_unshift($autoloaders, $callback);
+ $this->setAutoloaders($autoloaders);
+
+ $namespace = (array) $namespace;
+ foreach ($namespace as $ns) {
+ $autoloaders = $this->getNamespaceAutoloaders($ns);
+ array_unshift($autoloaders, $callback);
+ $this->_setNamespaceAutoloaders($autoloaders, $ns);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Append an autoloader to the autoloader stack
+ *
+ * @param object|array|string $callback PHP callback or Zend_Loader_Autoloader_Interface implementation
+ * @param string|array $namespace Specific namespace(s) under which to register callback
+ * @return Zend_Loader_Autoloader
+ */
+ public function pushAutoloader($callback, $namespace = '')
+ {
+ $autoloaders = $this->getAutoloaders();
+ array_push($autoloaders, $callback);
+ $this->setAutoloaders($autoloaders);
+
+ $namespace = (array) $namespace;
+ foreach ($namespace as $ns) {
+ $autoloaders = $this->getNamespaceAutoloaders($ns);
+ array_push($autoloaders, $callback);
+ $this->_setNamespaceAutoloaders($autoloaders, $ns);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Remove an autoloader from the autoloader stack
+ *
+ * @param object|array|string $callback PHP callback or Zend_Loader_Autoloader_Interface implementation
+ * @param null|string|array $namespace Specific namespace(s) from which to remove autoloader
+ * @return Zend_Loader_Autoloader
+ */
+ public function removeAutoloader($callback, $namespace = null)
+ {
+ if (null === $namespace) {
+ $autoloaders = $this->getAutoloaders();
+ if (false !== ($index = array_search($callback, $autoloaders, true))) {
+ unset($autoloaders[$index]);
+ $this->setAutoloaders($autoloaders);
+ }
+
+ foreach ($this->_namespaceAutoloaders as $ns => $autoloaders) {
+ if (false !== ($index = array_search($callback, $autoloaders, true))) {
+ unset($autoloaders[$index]);
+ $this->_setNamespaceAutoloaders($autoloaders, $ns);
+ }
+ }
+ } else {
+ $namespace = (array) $namespace;
+ foreach ($namespace as $ns) {
+ $autoloaders = $this->getNamespaceAutoloaders($ns);
+ if (false !== ($index = array_search($callback, $autoloaders, true))) {
+ unset($autoloaders[$index]);
+ $this->_setNamespaceAutoloaders($autoloaders, $ns);
+ }
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Constructor
+ *
+ * Registers instance with spl_autoload stack
+ *
+ * @return void
+ */
+ protected function __construct()
+ {
+ spl_autoload_register(array(__CLASS__, 'autoload'));
+ $this->_internalAutoloader = array($this, '_autoload');
+ }
+
+ /**
+ * Internal autoloader implementation
+ *
+ * @param string $class
+ * @return bool
+ */
+ protected function _autoload($class)
+ {
+ $callback = $this->getDefaultAutoloader();
+ try {
+ if ($this->suppressNotFoundWarnings()) {
+ @call_user_func($callback, $class);
+ } else {
+ call_user_func($callback, $class);
+ }
+ return $class;
+ } catch (Zend_Exception $e) {
+ return false;
+ }
+ }
+
+ /**
+ * Set autoloaders for a specific namespace
+ *
+ * @param array $autoloaders
+ * @param string $namespace
+ * @return Zend_Loader_Autoloader
+ */
+ protected function _setNamespaceAutoloaders(array $autoloaders, $namespace = '')
+ {
+ $namespace = (string) $namespace;
+ $this->_namespaceAutoloaders[$namespace] = $autoloaders;
+ return $this;
+ }
+
+ /**
+ * Retrieve the filesystem path for the requested ZF version
+ *
+ * @param string $path
+ * @param string $version
+ * @return void
+ */
+ protected function _getVersionPath($path, $version)
+ {
+ $type = $this->_getVersionType($version);
+
+ if ($type == 'latest') {
+ $version = 'latest';
+ }
+
+ $availableVersions = $this->_getAvailableVersions($path, $version);
+ if (empty($availableVersions)) {
+ throw new Zend_Loader_Exception('No valid ZF installations discovered');
+ }
+
+ $matchedVersion = array_pop($availableVersions);
+ return $matchedVersion;
+ }
+
+ /**
+ * Retrieve the ZF version type
+ *
+ * @param string $version
+ * @return string "latest", "major", "minor", or "specific"
+ * @throws Zend_Loader_Exception if version string contains too many dots
+ */
+ protected function _getVersionType($version)
+ {
+ if (strtolower($version) == 'latest') {
+ return 'latest';
+ }
+
+ $parts = explode('.', $version);
+ $count = count($parts);
+ if (1 == $count) {
+ return 'major';
+ }
+ if (2 == $count) {
+ return 'minor';
+ }
+ if (3 < $count) {
+ throw new Zend_Loader_Exception('Invalid version string provided');
+ }
+ return 'specific';
+ }
+
+ /**
+ * Get available versions for the version type requested
+ *
+ * @param string $path
+ * @param string $version
+ * @return array
+ */
+ protected function _getAvailableVersions($path, $version)
+ {
+ if (!is_dir($path)) {
+ throw new Zend_Loader_Exception('Invalid ZF path provided');
+ }
+
+ $path = rtrim($path, '/');
+ $path = rtrim($path, '\\');
+ $versionLen = strlen($version);
+ $versions = array();
+ $dirs = glob("$path/*", GLOB_ONLYDIR);
+ foreach ((array) $dirs as $dir) {
+ $dirName = substr($dir, strlen($path) + 1);
+ if (!preg_match('/^(?:ZendFramework-)?(\d+\.\d+\.\d+((a|b|pl|pr|p|rc)\d+)?)(?:-minimal)?$/i', $dirName, $matches)) {
+ continue;
+ }
+
+ $matchedVersion = $matches[1];
+
+ if (('latest' == $version)
+ || ((strlen($matchedVersion) >= $versionLen)
+ && (0 === strpos($matchedVersion, $version)))
+ ) {
+ $versions[$matchedVersion] = $dir . '/library';
+ }
+ }
+
+ uksort($versions, 'version_compare');
+ return $versions;
+ }
+}
diff --git a/3rdparty/Zend/Loader/Autoloader/Interface.php b/3rdparty/Zend/Loader/Autoloader/Interface.php
new file mode 100644
index 000000000..768b734fe
--- /dev/null
+++ b/3rdparty/Zend/Loader/Autoloader/Interface.php
@@ -0,0 +1,43 @@
+toArray();
+ }
+ if (!is_array($options)) {
+ require_once 'Zend/Loader/Exception.php';
+ throw new Zend_Loader_Exception('Options must be passed to resource loader constructor');
+ }
+
+ $this->setOptions($options);
+
+ $namespace = $this->getNamespace();
+ if ((null === $namespace)
+ || (null === $this->getBasePath())
+ ) {
+ require_once 'Zend/Loader/Exception.php';
+ throw new Zend_Loader_Exception('Resource loader requires both a namespace and a base path for initialization');
+ }
+
+ if (!empty($namespace)) {
+ $namespace .= '_';
+ }
+ require_once 'Zend/Loader/Autoloader.php';
+ Zend_Loader_Autoloader::getInstance()->unshiftAutoloader($this, $namespace);
+ }
+
+ /**
+ * Overloading: methods
+ *
+ * Allow retrieving concrete resource object instances using 'get()'
+ * syntax. Example:
+ *
+ * $loader = new Zend_Loader_Autoloader_Resource(array(
+ * 'namespace' => 'Stuff_',
+ * 'basePath' => '/path/to/some/stuff',
+ * ))
+ * $loader->addResourceType('Model', 'models', 'Model');
+ *
+ * $foo = $loader->getModel('Foo'); // get instance of Stuff_Model_Foo class
+ *
+ *
+ * @param string $method
+ * @param array $args
+ * @return mixed
+ * @throws Zend_Loader_Exception if method not beginning with 'get' or not matching a valid resource type is called
+ */
+ public function __call($method, $args)
+ {
+ if ('get' == substr($method, 0, 3)) {
+ $type = strtolower(substr($method, 3));
+ if (!$this->hasResourceType($type)) {
+ require_once 'Zend/Loader/Exception.php';
+ throw new Zend_Loader_Exception("Invalid resource type $type; cannot load resource");
+ }
+ if (empty($args)) {
+ require_once 'Zend/Loader/Exception.php';
+ throw new Zend_Loader_Exception("Cannot load resources; no resource specified");
+ }
+ $resource = array_shift($args);
+ return $this->load($resource, $type);
+ }
+
+ require_once 'Zend/Loader/Exception.php';
+ throw new Zend_Loader_Exception("Method '$method' is not supported");
+ }
+
+ /**
+ * Helper method to calculate the correct class path
+ *
+ * @param string $class
+ * @return False if not matched other wise the correct path
+ */
+ public function getClassPath($class)
+ {
+ $segments = explode('_', $class);
+ $namespaceTopLevel = $this->getNamespace();
+ $namespace = '';
+
+ if (!empty($namespaceTopLevel)) {
+ $namespace = array();
+ $topLevelSegments = count(explode('_', $namespaceTopLevel));
+ for ($i = 0; $i < $topLevelSegments; $i++) {
+ $namespace[] = array_shift($segments);
+ }
+ $namespace = implode('_', $namespace);
+ if ($namespace != $namespaceTopLevel) {
+ // wrong prefix? we're done
+ return false;
+ }
+ }
+
+ if (count($segments) < 2) {
+ // assumes all resources have a component and class name, minimum
+ return false;
+ }
+
+ $final = array_pop($segments);
+ $component = $namespace;
+ $lastMatch = false;
+ do {
+ $segment = array_shift($segments);
+ $component .= empty($component) ? $segment : '_' . $segment;
+ if (isset($this->_components[$component])) {
+ $lastMatch = $component;
+ }
+ } while (count($segments));
+
+ if (!$lastMatch) {
+ return false;
+ }
+
+ $final = substr($class, strlen($lastMatch) + 1);
+ $path = $this->_components[$lastMatch];
+ $classPath = $path . '/' . str_replace('_', '/', $final) . '.php';
+
+ if (Zend_Loader::isReadable($classPath)) {
+ return $classPath;
+ }
+
+ return false;
+ }
+
+ /**
+ * Attempt to autoload a class
+ *
+ * @param string $class
+ * @return mixed False if not matched, otherwise result if include operation
+ */
+ public function autoload($class)
+ {
+ $classPath = $this->getClassPath($class);
+ if (false !== $classPath) {
+ return include $classPath;
+ }
+ return false;
+ }
+
+ /**
+ * Set class state from options
+ *
+ * @param array $options
+ * @return Zend_Loader_Autoloader_Resource
+ */
+ public function setOptions(array $options)
+ {
+ // Set namespace first, see ZF-10836
+ if (isset($options['namespace'])) {
+ $this->setNamespace($options['namespace']);
+ unset($options['namespace']);
+ }
+
+ $methods = get_class_methods($this);
+ foreach ($options as $key => $value) {
+ $method = 'set' . ucfirst($key);
+ if (in_array($method, $methods)) {
+ $this->$method($value);
+ }
+ }
+ return $this;
+ }
+
+ /**
+ * Set namespace that this autoloader handles
+ *
+ * @param string $namespace
+ * @return Zend_Loader_Autoloader_Resource
+ */
+ public function setNamespace($namespace)
+ {
+ $this->_namespace = rtrim((string) $namespace, '_');
+ return $this;
+ }
+
+ /**
+ * Get namespace this autoloader handles
+ *
+ * @return string
+ */
+ public function getNamespace()
+ {
+ return $this->_namespace;
+ }
+
+ /**
+ * Set base path for this set of resources
+ *
+ * @param string $path
+ * @return Zend_Loader_Autoloader_Resource
+ */
+ public function setBasePath($path)
+ {
+ $this->_basePath = (string) $path;
+ return $this;
+ }
+
+ /**
+ * Get base path to this set of resources
+ *
+ * @return string
+ */
+ public function getBasePath()
+ {
+ return $this->_basePath;
+ }
+
+ /**
+ * Add resource type
+ *
+ * @param string $type identifier for the resource type being loaded
+ * @param string $path path relative to resource base path containing the resource types
+ * @param null|string $namespace sub-component namespace to append to base namespace that qualifies this resource type
+ * @return Zend_Loader_Autoloader_Resource
+ */
+ public function addResourceType($type, $path, $namespace = null)
+ {
+ $type = strtolower($type);
+ if (!isset($this->_resourceTypes[$type])) {
+ if (null === $namespace) {
+ require_once 'Zend/Loader/Exception.php';
+ throw new Zend_Loader_Exception('Initial definition of a resource type must include a namespace');
+ }
+ $namespaceTopLevel = $this->getNamespace();
+ $namespace = ucfirst(trim($namespace, '_'));
+ $this->_resourceTypes[$type] = array(
+ 'namespace' => empty($namespaceTopLevel) ? $namespace : $namespaceTopLevel . '_' . $namespace,
+ );
+ }
+ if (!is_string($path)) {
+ require_once 'Zend/Loader/Exception.php';
+ throw new Zend_Loader_Exception('Invalid path specification provided; must be string');
+ }
+ $this->_resourceTypes[$type]['path'] = $this->getBasePath() . '/' . rtrim($path, '\/');
+
+ $component = $this->_resourceTypes[$type]['namespace'];
+ $this->_components[$component] = $this->_resourceTypes[$type]['path'];
+ return $this;
+ }
+
+ /**
+ * Add multiple resources at once
+ *
+ * $types should be an associative array of resource type => specification
+ * pairs. Each specification should be an associative array containing
+ * minimally the 'path' key (specifying the path relative to the resource
+ * base path) and optionally the 'namespace' key (indicating the subcomponent
+ * namespace to append to the resource namespace).
+ *
+ * As an example:
+ *
+ * $loader->addResourceTypes(array(
+ * 'model' => array(
+ * 'path' => 'models',
+ * 'namespace' => 'Model',
+ * ),
+ * 'form' => array(
+ * 'path' => 'forms',
+ * 'namespace' => 'Form',
+ * ),
+ * ));
+ *
+ *
+ * @param array $types
+ * @return Zend_Loader_Autoloader_Resource
+ */
+ public function addResourceTypes(array $types)
+ {
+ foreach ($types as $type => $spec) {
+ if (!is_array($spec)) {
+ require_once 'Zend/Loader/Exception.php';
+ throw new Zend_Loader_Exception('addResourceTypes() expects an array of arrays');
+ }
+ if (!isset($spec['path'])) {
+ require_once 'Zend/Loader/Exception.php';
+ throw new Zend_Loader_Exception('addResourceTypes() expects each array to include a paths element');
+ }
+ $paths = $spec['path'];
+ $namespace = null;
+ if (isset($spec['namespace'])) {
+ $namespace = $spec['namespace'];
+ }
+ $this->addResourceType($type, $paths, $namespace);
+ }
+ return $this;
+ }
+
+ /**
+ * Overwrite existing and set multiple resource types at once
+ *
+ * @see Zend_Loader_Autoloader_Resource::addResourceTypes()
+ * @param array $types
+ * @return Zend_Loader_Autoloader_Resource
+ */
+ public function setResourceTypes(array $types)
+ {
+ $this->clearResourceTypes();
+ return $this->addResourceTypes($types);
+ }
+
+ /**
+ * Retrieve resource type mappings
+ *
+ * @return array
+ */
+ public function getResourceTypes()
+ {
+ return $this->_resourceTypes;
+ }
+
+ /**
+ * Is the requested resource type defined?
+ *
+ * @param string $type
+ * @return bool
+ */
+ public function hasResourceType($type)
+ {
+ return isset($this->_resourceTypes[$type]);
+ }
+
+ /**
+ * Remove the requested resource type
+ *
+ * @param string $type
+ * @return Zend_Loader_Autoloader_Resource
+ */
+ public function removeResourceType($type)
+ {
+ if ($this->hasResourceType($type)) {
+ $namespace = $this->_resourceTypes[$type]['namespace'];
+ unset($this->_components[$namespace]);
+ unset($this->_resourceTypes[$type]);
+ }
+ return $this;
+ }
+
+ /**
+ * Clear all resource types
+ *
+ * @return Zend_Loader_Autoloader_Resource
+ */
+ public function clearResourceTypes()
+ {
+ $this->_resourceTypes = array();
+ $this->_components = array();
+ return $this;
+ }
+
+ /**
+ * Set default resource type to use when calling load()
+ *
+ * @param string $type
+ * @return Zend_Loader_Autoloader_Resource
+ */
+ public function setDefaultResourceType($type)
+ {
+ if ($this->hasResourceType($type)) {
+ $this->_defaultResourceType = $type;
+ }
+ return $this;
+ }
+
+ /**
+ * Get default resource type to use when calling load()
+ *
+ * @return string|null
+ */
+ public function getDefaultResourceType()
+ {
+ return $this->_defaultResourceType;
+ }
+
+ /**
+ * Object registry and factory
+ *
+ * Loads the requested resource of type $type (or uses the default resource
+ * type if none provided). If the resource has been loaded previously,
+ * returns the previous instance; otherwise, instantiates it.
+ *
+ * @param string $resource
+ * @param string $type
+ * @return object
+ * @throws Zend_Loader_Exception if resource type not specified or invalid
+ */
+ public function load($resource, $type = null)
+ {
+ if (null === $type) {
+ $type = $this->getDefaultResourceType();
+ if (empty($type)) {
+ require_once 'Zend/Loader/Exception.php';
+ throw new Zend_Loader_Exception('No resource type specified');
+ }
+ }
+ if (!$this->hasResourceType($type)) {
+ require_once 'Zend/Loader/Exception.php';
+ throw new Zend_Loader_Exception('Invalid resource type specified');
+ }
+ $namespace = $this->_resourceTypes[$type]['namespace'];
+ $class = $namespace . '_' . ucfirst($resource);
+ if (!isset($this->_resources[$class])) {
+ $this->_resources[$class] = new $class;
+ }
+ return $this->_resources[$class];
+ }
+}
diff --git a/3rdparty/Zend/Loader/AutoloaderFactory.php b/3rdparty/Zend/Loader/AutoloaderFactory.php
new file mode 100644
index 000000000..3d872faed
--- /dev/null
+++ b/3rdparty/Zend/Loader/AutoloaderFactory.php
@@ -0,0 +1,211 @@
+
+ * array(
+ * '' => $autoloaderOptions,
+ * )
+ *
+ *
+ * The factory will then loop through and instantiate each autoloader with
+ * the specified options, and register each with the spl_autoloader.
+ *
+ * You may retrieve the concrete autoloader instances later using
+ * {@link getRegisteredAutoloaders()}.
+ *
+ * Note that the class names must be resolvable on the include_path or via
+ * the Zend library, using PSR-0 rules (unless the class has already been
+ * loaded).
+ *
+ * @param array|Traversable $options (optional) options to use. Defaults to Zend_Loader_StandardAutoloader
+ * @return void
+ * @throws Zend_Loader_Exception_InvalidArgumentException for invalid options
+ * @throws Zend_Loader_Exception_InvalidArgumentException for unloadable autoloader classes
+ */
+ public static function factory($options = null)
+ {
+ if (null === $options) {
+ if (!isset(self::$loaders[self::STANDARD_AUTOLOADER])) {
+ $autoloader = self::getStandardAutoloader();
+ $autoloader->register();
+ self::$loaders[self::STANDARD_AUTOLOADER] = $autoloader;
+ }
+
+ // Return so we don't hit the next check's exception (we're done here anyway)
+ return;
+ }
+
+ if (!is_array($options) && !($options instanceof Traversable)) {
+ require_once 'Exception/InvalidArgumentException.php';
+ throw new Zend_Loader_Exception_InvalidArgumentException(
+ 'Options provided must be an array or Traversable'
+ );
+ }
+
+ foreach ($options as $class => $options) {
+ if (!isset(self::$loaders[$class])) {
+ $autoloader = self::getStandardAutoloader();
+ if (!class_exists($class) && !$autoloader->autoload($class)) {
+ require_once 'Exception/InvalidArgumentException.php';
+ throw new Zend_Loader_Exception_InvalidArgumentException(sprintf(
+ 'Autoloader class "%s" not loaded',
+ $class
+ ));
+ }
+
+ // unfortunately is_subclass_of is broken on some 5.3 versions
+ // additionally instanceof is also broken for this use case
+ if (version_compare(PHP_VERSION, '5.3.7', '>=')) {
+ if (!is_subclass_of($class, 'Zend_Loader_SplAutoloader')) {
+ require_once 'Exception/InvalidArgumentException.php';
+ throw new Zend_Loader_Exception_InvalidArgumentException(sprintf(
+ 'Autoloader class %s must implement Zend\\Loader\\SplAutoloader',
+ $class
+ ));
+ }
+ }
+
+ if ($class === self::STANDARD_AUTOLOADER) {
+ $autoloader->setOptions($options);
+ } else {
+ $autoloader = new $class($options);
+ }
+ $autoloader->register();
+ self::$loaders[$class] = $autoloader;
+ } else {
+ self::$loaders[$class]->setOptions($options);
+ }
+ }
+ }
+
+ /**
+ * Get an list of all autoloaders registered with the factory
+ *
+ * Returns an array of autoloader instances.
+ *
+ * @return array
+ */
+ public static function getRegisteredAutoloaders()
+ {
+ return self::$loaders;
+ }
+
+ /**
+ * Retrieves an autoloader by class name
+ *
+ * @param string $class
+ * @return Zend_Loader_SplAutoloader
+ * @throws Zend_Loader_Exception_InvalidArgumentException for non-registered class
+ */
+ public static function getRegisteredAutoloader($class)
+ {
+ if (!isset(self::$loaders[$class])) {
+ require_once 'Exception/InvalidArgumentException.php';
+ throw new Zend_Loader_Exception_InvalidArgumentException(sprintf('Autoloader class "%s" not loaded', $class));
+ }
+ return self::$loaders[$class];
+ }
+
+ /**
+ * Unregisters all autoloaders that have been registered via the factory.
+ * This will NOT unregister autoloaders registered outside of the fctory.
+ *
+ * @return void
+ */
+ public static function unregisterAutoloaders()
+ {
+ foreach (self::getRegisteredAutoloaders() as $class => $autoloader) {
+ spl_autoload_unregister(array($autoloader, 'autoload'));
+ unset(self::$loaders[$class]);
+ }
+ }
+
+ /**
+ * Unregister a single autoloader by class name
+ *
+ * @param string $autoloaderClass
+ * @return bool
+ */
+ public static function unregisterAutoloader($autoloaderClass)
+ {
+ if (!isset(self::$loaders[$autoloaderClass])) {
+ return false;
+ }
+
+ $autoloader = self::$loaders[$autoloaderClass];
+ spl_autoload_unregister(array($autoloader, 'autoload'));
+ unset(self::$loaders[$autoloaderClass]);
+ return true;
+ }
+
+ /**
+ * Get an instance of the standard autoloader
+ *
+ * Used to attempt to resolve autoloader classes, using the
+ * StandardAutoloader. The instance is marked as a fallback autoloader, to
+ * allow resolving autoloaders not under the "Zend" or "Zend" namespaces.
+ *
+ * @return Zend_Loader_SplAutoloader
+ */
+ protected static function getStandardAutoloader()
+ {
+ if (null !== self::$standardAutoloader) {
+ return self::$standardAutoloader;
+ }
+
+ // Extract the filename from the classname
+ $stdAutoloader = substr(strrchr(self::STANDARD_AUTOLOADER, '_'), 1);
+
+ if (!class_exists(self::STANDARD_AUTOLOADER)) {
+ require_once dirname(__FILE__) . "/$stdAutoloader.php";
+ }
+ $loader = new Zend_Loader_StandardAutoloader();
+ self::$standardAutoloader = $loader;
+ return self::$standardAutoloader;
+ }
+}
diff --git a/3rdparty/Zend/Loader/ClassMapAutoloader.php b/3rdparty/Zend/Loader/ClassMapAutoloader.php
new file mode 100644
index 000000000..c1923ac5f
--- /dev/null
+++ b/3rdparty/Zend/Loader/ClassMapAutoloader.php
@@ -0,0 +1,248 @@
+setOptions($options);
+ }
+ }
+
+ /**
+ * Configure the autoloader
+ *
+ * Proxies to {@link registerAutoloadMaps()}.
+ *
+ * @param array|Traversable $options
+ * @return Zend_Loader_ClassMapAutoloader
+ */
+ public function setOptions($options)
+ {
+ $this->registerAutoloadMaps($options);
+ return $this;
+ }
+
+ /**
+ * Register an autoload map
+ *
+ * An autoload map may be either an associative array, or a file returning
+ * an associative array.
+ *
+ * An autoload map should be an associative array containing
+ * classname/file pairs.
+ *
+ * @param string|array $location
+ * @return Zend_Loader_ClassMapAutoloader
+ */
+ public function registerAutoloadMap($map)
+ {
+ if (is_string($map)) {
+ $location = $map;
+ if ($this === ($map = $this->loadMapFromFile($location))) {
+ return $this;
+ }
+ }
+
+ if (!is_array($map)) {
+ require_once dirname(__FILE__) . '/Exception/InvalidArgumentException.php';
+ throw new Zend_Loader_Exception_InvalidArgumentException('Map file provided does not return a map');
+ }
+
+ $this->map = array_merge($this->map, $map);
+
+ if (isset($location)) {
+ $this->mapsLoaded[] = $location;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Register many autoload maps at once
+ *
+ * @param array $locations
+ * @return Zend_Loader_ClassMapAutoloader
+ */
+ public function registerAutoloadMaps($locations)
+ {
+ if (!is_array($locations) && !($locations instanceof Traversable)) {
+ require_once dirname(__FILE__) . '/Exception/InvalidArgumentException.php';
+ throw new Zend_Loader_Exception_InvalidArgumentException('Map list must be an array or implement Traversable');
+ }
+ foreach ($locations as $location) {
+ $this->registerAutoloadMap($location);
+ }
+ return $this;
+ }
+
+ /**
+ * Retrieve current autoload map
+ *
+ * @return array
+ */
+ public function getAutoloadMap()
+ {
+ return $this->map;
+ }
+
+ /**
+ * Defined by Autoloadable
+ *
+ * @param string $class
+ * @return void
+ */
+ public function autoload($class)
+ {
+ if (isset($this->map[$class])) {
+ require_once $this->map[$class];
+ }
+ }
+
+ /**
+ * Register the autoloader with spl_autoload registry
+ *
+ * @return void
+ */
+ public function register()
+ {
+ if (version_compare(PHP_VERSION, '5.3.0', '>=')) {
+ spl_autoload_register(array($this, 'autoload'), true, true);
+ } else {
+ spl_autoload_register(array($this, 'autoload'), true);
+ }
+ }
+
+ /**
+ * Load a map from a file
+ *
+ * If the map has been previously loaded, returns the current instance;
+ * otherwise, returns whatever was returned by calling include() on the
+ * location.
+ *
+ * @param string $location
+ * @return Zend_Loader_ClassMapAutoloader|mixed
+ * @throws Zend_Loader_Exception_InvalidArgumentException for nonexistent locations
+ */
+ protected function loadMapFromFile($location)
+ {
+ if (!file_exists($location)) {
+ require_once dirname(__FILE__) . '/Exception/InvalidArgumentException.php';
+ throw new Zend_Loader_Exception_InvalidArgumentException('Map file provided does not exist');
+ }
+
+ if (!$path = self::realPharPath($location)) {
+ $path = realpath($location);
+ }
+
+ if (in_array($path, $this->mapsLoaded)) {
+ // Already loaded this map
+ return $this;
+ }
+
+ $map = include $path;
+
+ return $map;
+ }
+
+ /**
+ * Resolve the real_path() to a file within a phar.
+ *
+ * @see https://bugs.php.net/bug.php?id=52769
+ * @param string $path
+ * @return string
+ */
+ public static function realPharPath($path)
+ {
+ if (strpos($path, 'phar:///') !== 0) {
+ return;
+ }
+
+ $parts = explode('/', str_replace(array('/','\\'), '/', substr($path, 8)));
+ $parts = array_values(array_filter($parts, array(__CLASS__, 'concatPharParts')));
+
+ array_walk($parts, array(__CLASS__, 'resolvePharParentPath'), $parts);
+
+ if (file_exists($realPath = 'phar:///' . implode('/', $parts))) {
+ return $realPath;
+ }
+ }
+
+ /**
+ * Helper callback for filtering phar paths
+ *
+ * @param string $part
+ * @return bool
+ */
+ public static function concatPharParts($part)
+ {
+ return ($part !== '' && $part !== '.');
+ }
+
+ /**
+ * Helper callback to resolve a parent path in a Phar archive
+ *
+ * @param string $value
+ * @param int $key
+ * @param array $parts
+ * @return void
+ */
+ public static function resolvePharParentPath($value, $key, &$parts)
+ {
+ if ($value !== '...') {
+ return;
+ }
+ unset($parts[$key], $parts[$key-1]);
+ $parts = array_values($parts);
+ }
+}
diff --git a/3rdparty/Zend/Loader/Exception.php b/3rdparty/Zend/Loader/Exception.php
new file mode 100644
index 000000000..394788bcb
--- /dev/null
+++ b/3rdparty/Zend/Loader/Exception.php
@@ -0,0 +1,35 @@
+_useStaticRegistry = $staticRegistryName;
+ if(!isset(self::$_staticPrefixToPaths[$staticRegistryName])) {
+ self::$_staticPrefixToPaths[$staticRegistryName] = array();
+ }
+ if(!isset(self::$_staticLoadedPlugins[$staticRegistryName])) {
+ self::$_staticLoadedPlugins[$staticRegistryName] = array();
+ }
+ }
+
+ foreach ($prefixToPaths as $prefix => $path) {
+ $this->addPrefixPath($prefix, $path);
+ }
+ }
+
+ /**
+ * Format prefix for internal use
+ *
+ * @param string $prefix
+ * @return string
+ */
+ protected function _formatPrefix($prefix)
+ {
+ if($prefix == "") {
+ return $prefix;
+ }
+
+ $nsSeparator = (false !== strpos($prefix, '\\'))?'\\':'_';
+ $prefix = rtrim($prefix, $nsSeparator) . $nsSeparator;
+ //if $nsSeprator == "\" and the prefix ends in "_\" remove trailing \
+ //https://github.com/zendframework/zf1/issues/152
+ if(($nsSeparator == "\\") && (substr($prefix,-2) == "_\\")) {
+ $prefix = substr($prefix, 0, -1);
+ }
+ return $prefix;
+ }
+
+ /**
+ * Add prefixed paths to the registry of paths
+ *
+ * @param string $prefix
+ * @param string $path
+ * @return Zend_Loader_PluginLoader
+ */
+ public function addPrefixPath($prefix, $path)
+ {
+ if (!is_string($prefix) || !is_string($path)) {
+ require_once 'Zend/Loader/PluginLoader/Exception.php';
+ throw new Zend_Loader_PluginLoader_Exception('Zend_Loader_PluginLoader::addPrefixPath() method only takes strings for prefix and path.');
+ }
+
+ $prefix = $this->_formatPrefix($prefix);
+ $path = rtrim($path, '/\\') . '/';
+
+ if ($this->_useStaticRegistry) {
+ self::$_staticPrefixToPaths[$this->_useStaticRegistry][$prefix][] = $path;
+ } else {
+ if (!isset($this->_prefixToPaths[$prefix])) {
+ $this->_prefixToPaths[$prefix] = array();
+ }
+ if (!in_array($path, $this->_prefixToPaths[$prefix])) {
+ $this->_prefixToPaths[$prefix][] = $path;
+ }
+ }
+ return $this;
+ }
+
+ /**
+ * Get path stack
+ *
+ * @param string $prefix
+ * @return false|array False if prefix does not exist, array otherwise
+ */
+ public function getPaths($prefix = null)
+ {
+ if ((null !== $prefix) && is_string($prefix)) {
+ $prefix = $this->_formatPrefix($prefix);
+ if ($this->_useStaticRegistry) {
+ if (isset(self::$_staticPrefixToPaths[$this->_useStaticRegistry][$prefix])) {
+ return self::$_staticPrefixToPaths[$this->_useStaticRegistry][$prefix];
+ }
+
+ return false;
+ }
+
+ if (isset($this->_prefixToPaths[$prefix])) {
+ return $this->_prefixToPaths[$prefix];
+ }
+
+ return false;
+ }
+
+ if ($this->_useStaticRegistry) {
+ return self::$_staticPrefixToPaths[$this->_useStaticRegistry];
+ }
+
+ return $this->_prefixToPaths;
+ }
+
+ /**
+ * Clear path stack
+ *
+ * @param string $prefix
+ * @return bool False only if $prefix does not exist
+ */
+ public function clearPaths($prefix = null)
+ {
+ if ((null !== $prefix) && is_string($prefix)) {
+ $prefix = $this->_formatPrefix($prefix);
+ if ($this->_useStaticRegistry) {
+ if (isset(self::$_staticPrefixToPaths[$this->_useStaticRegistry][$prefix])) {
+ unset(self::$_staticPrefixToPaths[$this->_useStaticRegistry][$prefix]);
+ return true;
+ }
+
+ return false;
+ }
+
+ if (isset($this->_prefixToPaths[$prefix])) {
+ unset($this->_prefixToPaths[$prefix]);
+ return true;
+ }
+
+ return false;
+ }
+
+ if ($this->_useStaticRegistry) {
+ self::$_staticPrefixToPaths[$this->_useStaticRegistry] = array();
+ } else {
+ $this->_prefixToPaths = array();
+ }
+
+ return true;
+ }
+
+ /**
+ * Remove a prefix (or prefixed-path) from the registry
+ *
+ * @param string $prefix
+ * @param string $path OPTIONAL
+ * @return Zend_Loader_PluginLoader
+ */
+ public function removePrefixPath($prefix, $path = null)
+ {
+ $prefix = $this->_formatPrefix($prefix);
+ if ($this->_useStaticRegistry) {
+ $registry =& self::$_staticPrefixToPaths[$this->_useStaticRegistry];
+ } else {
+ $registry =& $this->_prefixToPaths;
+ }
+
+ if (!isset($registry[$prefix])) {
+ require_once 'Zend/Loader/PluginLoader/Exception.php';
+ throw new Zend_Loader_PluginLoader_Exception('Prefix ' . $prefix . ' was not found in the PluginLoader.');
+ }
+
+ if ($path != null) {
+ $pos = array_search($path, $registry[$prefix]);
+ if (false === $pos) {
+ require_once 'Zend/Loader/PluginLoader/Exception.php';
+ throw new Zend_Loader_PluginLoader_Exception('Prefix ' . $prefix . ' / Path ' . $path . ' was not found in the PluginLoader.');
+ }
+ unset($registry[$prefix][$pos]);
+ } else {
+ unset($registry[$prefix]);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Normalize plugin name
+ *
+ * @param string $name
+ * @return string
+ */
+ protected function _formatName($name)
+ {
+ return ucfirst((string) $name);
+ }
+
+ /**
+ * Whether or not a Plugin by a specific name is loaded
+ *
+ * @param string $name
+ * @return Zend_Loader_PluginLoader
+ */
+ public function isLoaded($name)
+ {
+ $name = $this->_formatName($name);
+ if ($this->_useStaticRegistry) {
+ return isset(self::$_staticLoadedPlugins[$this->_useStaticRegistry][$name]);
+ }
+
+ return isset($this->_loadedPlugins[$name]);
+ }
+
+ /**
+ * Return full class name for a named plugin
+ *
+ * @param string $name
+ * @return string|false False if class not found, class name otherwise
+ */
+ public function getClassName($name)
+ {
+ $name = $this->_formatName($name);
+ if ($this->_useStaticRegistry
+ && isset(self::$_staticLoadedPlugins[$this->_useStaticRegistry][$name])
+ ) {
+ return self::$_staticLoadedPlugins[$this->_useStaticRegistry][$name];
+ } elseif (isset($this->_loadedPlugins[$name])) {
+ return $this->_loadedPlugins[$name];
+ }
+
+ return false;
+ }
+
+ /**
+ * Get path to plugin class
+ *
+ * @param mixed $name
+ * @return string|false False if not found
+ */
+ public function getClassPath($name)
+ {
+ $name = $this->_formatName($name);
+ if ($this->_useStaticRegistry
+ && !empty(self::$_staticLoadedPluginPaths[$this->_useStaticRegistry][$name])
+ ) {
+ return self::$_staticLoadedPluginPaths[$this->_useStaticRegistry][$name];
+ } elseif (!empty($this->_loadedPluginPaths[$name])) {
+ return $this->_loadedPluginPaths[$name];
+ }
+
+ if ($this->isLoaded($name)) {
+ $class = $this->getClassName($name);
+ $r = new ReflectionClass($class);
+ $path = $r->getFileName();
+ if ($this->_useStaticRegistry) {
+ self::$_staticLoadedPluginPaths[$this->_useStaticRegistry][$name] = $path;
+ } else {
+ $this->_loadedPluginPaths[$name] = $path;
+ }
+ return $path;
+ }
+
+ return false;
+ }
+
+ /**
+ * Load a plugin via the name provided
+ *
+ * @param string $name
+ * @param bool $throwExceptions Whether or not to throw exceptions if the
+ * class is not resolved
+ * @return string|false Class name of loaded class; false if $throwExceptions
+ * if false and no class found
+ * @throws Zend_Loader_Exception if class not found
+ */
+ public function load($name, $throwExceptions = true)
+ {
+ $name = $this->_formatName($name);
+ if ($this->isLoaded($name)) {
+ return $this->getClassName($name);
+ }
+
+ if ($this->_useStaticRegistry) {
+ $registry = self::$_staticPrefixToPaths[$this->_useStaticRegistry];
+ } else {
+ $registry = $this->_prefixToPaths;
+ }
+
+ $registry = array_reverse($registry, true);
+ $found = false;
+ if (false !== strpos($name, '\\')) {
+ $classFile = str_replace('\\', DIRECTORY_SEPARATOR, $name) . '.php';
+ } else {
+ $classFile = str_replace('_', DIRECTORY_SEPARATOR, $name) . '.php';
+ }
+ $incFile = self::getIncludeFileCache();
+ foreach ($registry as $prefix => $paths) {
+ $className = $prefix . $name;
+
+ if (class_exists($className, false)) {
+ $found = true;
+ break;
+ }
+
+ $paths = array_reverse($paths, true);
+
+ foreach ($paths as $path) {
+ $loadFile = $path . $classFile;
+ if (Zend_Loader::isReadable($loadFile)) {
+ include_once $loadFile;
+ if (class_exists($className, false)) {
+ if (null !== $incFile) {
+ self::_appendIncFile($loadFile);
+ }
+ $found = true;
+ break 2;
+ }
+ }
+ }
+ }
+
+ if (!$found) {
+ if (!$throwExceptions) {
+ return false;
+ }
+
+ $message = "Plugin by name '$name' was not found in the registry; used paths:";
+ foreach ($registry as $prefix => $paths) {
+ $message .= "\n$prefix: " . implode(PATH_SEPARATOR, $paths);
+ }
+ require_once 'Zend/Loader/PluginLoader/Exception.php';
+ throw new Zend_Loader_PluginLoader_Exception($message);
+ }
+
+ if ($this->_useStaticRegistry) {
+ self::$_staticLoadedPlugins[$this->_useStaticRegistry][$name] = $className;
+ } else {
+ $this->_loadedPlugins[$name] = $className;
+ }
+ return $className;
+ }
+
+ /**
+ * Set path to class file cache
+ *
+ * Specify a path to a file that will add include_once statements for each
+ * plugin class loaded. This is an opt-in feature for performance purposes.
+ *
+ * @param string $file
+ * @return void
+ * @throws Zend_Loader_PluginLoader_Exception if file is not writeable or path does not exist
+ */
+ public static function setIncludeFileCache($file)
+ {
+ if (!empty(self::$_includeFileCacheHandler)) {
+ flock(self::$_includeFileCacheHandler, LOCK_UN);
+ fclose(self::$_includeFileCacheHandler);
+ }
+
+ self::$_includeFileCacheHandler = null;
+
+ if (null === $file) {
+ self::$_includeFileCache = null;
+ return;
+ }
+
+ if (!file_exists($file) && !file_exists(dirname($file))) {
+ require_once 'Zend/Loader/PluginLoader/Exception.php';
+ throw new Zend_Loader_PluginLoader_Exception('Specified file does not exist and/or directory does not exist (' . $file . ')');
+ }
+ if (file_exists($file) && !is_writable($file)) {
+ require_once 'Zend/Loader/PluginLoader/Exception.php';
+ throw new Zend_Loader_PluginLoader_Exception('Specified file is not writeable (' . $file . ')');
+ }
+ if (!file_exists($file) && file_exists(dirname($file)) && !is_writable(dirname($file))) {
+ require_once 'Zend/Loader/PluginLoader/Exception.php';
+ throw new Zend_Loader_PluginLoader_Exception('Specified file is not writeable (' . $file . ')');
+ }
+
+ self::$_includeFileCache = $file;
+ }
+
+ /**
+ * Retrieve class file cache path
+ *
+ * @return string|null
+ */
+ public static function getIncludeFileCache()
+ {
+ return self::$_includeFileCache;
+ }
+
+ /**
+ * Append an include_once statement to the class file cache
+ *
+ * @param string $incFile
+ * @return void
+ */
+ protected static function _appendIncFile($incFile)
+ {
+ if (!isset(self::$_includeFileCacheHandler)) {
+ self::$_includeFileCacheHandler = fopen(self::$_includeFileCache, 'ab');
+
+ if (!flock(self::$_includeFileCacheHandler, LOCK_EX | LOCK_NB, $wouldBlock) || $wouldBlock) {
+ self::$_includeFileCacheHandler = false;
+ }
+ }
+
+ if (false !== self::$_includeFileCacheHandler) {
+ $line = "\n";
+ fwrite(self::$_includeFileCacheHandler, $line, strlen($line));
+ }
+ }
+}
diff --git a/3rdparty/Zend/Loader/PluginLoader/Exception.php b/3rdparty/Zend/Loader/PluginLoader/Exception.php
new file mode 100644
index 000000000..50e9d8c2f
--- /dev/null
+++ b/3rdparty/Zend/Loader/PluginLoader/Exception.php
@@ -0,0 +1,39 @@
+
+ * spl_autoload_register(array($this, 'autoload'));
+ *
+ *
+ * @return void
+ */
+ public function register();
+}
diff --git a/3rdparty/Zend/Loader/StandardAutoloader.php b/3rdparty/Zend/Loader/StandardAutoloader.php
new file mode 100644
index 000000000..31a5c1e45
--- /dev/null
+++ b/3rdparty/Zend/Loader/StandardAutoloader.php
@@ -0,0 +1,368 @@
+setOptions($options);
+ }
+ }
+
+ /**
+ * Configure autoloader
+ *
+ * Allows specifying both "namespace" and "prefix" pairs, using the
+ * following structure:
+ *
+ * array(
+ * 'namespaces' => array(
+ * 'Zend' => '/path/to/Zend/library',
+ * 'Doctrine' => '/path/to/Doctrine/library',
+ * ),
+ * 'prefixes' => array(
+ * 'Phly_' => '/path/to/Phly/library',
+ * ),
+ * 'fallback_autoloader' => true,
+ * )
+ *
+ *
+ * @param array|Traversable $options
+ * @return Zend_Loader_StandardAutoloader
+ */
+ public function setOptions($options)
+ {
+ if (!is_array($options) && !($options instanceof Traversable)) {
+ require_once dirname(__FILE__) . '/Exception/InvalidArgumentException.php';
+ throw new Zend_Loader_Exception_InvalidArgumentException('Options must be either an array or Traversable');
+ }
+
+ foreach ($options as $type => $pairs) {
+ switch ($type) {
+ case self::AUTOREGISTER_ZF:
+ if ($pairs) {
+ $this->registerPrefix('Zend', dirname(dirname(__FILE__)));
+ }
+ break;
+ case self::LOAD_NS:
+ if (is_array($pairs) || $pairs instanceof Traversable) {
+ $this->registerNamespaces($pairs);
+ }
+ break;
+ case self::LOAD_PREFIX:
+ if (is_array($pairs) || $pairs instanceof Traversable) {
+ $this->registerPrefixes($pairs);
+ }
+ break;
+ case self::ACT_AS_FALLBACK:
+ $this->setFallbackAutoloader($pairs);
+ break;
+ default:
+ // ignore
+ }
+ }
+ return $this;
+ }
+
+ /**
+ * Set flag indicating fallback autoloader status
+ *
+ * @param bool $flag
+ * @return Zend_Loader_StandardAutoloader
+ */
+ public function setFallbackAutoloader($flag)
+ {
+ $this->fallbackAutoloaderFlag = (bool) $flag;
+ return $this;
+ }
+
+ /**
+ * Is this autoloader acting as a fallback autoloader?
+ *
+ * @return bool
+ */
+ public function isFallbackAutoloader()
+ {
+ return $this->fallbackAutoloaderFlag;
+ }
+
+ /**
+ * Register a namespace/directory pair
+ *
+ * @param string $namespace
+ * @param string $directory
+ * @return Zend_Loader_StandardAutoloader
+ */
+ public function registerNamespace($namespace, $directory)
+ {
+ $namespace = rtrim($namespace, self::NS_SEPARATOR). self::NS_SEPARATOR;
+ $this->namespaces[$namespace] = $this->normalizeDirectory($directory);
+ return $this;
+ }
+
+ /**
+ * Register many namespace/directory pairs at once
+ *
+ * @param array $namespaces
+ * @return Zend_Loader_StandardAutoloader
+ */
+ public function registerNamespaces($namespaces)
+ {
+ if (!is_array($namespaces) && !$namespaces instanceof Traversable) {
+ require_once dirname(__FILE__) . '/Exception/InvalidArgumentException.php';
+ throw new Zend_Loader_Exception_InvalidArgumentException('Namespace pairs must be either an array or Traversable');
+ }
+
+ foreach ($namespaces as $namespace => $directory) {
+ $this->registerNamespace($namespace, $directory);
+ }
+ return $this;
+ }
+
+ /**
+ * Register a prefix/directory pair
+ *
+ * @param string $prefix
+ * @param string $directory
+ * @return Zend_Loader_StandardAutoloader
+ */
+ public function registerPrefix($prefix, $directory)
+ {
+ $prefix = rtrim($prefix, self::PREFIX_SEPARATOR). self::PREFIX_SEPARATOR;
+ $this->prefixes[$prefix] = $this->normalizeDirectory($directory);
+ return $this;
+ }
+
+ /**
+ * Register many namespace/directory pairs at once
+ *
+ * @param array $prefixes
+ * @return Zend_Loader_StandardAutoloader
+ */
+ public function registerPrefixes($prefixes)
+ {
+ if (!is_array($prefixes) && !$prefixes instanceof Traversable) {
+ require_once dirname(__FILE__) . '/Exception/InvalidArgumentException.php';
+ throw new Zend_Loader_Exception_InvalidArgumentException('Prefix pairs must be either an array or Traversable');
+ }
+
+ foreach ($prefixes as $prefix => $directory) {
+ $this->registerPrefix($prefix, $directory);
+ }
+ return $this;
+ }
+
+ /**
+ * Defined by Autoloadable; autoload a class
+ *
+ * @param string $class
+ * @return false|string
+ */
+ public function autoload($class)
+ {
+ $isFallback = $this->isFallbackAutoloader();
+ if (false !== strpos($class, self::NS_SEPARATOR)) {
+ if ($this->loadClass($class, self::LOAD_NS)) {
+ return $class;
+ } elseif ($isFallback) {
+ return $this->loadClass($class, self::ACT_AS_FALLBACK);
+ }
+ return false;
+ }
+ if (false !== strpos($class, self::PREFIX_SEPARATOR)) {
+ if ($this->loadClass($class, self::LOAD_PREFIX)) {
+ return $class;
+ } elseif ($isFallback) {
+ return $this->loadClass($class, self::ACT_AS_FALLBACK);
+ }
+ return false;
+ }
+ if ($isFallback) {
+ return $this->loadClass($class, self::ACT_AS_FALLBACK);
+ }
+ return false;
+ }
+
+ /**
+ * Register the autoloader with spl_autoload
+ *
+ * @return void
+ */
+ public function register()
+ {
+ spl_autoload_register(array($this, 'autoload'));
+ }
+
+ /**
+ * Error handler
+ *
+ * Used by {@link loadClass} during fallback autoloading in PHP versions
+ * prior to 5.3.0.
+ *
+ * @param mixed $errno
+ * @param mixed $errstr
+ * @return void
+ */
+ public function handleError($errno, $errstr)
+ {
+ $this->error = true;
+ }
+
+ /**
+ * Transform the class name to a filename
+ *
+ * @param string $class
+ * @param string $directory
+ * @return string
+ */
+ protected function transformClassNameToFilename($class, $directory)
+ {
+ // $class may contain a namespace portion, in which case we need
+ // to preserve any underscores in that portion.
+ $matches = array();
+ preg_match('/(?P.+\\\)?(?P[^\\\]+$)/', $class, $matches);
+
+ $class = (isset($matches['class'])) ? $matches['class'] : '';
+ $namespace = (isset($matches['namespace'])) ? $matches['namespace'] : '';
+
+ return $directory
+ . str_replace(self::NS_SEPARATOR, '/', $namespace)
+ . str_replace(self::PREFIX_SEPARATOR, '/', $class)
+ . '.php';
+ }
+
+ /**
+ * Load a class, based on its type (namespaced or prefixed)
+ *
+ * @param string $class
+ * @param string $type
+ * @return void
+ */
+ protected function loadClass($class, $type)
+ {
+ if (!in_array($type, array(self::LOAD_NS, self::LOAD_PREFIX, self::ACT_AS_FALLBACK))) {
+ require_once dirname(__FILE__) . '/Exception/InvalidArgumentException.php';
+ throw new Zend_Loader_Exception_InvalidArgumentException();
+ }
+
+ // Fallback autoloading
+ if ($type === self::ACT_AS_FALLBACK) {
+ // create filename
+ $filename = $this->transformClassNameToFilename($class, '');
+ if (version_compare(PHP_VERSION, '5.3.2', '>=')) {
+ $resolvedName = stream_resolve_include_path($filename);
+ if ($resolvedName !== false) {
+ return include $resolvedName;
+ }
+ return false;
+ }
+ $this->error = false;
+ set_error_handler(array($this, 'handleError'), E_WARNING);
+ include $filename;
+ restore_error_handler();
+ if ($this->error) {
+ return false;
+ }
+ return class_exists($class, false);
+ }
+
+ // Namespace and/or prefix autoloading
+ foreach ($this->$type as $leader => $path) {
+ if (0 === strpos($class, $leader)) {
+ // Trim off leader (namespace or prefix)
+ $trimmedClass = substr($class, strlen($leader));
+
+ // create filename
+ $filename = $this->transformClassNameToFilename($trimmedClass, $path);
+ if (file_exists($filename)) {
+ return include $filename;
+ }
+ return false;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Normalize the directory to include a trailing directory separator
+ *
+ * @param string $directory
+ * @return string
+ */
+ protected function normalizeDirectory($directory)
+ {
+ $last = $directory[strlen($directory) - 1];
+ if (in_array($last, array('/', '\\'))) {
+ $directory[strlen($directory) - 1] = DIRECTORY_SEPARATOR;
+ return $directory;
+ }
+ $directory .= DIRECTORY_SEPARATOR;
+ return $directory;
+ }
+
+}
diff --git a/COPYING b/COPYING
new file mode 100644
index 000000000..2def0e883
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,661 @@
+ GNU AFFERO GENERAL PUBLIC LICENSE
+ Version 3, 19 November 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+our General Public Licenses are intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
+
+ A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate. Many developers of free software are heartened and
+encouraged by the resulting cooperation. However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
+
+ The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community. It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server. Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
+
+ An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals. This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing under
+this license.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU Affero General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Remote Network Interaction; Use with the GNU General Public License.
+
+ Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your version
+supports such interaction) an opportunity to receive the Corresponding
+Source of your version by providing access to the Corresponding Source
+from a network server at no charge, through some standard or customary
+means of facilitating copying of software. This Corresponding Source
+shall include the Corresponding Source for any work covered by version 3
+of the GNU General Public License that is incorporated pursuant to the
+following paragraph.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the work with which it is combined will remain governed by version
+3 of the GNU General Public License.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU Affero General Public License from time to time. Such new versions
+will be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU Affero General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU Affero General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU Affero General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If your software can interact with users remotely through a computer
+network, you should also make sure that it provides a way for users to
+get its source. For example, if your program is a web application, its
+interface could display a "Source" link that leads users to an archive
+of the code. There are many ways you could offer source, and different
+solutions will be better for different programs; see section 13 for the
+specific requirements.
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU AGPL, see
+.
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 000000000..ade6dc420
--- /dev/null
+++ b/README.md
@@ -0,0 +1,23 @@
+# Search Lucene
+
+[](http://travis-ci.org/owncloud/search_lucene)
+[](https://scrutinizer-ci.com/g/owncloud/search_lucene/)
+[](https://scrutinizer-ci.com/g/owncloud/search_lucene/)
+
+The Search Lucene app adds a full text search for files stored in ownCloud. It is based on
+[Zend Search Lucene](http://framework.zend.com/manual/1.12/en/zend.search.lucene.html) and
+can index content from plain text, .docx, .xlsx, .pptx, .odt, .ods and .pdf files. The source
+code is [available on GitHub](https://github.com/owncloud/search_lucene)
+
+# Maintainers
+
+Maintainers wanted for additional features!
+
+* [Jörn Friedrich Dreyer](https://github.com/butonic)
+
+# Known limitations
+
+* Does not work with Encryption: the background indexing process does not have access to the
+ key needed to decrypt files when the user is not logged in.
+* Does not index files in external storage. For performance reasons.
+* Not all PDF versions can be indexed. The text extraction used for it is incompatible with newer PDF versions.
diff --git a/ajax/lucene.php b/ajax/lucene.php
deleted file mode 100644
index 9d33bd044..000000000
--- a/ajax/lucene.php
+++ /dev/null
@@ -1,84 +0,0 @@
-send('count', count($fileIds));
-
- $skippedDirs = explode(';', OCP\Config::getUserValue(OCP\User::getUser(), 'search_lucene', 'skipped_dirs', '.git;.svn;.CVS;.bzr'));
-
- foreach ($fileIds as $id) {
- $skipped = false;
-
- $fileStatus = OCA\Search_Lucene\Status::fromFileId($id);
-
- try{
- //before we start mark the file as error so we know there was a problem when the php execution dies
- $fileStatus->markError();
-
- $path = OC\Files\Filesystem::getPath($id);
- $eventSource->send('indexing', $path);
-
- foreach ($skippedDirs as $skippedDir) {
- if (strpos($path, '/' . $skippedDir . '/') !== false //contains dir
- || strrpos($path, '/' . $skippedDir) === strlen($path) - (strlen($skippedDir) + 1) // ends with dir
- ) {
- $result = $fileStatus->markSkipped();
- $skipped = true;
- break;
- }
- }
- if (!$skipped) {
- if (OCA\Search_Lucene\Indexer::indexFile($path, OCP\User::getUser())) {
- $result = $fileStatus->markIndexed();
- }
- }
-
- if (!$result) {
- OCP\JSON::error(array('message' => 'Could not index file.'));
- $eventSource->send('error', $path);
- }
- } catch (Exception $e) { //sqlite might report database locked errors when stock filescan is in progress
- //this also catches db locked exception that might come up when using sqlite
- \OCP\Util::writeLog('search_lucene',
- $e->getMessage() . ' Trace:\n' . $e->getTraceAsString(),
- \OCP\Util::ERROR);
- OCP\JSON::error(array('message' => 'Could not index file.'));
- $eventSource->send('error', $e->getMessage());
- //try to mark the file as new to let it reindex
- $fileStatus->markNew(); // Add UI to trigger rescan of files with status 'E'rror?
- }
- }
-
- $eventSource->send('done', '');
- $eventSource->close();
-}
-
-function handleOptimize() {
- OCA\Search_Lucene\Lucene::optimizeIndex();
-}
-
-if ($_GET['operation']) {
- switch ($_GET['operation']) {
- case 'index':
- index();
- break;
- case 'optimize':
- handleOptimize();
- break;
- default:
- OCP\JSON::error(array('cause' => 'Unknown operation'));
- }
-}
diff --git a/appinfo/app.php b/appinfo/app.php
index b186f6146..d63dbcccb 100644
--- a/appinfo/app.php
+++ b/appinfo/app.php
@@ -1,58 +1,13 @@
-* @copyright 2012 Jörn Dreyer
-* @license http://www.gnu.org/licenses/agpl-3.0 GNU Affero General Public License (AGPL) 3.0
-*
-* This library is free software; you can redistribute it and/or
-* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
-* License as published by the Free Software Foundation; either
-* version 3 of the License, or any later version.
-*
-* This library is distributed in the hope that it will be useful,
-* but WITHOUT ANY WARRANTY; without even the implied warranty of
-* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-* GNU AFFERO GENERAL PUBLIC LICENSE for more details.
-*
-* You should have received a copy of the GNU Lesser General Public
-* License along with this library. If not, see .
-*
-*/
-
-// --- Register classes -----------------------------------------------
-
-//add 3rdparty folder to include path
-$dir = dirname(dirname(__FILE__)).'/3rdparty';
-set_include_path(get_include_path() . PATH_SEPARATOR . $dir);
-
-OC::$CLASSPATH['OCA\Search_Lucene\Lucene'] = 'search_lucene/lib/lucene.php';
-OC::$CLASSPATH['OCA\Search_Lucene\Indexer'] = 'search_lucene/lib/indexer.php';
-OC::$CLASSPATH['OCA\Search_Lucene\Hooks'] = 'search_lucene/lib/hooks.php';
-
-OC::$CLASSPATH['OCA\Search_Lucene\Document\Pdf'] = 'search_lucene/document/Pdf.php';
-OC::$CLASSPATH['OCA\Search_Lucene\Document\OpenDocument'] = 'search_lucene/document/OpenDocument.php';
-OC::$CLASSPATH['OCA\Search_Lucene\Document\Odt'] = 'search_lucene/document/Odt.php';
-OC::$CLASSPATH['OCA\Search_Lucene\Document\Ods'] = 'search_lucene/document/Ods.php';
-
-OC::$CLASSPATH['Zend_Search_Lucene'] = 'search_lucene/3rdparty/Zend/Search/Lucene.php';
-OC::$CLASSPATH['Zend_Search_Lucene_Index_Term'] = 'search_lucene/3rdparty/Zend/Search/Lucene/Index/Term.php';
-OC::$CLASSPATH['Zend_Search_Lucene_Search_Query_Term'] = 'search_lucene/3rdparty/Zend/Search/Lucene/Search/Query/Term.php';
-OC::$CLASSPATH['Zend_Search_Lucene_Field'] = 'search_lucene/3rdparty/Zend/Search/Lucene/Field.php';
-OC::$CLASSPATH['Zend_Search_Lucene_Document'] = 'search_lucene/3rdparty/Zend/Search/Lucene/Document.php';
-OC::$CLASSPATH['Zend_Search_Lucene_Document_Html'] = 'search_lucene/3rdparty/Zend/Search/Lucene/Document/Html.php';
-OC::$CLASSPATH['Zend_Search_Lucene_Document_Docx'] = 'search_lucene/3rdparty/Zend/Search/Lucene/Document/Docx.php';
-OC::$CLASSPATH['Zend_Search_Lucene_Document_Xlsx'] = 'search_lucene/3rdparty/Zend/Search/Lucene/Document/Xlsx.php';
-OC::$CLASSPATH['Zend_Search_Lucene_Document_Pptx'] = 'search_lucene/3rdparty/Zend/Search/Lucene/Document/Pptx.php';
-OC::$CLASSPATH['Zend_Search_Lucene_Analysis_Analyzer'] = 'search_lucene/3rdparty/Zend/Search/Lucene/Analysis/Analyzer.php';
-
-OC::$CLASSPATH['getID3'] = 'getid3/getid3.php';
-
-OC::$CLASSPATH['App_Search_Helper_PdfParser'] = 'search_lucene/3rdparty/pdf2text.php';
-
-OC::$CLASSPATH['Zend_Pdf'] = 'search_lucene/3rdparty/Zend/Pdf.php';
+ * ownCloud - search_lucene
+ *
+ * This file is licensed under the Affero General Public License version 3 or
+ * later. See the COPYING file.
+ *
+ * @author Jörn Friedrich Dreyer
+ * @copyright Jörn Friedrich Dreyer 2012-2014
+ */
// --- always add js & css -----------------------------------------------
@@ -62,8 +17,16 @@
// --- replace default file search provider -----------------------------------------------
//remove other providers
-OC_Search::removeProvider('OC_Search_Provider_File');
-OC_Search::registerProvider('OCA\Search_Lucene\Lucene');
+\OC::$server->getSearch()->registerProvider('OCA\Search_Lucene\Search\LuceneProvider');
+
+// add background job for index optimization:
+
+$arguments = array('user' => \OCP\User::getUser());
+
+//only when we know for which user:
+if ($arguments['user']) {
+ \OCP\BackgroundJob::registerJob( 'OCA\Search_Lucene\Jobs\OptimizeJob', $arguments );
+}
// --- add hooks -----------------------------------------------
@@ -73,19 +36,19 @@
OCP\Util::connectHook(
OC\Files\Filesystem::CLASSNAME,
OC\Files\Filesystem::signal_post_write,
- 'OCA\Search_Lucene\Hooks',
- OCA\Search_Lucene\Hooks::handle_post_write);
+ 'OCA\Search_Lucene\Hooks\Files',
+ OCA\Search_Lucene\Hooks\Files::handle_post_write);
//connect to the filesystem for renaming
OCP\Util::connectHook(
OC\Files\Filesystem::CLASSNAME,
OC\Files\Filesystem::signal_post_rename,
- 'OCA\Search_Lucene\Hooks',
- OCA\Search_Lucene\Hooks::handle_post_rename);
+ 'OCA\Search_Lucene\Hooks\Files',
+ OCA\Search_Lucene\Hooks\Files::handle_post_rename);
//listen for file deletions to clean the database
OCP\Util::connectHook(
OC\Files\Filesystem::CLASSNAME,
- OC\Files\Filesystem::signal_delete,
- 'OCA\Search_Lucene\Hooks',
- OCA\Search_Lucene\Hooks::handle_delete);
+ 'post_delete', //FIXME add referenceable constant in core
+ 'OCA\Search_Lucene\Hooks\Files',
+ OCA\Search_Lucene\Hooks\Files::handle_delete);
diff --git a/appinfo/application.php b/appinfo/application.php
new file mode 100644
index 000000000..d5e78953e
--- /dev/null
+++ b/appinfo/application.php
@@ -0,0 +1,123 @@
+
+ * @copyright Jörn Friedrich Dreyer 2012-2014
+ */
+
+namespace OCA\Search_Lucene\AppInfo;
+
+use OCA\Search_Lucene\Controller\ApiController;
+use OCA\Search_Lucene\Core\Db;
+use OCA\Search_Lucene\Core\Logger;
+use OCA\Search_Lucene\Db\StatusMapper;
+use OCA\Search_Lucene\Lucene\Index;
+use OCA\Search_Lucene\Lucene\Indexer;
+use OCA\Search_Lucene\Core\Files;
+use OCP\AppFramework\App;
+
+//add 3rdparty folder to include path
+set_include_path(get_include_path() . PATH_SEPARATOR . __DIR__.'/../3rdparty');
+
+class Application extends App {
+
+ public function __construct (array $urlParams=array()) {
+ parent::__construct('search_lucene', $urlParams);
+
+ $container = $this->getContainer();
+
+ require_once __DIR__ . '/../3rdparty/Zend/Loader/Autoloader.php';
+ \Zend_Loader_Autoloader::getInstance();
+
+ /**
+ * Controller
+ */
+ $container->registerService('ApiController', function($c) {
+ return new ApiController(
+ $c->query('AppName'),
+ $c->query('Request'),
+ $c->query('StatusMapper'),
+ $c->query('Index'),
+ $c->query('Indexer')
+ );
+ });
+
+ /**
+ * Lucene
+ */
+ $container->registerService('Index', function($c) {
+ $index = new Index(
+ $c->query('FileUtility'),
+ $c->query('Logger')
+ );
+ $index->openOrCreate();
+
+ return $index;
+ });
+
+ $container->registerService('Indexer', function($c) {
+ return new Indexer(
+ $c->query('FileUtility'),
+ $c->query('ServerContainer'),
+ $c->query('Index'),
+ $c->query('SkippedDirs'),
+ $c->query('StatusMapper'),
+ $c->query('Logger')
+ );
+ });
+
+ $container->registerService('SkippedDirs', function($c) {
+ return explode(
+ ';',
+ \OCP\Config::getUserValue($c->query('UserId'), 'search_lucene', 'skipped_dirs', '.git;.svn;.CVS;.bzr')
+ );
+ });
+
+ /**
+ * Mappers
+ */
+ $container->registerService('StatusMapper', function($c) {
+ return new StatusMapper(
+ $c->query('Db'),
+ $c->query('Logger')
+ );
+ });
+
+ /**
+ * Core
+ */
+ $container->registerService('UserId', function($c) {
+ return $c->query('ServerContainer')->getUserSession()->getUser()->getUID();
+ });
+
+ $container->registerService('Logger', function($c) {
+ return new Logger(
+ $c->query('AppName'),
+ $c->query('ServerContainer')->getLogger()
+ );
+ });
+
+ $container->registerService('Db', function($c) {
+ return $c->query('ServerContainer')->getDb();
+ });
+
+ $container->registerService('FileUtility', function($c) {
+ return new Files(
+ $c->query('ServerContainer')->getUserManager(),
+ $c->query('ServerContainer')->getUserSession(),
+ $c->query('RootFolder')
+ );
+ });
+
+ $container->registerService('RootFolder', function($c) {
+ return $c->query('ServerContainer')->getRootFolder();
+ });
+
+ }
+
+
+}
\ No newline at end of file
diff --git a/appinfo/info.xml b/appinfo/info.xml
index d5e09f789..c71da992b 100644
--- a/appinfo/info.xml
+++ b/appinfo/info.xml
@@ -7,29 +7,36 @@
- 4.93
+ 7
true
Activate this app when you want to be
able to find files by searching their content.
The implementation does not search the files
- directly but will build a so called
- reverse index.
+ directly but will build a reverse index based on lucene.
+
Currently only your personal files will be indexed,
- and only you index is searched, so you will not be able
+ and only your index is searched, so you will not be able
to find files that have been shared with you or that
- reside on remote mount points.
- Every time a file has been written owncloud will
- index it in the background. Furthermore, when
+ reside on remote mount points.
+
+ Every time a file has been written ownCloud will
+ index it with a background job. Furthermore, when
clicking the search box it will check if any
- files need indexing. This might add a lot of
- background jobs after first enabling this app,
- depending on the amount of files you have.
+ files need indexing. It might take a while to build
+ the index after first enabling this app,
+ depending on the amount of files you have.
+
Even thought file indexing is in progress you will
still be able to retrieve file search results for any
- indexed files.
- We currenty support plain text, HTML and PDF files.
- MS Office 2007 and Open/Libre Office are on the roadmap.
+ indexed files.
+
+ We currently support text, latex, HTML, PDF, MS Office 2007
+ or newer and Open/Libre Office files.
+
+ Depending on the file size and version of PDF / office files
+ indexing may fail. This is a known issue and help is welcome
+ to get these problems reproduced and sorted out.
166057
diff --git a/appinfo/preupdate.php b/appinfo/preupdate.php
index 5808f4793..177f22033 100644
--- a/appinfo/preupdate.php
+++ b/appinfo/preupdate.php
@@ -1,36 +1,18 @@
+ * @copyright Jörn Friedrich Dreyer 2012-2014
+ */
$currentVersion=OCP\Config::getAppValue('search_lucene', 'installed_version');
-if (version_compare($currentVersion, '0.5.2', '<')) {
-
- //delete duplicate id entries
-
- $dbtype = OCP\Config::getSystemValue('dbtype', 'sqlite3');
-
- if ($dbtype === 'mysql') {
- // fix MySQL ERROR 1093 (HY000), see http://stackoverflow.com/a/12969601
- $sql = 'DELETE FROM `*PREFIX*lucene_status`
- WHERE `fileid` IN (
- SELECT `fileid` FROM (
- SELECT `fileid`
- FROM `*PREFIX*lucene_status`
- GROUP BY `fileid`
- HAVING count(`status`) > 1
- ) AS `mysqlerr1093hack`
- )
- ';
- } else {
- $sql = 'DELETE FROM `*PREFIX*lucene_status`
- WHERE `fileid` IN (
- SELECT `fileid`
- FROM `*PREFIX*lucene_status`
- GROUP BY `fileid`
- HAVING count(`status`) > 1
- )
- ';
- }
-
- $stmt = OCP\DB::prepare($sql);
+if (version_compare($currentVersion, '0.6.0', '<')) {
+ //force reindexing of files
+ $stmt = OCP\DB::prepare('DELETE FROM `*PREFIX*lucene_status`');
$stmt->execute();
}
\ No newline at end of file
diff --git a/appinfo/routes.php b/appinfo/routes.php
new file mode 100644
index 000000000..3a59412f1
--- /dev/null
+++ b/appinfo/routes.php
@@ -0,0 +1,27 @@
+
+ * @copyright Jörn Friedrich Dreyer 2014
+ */
+
+namespace OCA\Search_Lucene\AppInfo;
+
+/**
+ * Create your routes in here. The name is the lowercase name of the controller
+ * without the controller part, the stuff after the hash is the method.
+ * e.g. page#index -> PageController->index()
+ *
+ * The controller class has to be registered in the application.php file since
+ * it's instantiated in there
+ */
+$application = new Application();
+
+$application->registerRoutes($this, array('routes' => array(
+ array('name' => 'api#index', 'url' => '/indexer/index', 'verb' => 'GET'),
+ array('name' => 'api#optimize', 'url' => '/indexer/optimize', 'verb' => 'POST'),
+)));
diff --git a/appinfo/update.php b/appinfo/update.php
index 303355ca2..9488b0835 100644
--- a/appinfo/update.php
+++ b/appinfo/update.php
@@ -1,11 +1,17 @@
+ * @copyright Jörn Friedrich Dreyer 2012-2014
+ */
-$currentVersion=OCP\Config::getAppValue('search_lucene', 'installed_version');
+$currentVersion = OCP\Config::getAppValue('search_lucene', 'installed_version');
if (version_compare($currentVersion, '0.5.0', '<')) {
- //force reindexing of files
- $stmt = OCP\DB::prepare('DELETE FROM `*PREFIX*lucene_status` WHERE 1=1');
- $stmt->execute();
//clear old background jobs
$stmt = OCP\DB::prepare('DELETE FROM `*PREFIX*queuedtasks` WHERE `app`=?');
$stmt->execute(array('search_lucene'));
diff --git a/appinfo/version b/appinfo/version
index be14282b7..09a3acfa1 100644
--- a/appinfo/version
+++ b/appinfo/version
@@ -1 +1 @@
-0.5.3
+0.6.0
\ No newline at end of file
diff --git a/controller/apicontroller.php b/controller/apicontroller.php
new file mode 100644
index 000000000..7bd66985a
--- /dev/null
+++ b/controller/apicontroller.php
@@ -0,0 +1,69 @@
+
+ * @copyright Jörn Friedrich Dreyer 2012-2014
+ */
+
+namespace OCA\Search_Lucene\Controller;
+
+
+use OCA\Search_Lucene\Db\StatusMapper;
+use OCA\Search_Lucene\Lucene\Index;
+use OCA\Search_Lucene\Lucene\Indexer;
+use \OCP\IRequest;
+use \OCP\AppFramework\Controller;
+
+class ApiController extends Controller {
+
+ private $mapper;
+ private $index;
+ private $indexer;
+
+ public function __construct($appName, IRequest $request, StatusMapper $mapper, Index $index, Indexer $indexer) {
+ parent::__construct($appName, $request);
+ $this->mapper = $mapper;
+ $this->index = $index;
+ $this->indexer = $indexer;
+ }
+
+
+ /**
+ * index the given fileIds or, if not given, all unindexed files
+ * @NoAdminRequired
+ */
+ public function index($fileId) {
+ if ( isset($fileId) ){
+ $fileIds = array($fileId);
+ } else {
+ $fileIds = $this->mapper->getUnindexed();
+ }
+
+ //TODO use public api when available in \OCP\AppFramework\IApi
+ $eventSource = new \OC_EventSource();
+ $eventSource->send('count', count($fileIds));
+
+ $this->indexer->indexFiles($fileIds, $eventSource);
+
+ $eventSource->send('done', '');
+ $eventSource->close();
+
+ // end script execution to prevent app framework from sending headers after
+ // the eventsource is closed
+ exit();
+ }
+
+
+ /**
+ * Optimize the index
+ * @NoAdminRequired
+ */
+ public function optimize() {
+ $this->index->optimizeIndex();
+ }
+
+}
\ No newline at end of file
diff --git a/core/files.php b/core/files.php
new file mode 100644
index 000000000..42aaa809b
--- /dev/null
+++ b/core/files.php
@@ -0,0 +1,110 @@
+
+ * @copyright Jörn Friedrich Dreyer 2012-2014
+ */
+
+namespace OCA\Search_Lucene\Core;
+use OCP\Files\Folder;
+use OCP\IUserManager;
+use OCP\IUserSession;
+
+class Files {
+
+ /**
+ * @var \OCP\IUserManager
+ */
+ private $userManager;
+ /**
+ * @var \OCP\IUserSession
+ */
+ private $userSession;
+
+ /**
+ * @var \OC\Files\Node\Folder
+ */
+ private $rootFolder;
+
+ public function __construct(IUserManager $userManager, IUserSession $userSession, Folder $rootFolder){
+ $this->userManager = $userManager;
+ $this->userSession = $userSession;
+ $this->rootFolder = $rootFolder;
+ }
+ /**
+ * Returns a folder for the users 'files' folder
+ * Warning, this will tear down the current filesystem
+ *
+ * @param string $userId
+ * @return \OCP\Files\Folder
+ * @throws SetUpException
+ */
+ public function setUpUserFolder($userId = null) {
+
+ $userHome = $this->setUpUserHome($userId);
+
+ return $this->getOrCreateSubFolder($userHome, 'files');
+
+ }
+
+ /**
+ * @param string $userId
+ * @return null|\OCP\Files\Folder
+ * @throws SetUpException
+ */
+ public function setUpIndexFolder($userId = null) {
+ // TODO profile: encrypt the index on logout, decrypt on login
+ //return OCP\Files::getStorage('search_lucene');
+ // FIXME \OC::$server->getAppFolder() returns '/search'
+ //$indexFolder = \OC::$server->getAppFolder();
+
+ $userHome = $this->setUpUserHome($userId);
+
+ return $this->getOrCreateSubFolder($userHome, 'lucene_index');
+ }
+
+ /**
+ * @param string $userId
+ * @return null|\OCP\Files\Folder
+ * @throws SetUpException
+ */
+ public function setUpUserHome($userId = null) {
+
+ if (is_null($userId)) {
+ $user = $this->userSession->getUser();
+ } else {
+ $user = $this->userManager->get($userId);
+ }
+ if (is_null($user) || !$this->userManager->userExists($user->getUID())) {
+ throw new SetUpException('could not set up user home for '.json_encode($user));
+ }
+ //if ($user !== $this->userSession->getUser()) {
+ if ($user !== $this->userSession->getUser()) {
+ \OC_Util::tearDownFS();
+ $this->userSession->setUser($user);
+ }
+ \OC_Util::setupFS($user->getUID());
+
+ return $this->getOrCreateSubFolder($this->rootFolder, '/' . $user->getUID());
+
+ }
+
+ /**
+ * @param \OCP\Files\Folder $parent
+ * @param string $folderName
+ * @return null|\OCP\Files\Folder
+ * @throws SetUpException
+ */
+ private function getOrCreateSubFolder(Folder $parent, $folderName) {
+ if($parent->nodeExists($folderName)) {
+ return $parent->get($folderName);
+ } else {
+ return $parent->newFolder($folderName);
+ }
+ }
+
+}
diff --git a/core/logger.php b/core/logger.php
new file mode 100644
index 000000000..18623223e
--- /dev/null
+++ b/core/logger.php
@@ -0,0 +1,48 @@
+
+ * @copyright Jörn Friedrich Dreyer 2012-2014
+ */
+
+namespace OCA\Search_Lucene\Core;
+use OCP\ILogger;
+
+/**
+ * Class Logger
+ *
+ * inserts the app name when not set in context
+ *
+ * @package OCA\Search_Lucene\Core
+ */
+class Logger extends \OC\Log {
+
+ private $appName;
+ private $logger;
+
+ public function __construct($appName, ILogger $logger) {
+ $this->appName = $appName;
+ $this->logger = $logger;
+ }
+
+ /**
+ * Logs with an arbitrary level.
+ *
+ * @param mixed $level
+ * @param string $message
+ * @param array $context
+ *
+ * @return void
+ */
+ public function log($level, $message, array $context = array()) {
+ if (empty($context['app'])) {
+ $context['app'] = $this->appName;
+ }
+ $this->logger->log($level, $message, $context);
+ }
+}
+
diff --git a/core/setupexception.php b/core/setupexception.php
new file mode 100644
index 000000000..765cad73e
--- /dev/null
+++ b/core/setupexception.php
@@ -0,0 +1,4 @@
+
+ * @copyright Jörn Friedrich Dreyer 2012-2014
+ */
+
+namespace OCA\Search_Lucene\Db;
+
+use OCP\AppFramework\Db\Entity;
+
+/**
+ * @method integer getFileId()
+ * @method void setFileId(integer $fileId)
+ * @method string getStatus()
+ * @method setStatus(string $status)
+ */
+class Status extends Entity {
+
+ const STATUS_NEW = 'N';
+ const STATUS_INDEXED = 'I';
+ const STATUS_SKIPPED = 'S';
+ const STATUS_UNINDEXED = 'U';
+ const STATUS_VANISHED = 'V';
+ const STATUS_ERROR = 'E';
+
+ public $fileId;
+ public $status;
+
+ // we use fileid as the primary key
+ private $_fieldTypes = array('fileId' => 'integer');
+
+ /**
+ * @return an array with attribute and type
+ */
+ public function getFieldTypes() {
+ return $this->_fieldTypes;
+ }
+
+ /**
+ * Adds type information for a field so that its automatically casted to
+ * that value once its being returned from the database
+ * @param string $fieldName the name of the attribute
+ * @param string $type the type which will be used to call settype()
+ */
+ protected function addType($fieldName, $type){
+ $this->_fieldTypes[$fieldName] = $type;
+ }
+
+ // we need to overwrite the setter because it would otherwise use _fieldTypes of the Entity class
+ protected function setter($name, $args) {
+ // setters should only work for existing attributes
+ if(property_exists($this, $name)){
+ if($this->$name === $args[0]) {
+ return;
+ }
+ $this->markFieldUpdated($name);
+
+ // if type definition exists, cast to correct type
+ if($args[0] !== null && array_key_exists($name, $this->_fieldTypes)) {
+ settype($args[0], $this->_fieldTypes[$name]);
+ }
+ $this->$name = $args[0];
+
+ } else {
+ throw new \BadFunctionCallException($name .
+ ' is not a valid attribute');
+ }
+ }
+
+ /**
+ * Transform a database columnname to a property
+ * @param string $columnName the name of the column
+ * @return string the property name
+ */
+ public function columnToProperty($columnName) {
+ if ($columnName === 'fileid') {
+ $property = 'fileId';
+ } else {
+ $property = parent::columnToProperty($columnName);
+ }
+ return $property;
+ }
+
+ /**
+ * Transform a property to a database column name
+ * for search_lucene we don't magically insert a _ for CamelCase
+ * @param string $property the name of the property
+ * @return string the column name
+ */
+ public function propertyToColumn($property){
+ $column = strtolower($property);
+ return $column;
+ }
+}
diff --git a/db/statusmapper.php b/db/statusmapper.php
new file mode 100644
index 000000000..ad83ae6b4
--- /dev/null
+++ b/db/statusmapper.php
@@ -0,0 +1,267 @@
+
+ * @copyright Jörn Friedrich Dreyer 2012-2014
+ */
+
+namespace OCA\Search_Lucene\Db;
+
+use OC\Files\Filesystem;
+use OCA\Search_Lucene\Core\Db;
+use OCP\AppFramework\Db\DoesNotExistException;
+use OCP\AppFramework\Db\Entity;
+use OCP\AppFramework\Db\Mapper;
+use OCP\IDb;
+use OCP\ILogger;
+
+/**
+ * @author Jörn Dreyer
+ */
+class StatusMapper extends Mapper {
+
+ private $logger;
+
+ public function __construct(IDb $db, ILogger $logger){
+ parent::__construct($db, 'lucene_status', '\OCA\Search_Lucene\Db\Status');
+ $this->logger = $logger;
+ }
+
+
+ /**
+ * Deletes a status from the table
+ * @param Entity|integer $statusOrId the status that should be deleted
+ */
+ public function delete($statusOrId){
+ if ($statusOrId instanceof Status) {
+ $statusOrId = $statusOrId->getFileId();
+ }
+ $sql = 'DELETE FROM `' . $this->tableName . '` WHERE `fileid` = ?';
+ $this->execute($sql, array($statusOrId));
+ }
+
+ /**
+ * Creates a new entry in the db from an entity
+ * @param Entity $entity the entity that should be created
+ * @return Entity the saved entity with the set id
+ */
+ public function insert(Entity $entity){
+ // get updated fields to save, fields have to be set using a setter to
+ // be saved
+ $properties = $entity->getUpdatedFields();
+ $values = '';
+ $columns = '';
+ $params = array();
+
+ // build the fields
+ $i = 0;
+ foreach($properties as $property => $updated) {
+ $column = $entity->propertyToColumn($property);
+ $getter = 'get' . ucfirst($property);
+
+ $columns .= '`' . $column . '`';
+ $values .= '?';
+
+ // only append colon if there are more entries
+ if($i < count($properties)-1){
+ $columns .= ',';
+ $values .= ',';
+ }
+
+ array_push($params, $entity->$getter());
+ $i++;
+
+ }
+
+ $sql = 'INSERT INTO `' . $this->tableName . '`(' .
+ $columns . ') VALUES(' . $values . ')';
+
+ $this->execute($sql, $params);
+
+ $entity->setFileId((int) $this->db->getInsertId($this->tableName));
+ return $entity;
+ }
+
+ /**
+ * Updates an entry in the db from a status
+ * @throws \InvalidArgumentException if entity has no id
+ * @param Entity $entity the status that should be created
+ */
+ public function update(Entity $entity){
+ // if entity wasn't changed it makes no sense to run a db query
+ $properties = $entity->getUpdatedFields();
+ if(count($properties) === 0) {
+ return $entity;
+ }
+
+ // entity needs an id
+ $fileId = $entity->getFileId();
+ if($fileId === null){
+ throw new \InvalidArgumentException(
+ 'Entity which should be updated has no fileId');
+ }
+
+ // get updated fields to save, fields have to be set using a setter to
+ // be saved
+ // dont update the fileId field
+ unset($properties['fileId']);
+
+ $columns = '';
+ $params = array();
+
+ // build the fields
+ $i = 0;
+ foreach($properties as $property => $updated) {
+
+ $column = $entity->propertyToColumn($property);
+ $getter = 'get' . ucfirst($property);
+
+ $columns .= '`' . $column . '` = ?';
+
+ // only append colon if there are more entries
+ if($i < count($properties)-1){
+ $columns .= ',';
+ }
+
+ array_push($params, $entity->$getter());
+ $i++;
+ }
+
+ $sql = 'UPDATE `' . $this->tableName . '` SET ' .
+ $columns . ' WHERE `fileid` = ?';
+ array_push($params, $fileId);
+
+ $this->execute($sql, $params);
+ }
+
+
+ /**
+ * get the list of all unindexed files for the user
+ *
+ * @return array
+ */
+ public function getUnindexed() {
+ $files = array();
+ $absoluteRoot = Filesystem::getView()->getAbsolutePath('/');
+ $mounts = Filesystem::getMountPoints($absoluteRoot);
+ $mount = Filesystem::getMountPoint($absoluteRoot);
+ if (!in_array($mount, $mounts)) {
+ $mounts[] = $mount;
+ }
+
+ $query = $this->db->prepareQuery('
+ SELECT `*PREFIX*filecache`.`fileid`
+ FROM `*PREFIX*filecache`
+ LEFT JOIN `*PREFIX*lucene_status`
+ ON `*PREFIX*filecache`.`fileid` = `*PREFIX*lucene_status`.`fileid`
+ WHERE `storage` = ?
+ AND ( `status` IS NULL OR `status` = ? )
+ AND `path` LIKE \'files/%\'
+ ');
+
+ foreach ($mounts as $mount) {
+ if (is_string($mount)) {
+ $storage = Filesystem::getStorage($mount);
+ } else if ($mount instanceof \OC\Files\Mount\Mount) {
+ $storage = $mount->getStorage();
+ } else {
+ $storage = null;
+ $this->logger->
+ debug( 'expected string or instance of \OC\Files\Mount\Mount got ' . json_encode($mount) );
+ }
+ //only index local files for now
+ if ($storage->isLocal()) {
+ $cache = $storage->getCache();
+ $numericId = $cache->getNumericStorageId();
+
+ $result = $query->execute(array($numericId, Status::STATUS_NEW));
+
+ while ($row = $result->fetchRow()) {
+ $files[] = $row['fileid'];
+ }
+ }
+ }
+ return $files;
+ }
+
+
+ /**
+ * @param $fileId
+ * @return Status
+ */
+ public function getOrCreateFromFileId($fileId) {
+ $sql = '
+ SELECT `fileid`, `status`
+ FROM ' . $this->tableName . '
+ WHERE `fileid` = ?
+ ';
+ try {
+ return $this->findEntity($sql, array($fileId));
+ } catch (DoesNotExistException $e) {
+ $status = new Status();
+ $status->setFileId($fileId);
+ $status->setStatus(Status::STATUS_NEW);
+ return $this->insert($status);
+ }
+ }
+
+ // always write status to db immediately
+ public function markNew(Status $status) {
+ $status->setStatus(Status::STATUS_NEW);
+ return $this->update($status);
+ }
+
+ public function markIndexed(Status $status) {
+ $status->setStatus(Status::STATUS_INDEXED);
+ return $this->update($status);
+ }
+
+ public function markSkipped(Status $status) {
+ $status->setStatus(Status::STATUS_SKIPPED);
+ return $this->update($status);
+ }
+
+ public function markUnIndexed(Status $status) {
+ $status->setStatus(Status::STATUS_UNINDEXED);
+ return $this->update($status);
+ }
+
+ public function markVanished(Status $status) {
+ $status->setStatus(Status::STATUS_VANISHED);
+ return $this->update($status);
+ }
+
+ public function markError(Status $status) {
+ $status->setStatus(Status::STATUS_ERROR);
+ return $this->update($status);
+ }
+
+ /**
+ * @return int[]
+ */
+ public function getDeleted() {
+ $files = array();
+
+ $query = $this->db->prepareQuery('
+ SELECT `*PREFIX*lucene_status`.`fileid`
+ FROM `*PREFIX*lucene_status`
+ LEFT JOIN `*PREFIX*filecache`
+ ON `*PREFIX*filecache`.`fileid` = `*PREFIX*lucene_status`.`fileid`
+ WHERE `*PREFIX*filecache`.`fileid` IS NULL
+ ');
+
+ $result = $query->execute();
+
+ while ($row = $result->fetchRow()) {
+ $files[] = $row['fileid'];
+ }
+
+ return $files;
+
+ }
+
+}
diff --git a/document/Ods.php b/document/ods.php
similarity index 92%
rename from document/Ods.php
rename to document/ods.php
index 0fe15938e..956f030de 100644
--- a/document/Ods.php
+++ b/document/ods.php
@@ -1,4 +1,13 @@
+ * @copyright Jörn Friedrich Dreyer 2012-2014
+ */
namespace OCA\Search_Lucene\Document;
/**
diff --git a/document/Odt.php b/document/odt.php
similarity index 92%
rename from document/Odt.php
rename to document/odt.php
index 8869c2cbd..2a78181ea 100644
--- a/document/Odt.php
+++ b/document/odt.php
@@ -1,4 +1,13 @@
+ * @copyright Jörn Friedrich Dreyer 2012-2014
+ */
namespace OCA\Search_Lucene\Document;
/**
diff --git a/document/OpenDocument.php b/document/opendocument.php
similarity index 91%
rename from document/OpenDocument.php
rename to document/opendocument.php
index 4abbdf6b7..ac139a984 100644
--- a/document/OpenDocument.php
+++ b/document/opendocument.php
@@ -1,4 +1,13 @@
+ * @copyright Jörn Friedrich Dreyer 2012-2014
+ */
namespace OCA\Search_Lucene\Document;
/**
diff --git a/document/Pdf.php b/document/pdf.php
similarity index 85%
rename from document/Pdf.php
rename to document/pdf.php
index 865e32874..34ae5db55 100644
--- a/document/Pdf.php
+++ b/document/pdf.php
@@ -1,7 +1,17 @@
+ * @copyright Jörn Friedrich Dreyer 2012-2014
+ */
namespace OCA\Search_Lucene\Document;
+use OCA\Search_Lucene\Utility\PdfParser;
use \OCP\Util;
/**
* PDF document
@@ -36,10 +46,9 @@ private function __construct($data, $storeContent) {
//TODO handle PDF 1.6 metadata Zend_Pdf::getMetadata()
//do the content extraction
- $pdfParse = new \App_Search_Helper_PdfParser();
+ $pdfParse = new PdfParser();
$body = $pdfParse->pdf2txt($zendpdf->render());
-
if ($body != '') {
// Store contents
if ($storeContent) {
diff --git a/hooks/files.php b/hooks/files.php
new file mode 100644
index 000000000..25419ddbb
--- /dev/null
+++ b/hooks/files.php
@@ -0,0 +1,153 @@
+
+ * @copyright Jörn Friedrich Dreyer 2012-2014
+ */
+
+namespace OCA\Search_Lucene\Hooks;
+
+use OCA\Search_Lucene\AppInfo\Application;
+use OCA\Search_Lucene\Core\Logger;
+use OCA\Search_Lucene\Db\StatusMapper;
+use OCA\Search_Lucene\Lucene\Index;
+use OCP\BackgroundJob;
+
+/**
+ *
+ * @author Jörn Dreyer
+ */
+class Files {
+
+ /**
+ * handle for indexing file
+ *
+ * @param string $path
+ */
+ const handle_post_write = 'indexFile';
+
+ /**
+ * handle for renaming file
+ *
+ * @param string $path
+ */
+ const handle_post_rename = 'renameFile';
+
+ /**
+ * handle for removing file
+ *
+ * @param string $path
+ */
+ const handle_delete = 'deleteFile';
+
+ /**
+ * handle file writes (triggers reindexing)
+ *
+ * the file indexing is queued as a background job
+ *
+ * @author Jörn Dreyer
+ *
+ * @param $param array from postWriteFile-Hook
+ */
+ public static function indexFile(array $param) {
+
+ $app = new Application();
+ $container = $app->getContainer();
+ $userId = $container->query('UserId');
+
+ if (!empty($userId)) {
+
+ // mark written file as new
+ $userFolder = $container->query('ServerContainer')->getUserFolder();
+ $node = $userFolder->get($param['path']);
+ /** @var StatusMapper $mapper */
+ $mapper = $container->query('StatusMapper');
+ $status = $mapper->getOrCreateFromFileId($node->getId());
+
+ // only index files
+ if ($node instanceof \OCP\Files\File) {
+ $mapper->markNew($status);
+ } else {
+ $mapper->markSkipped($status);
+ }
+
+ //Add Background Job:
+ BackgroundJob::registerJob( 'OCA\Search_Lucene\Jobs\IndexJob', array('user' => $userId) );
+ } else {
+ $container->query('Logger')->debug(
+ 'Hook indexFile could not determine user when called with param '.json_encode($param)
+ );
+ }
+ }
+
+ /**
+ * handle file renames (triggers indexing and deletion)
+ *
+ * @author Jörn Dreyer
+ *
+ * @param $param array from postRenameFile-Hook
+ */
+ public static function renameFile(array $param) {
+ $app = new Application();
+ $container = $app->getContainer();
+
+ if (!empty($param['oldpath'])) {
+ //delete from lucene index
+ $container->query('Index')->deleteFile($param['oldpath']);
+ }
+
+ if (!empty($param['newpath'])) {
+ $userFolder = $container->query('ServerContainer')->getUserFolder();
+ $node = $userFolder->get($param['newpath']);
+
+ // only index files
+ if ($node instanceof \OCP\Files\File) {
+ $mapper = $container->query('StatusMapper');
+ $mapper->getOrCreateFromFileId($node->getId());
+ self::indexFile(array('path'=>$param['newpath']));
+ }
+
+ }
+ }
+
+ /**
+ * deleteFile triggers the removal of any deleted files from the index
+ *
+ * @author Jörn Dreyer
+ *
+ * @param $param array from deleteFile-Hook
+ */
+ static public function deleteFile(array $param) {
+ // we cannot use post_delete as $param would not contain the id
+ // of the deleted file and we could not fetch it with getId
+ $app = new Application();
+ $container = $app->getContainer();
+
+ /** @var Index $index */
+ $index = $container->query('Index');
+
+ /** @var StatusMapper $mapper */
+ $mapper = $container->query('StatusMapper');
+
+ /** @var Logger $logger */
+ $logger = $container->query('Logger');
+
+ $deletedIds = $mapper->getDeleted();
+ $count = 0;
+ foreach ($deletedIds as $fileId) {
+ $logger->debug( 'deleting status for ('.$fileId.') ' );
+ //delete status
+ $mapper->delete($fileId);
+ //delete from lucene
+ $count += $index->deleteFile($fileId);
+
+ }
+ $logger->debug( 'removed '.$count.' files from index' );
+
+ }
+
+}
diff --git a/jobs/indexjob.php b/jobs/indexjob.php
new file mode 100644
index 000000000..7ed44b3e1
--- /dev/null
+++ b/jobs/indexjob.php
@@ -0,0 +1,43 @@
+
+ * @copyright Jörn Friedrich Dreyer 2012-2014
+ */
+
+namespace OCA\Search_Lucene\Jobs;
+
+use OCA\Search_Lucene\AppInfo\Application;
+
+class IndexJob extends \OC\BackgroundJob\QueuedJob {
+
+ public function run($arguments){
+ $app = new Application();
+ $container = $app->getContainer();
+
+ /** @var Logger $logger */
+ $logger = $container->query('Logger');
+
+ if (isset($arguments['user'])) {
+ $userId = $arguments['user'];
+
+ $folder = $container->query('FileUtility')->setUpUserFolder($userId);
+
+ if ($folder) {
+
+ $fileIds = $container->query('StatusMapper')->getUnindexed();
+
+ $logger->debug('background job indexing '.count($fileIds).' files for '.$userId );
+
+ $container->query('Indexer')->indexFiles($fileIds);
+
+ }
+ } else {
+ $logger->debug('indexer job did not receive user in arguments: '.json_encode($arguments));
+ }
+ }
+}
diff --git a/jobs/optimizejob.php b/jobs/optimizejob.php
new file mode 100644
index 000000000..0c2dc9dac
--- /dev/null
+++ b/jobs/optimizejob.php
@@ -0,0 +1,37 @@
+
+ * @copyright Jörn Friedrich Dreyer 2012-2014
+ */
+
+namespace OCA\Search_Lucene\Jobs;
+
+use OCA\Search_Lucene\AppInfo\Application;
+
+class OptimizeJob extends \OC\BackgroundJob\TimedJob {
+
+ public function __construct() {
+ $this->setInterval(86400); //execute at most once a day
+ }
+
+ public function run($arguments){
+ $app = new Application();
+ $container = $app->getContainer();
+ /** @var Logger $logger */
+ $logger = $container->query('Logger');
+
+ if (!empty($arguments['user'])) {
+ $userId = $arguments['user'];
+ $logger->debug('background job optimizing index for '.$userId );
+ $container->query('FileUtility')->setUpIndexFolder($userId);
+ $container->query('Index')->optimizeIndex();
+ } else {
+ $logger->debug('indexer job did not receive user in arguments: '.json_encode($arguments) );
+ }
+ }
+}
diff --git a/js/checker.js b/js/checker.js
index 84b218f3e..c0e84e123 100644
--- a/js/checker.js
+++ b/js/checker.js
@@ -5,7 +5,7 @@ function luceneIndexFiles() {
}
t('search_lucene', 'Indexing... {count} files left', {count: 0}); //preload translations
luceneIndexFiles.active = true;
- updateEventSource = new OC.EventSource(OC.filePath('search_lucene', 'ajax', 'lucene.php'), {operation: 'index'});
+ updateEventSource = new OC.EventSource(OC.generateUrl('/apps/search_lucene/indexer/index', {}));
updateEventSource.listen('count', function (unIndexedCount) {
count = unIndexedCount;
if (count > 0) {
@@ -53,5 +53,10 @@ $(document).ready(function () {
//hovering over it shows the current file
//clicking it stops the indexer: ⌛
+ OC.search.resultTypes.lucene = t('search_lucene', 'In');
+
+ OC.search.customResults.lucene = function (row, item){
+ row.find('td.result .text').text(t('search_lucene', 'Score: {score}', {score: Math.round(item.score*100)/100}));
+ };
});
diff --git a/lib/hooks.php b/lib/hooks.php
deleted file mode 100644
index 194cb4092..000000000
--- a/lib/hooks.php
+++ /dev/null
@@ -1,142 +0,0 @@
-
- */
-class Hooks {
-
- /**
- * classname which used for hooks handling
- * used as signalclass in OC_Hooks::emit()
- */
- const CLASSNAME = 'Hooks';
-
- /**
- * handle for indexing file
- *
- * @param string $path
- */
- const handle_post_write = 'indexFile';
-
- /**
- * handle for renaming file
- *
- * @param string $path
- */
- const handle_post_rename = 'renameFile';
-
- /**
- * handle for removing file
- *
- * @param string $path
- */
- const handle_delete = 'deleteFile';
-
- /**
- * handle file writes (triggers reindexing)
- *
- * the file indexing is queued as a background job
- *
- * @author Jörn Dreyer
- *
- * @param $param array from postWriteFile-Hook
- */
- public static function indexFile(array $param) {
- if (isset($param['path'])) {
- $param['user'] = \OCP\User::getUser();
- //Add Background Job:
- BackgroundJob::addQueuedTask(
- 'search_lucene',
- 'OCA\Search_Lucene\Hooks',
- 'doIndexFile',
- json_encode($param) );
- } else {
- Util::writeLog('search_lucene',
- 'missing path parameter',
- Util::WARN);
- }
- }
- static public function doIndexFile($param) {
- $data = json_decode($param);
- if ( ! isset($data->path) ) {
- Util::writeLog('search_lucene',
- 'missing path parameter',
- Util::WARN);
- return false;
- }
- if ( ! isset($data->user) ) {
- Util::writeLog('search_lucene',
- 'missing user parameter',
- Util::WARN);
- return false;
- }
- Indexer::indexFile($data->path, $data->user);
- }
-
- /**
- * handle file renames (triggers indexing and deletion)
- *
- * @author Jörn Dreyer
- *
- * @param $param array from postRenameFile-Hook
- */
- public static function renameFile(array $param) {
- if (isset($param['newpath'])) {
- self::indexFile(array('path'=>$param['newpath']));
- }
- if (isset($param['oldpath'])) {
- self::deleteFile(array('path'=>$param['oldpath']));
- }
- }
-
- /**
- * remove a file from the lucene index when deleting a file
- *
- * file deletion from the index is queued as a background job
- *
- * @author Jörn Dreyer
- *
- * @param $param array from postDeleteFile-Hook
- */
- static public function deleteFile(array $param) {
- // we cannot use post_delete as $param would not contain the id
- // of the deleted file and we could not fetch it with getId
- if (isset($param['path'])) {
- $param['user'] = \OCP\User::getUser();
- //Add Background Job:
- BackgroundJob::addQueuedTask(
- 'search_lucene',
- 'OCA\Search_Lucene\Hooks',
- 'doDeleteFile',
- json_encode($param) );
- } else {
- Util::writeLog('search_lucene',
- 'missing path parameter',
- Util::WARN);
- }
-
- }
- static public function doDeleteFile($param) {
- $data = json_decode($param);
- if ( ! isset($data->path) ) {
- Util::writeLog('search_lucene',
- 'missing path parameter',
- Util::WARN);
- return false;
- }
- if ( ! isset($data->user) ) {
- Util::writeLog('search_lucene',
- 'missing user parameter',
- Util::WARN);
- return false;
- }
- Lucene::deleteFile($data->path, $data->user);
- }
-
-}
diff --git a/lib/indexer.php b/lib/indexer.php
deleted file mode 100644
index db8bb03cb..000000000
--- a/lib/indexer.php
+++ /dev/null
@@ -1,304 +0,0 @@
-
- */
-class Indexer {
-
- /**
- * classname which used for hooks handling
- * used as signalclass in OC_Hooks::emit()
- */
- const CLASSNAME = 'Indexer';
-
- /**
- * index a file
- *
- * @author Jörn Dreyer
- *
- * @param string $path the path of the file
- *
- * @return bool
- */
- static public function indexFile($path = '', $user = null) {
-
- if (!Filesystem::isValidPath($path)) {
- return;
- }
- if ($path === '') {
- //ignore the empty path element
- return false;
- }
-
- if (is_null($user)) {
- $view = Filesystem::getView();
- $user = \OCP\User::getUser();
- } else {
- $view = new \OC\Files\View('/' . $user . '/files');
- }
-
- if ( ! $view ) {
- Util::writeLog('search_lucene',
- 'could not resolve filesystem view',
- Util::WARN);
- return false;
- }
-
- if(!$view->file_exists($path)) {
- Util::writeLog('search_lucene',
- 'file vanished, ignoring',
- Util::DEBUG);
- return true;
- }
-
- $root = $view->getRoot();
- $pk = md5($root . $path);
-
- // the cache already knows mime and other basic stuff
- $data = $view->getFileInfo($path);
- if (isset($data['mimetype'])) {
- $mimeType = $data['mimetype'];
-
- // initialize plain lucene document
- $doc = new \Zend_Search_Lucene_Document();
-
- // index content for local files only
- $localFile = $view->getLocalFile($path);
-
- if ( $localFile ) {
- //try to use special lucene document types
-
- if ('text/plain' === $mimeType) {
-
- $body = $view->file_get_contents($path);
-
- if ($body != '') {
- $doc->addField(\Zend_Search_Lucene_Field::UnStored('body', $body));
- }
-
- } else if ('text/html' === $mimeType) {
-
- //TODO could be indexed, even if not local
- $doc = \Zend_Search_Lucene_Document_Html::loadHTML($view->file_get_contents($path));
-
- } else if ('application/pdf' === $mimeType) {
-
- $doc = Pdf::loadPdf($view->file_get_contents($path));
-
- // commented the mimetype checks, as the zend classes only understand docx and not doc files.
- // FIXME distinguish doc and docx, xls and xlsx, ppt and pptx, in oc core mimetype helper ...
- //} else if ('application/msword' === $mimeType) {
- } else if (strtolower(substr($data['name'], -5)) === '.docx') {
-
- $doc = \Zend_Search_Lucene_Document_Docx::loadDocxFile($localFile);
-
- //} else if ('application/msexcel' === $mimeType) {
- } else if (strtolower(substr($data['name'], -5)) === '.xlsx') {
-
- $doc = \Zend_Search_Lucene_Document_Xlsx::loadXlsxFile($localFile);
-
- //} else if ('application/mspowerpoint' === $mimeType) {
- } else if (strtolower(substr($data['name'], -5)) === '.pptx') {
-
- $doc = \Zend_Search_Lucene_Document_Pptx::loadPptxFile($localFile);
-
- } else if (strtolower(substr($data['name'], -4)) === '.odt') {
-
- $doc = Odt::loadOdtFile($localFile);
-
- } else if (strtolower(substr($data['name'], -4)) === '.ods') {
-
- $doc = Ods::loadOdsFile($localFile);
-
- }
- }
-
-
- // Store filecache id as unique id to lookup by when deleting
- $doc->addField(\Zend_Search_Lucene_Field::Keyword('pk', $pk));
-
- // Store filename
- $doc->addField(\Zend_Search_Lucene_Field::Text('filename', $data['name'], 'UTF-8'));
-
- // Store document path to identify it in the search results
- $doc->addField(\Zend_Search_Lucene_Field::Text('path', $path, 'UTF-8'));
-
- $doc->addField(\Zend_Search_Lucene_Field::unIndexed('size', $data['size']));
-
- $doc->addField(\Zend_Search_Lucene_Field::unIndexed('mimetype', $mimeType));
-
- //self::extractMetadata($doc, $path, $view, $mimeType);
-
- Lucene::updateFile($doc, $path, $user);
-
- return true;
-
- } else {
- Util::writeLog(
- 'search_lucene',
- 'need mimetype for content extraction',
- Util::ERROR
- );
- return false;
- }
- }
-
-
- /**
- * extract the metadata from a file
- *
- * uses getid3 to extract metadata.
- * if possible also adds content (currently only for plain text files)
- * hint: use OC\Files\Filesystem::getFileInfo($path) to get metadata for the last param
- *
- * @author Jörn Dreyer
- *
- * @param Zend_Search_Lucene_Document $doc to add the metadata to
- * @param string $path path of the file to extract metadata from
- * @param string $mimetype depending on the mimetype different extractions are performed
- *
- * @return void
- */
- private static function extractMetadata(
- \Zend_Search_Lucene_Document $doc,
- $path,
- \OC\Files\View $view,
- $mimetype
- ) {
-
- $file = $view->getLocalFile($path);
- if (is_dir($file)) {
- // Don't lose time analizing a directory for file-specific metadata
- return;
- }
- $getID3 = new \getID3();
- $getID3->encoding = 'UTF-8';
- $data = $getID3->analyze($file);
-
- // TODO index meta information from media files?
-
- //show me what you got
- /*foreach ($data as $key => $value) {
- Util::writeLog('search_lucene',
- 'getid3 extracted '.$key.': '.$value,
- Util::DEBUG);
- if (is_array($value)) {
- foreach ($value as $k => $v) {
- Util::writeLog('search_lucene',
- ' ' . $value .'-' .$k.': '.$v,
- Util::DEBUG);
- }
- }
- }*/
-
- if ('application/pdf' === $mimetype) {
- try {
- $zendpdf = \Zend_Pdf::parse($view->file_get_contents($path));
-
- //we currently only display the filename, so we only index metadata here
- if (isset($zendpdf->properties['Title'])) {
- $doc->addField(\Zend_Search_Lucene_Field::UnStored('title', $zendpdf->properties['Title']));
- }
- if (isset($zendpdf->properties['Author'])) {
- $doc->addField(\Zend_Search_Lucene_Field::UnStored('author', $zendpdf->properties['Author']));
- }
- if (isset($zendpdf->properties['Subject'])) {
- $doc->addField(\Zend_Search_Lucene_Field::UnStored('subject', $zendpdf->properties['Subject']));
- }
- if (isset($zendpdf->properties['Keywords'])) {
- $doc->addField(\Zend_Search_Lucene_Field::UnStored('keywords', $zendpdf->properties['Keywords']));
- }
- //TODO handle PDF 1.6 metadata Zend_Pdf::getMetadata()
-
- //do the content extraction
- $pdfParse = new \App_Search_Helper_PdfParser();
- $body = $pdfParse->pdf2txt($zendpdf->render());
-
- } catch (Exception $e) {
- Util::writeLog('search_lucene',
- $e->getMessage() . ' Trace:\n' . $e->getTraceAsString(),
- Util::ERROR);
- }
-
- }
-
- if ($body != '') {
- $doc->addField(\Zend_Search_Lucene_Field::UnStored('body', $body));
- }
-
- if (isset($data['error'])) {
- Util::writeLog(
- 'search_lucene',
- 'failed to extract meta information for ' . $view->getAbsolutePath($path) . ': ' . $data['error']['0'],
- Util::WARN
- );
-
- return;
- }
- }
-
- /**
- * get the list of all unindexed files for the user
- *
- * @return array
- */
- static public function getUnindexed() {
- $files = array();
- $absoluteRoot = Filesystem::getView()->getAbsolutePath('/');
- $mounts = Filesystem::getMountPoints($absoluteRoot);
- $mount = Filesystem::getMountPoint($absoluteRoot);
- if (!in_array($mount, $mounts)) {
- $mounts[] = $mount;
- }
-
- $query = \OCP\DB::prepare('
- SELECT `*PREFIX*filecache`.`fileid`
- FROM `*PREFIX*filecache`
- LEFT JOIN `*PREFIX*lucene_status`
- ON `*PREFIX*filecache`.`fileid` = `*PREFIX*lucene_status`.`fileid`
- WHERE `storage` = ?
- AND `status` IS NULL OR `status` = ?
- ');
-
- foreach ($mounts as $mount) {
- if (is_string($mount)) {
- $storage = Filesystem::getStorage($mount);
- } else if ($mount instanceof \OC\Files\Mount\Mount) {
- $storage = $mount->getStorage();
- } else {
- $storage = null;
- Util::writeLog('search_lucene',
- 'expected string or instance of \OC\Files\Mount\Mount got ' . json_encode($mount),
- Util::DEBUG);
- }
- //only index local files for now
- if ($storage instanceof \OC\Files\Storage\Local) {
- $cache = $storage->getCache();
- $numericId = $cache->getNumericStorageId();
-
- $result = $query->execute(array($numericId, 'N'));
- if (\OCP\DB::isError($result)) {
- Util::writeLog(
- 'search_lucene',
- 'failed to find unindexed files: '.\OCP\DB::getErrorMessage($result),
- Util::WARN
- );
- return false;
- }
- while ($row = $result->fetchRow()) {
- $files[] = $row['fileid'];
- }
- }
- }
- return $files;
- }
-
-}
diff --git a/lib/lucene.php b/lib/lucene.php
deleted file mode 100644
index b225d33a5..000000000
--- a/lib/lucene.php
+++ /dev/null
@@ -1,317 +0,0 @@
-
- */
-class Lucene extends \OC_Search_Provider {
-
- /**
- * classname which used for hooks handling
- * used as signalclass in OC_Hooks::emit()
- */
- const CLASSNAME = 'Lucene';
-
- /**
- * opens or creates the users lucene index
- *
- * stores the index in //lucene_index
- *
- * @author Jörn Dreyer
- *
- * @return Zend_Search_Lucene_Interface
- */
- public static function openOrCreate($user = null) {
-
- if ($user == null) {
- $user = User::getUser();
- }
-
- try {
-
- \Zend_Search_Lucene_Analysis_Analyzer::setDefault(
- new \Zend_Search_Lucene_Analysis_Analyzer_Common_TextNum_CaseInsensitive()
- ); //let lucene search for numbers as well as words
-
- // Create index
- //$ocFilesystemView = OCP\Files::getStorage('search_lucene'); // encrypt the index on logout, decrypt on login
-
- $indexUrl = \OC_User::getHome($user) . '/lucene_index';
- if (file_exists($indexUrl)) {
- $index = \Zend_Search_Lucene::open($indexUrl);
- } else {
- $index = \Zend_Search_Lucene::create($indexUrl);
- //todo index all user files
- }
- } catch ( Exception $e ) {
- Util::writeLog(
- 'search_lucene',
- $e->getMessage().' Trace:\n'.$e->getTraceAsString(),
- Util::ERROR
- );
- return null;
- }
-
-
- return $index;
- }
-
- /**
- * optimizes the lucene index
- *
- *
- * @author Jörn Dreyer
- *
- * @param Zend_Search_Lucene_Interface $index an optional index
- *
- * @return void
- */
- static public function optimizeIndex(
- \Zend_Search_Lucene_Interface $index = null
- ) {
-
- if ($index === null) {
- $index = self::openOrCreate();
- }
-
- Util::writeLog(
- 'search_lucene',
- 'optimizing index ',
- Util::DEBUG
- );
-
- $index->optimize();
-
- }
-
- /**
- * upates a file in the lucene index
- *
- * 1. the file is deleted from the index
- * 2. the file is readded to the index
- * 3. the file is marked as index in the status table
- *
- * @author Jörn Dreyer
- *
- * @param Zend_Search_Lucene_Document $doc the document to store for the path
- * @param string $path path to the document to update
- *
- * @return void
- */
- static public function updateFile(
- \Zend_Search_Lucene_Document $doc,
- $path = '',
- $user = null,
- \Zend_Search_Lucene_Interface $index = null
- ) {
-
- if ($index === null) {
- $index = self::openOrCreate($user);
- }
-
- // TODO profile perfomance for searching before adding to index
- self::deleteFile($path, $user, $index);
-
- Util::writeLog(
- 'search_lucene',
- 'adding ' . $path ,
- Util::DEBUG
- );
-
- // Add document to the index
- $index->addDocument($doc);
-
- $index->commit();
-
- }
-
- /**
- * removes a file frome the lucene index
- *
- * @author Jörn Dreyer
- *
- * @param string $path path to the document to remove from the index
- * @param Zend_Search_Lucene_Interface $index optional can be passed ro reuse an existing instance
- *
- * @return void
- */
- static public function deleteFile(
- $path,
- $user = null,
- \Zend_Search_Lucene_Interface $index = null
- ) {
-
- if ( $path === '' ) {
- //ignore the empty path element
- return;
- }
-
- if (is_null($user)) {
- $view = Filesystem::getView();
- $user = \OCP\User::getUser();
- } else {
- $view = new \OC\Files\View('/' . $user . '/files');
- }
-
- if ( ! $view ) {
- Util::writeLog(
- 'search_lucene',
- 'could not resolve filesystem view',
- Util::WARN
- );
- return false;
- }
-
- if ($index === null) {
- $index = self::openOrCreate($user);
- }
-
- $root= $view->getRoot();
- $pk = md5($root.$path);
-
- Util::writeLog(
- 'search_lucene',
- 'searching hits for pk:' . $pk,
- Util::DEBUG
- );
-
-
- $hits = $index->find( 'pk:' . $pk ); //id would be internal to lucene
-
- Util::writeLog(
- 'search_lucene',
- 'found ' . count($hits) . ' hits ',
- Util::DEBUG
- );
-
- foreach ($hits as $hit) {
- Util::writeLog(
- 'search_lucene',
- 'removing ' . $hit->id . ':' . $hit->path . ' from index',
- Util::DEBUG
- );
- $index->delete($hit);
- }
- }
-
- /**
- * performs a search on the users index
- *
- * @author Jörn Dreyer
- *
- * @param string $query lucene search query
- * @return array of OC_Search_Result
- */
- public function search($query){
- $results=array();
- if ( $query !== null ) {
- // * query * kills performance for bigger indexes
- // query * works ok
- // query is still best
- //FIXME emulates the old search but breaks all the nice lucene search query options
- //$query = '*' . $query . '*';
- //if (strpos($query, '*')===false) {
- // $query = $query.='*'; // append query *, works ok
- // TODO add end user guide for search terms ...
- //}
- try {
- $index = self::openOrCreate();
- //default is 3, 0 needed to keep current search behaviour
- //Zend_Search_Lucene_Search_Query_Wildcard::setMinPrefixLength(0);
-
- //$term = new Zend_Search_Lucene_Index_Term($query);
- //$query = new Zend_Search_Lucene_Search_Query_Term($term);
-
- $hits = $index->find($query);
-
- //limit results. we cant show more than ~30 anyway. TODO use paging later
- for ($i = 0; $i < 30 && $i < count($hits); $i++) {
- $results[] = self::asOCSearchResult($hits[$i]);
- }
-
- } catch ( Exception $e ) {
- Util::writeLog(
- 'search_lucene',
- $e->getMessage().' Trace:\n'.$e->getTraceAsString(),
- Util::ERROR
- );
- }
-
- }
- return $results;
- }
-
- /**
- * converts a zend lucene search object to a OC_SearchResult
- *
- * Example:
- *
- * Text | Some Document.txt
- * | /path/to/file, 148kb, Score: 0.55
- *
- * @author Jörn Dreyer
- *
- * @param Zend_Search_Lucene_Search_QueryHit $hit The Lucene Search Result
- * @return OC_Search_Result an OC_Search_Result
- */
- private static function asOCSearchResult(\Zend_Search_Lucene_Search_QueryHit $hit) {
-
- $mimeBase = self::baseTypeOf($hit->mimetype);
-
- switch($mimeBase){
- case 'audio':
- $type='Music';
- break;
- case 'text':
- $type='Text';
- break;
- case 'image':
- $type='Images';
- break;
- default:
- if ($hit->mimetype=='application/xml') {
- $type='Text';
- } else {
- $type='Files';
- }
- }
-
- switch ($hit->mimetype) {
- case 'httpd/unix-directory':
- $url = Util::linkTo('files', 'index.php') . '?dir='.$hit->path;
- break;
- default:
- $url = \OC::$server->getRouter()->generate('download', array('file'=>$hit->path));
- }
-
- return new \OC_Search_Result(
- basename($hit->path),
- dirname($hit->path)
- . ', ' . \OCP\Util::humanFileSize($hit->size)
- . ', Score: ' . number_format($hit->score, 2),
- $url,
- $type,
- dirname($hit->path)
- );
- }
-
- /**
- * get the base type of a mimetype string
- *
- * returns 'text' for 'text/plain'
- *
- * @author Jörn Dreyer
- *
- * @param string $mimetype mimetype
- * @return string basetype
- */
- public static function baseTypeOf($mimetype) {
- return substr($mimetype, 0, strpos($mimetype, '/'));
- }
-
-}
diff --git a/lib/status.php b/lib/status.php
deleted file mode 100644
index 5b243f271..000000000
--- a/lib/status.php
+++ /dev/null
@@ -1,85 +0,0 @@
-
- */
-class Status {
-
- const STATUS_NEW = 'N';
- const STATUS_INDEXED = 'I';
- const STATUS_SKIPPED = 'S';
- const STATUS_ERROR = 'E';
-
- private $fileId;
- private $status;
-
- public function __construct($fileId, $status = null) {
- $this->fileId = $fileId;
- $this->status = $status;
- }
- public static function fromFileId($fileId) {
- $status = self::get($fileId);
- if ($status) {
- return new Status($fileId, $status);
- } else {
- return new Status($fileId, null);
- }
- }
- // always write status to db immediately
- public function markNew() {
- $this->status = self::STATUS_NEW;
- return $this->store();
- }
- public function markIndexed() {
- $this->status = self::STATUS_INDEXED;
- return $this->store();
- }
- public function markSkipped() {
- $this->status = self::STATUS_SKIPPED;
- return $this->store();
- }
- public function markError() {
- $this->status = self::STATUS_ERROR;
- return $this->store();
- }
- private function store() {
- $savedStatus = self::get($this->fileId);
- if ($savedStatus) {
- return self::update($this->fileId, $this->status);
- } else {
- return self::insert($this->fileId, $this->status);
- }
- }
-
- private static function get($fileId) {
- $query = \OC_DB::prepare('
- SELECT `status`
- FROM `*PREFIX*lucene_status`
- WHERE `fileid` = ?
- ');
- $result = $query->execute(array($fileId));
- $row = $result->fetchRow();
- if ($row) {
- return $row['status'];
- } else {
- return null;
- }
- }
- private static function insert($fileId, $status) {
- $query = \OC_DB::prepare('
- INSERT INTO `*PREFIX*lucene_status` VALUES (?,?)
- ');
- return $query->execute(array($fileId, $status));
- }
- private static function update($fileId, $status) {
- $query = \OC_DB::prepare('
- UPDATE `*PREFIX*lucene_status`
- SET `status` = ?
- WHERE `fileid` = ?
- ');
- return $query->execute(array($status, $fileId));
- }
-}
diff --git a/lucene/index.php b/lucene/index.php
new file mode 100644
index 000000000..6bd04425f
--- /dev/null
+++ b/lucene/index.php
@@ -0,0 +1,150 @@
+
+ * @copyright Jörn Friedrich Dreyer 2012-2014
+ */
+
+namespace OCA\Search_Lucene\Lucene;
+use OCA\Search_Lucene\Core\Files;
+use OCP\ILogger;
+
+/**
+ * @author Jörn Dreyer
+ */
+class Index {
+
+ public $files;
+ /**
+ * @var \Zend_Search_Lucene
+ */
+ public $index;
+ /**
+ * @var \OCP\ILogger
+ */
+ public $logger;
+
+ public function __construct(Files $files, ILogger $logger) {
+ $this->files = $files;
+ $this->logger = $logger;
+ }
+
+ /**
+ * opens or creates the given lucene index
+ *
+ * @author Jörn Dreyer
+ *
+ * @throws \Exception
+ */
+ public function openOrCreate() {
+
+ $indexFolder = $this->files->setUpIndexFolder();
+
+ if (is_null($indexFolder)) {
+ throw new \Exception('Could not set up index folder');
+ }
+
+ $storage = $indexFolder->getStorage();
+ $localPath = $storage->getLocalFile($indexFolder->getInternalPath());
+
+ //let lucene search for numbers as well as words
+ \Zend_Search_Lucene_Analysis_Analyzer::setDefault(
+ new \Zend_Search_Lucene_Analysis_Analyzer_Common_Utf8Num_CaseInsensitive()
+ );
+
+ // can we use the index?
+ if ($indexFolder->nodeExists('v0.6.0')) {
+ // correct index present
+ $this->index = \Zend_Search_Lucene::open($localPath);
+ } else {
+ $this->logger->info( 'recreating outdated lucene index' );
+ $indexFolder->delete();
+ $this->index = \Zend_Search_Lucene::create($localPath);
+ $indexFolder->newFile('v0.6.0');
+ }
+
+ }
+
+ /**
+ * optimizes the lucene index
+ *
+ * @author Jörn Dreyer
+ *
+ * @return void
+ */
+ public function optimizeIndex() {
+ $this->logger->debug( 'optimizing index' );
+ $this->index->optimize();
+ }
+
+ /**
+ * upates a file in the lucene index
+ *
+ * 1. the file is deleted from the index
+ * 2. the file is readded to the index
+ * 3. the file is marked as index in the status table
+ *
+ * @author Jörn Dreyer
+ *
+ * @param \Zend_Search_Lucene_Document $doc the document to store for the path
+ * @param int $fileId file id to update
+ * @param bool $commit
+ *
+ * @return void
+ */
+ public function updateFile(
+ \Zend_Search_Lucene_Document $doc,
+ $fileId,
+ $commit = true
+ ) {
+
+ // TODO profile perfomance for searching before adding to index
+ $this->deleteFile($fileId);
+
+ $this->logger->debug( 'adding ' . $fileId .' '.json_encode($doc) );
+
+ // Add document to the index
+ $this->index->addDocument($doc);
+
+ if ($commit) {
+ $this->index->commit();
+ }
+
+ }
+
+ /**
+ * removes a file from the lucene index
+ *
+ * @author Jörn Dreyer
+ *
+ * @param int $fileId file id to remove from the index
+ *
+ * @return int count of deleted documents in the index
+ */
+ public function deleteFile($fileId) {
+
+ $hits = $this->index->find( 'fileId:' . $fileId );
+
+ $this->logger->debug( 'found ' . count($hits) . ' hits for file id ' . $fileId );
+
+ foreach ($hits as $hit) {
+ $this->logger->debug( 'removing ' . $hit->id . ':' . $hit->path . ' from index' );
+ $this->index->delete($hit);
+ }
+
+ return count($hits);
+ }
+
+ public function find ($query) {
+ return $this->index->find($query);
+ }
+
+ public function commit () {
+ $this->index->commit();
+ }
+
+}
diff --git a/lucene/indexer.php b/lucene/indexer.php
new file mode 100644
index 000000000..bbea29ad8
--- /dev/null
+++ b/lucene/indexer.php
@@ -0,0 +1,227 @@
+
+ * @copyright Jörn Friedrich Dreyer 2012-2014
+ */
+
+namespace OCA\Search_Lucene\Lucene;
+
+use OCA\Search_Lucene\Core\Files;
+use OCA\Search_Lucene\Db\StatusMapper;
+use OCA\Search_Lucene\Document\Ods;
+use OCA\Search_Lucene\Document\Odt;
+use OCA\Search_Lucene\Document\Pdf;
+use OCP\ILogger;
+use OCP\IServerContainer;
+
+/**
+ * @author Jörn Dreyer
+ */
+class Indexer {
+
+ /**
+ * @var \OCA\Search_Lucene\Core\Files
+ */
+ private $files;
+ /**
+ * @var \OCP\IServerContainer
+ */
+ private $server;
+ /**
+ * @var Index
+ */
+ private $index;
+ /**
+ * @var array
+ */
+ private $skippedDirs;
+ /**
+ * @var \OCA\Search_Lucene\Db\StatusMapper
+ */
+ private $mapper;
+ /**
+ * @var \OCP\ILogger
+ */
+ private $logger;
+
+ public function __construct(Files $files, IServerContainer $server, Index $index, array $skippedDirs, StatusMapper $mapper, ILogger $logger) {
+ $this->files = $files;
+ $this->server = $server;
+ $this->index = $index;
+ $this->skippedDirs = $skippedDirs;
+ $this->mapper = $mapper;
+ $this->logger = $logger;
+ }
+
+ public function indexFiles (array $fileIds, \OC_EventSource $eventSource = null) {
+
+ foreach ($fileIds as $id) {
+
+ $fileStatus = $this->mapper->getOrCreateFromFileId($id);
+
+ try {
+ // before we start mark the file as error so we know there
+ // was a problem in case the php execution dies and we don't try
+ // the file again
+ $this->mapper->markError($fileStatus);
+
+ /** @var \OCP\Files\Node[] $nodes */
+ $nodes = $this->server->getUserFolder()->getById($id);
+ // getById can return more than one id because the containing storage might be mounted more than once
+ // Since we only want to index the file once, we only use the first entry
+
+ if (isset($nodes[0])) {
+ /** @var \OCP\Files\File $node */
+ $node = $nodes[0];
+ } else {
+ throw new VanishedException($id);
+ }
+
+ if ( ! $node instanceof \OCP\Files\File ) {
+ throw new NotIndexedException();
+ }
+
+ $path = $node->getPath();
+
+ foreach ($this->skippedDirs as $skippedDir) {
+ if (strpos($path, '/' . $skippedDir . '/') !== false //contains dir
+ || strrpos($path, '/' . $skippedDir) === strlen($path) - (strlen($skippedDir) + 1) // ends with dir
+ ) {
+ throw new SkippedException('skipping file '.$id.':'.$path);
+ }
+ }
+
+ if ($eventSource) {
+ $eventSource->send('indexing', $path);
+ }
+
+ if ($this->indexFile($node, false)) {
+ $this->mapper->markIndexed($fileStatus);
+ }
+
+ } catch (VanishedException $e) {
+
+ $this->mapper->markVanished($fileStatus);
+
+ } catch (NotIndexedException $e) {
+
+ $this->mapper->markUnIndexed($fileStatus);
+
+ } catch (SkippedException $e) {
+
+ $this->mapper->markSkipped($fileStatus);
+ $this->logger->debug( $e->getMessage() );
+
+ } catch (\Exception $e) {
+ //sqlite might report database locked errors when stock filescan is in progress
+ //this also catches db locked exception that might come up when using sqlite
+ $this->logger->error($e->getMessage() . ' Trace:\n' . $e->getTraceAsString() );
+ $this->mapper->markError($fileStatus);
+ // TODO Add UI to trigger rescan of files with status 'E'rror?
+ if ($eventSource) {
+ $eventSource->send('error', $e->getMessage());
+ }
+ }
+ }
+
+ $this->index->commit();
+ }
+
+ /**
+ * index a file
+ *
+ * @author Jörn Dreyer
+ *
+ * @param \OCP\Files\File $file the file to be indexed
+ * @param bool $commit
+ *
+ * @return bool true when something was stored in the index, false otherwise (eg, folders are not indexed)
+ * @throws \OCA\Search_Lucene\Lucene\NotIndexedException when an unsupported file type is encountered
+ */
+ public function indexFile(\OCP\Files\File $file, $commit = true) {
+
+ // we decide how to index on mime type or file extension
+ $mimeType = $file->getMimeType();
+ $fileExtension = strtolower(pathinfo($file->getName(), PATHINFO_EXTENSION));
+
+ // initialize plain lucene document
+ $doc = new \Zend_Search_Lucene_Document();
+
+ // index content for local files only
+ $storage = $file->getStorage();
+
+ if ($storage->isLocal()) {
+
+ $path = $storage->getLocalFile($file->getInternalPath());
+
+ //try to use special lucene document types
+
+ if ('text/html' === $mimeType) {
+
+ //TODO could be indexed, even if not local
+ $doc = \Zend_Search_Lucene_Document_Html::loadHTML($file->getContent());
+ } else if ('text/' === substr($mimeType, 0, 5)
+ || 'application/x-tex' === $mimeType) {
+
+ $body = $file->getContent();
+
+ if ($body != '') {
+ $doc->addField(\Zend_Search_Lucene_Field::UnStored('body', $body));
+ }
+
+ } else if ('application/pdf' === $mimeType) {
+
+ $doc = Pdf::loadPdf($file->getContent());
+
+ // the zend classes only understand docx and not doc files
+ } else if ($fileExtension === 'docx') {
+
+ $doc = \Zend_Search_Lucene_Document_Docx::loadDocxFile($path);
+
+ //} else if ('application/msexcel' === $mimeType) {
+ } else if ($fileExtension === 'xlsx') {
+
+ $doc = \Zend_Search_Lucene_Document_Xlsx::loadXlsxFile($path);
+
+ //} else if ('application/mspowerpoint' === $mimeType) {
+ } else if ($fileExtension === 'pptx') {
+
+ $doc = \Zend_Search_Lucene_Document_Pptx::loadPptxFile($path);
+
+ } else if ($fileExtension === 'odt') {
+
+ $doc = Odt::loadOdtFile($path);
+
+ } else if ($fileExtension === 'ods') {
+
+ $doc = Ods::loadOdsFile($path);
+
+ } else {
+ throw new NotIndexedException();
+ }
+ }
+
+ // Store filecache id as unique id to lookup by when deleting
+ $doc->addField(\Zend_Search_Lucene_Field::Keyword('fileId', $file->getId()));
+
+ // Store document path for the search results
+ $doc->addField(\Zend_Search_Lucene_Field::Text('path', $file->getPath(), 'UTF-8'));
+
+ $doc->addField(\Zend_Search_Lucene_Field::unIndexed('mtime', $file->getMTime()));
+
+ $doc->addField(\Zend_Search_Lucene_Field::unIndexed('size', $file->getSize()));
+
+ $doc->addField(\Zend_Search_Lucene_Field::unIndexed('mimetype', $mimeType));
+
+ $this->index->updateFile($doc, $file->getId(), $commit);
+
+ return true;
+
+ }
+
+}
diff --git a/lucene/notindexedexception.php b/lucene/notindexedexception.php
new file mode 100644
index 000000000..ee269b78d
--- /dev/null
+++ b/lucene/notindexedexception.php
@@ -0,0 +1,14 @@
+
+ * @copyright Jörn Friedrich Dreyer 2012-2014
+ */
+
+namespace OCA\Search_Lucene\Lucene;
+
+class NotIndexedException extends \Exception {}
\ No newline at end of file
diff --git a/lucene/skippedexception.php b/lucene/skippedexception.php
new file mode 100644
index 000000000..a20a240ab
--- /dev/null
+++ b/lucene/skippedexception.php
@@ -0,0 +1,14 @@
+
+ * @copyright Jörn Friedrich Dreyer 2012-2014
+ */
+
+namespace OCA\Search_Lucene\Lucene;
+
+class SkippedException extends \Exception {}
\ No newline at end of file
diff --git a/lucene/vanishedexception.php b/lucene/vanishedexception.php
new file mode 100644
index 000000000..845eaa575
--- /dev/null
+++ b/lucene/vanishedexception.php
@@ -0,0 +1,14 @@
+
+ * @copyright Jörn Friedrich Dreyer 2012-2014
+ */
+
+namespace OCA\Search_Lucene\Lucene;
+
+class VanishedException extends \Exception {}
\ No newline at end of file
diff --git a/search/luceneprovider.php b/search/luceneprovider.php
new file mode 100644
index 000000000..5dcc2e6d1
--- /dev/null
+++ b/search/luceneprovider.php
@@ -0,0 +1,82 @@
+
+ * @copyright Jörn Friedrich Dreyer 2012-2014
+ */
+
+namespace OCA\Search_Lucene\Search;
+
+use OCA\Search_Lucene\AppInfo\Application;
+
+/**
+ * @author Jörn Dreyer
+ */
+class LuceneProvider extends \OCP\Search\Provider {
+
+ /**
+ * performs a search on the users index
+ *
+ * @author Jörn Dreyer
+ *
+ * @param string $query lucene search query
+ * @return \OCA\Search_Lucene\Search\LuceneResult[]
+ */
+ public function search($query){
+
+ $app = new Application();
+ $container = $app->getContainer();
+
+ $results=array();
+ if ( $query !== null ) {
+ // * query * kills performance for bigger indexes
+ // query * works ok
+ // query is still best
+ //FIXME emulates the old search but breaks all the nice lucene search query options
+ //$query = '*' . $query . '*';
+ //if (strpos($query, '*')===false) {
+ // $query = $query.='*'; // append query *, works ok
+ // TODO add end user guide for search terms ...
+ //}
+ try {
+ $index = $container->query('Index');
+ //default is 3, 0 needed to keep current search behaviour
+ //Zend_Search_Lucene_Search_Query_Wildcard::setMinPrefixLength(0);
+
+ //$term = new Zend_Search_Lucene_Index_Term($query);
+ //$query = new Zend_Search_Lucene_Search_Query_Term($term);
+
+ $hits = $index->find($query);
+
+ //limit results. we cant show more than ~30 anyway. TODO use paging later
+ for ($i = 0; $i < 30 && $i < count($hits); $i++) {
+ $results[] = new LuceneResult($hits[$i]);
+ }
+
+ } catch ( \Exception $e ) {
+ $container->query('Logger')->error( $e->getMessage().' Trace:\n'.$e->getTraceAsString() );
+ }
+
+ }
+ return $results;
+ }
+
+ /**
+ * get the base type of a mimetype string
+ *
+ * returns 'text' for 'text/plain'
+ *
+ * @author Jörn Dreyer
+ *
+ * @param string $mimetype mimetype
+ * @return string basetype
+ */
+ public static function baseTypeOf($mimetype) {
+ return substr($mimetype, 0, strpos($mimetype, '/'));
+ }
+
+}
\ No newline at end of file
diff --git a/search/luceneresult.php b/search/luceneresult.php
new file mode 100644
index 000000000..036715efc
--- /dev/null
+++ b/search/luceneresult.php
@@ -0,0 +1,83 @@
+
+ * @copyright Jörn Friedrich Dreyer 2012-2014
+ */
+
+namespace OCA\Search_Lucene\Search;
+use OC\Files\Filesystem;
+
+/**
+ * A found file
+ */
+class LuceneResult extends \OC\Search\Result\File {
+
+ /**
+ * Type name; translated in templates
+ * @var string
+ */
+ public $type = 'lucene';
+
+ /**
+ * @var float
+ */
+ public $score;
+
+ /**
+ * Create a new content search result
+ * @param \Zend_Search_Lucene_Search_QueryHit $hit file data given by provider
+ */
+ public function __construct(\Zend_Search_Lucene_Search_QueryHit $hit) {
+ $this->id = (string)$hit->fileId;
+ $this->path = $this->getRelativePath($hit->path);
+ $this->name = basename($this->path);
+ $this->size = (int)$hit->size;
+ $this->score = $hit->score;
+ $this->link = \OCP\Util::linkTo(
+ 'files',
+ 'index.php',
+ array('dir' => dirname($this->path), 'file' => $this->name)
+ );
+ $this->permissions = $this->getPermissions($this->path);
+ $this->modified = (int)$hit->mtime;
+ $this->mime_type = $hit->mimetype;
+ }
+
+ protected function getRelativePath ($path) {
+ $root = \OC::$server->getUserFolder();
+ return $root->getRelativePath($path);
+ }
+
+ /**
+ * Determine permissions for a given file path
+ * @param string $path
+ * @return int
+ */
+ function getPermissions($path) {
+ // add read permissions
+ $permissions = \OCP\PERMISSION_READ;
+ // get directory
+ $fileinfo = pathinfo($path);
+ $dir = $fileinfo['dirname'] . '/';
+ // add update permissions
+ if (Filesystem::isUpdatable($dir)) {
+ $permissions |= \OCP\PERMISSION_UPDATE;
+ }
+ // add delete permissions
+ if (Filesystem::isDeletable($dir)) {
+ $permissions |= \OCP\PERMISSION_DELETE;
+ }
+ // add share permissions
+ if (Filesystem::isSharable($dir)) {
+ $permissions |= \OCP\PERMISSION_SHARE;
+ }
+ // return
+ return $permissions;
+ }
+
+}
diff --git a/tests/unit/README.md b/tests/unit/README.md
new file mode 100644
index 000000000..4d0e83e7c
--- /dev/null
+++ b/tests/unit/README.md
@@ -0,0 +1,60 @@
+Testing search lucene
+=====================
+
+hooks.php
+---------
+
+make sure
+- [ ] indexFile() registers an IndexJob
+- [ ] renameFile() deletes the file from lucene index and adds an IndexJob
+- [ ] deleteFile() deletes the file from lucene index
+
+indexer.php
+-----------
+
+- [ ] indexFile() adds file to the index if possible (different filetypes)
+- [ ] indexFiles() indexes files and changes status accordingly
+
+
+indexjob.php
+------------
+
+- [ ] run() sets up correct FS and indexes all unindexed files
+
+lucene.php
+----------
+
+- [ ] openOrCreate() creates the index on the fly, opens existing index, check readonly index?
+- [ ] optimizeIndex() optimizes the index?
+- [ ] updateFile() deletes the old entry and adds a new one
+- [ ] deleteFile() deletes the old entry
+- [ ] find() finds an entry, try various queries
+
+optimizejob.php
+---------------
+
+- [ ] run() cleans up entries without a fileid (removing old pk entries), optimizes the index
+
+searchprovider.php
+------------------
+
+- [x] creates valid oc search result objects
+
+status.php
+----------
+
+
+- [x] fromFileId() loads a status
+- [x] markNew() sets status to N
+- [x] markIndexed() sets status to I
+- [x] markSkipped() sets status to S
+- [x] markError() sets status to E
+- [ ] delete() deletes a status
+- [ ] getUnindexed() return list of unindexed file ids
+- [ ] getDeleted() returns list of deleted file ids
+
+testcase.php
+------------
+
+- [x] setUp() create folder and two files
+
diff --git a/tests/unit/bootstrap.php b/tests/unit/bootstrap.php
new file mode 100644
index 000000000..9bc036d21
--- /dev/null
+++ b/tests/unit/bootstrap.php
@@ -0,0 +1,17 @@
+
+
+
+ .
+
+
+
+
+
+ ../../../search_lucene
+
+ ../../../search_lucene/3rdparty
+ ../../../search_lucene/l10n
+ ../../../search_lucene/tests
+
+
+
+
+
+
+
+
+
diff --git a/tests/unit/testcase.php b/tests/unit/testcase.php
new file mode 100644
index 000000000..eb0f033fc
--- /dev/null
+++ b/tests/unit/testcase.php
@@ -0,0 +1,159 @@
+.
+ *
+ */
+
+namespace OCA\Search_Lucene\Tests\Unit;
+
+use OC\Files\Storage\Storage;
+use OC\Files\View;
+use OCA\Search_Lucene\AppInfo\Application;
+use OCA\Search_Lucene\Db\Status;
+use OCA\Search_Lucene\Db\StatusMapper;
+use PHPUnit_Framework_TestCase;
+
+abstract class TestCase extends PHPUnit_Framework_TestCase {
+
+ /**
+ * @var \OC\Files\Storage\Storage $storage
+ */
+ private $storage;
+
+ /**
+ *
+ * @var string $userName user name
+ */
+ private $userName;
+
+ /**
+ * @var \OC\Files\Cache\Scanner
+ */
+ protected $scanner;
+
+ //for search lucene
+ public function setUp() {
+
+ $app = new Application();
+ $container = $app->getContainer();
+
+ // reset backend
+ $um = $container->getServer()->getUserManager();
+ $us = $container->getServer()->getUserSession();
+ $um->clearBackends();
+ $um->registerBackend(new \OC_User_Database());
+
+ // create test user
+ $this->userName = 'test';
+ \OC_User::deleteUser($this->userName);
+ $um->createUser($this->userName, $this->userName);
+
+ \OC_Util::tearDownFS();
+ $us->setUser(null);
+ \OC\Files\Filesystem::tearDown();
+ \OC_Util::setupFS($this->userName);
+
+ $us->setUser($um->get($this->userName));
+
+ $view = new \OC\Files\View('/' . $this->userName . '/files');
+
+ // setup files
+ $filesToCopy = array(
+ 'documents' => array(
+ 'document.pdf',
+ 'document.docx',
+ 'document.odt',
+ 'document.txt',
+ ),
+ /*
+ 'music' => array(
+ 'projekteva-letitrain.mp3',
+ ),
+ 'photos' => array(
+ 'photo.jpg',
+ ),
+ 'videos' => array(
+ 'BigBuckBunny_320x180.mp4',
+ ),
+ */
+ );
+ $count = 0;
+ foreach($filesToCopy as $folder => $files) {
+ foreach($files as $file) {
+ $imgData = file_get_contents(__DIR__ . '/data/' . $file);
+ $view->mkdir($folder);
+ $path = $folder . '/' . $file;
+ $view->file_put_contents($path, $imgData);
+
+ // set mtime to get fixed sorting with respect to recentFiles
+ $count++;
+ $view->touch($path, 1000 + $count);
+ }
+ }
+
+ list($storage, $internalPath) = $view->resolvePath('');
+ /** @var $storage Storage */
+ $this->storage = $storage;
+ $this->scanner = $storage->getScanner();
+
+ // hookup scanner
+ /*
+ $this->scanner->listen('\OC\Files\Cache\Scanner', 'postScanFile', function($path, $storage) {
+ $h = new Hooks();
+ $h->postScanFile($path, $storage);
+ });
+ */
+
+ $this->scanner->scan('');
+
+ // init 3rdparty classloader
+ //new Application();
+ }
+
+ public function tearDown() {
+ if (is_null($this->storage)) {
+ return;
+ }
+ $cache = $this->storage->getCache();
+ $ids = $cache->getAll();
+ $cache->clear();
+ $app = new Application();
+ $container = $app->getContainer();
+ /** @var StatusMapper $mapper */
+ $mapper = $container->query('StatusMapper');
+ foreach ($ids as $id) {
+ $status = new Status();
+ $status->setFileId($id);
+ $mapper->delete($status);
+ }
+ }
+
+ protected function getFileId($path) {
+
+ $view = new View('/' . $this->userName . '/files');
+ $fileInfo = $view->getFileInfo($path);
+
+ if (! empty($fileInfo)) {
+ return $fileInfo->getId();
+ }
+
+ return null;
+ }
+}
diff --git a/tests/unit/testdocumentpdf.php b/tests/unit/testdocumentpdf.php
new file mode 100644
index 000000000..8f84a5d6c
--- /dev/null
+++ b/tests/unit/testdocumentpdf.php
@@ -0,0 +1,38 @@
+.
+ *
+ */
+
+namespace OCA\Search_Lucene\Tests\Unit;
+
+use OCA\Search_Lucene\Document\Pdf;
+
+class TestDocumentPdf extends \PHPUnit_Framework_TestCase {
+
+ function testUpdate() {
+
+ $data = file_get_contents(__DIR__ . '/data/document.pdf');
+
+ $doc = Pdf::loadPdf($data, true);
+
+ echo $doc->getFieldValue('body');
+ }
+}
diff --git a/tests/unit/testindex.php b/tests/unit/testindex.php
new file mode 100644
index 000000000..f5c2c3b5d
--- /dev/null
+++ b/tests/unit/testindex.php
@@ -0,0 +1,86 @@
+.
+ *
+ */
+
+namespace OCA\Search_Lucene\Tests\Unit;
+
+use OCA\Search_Lucene\AppInfo\Application;
+use OCA\Search_Lucene\Lucene\Index;
+
+class TestIndex extends TestCase {
+
+ function testUpdate() {
+
+ // preparation
+ $app = new Application();
+ $container = $app->getContainer();
+
+ // get an index
+ /** @var Index $index */
+ $index = $container->query('Index');
+
+ // add a document
+ $doc = new \Zend_Search_Lucene_Document();
+
+ $doc->addField(\Zend_Search_Lucene_Field::Keyword('fileId', '1'));
+ $doc->addField(\Zend_Search_Lucene_Field::Text('path', '/somewhere/deep/down/the/rabbit/hole' , 'UTF-8'));
+ $doc->addField(\Zend_Search_Lucene_Field::Text('users', 'alice' , 'UTF-8'));
+
+ $index->index->addDocument($doc);
+ $index->commit();
+
+ // search for it
+ $idTerm = new \Zend_Search_Lucene_Index_Term('1', 'fileId');
+ $idQuery = new \Zend_Search_Lucene_Search_Query_Term($idTerm);
+
+ $query = new \Zend_Search_Lucene_Search_Query_Boolean();
+ $query->addSubquery($idQuery);
+ /** @var \Zend_Search_Lucene_Search_QueryHit $hit */
+ $hits = $index->find($query);
+ // get the document from the query hit
+ $foundDoc = $hits[0]->getDocument();
+ $this->assertEquals('alice', $foundDoc->getFieldValue('users'));
+
+ // delete the document from the index
+ //$index->index->delete($hit);
+
+ // change the 'users' key of the document
+ $foundDoc->addField(\Zend_Search_Lucene_Field::Text('users', 'bob' , 'UTF-8'));
+ $this->assertEquals('bob', $foundDoc->getFieldValue('users'));
+
+ // add the document back to the index
+ $index->updateFile($foundDoc, '1');
+
+
+ $idTerm2 = new \Zend_Search_Lucene_Index_Term('1', 'fileId');
+ $idQuery2 = new \Zend_Search_Lucene_Search_Query_Term($idTerm2);
+
+ $query2 = new \Zend_Search_Lucene_Search_Query_Boolean();
+ $query2->addSubquery($idQuery2);
+ /** @var \Zend_Search_Lucene_Search_QueryHit $hit */
+ $hits2 = $index->find($query2);
+ // get the document from the query hit
+ $foundDoc2 = $hits2[0]->getDocument();
+ $this->assertEquals('bob', $foundDoc2->getFieldValue('users'));
+
+ }
+}
diff --git a/tests/unit/testsearchprovider.php b/tests/unit/testsearchprovider.php
new file mode 100644
index 000000000..b02c94e08
--- /dev/null
+++ b/tests/unit/testsearchprovider.php
@@ -0,0 +1,166 @@
+.
+ *
+ */
+
+namespace OCA\Search_Lucene\Tests\Unit;
+
+
+//add 3rdparty folder to include path
+set_include_path(get_include_path() . PATH_SEPARATOR . __DIR__.'/../../3rdparty');
+
+//initialize zend autoloader
+require_once __DIR__ . '/../../3rdparty/Zend/Loader/Autoloader.php';
+\Zend_Loader_Autoloader::getInstance();
+
+class DummyIndex implements \Zend_Search_Lucene_Interface {
+ var $documents = array();
+ public function addDocument(\Zend_Search_Lucene_Document $document) {
+ $this->documents[] = $document;
+ }
+ public function getDocument($id) {
+ return $this->documents[$id];
+ }
+ public function addReference() {}
+ public function closeTermsStream() {}
+ public function commit() {}
+ public function count() {}
+ public function currentTerm() {}
+ public function delete($id) {}
+ public function docFreq(\Zend_Search_Lucene_Index_Term $term) {}
+ public function find($query) {}
+ public function getDirectory() {}
+ public function getFieldNames($indexed = false) {}
+ public function getFormatVersion() {}
+ public function getMaxBufferedDocs() {}
+ public function getMaxMergeDocs() {}
+ public function getMergeFactor() {}
+ public function getSimilarity() {}
+ public function hasDeletions() {}
+ public function hasTerm(\Zend_Search_Lucene_Index_Term $term) {}
+ public function isDeleted($id) {}
+ public function maxDoc() {}
+ public function nextTerm() {}
+ public function norm($id, $fieldName) {}
+ public function numDocs() {}
+ public function optimize() {}
+ public function removeReference() {}
+ public function resetTermsStream() {}
+ public function setFormatVersion($formatVersion) {}
+ public function setMaxBufferedDocs($maxBufferedDocs) {}
+ public function setMaxMergeDocs($maxMergeDocs) {}
+ public function setMergeFactor($mergeFactor) {}
+ public function skipTo(\Zend_Search_Lucene_Index_Term $prefix) {}
+ public function termDocs(\Zend_Search_Lucene_Index_Term $term, $docsFilter = null) {}
+ public function termDocsFilter(\Zend_Search_Lucene_Index_Term $term, $docsFilter = null) {}
+ public function termFreqs(\Zend_Search_Lucene_Index_Term $term, $docsFilter = null) {}
+ public function termPositions(\Zend_Search_Lucene_Index_Term $term, $docsFilter = null) {}
+ public function terms() {}
+ public function undeleteAll() {}
+ public static function getActualGeneration(\Zend_Search_Lucene_Storage_Directory $directory) {}
+ public static function getDefaultSearchField() {}
+ public static function getResultSetLimit() {}
+ public static function getSegmentFileName($generation) {}
+ public static function setDefaultSearchField($fieldName) {}
+ public static function setResultSetLimit($limit) {}
+}
+
+class TestSearchProvider extends TestCase {
+
+ /**
+ * @dataProvider searchResultDataProvider
+ */
+ function testSearchLuceneResultContent(\Zend_Search_Lucene_Search_QueryHit $hit, $fileId, $name, $path, $size, $score, $mimeType, $modified, $container) {
+
+ $searchResult = new \OCA\Search_Lucene\Search\LuceneResult($hit);
+
+ $this->assertInstanceOf('OCA\Search_Lucene\Search\LuceneResult', $searchResult);
+ $this->assertEquals($fileId, $searchResult->id);
+ $this->assertEquals('lucene', $searchResult->type);
+ $this->assertEquals($path, $searchResult->path);
+ $this->assertEquals($name, $searchResult->name);
+ $this->assertEquals($mimeType, $searchResult->mime_type);
+ $this->assertEquals($size, $searchResult->size);
+ $this->assertEquals($score, $searchResult->score);
+ $this->assertEquals($modified, $searchResult->modified);
+ }
+
+ public function searchResultDataProvider() {
+
+ $index = new DummyIndex();
+
+ $doc1 = new \Zend_Search_Lucene_Document();
+ $doc1->addField(\Zend_Search_Lucene_Field::Keyword('fileId', 10));
+ $doc1->addField(\Zend_Search_Lucene_Field::Text('path', '/test/files/documents/document.txt', 'UTF-8'));
+ $doc1->addField(\Zend_Search_Lucene_Field::unIndexed('mtime', 1234567));
+ $doc1->addField(\Zend_Search_Lucene_Field::unIndexed('size', 123));
+ $doc1->addField(\Zend_Search_Lucene_Field::unIndexed('mimetype', 'text/plain'));
+ $index->addDocument($doc1);
+
+ $doc2 = new \Zend_Search_Lucene_Document();
+ $doc2->addField(\Zend_Search_Lucene_Field::Keyword('fileId', 20));
+ $doc2->addField(\Zend_Search_Lucene_Field::Text('path', '/test/files/documents/document.pdf', 'UTF-8'));
+ $doc2->addField(\Zend_Search_Lucene_Field::unIndexed('mtime', 1234567));
+ $doc2->addField(\Zend_Search_Lucene_Field::unIndexed('size', 1234));
+ $doc2->addField(\Zend_Search_Lucene_Field::unIndexed('mimetype', 'application/pdf'));
+ $index->addDocument($doc2);
+
+ $doc3 = new \Zend_Search_Lucene_Document();
+ $doc3->addField(\Zend_Search_Lucene_Field::Keyword('fileId', 30));
+ $doc3->addField(\Zend_Search_Lucene_Field::Text('path', '/test/files/documents/document.mp3', 'UTF-8'));
+ $doc3->addField(\Zend_Search_Lucene_Field::unIndexed('mtime', 1234567));
+ $doc3->addField(\Zend_Search_Lucene_Field::unIndexed('size', 12341234));
+ $doc3->addField(\Zend_Search_Lucene_Field::unIndexed('mimetype', 'audio/mp3'));
+ $index->addDocument($doc3);
+
+ $doc4 = new \Zend_Search_Lucene_Document();
+ $doc4->addField(\Zend_Search_Lucene_Field::Keyword('fileId', 40));
+ $doc4->addField(\Zend_Search_Lucene_Field::Text('path', '/test/files/documents/document.jpg', 'UTF-8'));
+ $doc4->addField(\Zend_Search_Lucene_Field::unIndexed('mtime', 1234567));
+ $doc4->addField(\Zend_Search_Lucene_Field::unIndexed('size', 1234123));
+ $doc4->addField(\Zend_Search_Lucene_Field::unIndexed('mimetype', 'image/jpg'));
+ $index->addDocument($doc4);
+
+ $hit1 = new \Zend_Search_Lucene_Search_QueryHit($index);
+ $hit1->score = 0.4;
+ $hit1->id = 0;
+
+ $hit2 = new \Zend_Search_Lucene_Search_QueryHit($index);
+ $hit2->score = 0.31;
+ $hit2->id = 1;
+
+ $hit3 = new \Zend_Search_Lucene_Search_QueryHit($index);
+ $hit3->score = 0.299;
+ $hit3->id = 2;
+
+ $hit4 = new \Zend_Search_Lucene_Search_QueryHit($index);
+ $hit4->score = 0.001;
+ $hit4->id = 3;
+
+ return array(
+ // hit, name, size, score, mime_type, container
+ array($hit1, '10', 'document.txt', '/documents/document.txt', 123, 0.4, 'text/plain', 1234567, 'documents'),
+ array($hit2, '20', 'document.pdf', '/documents/document.pdf', 1234, 0.31, 'application/pdf', 1234567, 'documents'),
+ array($hit3, '30', 'document.mp3', '/documents/document.mp3', 12341234, 0.299, 'audio/mp3', 1234567, 'documents'),
+ array($hit4, '40', 'document.jpg', '/documents/document.jpg', 1234123, 0.001, 'image/jpg', 1234567, 'documents'),
+ );
+ }
+}
diff --git a/tests/unit/teststatus.php b/tests/unit/teststatus.php
new file mode 100644
index 000000000..b26f441e9
--- /dev/null
+++ b/tests/unit/teststatus.php
@@ -0,0 +1,96 @@
+.
+ *
+ */
+
+namespace OCA\Search_Lucene\Tests\Unit;
+
+use OCA\Search_Lucene\AppInfo\Application;
+use OCA\Search_Lucene\Db\StatusMapper;
+use OCA\Search_Lucene\Db\Status;
+
+class TestStatus extends TestCase {
+
+ /**
+ * @dataProvider statusDataProvider
+ */
+ function testMarkingMethods($fileName, $method, $expectedStatus) {
+
+ // preparation
+ $fileId = $this->getFileId($fileName);
+ $this->assertNotNull($fileId, 'Precondition failed: file id not found!');
+
+ $app = new Application();
+ $container = $app->getContainer();
+ /** @var StatusMapper $mapper */
+ $mapper = $container->query('StatusMapper');
+
+ // run test
+ $status = new Status();
+ $status->setFileId($fileId);
+ $mapper->$method($status);
+
+ $this->assertInstanceOf('OCA\Search_Lucene\Db\Status', $status);
+ $this->assertEquals($fileId, $status->getFileId());
+ $this->assertEquals($expectedStatus, $status->getStatus());
+
+ //check after loading from db
+ $status2 = $mapper->getOrCreateFromFileId($fileId);
+
+ $this->assertInstanceOf('OCA\Search_Lucene\Db\Status', $status2);
+ $this->assertEquals($fileId, $status2->getFileId());
+ $this->assertEquals($status->getFileId(), $status2->getFileId());
+ $this->assertEquals($expectedStatus, $status2->getStatus());
+
+ }
+
+ public function statusDataProvider() {
+ return array(
+ array('/documents/document.pdf', 'markNew', Status::STATUS_NEW),
+ array('/documents/document.pdf', 'markSkipped', Status::STATUS_SKIPPED),
+ array('/documents/document.pdf', 'markIndexed', Status::STATUS_INDEXED),
+ array('/documents/document.pdf', 'markUnIndexed', Status::STATUS_UNINDEXED),
+ array('/documents/document.pdf', 'markError', Status::STATUS_ERROR),
+ array('/documents/document.pdf', 'markVanished', Status::STATUS_VANISHED),
+
+ array('/documents/document.docx', 'markNew', Status::STATUS_NEW),
+ array('/documents/document.docx', 'markSkipped', Status::STATUS_SKIPPED),
+ array('/documents/document.docx', 'markIndexed', Status::STATUS_INDEXED),
+ array('/documents/document.docx', 'markUnIndexed', Status::STATUS_UNINDEXED),
+ array('/documents/document.docx', 'markError', Status::STATUS_ERROR),
+ array('/documents/document.docx', 'markVanished', Status::STATUS_VANISHED),
+
+ array('/documents/document.odt', 'markNew', Status::STATUS_NEW),
+ array('/documents/document.odt', 'markSkipped', Status::STATUS_SKIPPED),
+ array('/documents/document.odt', 'markIndexed', Status::STATUS_INDEXED),
+ array('/documents/document.odt', 'markUnIndexed', Status::STATUS_UNINDEXED),
+ array('/documents/document.odt', 'markError', Status::STATUS_ERROR),
+ array('/documents/document.odt', 'markVanished', Status::STATUS_VANISHED),
+
+ array('/documents/document.txt', 'markNew', Status::STATUS_NEW),
+ array('/documents/document.txt', 'markSkipped', Status::STATUS_SKIPPED),
+ array('/documents/document.txt', 'markIndexed', Status::STATUS_INDEXED),
+ array('/documents/document.txt', 'markUnIndexed', Status::STATUS_UNINDEXED),
+ array('/documents/document.txt', 'markError', Status::STATUS_ERROR),
+ array('/documents/document.txt', 'markVanished', Status::STATUS_VANISHED),
+ );
+ }
+}
diff --git a/3rdparty/pdf2text.php b/utility/pdfparser.php
similarity index 98%
rename from 3rdparty/pdf2text.php
rename to utility/pdfparser.php
index 757c67e26..290bf0c7b 100644
--- a/3rdparty/pdf2text.php
+++ b/utility/pdfparser.php
@@ -16,7 +16,10 @@
* @link https://github.com/philipnorton42/PDFSearch
* @link http://www.hashbangcode.com/blog/zend-lucene-and-pdf-documents-part-2-pdf-data-extraction-437.html
*/
-class App_Search_Helper_PdfParser {
+
+namespace OCA\Search_Lucene\Utility;
+
+class PdfParser {
/**
* Convert a PDF into text.