diff --git a/search2.php b/search2.php index a2e90fc..63b5378 100644 --- a/search2.php +++ b/search2.php @@ -35,15 +35,20 @@ * Contributor( s): ______________________________________ */ +// TODO: do we have to serialise/unserialise the results. this is not optimal!!! + session_start(); require_once("config/dmsDefaults.php"); +require_once(KT_DIR . '/search2/indexing/indexerCore.inc.php'); + require_once(KT_LIB_DIR . "/unitmanagement/Unit.inc"); require_once(KT_LIB_DIR . "/templating/templating.inc.php"); require_once(KT_LIB_DIR . "/dispatcher.inc.php"); require_once(KT_LIB_DIR . "/widgets/forms.inc.php"); require_once(KT_LIB_DIR . "/actions/bulkaction.php"); -require_once(KT_DIR . '/search2/search/search.inc.php'); + +require_once(KT_LIB_DIR . '/browse/DocumentCollection.inc.php'); require_once(KT_LIB_DIR . '/documentmanagement/Document.inc'); require_once(KT_LIB_DIR . '/browse/PartialQuery.inc.php'); @@ -117,7 +122,6 @@ function search2QuerySort($sSortColumn, $sSortOrder) return; } - $results = unserialize($_SESSION['search2_results']); usort($results, 'search2queryCompare'); @@ -132,38 +136,70 @@ function search2QuerySort($sSortColumn, $sSortOrder) */ class Search2Query extends PartialQuery { - function getFolderCount() { return 0; } + function _count($type) + { + $count = 0; + $results = unserialize($_SESSION['search2_results']); + + switch ($type) + { + case 'Document': + return count($results['docs']) + count($results['shortdocs']); + case 'Folder': + return count($results['folders']) + count($results['shortfolders']); + default: + return 0; + } + } + + function getFolderCount() + { + return $this->_count('Folder'); + } function getDocumentCount() { - $results = $_SESSION['search2_results']; - if(isset($results) && !empty($results)){ - return count(unserialize($results)); + return $this->_count('Document'); + } + + function getItems($type, $iStart, $iSize, $sSortColumn, $sSortOrder) + { + // TODO: quick hack. do this more optimally!!!! + $results = unserialize($_SESSION['search2_results']); + + switch ($type) + { + case 'Document': + $type = 'docs'; + break; + case 'Folder': + $type = 'folders'; + break; } - return 0; + + $resultArray = $results[$type]; + foreach($results['short' . $type] as $rec) + { + $resultArray[] = $rec; + } + + $resultArray = array_slice($resultArray, $iStart, $iSize); + $results = array(); + foreach($resultArray as $rec) + { + $results[] = array('id'=>$rec->Id); + } + + return $results; } function getFolders($iBatchSize, $iBatchStart, $sSortColumn, $sSortOrder, $sJoinClause = null, $aJoinParams = null) { - return array(); + return $this->getItems('Folder', $iBatchStart, $iBatchSize, $sSortColumn, $sSortOrder); } function getDocuments($iBatchSize, $iBatchStart, $sSortColumn, $sSortOrder, $sJoinClause = null, $aJoinParams = null) { - search2QuerySort($_GET['sort_on'], $_GET['sort_order']); - $results = unserialize($_SESSION['search2_results']); - - $batch = array(); - - $no_results = count($results); - for($i=0;$i<$no_results;$i++) - { - if ($i < $iBatchStart) continue; - if ($i > $iBatchStart + $iBatchSize) continue; - - $batch[] = array('id'=>$results[$i]->DocumentID); - } - - return $batch; + return $this->getItems('Document', $iBatchStart, $iBatchSize, $sSortColumn, $sSortOrder); } } @@ -208,10 +244,12 @@ class SearchDispatcher extends KTStandardDispatcher { { $expr = parseExpression($query); - $result = $expr->evaluate(); - usort($result, 'rank_compare'); + $results = $expr->evaluate(); + $results = resolveSearchShortcuts($results); - $_SESSION['search2_results'] = serialize($result); + usort($result['docs'], 'rank_compare'); + + $_SESSION['search2_results'] = serialize($results); $_SESSION['search2_query'] = $query; $_SESSION['search2_sort'] = 'rank'; @@ -410,6 +448,15 @@ class SearchDispatcher extends KTStandardDispatcher { $results = unserialize($_SESSION['search2_results']); + // NOTE: sorting results (when it is implemented) might have to be done per section, as it is done with the browse view + + $resultArray = $results['docs']; + foreach($results['folders'] as $f) $resultArray[] = $f; + foreach($results['shortdocs'] as $d) $resultArray[] = $d; + foreach($results['shortfolders'] as $f) $resultArray[] = $f; + + $results = $resultArray; + if (!is_array($results) || count($results) == 0) { $results=array(); diff --git a/search2/indexing/indexerCore.inc.php b/search2/indexing/indexerCore.inc.php index 84c9527..bcf91aa 100755 --- a/search2/indexing/indexerCore.inc.php +++ b/search2/indexing/indexerCore.inc.php @@ -41,17 +41,142 @@ define('SEARCH2_INDEXER_DIR',realpath(dirname(__FILE__)) . '/'); require_once('indexing/extractorCore.inc.php'); require_once(KT_DIR . '/plugins/ktcore/scheduler/schedulerUtil.php'); - class IndexerInconsistencyException extends Exception {}; +// TODO: Query Result Items code should be moved into the Search section. It has less to do with indexing... + class QueryResultItem { - protected $document_id; + protected $id; protected $title; protected $rank; protected $text; - protected $filesize; protected $fullpath; + + public function __construct($id, $title, $rank, $text, $fullpath) + { + $this->id = $id; + $this->title = $title; + $this->rank = $rank; + $this->text = $text; + $this->fullpath = $fullpath; + } + + public function getId() { return $this->id; } + + public function getIsProxy() { return $this instanceof ProxyResultItem; } + public function getIsFolder() { return substr(get_class($this), 0, 6) == 'Folder' ; } + public function getIsDocument() { return substr(get_class($this), 0, 8) == 'Document' ; } + + public function setRank($value) + { + $this->rank = number_format($value,2,'.',','); + } + + public function getIsLive() + { + return true; + } + + public function setTitle($value) + { + $this->title = $value; + } + + public function setText($value) + { + $this->text = $value; + } + + public function getRelevance() { return (float) $this->rank; } + public function getRank() { return $this->getRelevance(); } + public function getText() { return (string) $this->text; } + public function getTitle() { return (string) $this->title; } + public function getFullPath() { return (string) $this->fullpath; } + + protected function __get($property) + { + if (empty($property)) + { + return ''; + } + + $method = 'get' . $property; + if (method_exists($this, $method)) + { + return $this->$method(); + } + return $this->getUnknown(); + } + + protected function getUnknown() + { + return _kt('n/a'); + } + + protected function __set($property, $value) + { + if (empty($property)) + { + return ''; + } + + $method = 'set' . $property; + if (method_exists($this, $method)) + { + return $this->$method($value); + } + throw new Exception("Unknown property '$property' to set on QueryResultItem"); + } +} + +class ProxyResultItem extends QueryResultItem +{ + protected $proxy; + protected $proxyId; + + public function __construct($proxyId, $proxy) + { + parent::__construct($proxyId, $proxy->getTitle, $proxy->getRank(), $proxy->getText(), $proxy->getFullPath()); + $this->proxyId = $proxyId; + $this->proxy = $proxy; + } + + public function getId() { return $this->proxyId; } + public function getTitle() { return $this->proxy->getTitle(); } + public function getRealId() { return $this->proxy->getId(); } + + protected function __get($property) + { + $method = 'get' . $property; + + if (method_exists($this, $method)) + { + return $this->$method(); + } + else + { + return $this->proxy->$method(); + } + } + + protected function __set($property, $value) + { + $method = 'set' . $property; + if (method_exists($this, $method)) + { + return $this->$method($value); + } + else + { + return $this->proxy->$method($value); + } + } +} + +class DocumentResultItem extends QueryResultItem +{ + protected $filesize; protected $live; protected $version; protected $mimeType; @@ -78,17 +203,14 @@ class QueryResultItem protected $mimeDisplay; protected $oemDocumentNo; - public function __construct($document_id, $rank=null, $title=null, $text=null) + public function __construct($document_id, $rank=null, $title=null, $text=null, $fullpath = null) { - $this->document_id=(int) $document_id; - $this->rank= $rank; - $this->title=$title; - $this->text = $text; + parent::__construct($document_id, $title, $rank, $text, $fullpath); $this->live = true; $this->loadDocumentInfo(); } - protected function __isset($property) + /*protected function __isset($property) { switch($property) { @@ -101,8 +223,9 @@ class QueryResultItem throw new Exception("Unknown property '$property' to get on QueryResultItem"); } return true; // should not be reached - } + }*/ + // TODO: this is bad. must refactor to do the query on the group of documents. public function loadDocumentInfo() { global $default; @@ -127,7 +250,7 @@ class QueryResultItem LEFT JOIN users cbu ON d.creator_id=cbu.id LEFT JOIN users ou ON d.owner_id=ou.id WHERE - d.id=$this->document_id"; + d.id=$this->id"; $result = DBUtil::getOneResult($sql); @@ -186,107 +309,116 @@ class QueryResultItem $this->mimeType = $result['mimetype']; $this->mimeIconPath = $result['mime_icon_path']; + if (empty($this->mimeIconPath)) + { + $this->mimeIconPath = 'unspecified_type'; + } $this->mimeDisplay = $result['mime_display']; $this->storagePath = $result['storage_path']; $this->status = Document::getStatusString($result['status_id']); } - protected function __get($property) - { - switch($property) - { - case null: return ''; - case 'DocumentID': return (int) $this->document_id; - case 'Relevance': - case 'Rank': return (float) $this->rank; - case 'Text': return (string) $this->text; - case 'Title': return (string) $this->title; - case 'FullPath': return (string) $this->fullpath; - case 'IsLive': return (bool) $this->live; - case 'Filesize': return $this->filesize; - case 'Version': return (string) $this->version; - case 'Filename': return (string)$this->filename; - case 'FolderId': return (int)$this->folderId; - case 'OemDocumentNo': return (string) $this->oemDocumentNo; - case 'Document': - if (is_null($this->document)) + public function getDocumentID() { return $this->getId(); } + public function getIsLive() { return (bool) $this->live; } + public function getFilesize() { return $this->filesize; } + public function getVersion() { return (string) $this->version; } + public function getFilename() { return (string)$this->filename; } + public function getFolderId() { return (int)$this->folderId; } + public function getOemDocumentNo() { return (string) $this->oemDocumentNo; } + public function getDocument() { if (is_null($this->document)) { - $this->document = Document::get($this->document_id); + $this->document = Document::get($this->id); } - return $this->document; - case 'IsAvailable': - return $this->Document->isLive(); - case 'CheckedOutUser': - case 'CheckedOutBy': - return (string) $this->checkedOutUser; - case 'WorkflowOnly': - case 'Workflow': - return (string)$this->workflow; - case 'WorkflowStateOnly': - case 'WorkflowState': - return (string)$this->workflowState; - case 'WorkflowAndState': - if (is_null($this->workflow)) + return $this->document; } + public function getIsAvailable() { return $this->Document->isLive(); } + public function getCheckedOutUser() { return (string) $this->checkedOutUser; } + public function getCheckedOutByr() { return $this->getCheckedOutUser(); } + public function getWorkflowOnly() { return (string)$this->workflow; } + public function getWorkflow() { return $this->getWorkflow(); } + public function getWorkflowStateOnly() { return (string)$this->workflowState; } + public function getWorkflowState() { return $this->getWorkflowStateOnly(); } + public function getWorkflowAndState() { if (is_null($this->workflow)) { return ''; } - return "$this->workflow - $this->workflowState"; - case 'MimeType': - return (string) $this->mimeType; - case 'MimeIconPath': - return (string) $this->mimeIconPath; - case 'MimeDisplay': - return (string) $this->mimeDisplay; - case 'DateCheckedOut': - return (string) $this->dateCheckedout; - case 'ModifiedBy': - return (string) $this->modifiedBy; - case 'DateModified': - return (string) $this->dateModified; - case 'CreatedBy': - return (string) $this->createdBy; - case 'DateCreated': - return (string) $this->dateCreated; - case 'Owner': - case 'OwnedBy': - return (string) $this->owner; - case 'IsImmutable': - case 'Immutable': - return (bool) $this->immutable; - case 'Status': - return $this->status; - case 'StoragePath': - return $this->storagePath; - case 'DocumentType': - return $this->documentType; - case 'Permissions': - return 'not available'; - case 'CanBeReadByUser': - if (!$this->live) + return "$this->workflow - $this->workflowState"; } + public function getMimeType() { return (string) $this->mimeType; } + public function getMimeIconPath() { return (string) $this->mimeIconPath; } + public function getMimeDisplay() { return (string) $this->mimeDisplay; } + public function getDateCheckedOut() { return (string) $this->dateCheckedout; } + public function getModifiedBy() { return (string) $this->modifiedBy; } + public function getDateModified() { return (string) $this->dateModified; } + public function getCreatedBy() { return (string) $this->createdBy; } + public function getDateCreated() { return (string) $this->dateCreated; } + public function getOwner() { return (string) $this->owner; } + public function getOwnedBy() { return $this->getOwner(); } + public function getIsImmutable() { return (bool) $this->immutable; } + public function getImmutable() { return $this->getIsImmutable(); } + public function getStatus() { return $this->status; } + public function getStoragePath() { return $this->storagePath; } + public function getDocumentType() { return $this->documentType; } + public function getPermissions() { return 'not available'; } + public function getCanBeReadByUser() { if (!$this->live) return false; if (Permission::userHasDocumentReadPermission($this->Document)) return true; if (Permission::adminIsInAdminMode()) return true; - return false; - default: - throw new Exception("Unknown property '$property' to get on QueryResultItem"); - } - return ''; // Should not be reached + return false; } + + +} + +class FolderResultItem extends QueryResultItem +{ + protected $folder; + protected $createdBy; + protected $parentId; + + public function __construct($folder_id, $rank=null, $title=null, $text=null, $fullpath = null) + { + parent::__construct($folder_id, $title, $rank, $text, $fullpath); + $this->loadDocumentInfo(); } - protected function __set($property, $value) + public function getFolderID() { return $this->getId(); } + public function getParentID() { return $this->parentId; } + public function getCreatedBy() { return $this->createdBy; } + public function getMimeIconPath() { return 'folder'; } + public function getFolder() { + return $this->folder; } + + public function loadDocumentInfo() { - switch($property) + global $default; + $this->folder = $folder = Folder::get($this->getFolderID()); + if (PEAR::isError($folder)) { - case 'Rank': $this->rank = number_format($value,2,'.',','); break; - case 'Title': $this->title = $value; break; - case 'Text': $this->text = $value; break; - default: - throw new Exception("Unknown property '$property' to set on QueryResultItem"); + throw new Exception('Database exception! There appears to be an error in the system: ' .$result->getMessage()); } + $this->title = $folder->getName(); + $this->fullpath = '/' . $folder->getFullPath(); + $this->parentId = $folder->getParentId(); + + $user = User::get($folder->getCreatorID()); + $this->createdBy = (PEAR::isError($user))?_kt('Unknown'):$user->getName(); } + +} + +class DocumentShortcutResultItem extends ProxyResultItem +{ + public function getDocumentID() { return $this->getId(); } + public function getMimeIconPath() { return $this->proxy->getMimeIconPath() . '_shortcut'; } + +} + +class FolderShortcutResultItem extends ProxyResultItem +{ + public function getFolderID() { return $this->getId(); } + public function getMimeIconPath() { return 'folder_shortcut'; } + } function MatchResultCompare($a, $b) diff --git a/search2/indexing/indexers/JavaXMLRPCLuceneIndexer.inc.php b/search2/indexing/indexers/JavaXMLRPCLuceneIndexer.inc.php index d95a5cb..f724630 100755 --- a/search2/indexing/indexers/JavaXMLRPCLuceneIndexer.inc.php +++ b/search2/indexing/indexers/JavaXMLRPCLuceneIndexer.inc.php @@ -187,14 +187,11 @@ class JavaXMLRPCLuceneIndexer extends Indexer { try { - $item = new QueryResultItem($document_id); - $item->Title = $hit->Title; - $item->Text = $hit->Content; - $item->Rank = $hit->Rank; + $item = new DocumentResultItem($document_id, $hit->Rank, $hit->Title, $hit->Content); if ($item->CanBeReadByUser) { - $results[$document_id] = $item; + $results['docs'][$document_id] = $item; } } catch(IndexerInconsistencyException $ex) diff --git a/search2/search/expr.inc.php b/search2/search/expr.inc.php index 6f7a293..18c8fc2 100755 --- a/search2/search/expr.inc.php +++ b/search2/search/expr.inc.php @@ -43,6 +43,8 @@ * */ +// TODO: search expression evaluation needs some optimisation + require_once('indexing/indexerCore.inc.php'); require_once('search/fieldRegistry.inc.php'); require_once('search/exprConstants.inc.php'); @@ -157,11 +159,28 @@ class Expr protected $expr_id; + protected $context; + public function __construct() { $this->expr_id = Expr::$node_id++; } + public function appliesToContext() + { + return ExprContext::DOCUMENT; + } + + public function setContext($context) + { + $this->context = $context; + } + + public function getContext() + { + return $this->context; + } + public function getExprId() { return $this->expr_id; @@ -406,6 +425,14 @@ class FieldExpr extends Expr } } +class ExprContext +{ + const DOCUMENT = 1; + const FOLDER = 2; + const DOCUMENT_AND_FOLDER = 3; +} + + class DBFieldExpr extends FieldExpr { /** @@ -966,24 +993,43 @@ class SQLQueryBuilder implements QueryBuilder private $sql; private $db; private $metadata; + private $context; - public function __construct() + public function __construct($context) { - $this->used_tables = array( - 'documents'=>1, - 'document_metadata_version'=>1, - 'document_content_version'=>0, - 'tag_words'=>0, - 'document_fields_link'=>0 - ); - - $this->aliases = array( - 'documents'=>'d', - 'document_metadata_version'=>'dmv', - 'document_content_version'=>'dcv', - 'tag_words'=>'tw', - 'document_fields_link'=>'pdfl' - ); + $this->context = $context; + + switch ($context) + { + case ExprContext::DOCUMENT: + $this->used_tables = array( + 'documents'=>1, + 'document_metadata_version'=>1, + 'document_content_version'=>0, + 'tag_words'=>0, + 'document_fields_link'=>0 + ); + + $this->aliases = array( + 'documents'=>'d', + 'document_metadata_version'=>'dmv', + 'document_content_version'=>'dcv', + 'tag_words'=>'tw', + 'document_fields_link'=>'pdfl' + ); + break; + case ExprContext::FOLDER: + $this->used_tables = array( + 'folders'=>1, + ); + + $this->aliases = array( + 'folders'=>'f', + ); + break; + default: + throw new Exception('This was not expected - Context = ' . $context); + } $this->sql = ''; $this->db = array(); @@ -1013,8 +1059,15 @@ class SQLQueryBuilder implements QueryBuilder } elseif ($expr->isDBExpr()) { - $this->db[] = & $parent; - $this->used_tables[$expr->getTable()]++; + if (($this->context & $expr->appliesToContext()) == $this->context) + { + $this->db[] = & $parent; + $tablename = $expr->getTable(); + if (array_key_exists($tablename, $this->used_tables)) + { + $this->used_tables[$tablename]++; + } + } } elseif ($expr->isOpExpr()) { @@ -1042,12 +1095,16 @@ class SQLQueryBuilder implements QueryBuilder if ($field->isMetadataField()) { - $this->metadata[] = $expr->getParent(); + $this->metadata[] = $expr->getParent(); } elseif ($field->isDBExpr()) { - $this->db[] = $expr->getParent(); - $this->used_tables[$field->getTable()]++; + $this->db[] = $expr->getParent(); + $tablename = $field->getTable(); + if (array_key_exists($tablename, $this->used_tables)) + { + $this->used_tables[$tablename]++; + } } } } @@ -1076,29 +1133,40 @@ class SQLQueryBuilder implements QueryBuilder $left = $expr->left(); $right = $expr->right(); $isNot = $expr->not(); - if ($left->isMetadataField()) + if ($left->isMetadataField() ) { - $offset = $this->resolveMetadataOffset($expr) + 1; - - $fieldset = $left->getField(); - $query = '('; - - if ($isNot) - { - $query .= "df$offset.name IS NULL OR "; - } - - $query .= '(' . "df$offset.name='$fieldset' AND " . $right->getSQL($left, "dfl$offset.value", $expr->op(), $isNot) . ')'; - - - $query .= ')'; - + if ($this->context == ExprContext::DOCUMENT) + { + $offset = $this->resolveMetadataOffset($expr) + 1; + + $fieldset = $left->getField(); + $query = '('; + + if ($isNot) + { + $query .= "df$offset.name IS NULL OR "; + } + + $query .= '(' . "df$offset.name='$fieldset' AND " . $right->getSQL($left, "dfl$offset.value", $expr->op(), $isNot) . ')'; + + $query .= ')'; + } + else + { + $query = 'false'; + } } else { - $fieldname = $this->getFieldnameFromExpr($expr); - - $query = $right->getSQL($left, $left->modifyName($fieldname), $expr->op(), $isNot); + if ($this->context == ExprContext::FOLDER && $left->getContext() != ExprContext::FOLDER) + { + $query = 'false'; + } + else + { + $fieldname = $this->getFieldnameFromExpr($expr); + $query = $right->getSQL($left, $left->modifyName($fieldname), $expr->op(), $isNot); + } } return $query; } @@ -1107,17 +1175,30 @@ class SQLQueryBuilder implements QueryBuilder { if (count($this->metadata) + count($this->db) == 0) { - throw new Exception('nothing to do'); + // return empty result set + return 'select 1 from documents where false'; } - // we are doing this because content table is dependant on metadata table - if ($this->used_tables['document_content_version'] > 0) $this->used_tables['document_metadata_version']++; - $sql = 'SELECT ' . "\n"; - $sql .= + if ($this->context == ExprContext::DOCUMENT) + { + // we are doing this because content table is dependant on metadata table + if ($this->used_tables['document_content_version'] > 0) + { + $this->used_tables['document_metadata_version']++; + } + + $sql .= ' DISTINCT d.id, dmv.name as title'; + } + else + { + $sql .= + ' DISTINCT f.id, f.name as title'; + } + $offset=0; foreach($this->db as $expr) @@ -1133,26 +1214,36 @@ class SQLQueryBuilder implements QueryBuilder } $sql .= - "\n" . 'FROM ' ."\n" . - ' documents d ' ."\n"; + "\n" . 'FROM ' ."\n"; - if ($this->used_tables['document_metadata_version'] > 0) - { - $sql .= ' INNER JOIN document_metadata_version dmv ON d.metadata_version_id=dmv.id' . "\n"; - } - if ($this->used_tables['document_content_version'] > 0) - { - $sql .= ' INNER JOIN document_content_version dcv ON dmv.content_version_id=dcv.id ' . "\n"; - } - if ($this->used_tables['document_fields_link'] > 0) + if ($this->context == ExprContext::DOCUMENT) { - $sql .= ' LEFT JOIN document_fields_link pdfl ON dmv.id=pdfl.metadata_version_id ' . "\n"; - } + $primaryAlias = 'd'; + $sql .= ' documents d ' ."\n"; - if ($this->used_tables['tag_words'] > 0) + if ($this->used_tables['document_metadata_version'] > 0) + { + $sql .= ' INNER JOIN document_metadata_version dmv ON d.metadata_version_id=dmv.id' . "\n"; + } + if ($this->used_tables['document_content_version'] > 0) + { + $sql .= ' INNER JOIN document_content_version dcv ON dmv.content_version_id=dcv.id ' . "\n"; + } + if ($this->used_tables['document_fields_link'] > 0) + { + $sql .= ' LEFT JOIN document_fields_link pdfl ON dmv.id=pdfl.metadata_version_id ' . "\n"; + } + + if ($this->used_tables['tag_words'] > 0) + { + $sql .= ' LEFT OUTER JOIN document_tags dt ON dt.document_id=d.id ' . "\n" . + ' LEFT OUTER JOIN tag_words tw ON dt.tag_id = tw.id ' . "\n"; + } + } + else { - $sql .= ' LEFT OUTER JOIN document_tags dt ON dt.document_id=d.id ' . "\n" . - ' LEFT OUTER JOIN tag_words tw ON dt.tag_id = tw.id ' . "\n"; + $primaryAlias = 'f'; + $sql .= ' folders f ' ."\n"; } $offset = 0; @@ -1171,17 +1262,18 @@ class SQLQueryBuilder implements QueryBuilder $offset++; } - - - $offset=0; - foreach($this->metadata as $expr) + if ($this->context == ExprContext::DOCUMENT) { - $offset++; - $field = $expr->left(); + $offset=0; + foreach($this->metadata as $expr) + { + $offset++; + $field = $expr->left(); - $fieldid = $field->getFieldId(); - $sql .= " LEFT JOIN document_fields_link dfl$offset ON dfl$offset.metadata_version_id=d.metadata_version_id AND dfl$offset.document_field_id=$fieldid" . "\n"; - $sql .= " LEFT JOIN document_fields df$offset ON df$offset.id=dfl$offset.document_field_id" . "\n"; + $fieldid = $field->getFieldId(); + $sql .= " LEFT JOIN document_fields_link dfl$offset ON dfl$offset.metadata_version_id=d.metadata_version_id AND dfl$offset.document_field_id=$fieldid" . "\n"; + $sql .= " LEFT JOIN document_fields df$offset ON df$offset.id=dfl$offset.document_field_id" . "\n"; + } } // Add permissions sql for read access @@ -1189,15 +1281,16 @@ class SQLQueryBuilder implements QueryBuilder $permId = $oPermission->getID(); $oUser = User::get($_SESSION['userID']); $aPermissionDescriptors = KTPermissionUtil::getPermissionDescriptorsForUser($oUser); - $sPermissionDescriptors = (!empty($aPermissionDescriptors)) ? implode(',', $aPermissionDescriptors) : ''; + $sPermissionDescriptors = empty($aPermissionDescriptors)? -1: implode(',', $aPermissionDescriptors); - $sql .= 'LEFT JOIN folders f ON d.folder_id = f.id '. "\n"; - - $sql .= 'INNER JOIN permission_lookups AS PL ON f.permission_lookup_id = PL.id '. "\n"; + $sql .= "INNER JOIN permission_lookups AS PL ON $primaryAlias.permission_lookup_id = PL.id\n"; $sql .= 'INNER JOIN permission_lookup_assignments AS PLA ON PL.id = PLA.permission_lookup_id AND PLA.permission_id = '.$permId. " \n"; + $sql .= "WHERE PLA.permission_descriptor_id IN ($sPermissionDescriptors) AND "; - $sql .= "WHERE PLA.permission_descriptor_id IN ($sPermissionDescriptors) AND dmv.status_id=1 AND d.status_id=1 AND \n "; - + if ($this->context == ExprContext::DOCUMENT) + { + $sql .= "dmv.status_id=1 AND d.status_id=1 AND \n "; + } return $sql; } @@ -1222,8 +1315,6 @@ class SQLQueryBuilder implements QueryBuilder private function resolveJoinOffset($expr) { - - $offset=0; foreach($this->db as $item) { @@ -1242,11 +1333,18 @@ class SQLQueryBuilder implements QueryBuilder $right = $expr->right(); if (DefaultOpCollection::isBoolean($expr)) { - $query = '(' . $this->buildCoreSQLExpr($left) . ' ' . $expr->op() . ' ' . $this->buildCoreSQLExpr($right) . ')'; + $query = '(' . $this->buildCoreSQLExpr($left) . ' ' . $expr->op() . ' ' . $this->buildCoreSQLExpr($right) . ')'; } else { - $query = $this->getSQLEvalExpr($expr); + if (($this->context & $expr->appliesToContext()) == $this->context) + { + $query = $this->getSQLEvalExpr($expr); + } + else + { + $query = 'false'; + } } return $query; @@ -1259,7 +1357,11 @@ class SQLQueryBuilder implements QueryBuilder $sql = $this->buildCoreSQL(); - $sql .= $this->buildCoreSQLExpr($expr); + $expr = $this->buildCoreSQLExpr($expr); + if ($expr != 'false') + { + $sql .= $expr; + } return $sql; } @@ -1299,19 +1401,22 @@ class SQLQueryBuilder implements QueryBuilder $sql .= $value->getSQL($field, $left->modifyName($fieldname), $expr->op(), $expr->not()); } - $moffset=0; - foreach($this->metadata as $expr) + if ($this->context == ExprContext::DOCUMENT) { - $moffset++; - if ($offset++) + $moffset=0; + foreach($this->metadata as $expr) { - $sql .= " $op\n " ; - } + $moffset++; + if ($offset++) + { + $sql .= " $op\n " ; + } - $field = $expr->left(); - $value = $expr->right(); + $field = $expr->left(); + $value = $expr->right(); - $sql .= $value->getSQL($field, "dfl$moffset.value", $expr->getOp()); + $sql .= $value->getSQL($field, "dfl$moffset.value", $expr->getOp()); + } } return $sql; @@ -1331,7 +1436,6 @@ class SQLQueryBuilder implements QueryBuilder if (substr($col, 0, 4) == 'expr' && is_numeric(substr($col, 4))) { - $exprno = substr($col, 4); if ($exprno <= count($this->db)) { @@ -1379,8 +1483,6 @@ class SQLQueryBuilder implements QueryBuilder } return '(' . implode(') AND (', $text) . ')'; } - - } @@ -1444,6 +1546,13 @@ class OpExpr extends Expr $this->has_text=$value; } + public function setContext($context) + { + parent::setContext($context); + $this->left()->setContext($context); + $this->right()->setContext($context); + } + public function getHasDb() { return $this->has_db; @@ -1619,7 +1728,6 @@ class OpExpr extends Expr $point = null; - if ($left_op_match && $right_op_match) { $point = 'point'; } $left_op_match_flex = $left_op_match || ($left->isOpExpr()); @@ -1682,6 +1790,11 @@ class OpExpr extends Expr return $this->getHasDb() && $this->getHasText(); } + public function appliesToContext() + { + return $this->left()->appliesToContext() | $this->right()->appliesToContext(); + } + /** * Enter description here... * @@ -1805,7 +1918,7 @@ class OpExpr extends Expr * @param array $rightres * @return array */ - protected static function intersect($leftres, $rightres) + protected static function _intersect($leftres, $rightres) { if (empty($leftres) || empty($rightres)) { @@ -1814,7 +1927,7 @@ class OpExpr extends Expr $result = array(); foreach($leftres as $item) { - $document_id = $item->DocumentID; + $document_id = $item->Id; if (!$item->IsLive) { @@ -1831,6 +1944,22 @@ class OpExpr extends Expr return $result; } + protected static function intersect($leftres, $rightres) + { + return array( + 'docs'=>self::_intersect($leftres['docs'],$rightres['docs']), + 'folders'=>self::_intersect($leftres['folders'],$rightres['folders']) + ); + } + + protected static function union($leftres, $rightres) + { + return array( + 'docs'=>self::_union($leftres['docs'],$rightres['docs']), + 'folders'=>self::_union($leftres['folders'],$rightres['folders']) + ); + } + /** * The objective of this function is to merge the results so that there is a union of the results, * but there should be no duplicates. @@ -1839,7 +1968,7 @@ class OpExpr extends Expr * @param array $rightres * @return array */ - protected static function union($leftres, $rightres) + protected static function _union($leftres, $rightres) { if (empty($leftres)) { @@ -1855,15 +1984,15 @@ class OpExpr extends Expr { if ($item->IsLive) { - $result[$item->DocumentID] = $item; + $result[$item->Id] = $item; } } foreach($rightres as $item) { - if (!array_key_exists($item->DocumentID, $result) || $item->Rank > $result[$item->DocumentID]->Rank) + if (!array_key_exists($item->Id, $result) || $item->Rank > $result[$item->Id]->Rank) { - $result[$item->DocumentID] = $item; + $result[$item->Id] = $item; } } return $result; @@ -1996,7 +2125,7 @@ class OpExpr extends Expr { if (empty($group)) { return array(); } - $exprbuilder = new SQLQueryBuilder(); + $exprbuilder = new SQLQueryBuilder($this->getContext()); if (count($group) == 1) { @@ -2020,11 +2149,18 @@ class OpExpr extends Expr foreach($rs as $item) { - $document_id = $item['id']; + $id = $item['id']; $rank = $exprbuilder->getRanking($item); - if (!array_key_exists($document_id, $results) || $rank > $results[$document_id]->Rank) + if (!array_key_exists($id, $results) || $rank > $results[$id]->Rank) { - $results[$document_id] = new QueryResultItem($document_id, $rank, $item['title'], $exprbuilder->getResultText($item)); + if ($this->context == ExprContext::DOCUMENT) + { + $results[$id] = new DocumentResultItem($id, $rank, $item['title'], $exprbuilder->getResultText($item)); + } + else + { + $results[$id] = new FolderResultItem($id, $rank, $item['title'], $exprbuilder->getResultText($item)); + } } } @@ -2034,7 +2170,10 @@ class OpExpr extends Expr private function exec_text_query($op, $group) { - if (empty($group)) { return array(); } + if (($this->getContext() != ExprContext::DOCUMENT) || empty($group)) + { + return array(); + } $exprbuilder = new TextQueryBuilder(); @@ -2060,12 +2199,21 @@ class OpExpr extends Expr } return $results; - - } - public function evaluate() + public function evaluate($context = ExprContext::DOCUMENT_AND_FOLDER) { + if ($context == ExprContext::DOCUMENT_AND_FOLDER) + { + $docs = $this->evaluate(ExprContext::DOCUMENT); + $folders = $this->evaluate(ExprContext::FOLDER); + + return array( + 'docs' => $docs['docs'], + 'folders' => $folders['folders']); + } + $this->setContext($context); + $left = $this->left(); $right = $this->right(); $op = $this->op(); @@ -2075,12 +2223,12 @@ class OpExpr extends Expr { $point = 'point'; } + $resultContext = ($this->getContext() == ExprContext::DOCUMENT)?'docs':'folders'; if ($point == 'merge') { - - $leftres = $left->evaluate(); - $rightres = $right->evaluate(); + $leftres = $left->evaluate($context); + $rightres = $right->evaluate($context); switch ($op) { case ExprOp::OP_AND: @@ -2099,31 +2247,33 @@ class OpExpr extends Expr { if ($this->isDBonly()) { - $result = $this->exec_db_query($op, array($this)); + $result[$resultContext] = $this->exec_db_query($op, array($this)); } elseif ($this->isTextOnly()) { - $result = $this->exec_text_query($op, array($this)); + $result[$resultContext] = $this->exec_text_query($op, array($this)); } elseif (in_array($op, array(ExprOp::OP_OR, ExprOp::OP_AND))) { + // do we get to this??? + // TODO: remove me please.... the simpleQuery stuff should go??? $db_group = array(); $text_group = array(); $this->explore($left, $right, $db_group, 'db'); $this->explore($left, $right, $text_group, 'text'); - $db_result = $this->exec_db_query($op, $db_group); - $text_result = $this->exec_text_query($op, $text_group); + $db_result[$resultContext] = $this->exec_db_query($op, $db_group); + $text_result[$resultContext] = $this->exec_text_query($op, $text_group); switch ($op) { case ExprOp::OP_AND: if ($this->debug) print "\n\npoint: intersect\n\n"; - $result = OpExpr::intersect($db_result, $text_result); + $result[$resultContext] = OpExpr::intersect($db_result, $text_result); break; case ExprOp::OP_OR: if ($this->debug) print "\n\nmerge: union\n\n"; - $result = OpExpr::union($db_result, $text_result); + $result[$resultContext] = OpExpr::union($db_result, $text_result); break; default: throw new Exception('how did this happen??'); @@ -2141,9 +2291,9 @@ class OpExpr extends Expr } $permResults = array(); - foreach($result as $idx=>$item) + foreach($result[$resultContext] as $idx=>$item) { - $permResults[$idx] = $item; + $permResults[$resultContext][$idx] = $item; } return $permResults; diff --git a/search2/search/fields/CreatedByField.inc.php b/search2/search/fields/CreatedByField.inc.php index 3d872c5..ab930a3 100755 --- a/search2/search/fields/CreatedByField.inc.php +++ b/search2/search/fields/CreatedByField.inc.php @@ -7,31 +7,31 @@ * Document Management Made Simple * Copyright (C) 2008 KnowledgeTree Inc. * Portions copyright The Jam Warehouse Software (Pty) Limited - * + * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU General Public License version 3 as published by the * Free Software Foundation. - * + * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. - * + * * You should have received a copy of the GNU General Public License * along with this program. If not, see . - * - * You can contact KnowledgeTree Inc., PO Box 7775 #87847, San Francisco, + * + * You can contact KnowledgeTree Inc., PO Box 7775 #87847, San Francisco, * California 94120-7775, or email info@knowledgetree.com. - * + * * The interactive user interfaces in modified source and object code versions * of this program must display Appropriate Legal Notices, as required under * Section 5 of the GNU General Public License version 3. - * + * * In accordance with Section 7(b) of the GNU General Public License version 3, * these Appropriate Legal Notices must retain the display of the "Powered by - * KnowledgeTree" logo and retain the original copyright notice. If the display of the + * KnowledgeTree" logo and retain the original copyright notice. If the display of the * logo is not reasonably feasible for technical reasons, the Appropriate Legal Notices - * must display the words "Powered by KnowledgeTree" and retain the original + * must display the words "Powered by KnowledgeTree" and retain the original * copyright notice. * Contributor( s): ______________________________________ * @@ -47,6 +47,25 @@ class CreatedByField extends DBFieldExpr $this->matchField('name'); } + public function appliesToContext() + { + return ExprContext::DOCUMENT | ExprContext::FOLDER; + } + + public function setContext($context) + { + parent::setContext($context); + switch($context) + { + case ExprContext::DOCUMENT: + $this->table = 'documents'; + break; + case ExprContext::FOLDER: + $this->table = 'folders'; + break; + } + } + public function getInputRequirements() { return array('value'=>array('type'=>FieldInputType::USER_LIST)); diff --git a/search2/search/fields/FullPathField.inc.php b/search2/search/fields/FullPathField.inc.php index 28665bc..b758b06 100644 --- a/search2/search/fields/FullPathField.inc.php +++ b/search2/search/fields/FullPathField.inc.php @@ -7,31 +7,31 @@ * Document Management Made Simple * Copyright (C) 2008 KnowledgeTree Inc. * Portions copyright The Jam Warehouse Software (Pty) Limited - * + * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU General Public License version 3 as published by the * Free Software Foundation. - * + * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. - * + * * You should have received a copy of the GNU General Public License * along with this program. If not, see . - * - * You can contact KnowledgeTree Inc., PO Box 7775 #87847, San Francisco, + * + * You can contact KnowledgeTree Inc., PO Box 7775 #87847, San Francisco, * California 94120-7775, or email info@knowledgetree.com. - * + * * The interactive user interfaces in modified source and object code versions * of this program must display Appropriate Legal Notices, as required under * Section 5 of the GNU General Public License version 3. - * + * * In accordance with Section 7(b) of the GNU General Public License version 3, * these Appropriate Legal Notices must retain the display of the "Powered by - * KnowledgeTree" logo and retain the original copyright notice. If the display of the + * KnowledgeTree" logo and retain the original copyright notice. If the display of the * logo is not reasonably feasible for technical reasons, the Appropriate Legal Notices - * must display the words "Powered by KnowledgeTree" and retain the original + * must display the words "Powered by KnowledgeTree" and retain the original * copyright notice. * Contributor( s): ______________________________________ * @@ -47,6 +47,25 @@ class FullPathField extends DBFieldExpr $this->setAlias('FullPath'); } + public function setContext($context) + { + parent::setContext($context); + switch($context) + { + case ExprContext::DOCUMENT: + $this->table = 'documents'; + break; + case ExprContext::FOLDER: + $this->table = 'folders'; + break; + } + } + + public function appliesToContext($context) + { + return ExprContext::DOCUMENT | ExprContext::FOLDER; + } + public function getInputRequirements() { return array('value'=>array('type'=>FieldInputType::TEXT)); diff --git a/search2/search/fields/TitleField.inc.php b/search2/search/fields/TitleField.inc.php index 6cda879..abddf9f 100755 --- a/search2/search/fields/TitleField.inc.php +++ b/search2/search/fields/TitleField.inc.php @@ -7,31 +7,31 @@ * Document Management Made Simple * Copyright (C) 2008 KnowledgeTree Inc. * Portions copyright The Jam Warehouse Software (Pty) Limited - * + * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU General Public License version 3 as published by the * Free Software Foundation. - * + * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. - * + * * You should have received a copy of the GNU General Public License * along with this program. If not, see . - * - * You can contact KnowledgeTree Inc., PO Box 7775 #87847, San Francisco, + * + * You can contact KnowledgeTree Inc., PO Box 7775 #87847, San Francisco, * California 94120-7775, or email info@knowledgetree.com. - * + * * The interactive user interfaces in modified source and object code versions * of this program must display Appropriate Legal Notices, as required under * Section 5 of the GNU General Public License version 3. - * + * * In accordance with Section 7(b) of the GNU General Public License version 3, * these Appropriate Legal Notices must retain the display of the "Powered by - * KnowledgeTree" logo and retain the original copyright notice. If the display of the + * KnowledgeTree" logo and retain the original copyright notice. If the display of the * logo is not reasonably feasible for technical reasons, the Appropriate Legal Notices - * must display the words "Powered by KnowledgeTree" and retain the original + * must display the words "Powered by KnowledgeTree" and retain the original * copyright notice. * Contributor( s): ______________________________________ * @@ -39,13 +39,31 @@ class TitleField extends DBFieldExpr { - public function __construct() { parent::__construct('name', 'document_metadata_version', _kt('Title')); $this->setAlias('Title'); } + public function setContext($context) + { + parent::setContext($context); + switch($context) + { + case ExprContext::DOCUMENT: + $this->table = 'document_metadata_version'; + break; + case ExprContext::FOLDER: + $this->table = 'folders'; + break; + } + } + + public function appliesToContext() + { + return ExprContext::DOCUMENT | ExprContext::FOLDER; + } + public function getInputRequirements() { return array('value'=>array('type'=>FieldInputType::TEXT)); diff --git a/search2/search/search.inc.php b/search2/search/search.inc.php index a36db8d..f9a6e44 100755 --- a/search2/search/search.inc.php +++ b/search2/search/search.inc.php @@ -42,6 +42,8 @@ require_once('search/fieldRegistry.inc.php'); require_once('search/expr.inc.php'); require_once(KT_LIB_DIR . '/security/Permission.inc'); +// TODO: move standalone functions into a class.... what was I thinking? + function rank_compare($a, $b) { if ($a->Rank == $b->Rank) @@ -66,6 +68,8 @@ function searchfix($str) return str_replace(array("\n","\r"), array('',''), addslashes($str)); } +// TODO: replace manual json construction with json_encode(). + class SearchHelper { public static function correctPath($path) @@ -366,8 +370,6 @@ class SearchHelper dtfl.document_type_id=$documentTypeID ORDER BY df.name"; - - } else { @@ -566,7 +568,8 @@ function processSearchExpression($query) { $expr = parseExpression($query); - $rs = $expr->evaluate(); + $rs = $expr->evaluate(ExprContext::DOCUMENT); + $rs = $rs['docs']; usort($rs, 'rank_compare'); $results = array(); @@ -629,4 +632,57 @@ function processSearchExpression($query) } } +function resolveSearchShortcuts($result) +{ + $oPermission =& KTPermission::getByName('ktcore.permissions.read'); + $permId = $oPermission->getID(); + + $oUser = User::get($_SESSION['userID']); + $aPermissionDescriptors = KTPermissionUtil::getPermissionDescriptorsForUser($oUser); + $sPermissionDescriptors = empty($aPermissionDescriptors)? -1: implode(',', $aPermissionDescriptors); + + $documentIds = implode(',',array_keys($result['docs'])); + $linkedDocuments = array(); + if (!empty($documentIds)) + { + $sql = "SELECT d.id, d.linked_document_id from documents d "; + $sql .= 'INNER JOIN permission_lookups AS PL ON d.permission_lookup_id = PL.id '. "\n"; + $sql .= 'INNER JOIN permission_lookup_assignments AS PLA ON PL.id = PLA.permission_lookup_id AND PLA.permission_id = '.$permId. " \n"; + $sql .= " WHERE d.linked_document_id in ($documentIds) AND PLA.permission_descriptor_id IN ($sPermissionDescriptors)"; + + $rs = DBUtil::getResultArray($sql); + + foreach($rs as $row) + { + $id = $row['id']; + $linked_id = $row['linked_document_id']; + + $result['shortdocs'][$id] = new DocumentShortcutResultItem($id, $result['docs'][$linked_id]); + } + } + + $folderIds = implode(',',array_keys($result['folders'])); + $linkedFolders = array(); + + if (!empty($folderIds)) + { + + $sql = "SELECT f.id, f.linked_folder_id from folders f "; + $sql .= 'INNER JOIN permission_lookups AS PL ON f.permission_lookup_id = PL.id '. "\n"; + $sql .= 'INNER JOIN permission_lookup_assignments AS PLA ON PL.id = PLA.permission_lookup_id AND PLA.permission_id = '.$permId. " \n"; + $sql .= " WHERE f.linked_folder_id in ($folderIds) AND PLA.permission_descriptor_id IN ($sPermissionDescriptors)"; + + $rs = DBUtil::getResultArray($sql); + + foreach($rs as $row) + { + $id = $row['id']; + $linked_id = $row['linked_folder_id']; + + $result['shortfolders'][$id] = new FolderShortcutResultItem($id, $result['folders'][$linked_id]); + } + } + return $result; +} + ?> diff --git a/templates/ktcore/search2/search_results.smarty b/templates/ktcore/search2/search_results.smarty index 2d4d2ef..33f340a 100755 --- a/templates/ktcore/search2/search_results.smarty +++ b/templates/ktcore/search2/search_results.smarty @@ -46,7 +46,6 @@ function saveSearch() url='{/literal}{$rootUrl}{literal}/search2/ajax/saveExpr.php?txtName='+ escape(txtName.value) + '&txtQuery=' + escape('{/literal}{$txtQuery|escape:'quotes'}{literal}'); - Ext.Ajax.request( { url: url, @@ -132,57 +131,82 @@ function onShowAll(showall) {assign var=cbid value=0} - {foreach item=document from=$results} + {i18n arg_count=$numResults}Search results found: #count#{/i18n} +
+
+ + {foreach item=hit from=$results} + {if $hit->IsDocument} + {i18n}Document ID:{/i18n} {$hit->Id} +    + {i18n}Version:{/i18n} {$hit->Version} + {else} + {i18n}Folder ID:{/i18n} {$hit->Id} + {/if} - - + + {if $hit->IsDocument} + {/if}
-   {$document->Title|truncate:80} - {if $document->Title != $document->Filename} + IsDocument}name="selection_d[]"{else}name="selection_f[]"{/if} id="cb{$cbid}" value="{$hit->Id}"> + + + {if $hit->IsDocument} +   {$hit->Title|truncate:80} + {if $hit->Title != $hit->Filename}    - - Filename: {$document->Filename|truncate:40} + - Filename: {$hit->Filename|truncate:40} {/if} - {if $document->IsAvailable} + {if $hit->IsAvailable}    - + {/if} - {if !$document->IsAvailable} + {if !$hit->IsAvailable}    * {i18n}NOT AVAILABLE{/i18n} * {/if} + {else} +   {$hit->Title|truncate:80} + + {/if} + - {i18n}Document ID:{/i18n} {$document->DocumentID} -    - {i18n}Version:{/i18n} {$document->Version}
{$document->Text}
{$document->FullPath|truncate:40}/{$document->Title|truncate:40} - {$document->Filesize} - +
{$hit->Text}
+ {if $hit->IsDocument} + {$hit->FullPath|truncate:40}/{$hit->Title|truncate:40} - {$hit->Filesize} + {else} + {$hit->FullPath|truncate:40} + {/if} - {i18n}Created By:{/i18n} {$document->CreatedBy} - {i18n}on{/i18n} {$document->DateCreated} + {i18n}Created By:{/i18n} {$hit->CreatedBy} + {if $hit->IsDocument} + {i18n}on{/i18n} {$hit->DateCreated} + {/if}
{if $workflow != ''} - {i18n}Workflow:{/i18n} $document->Workflow} + {i18n}Workflow:{/i18n} $hit->Workflow} {/if} - {if $document->CheckedOutUser != ''} - {i18n}Checked out by:{/i18n} {$document->CheckedOutUser} - {i18n}on{/i18n} {$document->DateCheckedOut} + {if $hit->CheckedOutUser != ''} + {i18n}Checked out by:{/i18n} {$hit->CheckedOutUser} + {i18n}on{/i18n} {$hit->DateCheckedOut} {else} - {i18n}Modified by:{/i18n} {$document->ModifiedBy} - {i18n}on{/i18n} {$document->DateModified} + {i18n}Modified by:{/i18n} {$hit->ModifiedBy} + {i18n}on{/i18n} {$hit->DateModified} {/if}