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 + +[![Build Status](https://secure.travis-ci.org/owncloud/search_lucene.png)](http://travis-ci.org/owncloud/search_lucene) +[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/owncloud/search_lucene/badges/quality-score.png)](https://scrutinizer-ci.com/g/owncloud/search_lucene/) +[![Scrutinizer Code Coverage](https://scrutinizer-ci.com/g/owncloud/search_lucene/badges/coverage.png)](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.