diff --git a/lib/api/ktcmis/ktcmis.inc.php b/lib/api/ktcmis/ktcmis.inc.php index 41024b6..b9fe28a 100644 --- a/lib/api/ktcmis/ktcmis.inc.php +++ b/lib/api/ktcmis/ktcmis.inc.php @@ -538,7 +538,7 @@ class KTObjectService extends KTCMISBase { * @param string $versioningState optional version state value: checkedout/major/minor * @return string $objectId The id of the created folder object */ - function createDocument($repositoryId, $typeId, $properties, $folderId = null, + public function createDocument($repositoryId, $typeId, $properties, $folderId = null, $contentStream = null, $versioningState = null) { $objectId = null; @@ -570,7 +570,7 @@ class KTObjectService extends KTCMISBase { * @param string $folderId The id of the folder which will be the parent of the created folder object * @return string $objectId The id of the created folder object */ - function createFolder($repositoryId, $typeId, $properties, $folderId) + public function createFolder($repositoryId, $typeId, $properties, $folderId) { $objectId = null; @@ -592,6 +592,34 @@ class KTObjectService extends KTCMISBase { } /** + * Moves a fileable object from one folder to another. + * + * @param object $repositoryId + * @param object $objectId + * @param object $changeToken [optional] + * @param object $targetFolderId + * @param object $sourceFolderId [optional] + */ + public function moveObject($repositoryId, $objectId, $changeToken = '', $targetFolderId, $sourceFolderId = null) + { + try { + $this->ObjectService->moveObject($repositoryId, $objectId, $changeToken, $targetFolderId, $sourceFolderId); + } + catch (Exception $e) + { + return array( + "status_code" => 1, + "message" => $e->getMessage() + ); + } + + return array( + 'status_code' => 0, + 'results' => $objectId + ); + } + + /** * Deletes an object from the repository * * @param string $repositoryId @@ -601,10 +629,10 @@ class KTObjectService extends KTCMISBase { */ // NOTE Invoking this service method on an object SHALL not delete the entire version series for a Document Object. // To delete an entire version series, use the deleteAllVersions() service - function deleteObject($repositoryId, $objectId, $changeToken = null) + public function deleteObject($repositoryId, $objectId, $changeToken = null) { try { - $result = $this->ObjectService->deleteObject($repositoryId, $objectId, $changeToken); + $this->ObjectService->deleteObject($repositoryId, $objectId, $changeToken); } catch (Exception $e) { diff --git a/lib/api/ktcmis/services/CMISObjectService.inc.php b/lib/api/ktcmis/services/CMISObjectService.inc.php index bcd197c..9629d6d 100644 --- a/lib/api/ktcmis/services/CMISObjectService.inc.php +++ b/lib/api/ktcmis/services/CMISObjectService.inc.php @@ -198,7 +198,7 @@ class CMISObjectService { { // TODO consider checking whether content is encoded (currently we expect encoded) // TODO choose between this and the alternative decode function (see CMISUtil class) - // The current one appears to be miles better (1/0/3 vs 14/4/57 on respective test files) + // this will require some basic benchmarking $contentStream = CMISUtil::decodeChunkedContentStream($contentStream); // NOTE There is a function in CMISUtil to do this, written for the unit tests but since KTUploadManager exists @@ -390,7 +390,7 @@ class CMISObjectService { $response = $this->ktapi->create_folder((int)$folderId, $properties['name'], $sig_username = '', $sig_password = '', $reason = ''); if ($response['status_code'] != 0) { - throw new StorageException('The repository was unable to create the folder - ' . $response['message']); + throw new StorageException('The repository was unable to create the folder: ' . $response['message']); } else { @@ -401,6 +401,74 @@ class CMISObjectService { } /** + * Moves a fileable object from one folder to another. + * + * @param object $repositoryId + * @param object $objectId + * @param object $changeToken [optional] + * @param object $targetFolderId + * @param object $sourceFolderId [optional] + */ + // TODO versioningException: The repository MAY throw this exception if the object is a non-current Document Version. + // TODO check whether object is in fact fileable? not strictly needed, but possibly should be here. + public function moveObject($repositoryId, $objectId, $changeToken = '', $targetFolderId, $sourceFolderId = null) + { + // The $sourceFolderId parameter SHALL be specified if the Repository supports the optional 'unfiling' capability + if (is_null($sourceFolderId)) + { + $RepositoryService = new CMISRepositoryService(); + $info = $RepositoryService->getRepositoryInfo($repositoryId); + $capabilities = $info->getCapabilities(); + // check for unfiling capability + // NOTE this is only required once/if KnowledgeTree allows the source folder id to be optional, + // but it is required for CMIS specification compliance. + if ($capabilities->hasCapabilityUnfiling() === 'true') { + throw new RuntimeException('The source folder id MUST be supplied when unfiling is supported.'); + } + } + + // Attempt to decode $objectId, use as is if not detected as encoded + $tmpObjectId = $objectId; + $tmpObjectId = CMISUtil::decodeObjectId($tmpObjectId, $typeId); + if ($tmpTypeId != 'Unknown') $objectId = $tmpObjectId; + + $targetFolderId = CMISUtil::decodeObjectId($targetFolderId); + + // check type id of object against allowed child types for destination folder + $CMISFolder = new CMISFolderObject($targetFolderId, $this->ktapi); + $allowed = $CMISFolder->getProperty('AllowedChildObjectTypeIds'); + if (!is_array($allowed) || !in_array($typeId, $allowed)) + { + throw new ConstraintViolationException('Parent folder may not hold objects of this type (' . $typeId . ')'); + } + + // throw updateConflictException if the operation is attempting to update an object that is no longer current (as determined by the repository). + $exists = CMISUtil::contentExists($typeId, $objectId, $this->ktapi); + if (!$exists) { + throw new updateConflictException('Unable to move the object as it cannot be found.'); + } + + // TODO add reasons and sig data + // attempt to move object + if ($typeId == 'Folder') { + $response = $this->ktapi->move_folder($objectId, $targetFolderId, $reason, $sig_username, $sig_password); + } + else if ($typeId == 'Document') { + $response = $this->ktapi->move_document($objectId, $targetFolderId, $reason, null, null, $sig_username, $sig_password); + } + else { + $response['status_code'] = 1; + $response['message'] = 'The object type could not be determined.'; + } + + // if failed, throw StorageException + if ($response['status_code'] != 0) + { + throw new StorageException('The repository was unable to move the object: ' . $response['message']); + } + } + + /** * Deletes an object from the repository * * @param string $repositoryId @@ -415,6 +483,7 @@ class CMISObjectService { // determine object type and internal id $objectId = CMISUtil::decodeObjectId($objectId, $typeId); + // TODO this should probably be a function, it is now used in two places... // throw updateConflictException if the operation is attempting to update an object that is no longer current (as determined by the repository). $exists = true; if ($typeId == 'Folder') { @@ -428,6 +497,7 @@ class CMISObjectService { if (PEAR::isError($object)) { $exists = false; } + // TODO check deleted status? } else { $exists = false; @@ -482,8 +552,6 @@ class CMISObjectService { if ($result['status_code'] == 1) { throw new RuntimeException('There was an error deleting the object: ' . $result['message']); } - - return true; } /** diff --git a/lib/api/ktcmis/util/CMISUtil.inc.php b/lib/api/ktcmis/util/CMISUtil.inc.php index 81f4285..f5991ce 100644 --- a/lib/api/ktcmis/util/CMISUtil.inc.php +++ b/lib/api/ktcmis/util/CMISUtil.inc.php @@ -397,30 +397,33 @@ class CMISUtil { return $temp; } - // TODO run evaluations on each of the following two functions and determine which - // is generally more efficienct - /** - * Alternative function for decoding chunked streams, this will decode in blocks of 4. - * Not sure which method is more efficient, this or the function below (this does not - * re-encode but I am working on removing that step for the other function.) + * Checks the contentStream and ensures that it is a correct base64 string; + * This is purely for clients such as CMISSpaces breaking the content into + * chunks before base64 encoding. + * + * If the stream is chunked, it is decoded in chunks and sent back as a single stream. + * If it is not chunked it is decoded as is and sent back as a single stream. + * + * NOTE there is an alternative version of this function called decodeChunkedContentStreamLong. + * that version checks line lengths, which should not be necessary. + * this version merely splits on one or two "=" which is less complex and possibly faster (test this assumption) + * (one or two "=" signs is the specified padding used for base64 encoding at the end of an encoded string, when needed) * - * NOTE The current one appears to be much slower (14/4/57 vs 1/0/3 on respective test files) - * - * @param string $contentStream the base64 encoded content stream - * @return string $decoded the decoded content stream + * @param object $contentStream + * @return string decoded */ - static public function decodeContentStream($contentStream) + static public function decodeChunkedContentStream($contentStream) { $decoded = ''; - $contentStream = preg_replace('/\r?\n+/', '', $contentStream); - - // decode in chunks or 4 chars at a time - for($i = 0, $len = strlen($contentStream); $i < $len; $i += 4) { - $decoded .= base64_decode(substr($contentStream, $i, 4)); + // split the content stream on ={1,2} + $parts = preg_split('/={1,2}/', $contentStream, null, PREG_SPLIT_NO_EMPTY); + foreach($parts as $part) { + // decode, append to output to be re-encoded + $decoded .= base64_decode($part); } - + return $decoded; } @@ -438,7 +441,7 @@ class CMISUtil { * @param object $contentStream * @return string decoded */ - static public function decodeChunkedContentStream($contentStream) + static public function decodeChunkedContentStreamLong($contentStream) { // check the content stream for any lines of unusual length (except the last line, which can be any length) $count = -1; @@ -464,7 +467,7 @@ class CMISUtil { { // check for a new chunk // either we have an equals sign (or two) - if (preg_match('/([^=]*={0,2})(.*)/', $line, $matches)) + if (preg_match('/([^=]*={1,2})(.*)/', $line, $matches)) { $lastChunk = $matches[1]; $nextChunk = $matches[2]; @@ -507,6 +510,39 @@ class CMISUtil { return $decoded; } + + /** + * Function to check whether a specified object exists within the KnowledgeTree system + * + * @param string $typeId + * @param string $objectId + * @param object $ktapi + * @return boolean + */ + public function contentExists($typeId, $objectId, &$ktapi) + { + $exists = true; + if ($typeId == 'Folder') + { + $object = $ktapi->get_folder_by_id($objectId); + if (PEAR::isError($object)) { + $exists = false; + } + } + else if ($typeId == 'Document') + { + $object = $ktapi->get_document_by_id($objectId); + if (PEAR::isError($object)) { + $exists = false; + } + // TODO check deleted status? + } + else { + $exists = false; + } + + return $exists; + } } diff --git a/webservice/atompub/cmis/KT_cmis_atom_server.services.inc.php b/webservice/atompub/cmis/KT_cmis_atom_server.services.inc.php index 8d3b5e4..5d6e746 100644 --- a/webservice/atompub/cmis/KT_cmis_atom_server.services.inc.php +++ b/webservice/atompub/cmis/KT_cmis_atom_server.services.inc.php @@ -36,11 +36,14 @@ include_once CMIS_ATOM_LIB_FOLDER . 'ObjectService.inc.php'; include_once CMIS_ATOM_LIB_FOLDER . 'VersioningService.inc.php'; include_once 'KT_cmis_atom_service_helper.inc.php'; -// TODO auth failed response requires WWW-Authenticate: Basic realm="KnowledgeTree DMS" header +// TODO consider changing all responses from the webservice layer to return PEAR errors or success results instead of the half/half we have at the moment. +// the half/half occurred because on initial services PEAR Error seemed unnecessary, but it has proven useful for some of the newer functions :) /** * AtomPub Service: folder */ +// TODO implement failure responses for documents and folders not found. (KTS-4364: http://issues.knowledgetree.com/browse/KTS-4364) +// NOTE what about documents which are deleted, need to respond that those are also not found. class KT_cmis_atom_service_folder extends KT_atom_service { /** @@ -92,7 +95,7 @@ class KT_cmis_atom_service_folder extends KT_atom_service { /** * Deals with folder service POST actions. - * This includes creation of both folders and documents. + * This includes creation/moving of both folders and documents. */ public function POST_action() { @@ -100,48 +103,101 @@ class KT_cmis_atom_service_folder extends KT_atom_service { $repositories = $RepositoryService->getRepositories(); $repositoryId = $repositories[0]['repositoryId']; + // set default action, objectId and typeId + $action = 'create'; + $objectId = null; + $typeId = null; + $folderId = $this->params[0]; $title = KT_cmis_atom_service_helper::getAtomValues($this->parsedXMLContent['@children'], 'title'); $summary = KT_cmis_atom_service_helper::getAtomValues($this->parsedXMLContent['@children'], 'summary'); $properties = array('name' => $title, 'summary' => $summary); - // determine whether this is a folder or a document create - // document create will have a content tag or containing base64 encoding of the document - $content = KT_cmis_atom_service_helper::getAtomValues($this->parsedXMLContent['@children'], 'content'); + // determine whether this is a folder or a document action + // document action create will have a content tag or containing base64 encoding of the document + // move action will have an existing id supplied as a parameter - not sure how this works yet as the CMIS clients we are + // testing don't support move functionality at this time (2009/07/23) and so we are presuming the following format: + // /folder//children/ + // also possible that there will be an existing ObjectId property, try to cater for both until we know how it really works + + // check for existing object id as parameter in url + if (isset($this->params[2])) + { + $action = 'move'; + $objectId = $this->params[2]; + } + + $cmisObjectProperties = KT_cmis_atom_service_helper::getCmisProperties($this->parsedXMLContent['@children']['cmis:object'] + [0]['@children']['cmis:properties'] + [0]['@children']); + + // check for existing object id as property of submitted object data + if (!empty($cmisObjectProperties['ObjectId'])) + { + $action = 'move'; + $objectId = $cmisObjectProperties['ObjectId']; + } + + // TODO there may be more to do for the checking of an existing object. + // e.g. verifying that it does indeed exist, and throwing an exception if it does not: + // "If the objected property is present but not valid an exception will be thrown" (from CMIS specification) + // NOTE this exception should be thrown in the service API code and not here. + + // determine type if object is being moved + if (!is_null($objectId)) { + CMISUtil::decodeObjectId($objectId, $typeId); + } - // check content for weird chars - $matches = array(); - preg_match('/[^\w\d\/\+\n]*/', $content, $matches); + // now check for content stream + $content = KT_cmis_atom_service_helper::getAtomValues($this->parsedXMLContent['@children'], 'content'); - if (is_null($content)) { + // check content for weird chars - don't think this serves a purpose any longer, should probably be removed. + // was meant to check for any non-base64 characters in the content string. + // preg_match('/[^\w\d\/\+=\n]*/', $content); + // TODO this will possibly need to change somewhat once Relationship Objects come into play. + if ((($action == 'create') && (is_null($content))) || ($typeId == 'Folder')) { $type = 'folder'; } else { $type = 'document'; } - $cmisObjectProperties = KT_cmis_atom_service_helper::getCmisProperties($this->parsedXMLContent['@children']['cmis:object'] - [0]['@children']['cmis:properties'] - [0]['@children']); - $ObjectService = new ObjectService(KT_cmis_atom_service_helper::getKt()); - if ($type == 'folder') - $newObjectId = $ObjectService->createFolder($repositoryId, ucwords($cmisObjectProperties['ObjectTypeId']), $properties, $folderId); - else - $newObjectId = $ObjectService->createDocument($repositoryId, ucwords($cmisObjectProperties['ObjectTypeId']), $properties, $folderId, $content); - - // check if returned Object Id is a valid CMIS Object Id - CMISUtil::decodeObjectId($newObjectId, $typeId); + $success = false; + $error = null; + if ($action == 'create') + { + if ($type == 'folder') + $newObjectId = $ObjectService->createFolder($repositoryId, ucwords($cmisObjectProperties['ObjectTypeId']), $properties, $folderId); + else + $newObjectId = $ObjectService->createDocument($repositoryId, ucwords($cmisObjectProperties['ObjectTypeId']), $properties, $folderId, $content); + + // check if returned Object Id is a valid CMIS Object Id + CMISUtil::decodeObjectId($newObjectId, $typeId); + if ($typeId != 'Unknown') $success = true; + else $error = $newObjectId['message']; + } + else if ($action == 'move') + { + $result = $ObjectService->moveObject($repositoryId, $objectId, '', $folderId); + + if (!PEAR::isError($result)) $success = true; + else $error = $result->getMessage(); + + // same object as before + $newObjectId = $objectId; + $typeId = ucwords($type); + } - if ($typeId != 'Unknown') + if ($success) { - $this->setStatus(self::STATUS_CREATED); + $this->setStatus(($action == 'create') ? self::STATUS_CREATED : self::STATUS_UPDATED); $feed = KT_cmis_atom_service_helper::getObjectFeed($ObjectService, $repositoryId, $newObjectId, 'POST'); } else { - $feed = KT_cmis_atom_service_helper::getErrorFeed($this, self::STATUS_SERVER_ERROR, $newObjectId['message']); + $feed = KT_cmis_atom_service_helper::getErrorFeed($this, self::STATUS_SERVER_ERROR, $error); } //Expose the responseFeed diff --git a/webservice/classes/atompub/cmis/ObjectService.inc.php b/webservice/classes/atompub/cmis/ObjectService.inc.php index 4266046..edc9059 100644 --- a/webservice/classes/atompub/cmis/ObjectService.inc.php +++ b/webservice/classes/atompub/cmis/ObjectService.inc.php @@ -32,6 +32,34 @@ class ObjectService extends KTObjectService { } /** + * Creates a new document within the repository + * + * @param string $repositoryId The repository to which the document must be added + * @param string $typeId Object Type id for the document object being created + * @param array $properties Array of properties which must be applied to the created document object + * @param string $folderId The id of the folder which will be the parent of the created document object + * This parameter is optional IF unfilingCapability is supported + * @param contentStream $contentStream optional content stream data + * @param string $versioningState optional version state value: checkedout/major/minor + * @return string $objectId The id of the created folder object + */ + // TODO throw ConstraintViolationException if: + // value of any of the properties violates the min/max/required/length constraints + // specified in the property definition in the Object-Type. + public function createDocument($repositoryId, $typeId, $properties, $folderId = null, + $contentStream = null, $versioningState = null) + { + $result = parent::createDocument($repositoryId, $typeId, $properties, $folderId, $contentStream, $versioningState); + + if ($result['status_code'] == 0) { + return $result['results']; + } + else { + return $result; + } + } + + /** * Creates a new folder within the repository * * @param string $repositoryId The repository to which the folder must be added @@ -40,7 +68,7 @@ class ObjectService extends KTObjectService { * @param string $folderId The id of the folder which will be the parent of the created folder object * @return string $objectId The id of the created folder object */ - function createFolder($repositoryId, $typeId, $properties, $folderId) + public function createFolder($repositoryId, $typeId, $properties, $folderId) { $result = parent::createFolder($repositoryId, $typeId, $properties, $folderId); @@ -51,32 +79,25 @@ class ObjectService extends KTObjectService { return $result; } } - + /** - * Creates a new document within the repository - * - * @param string $repositoryId The repository to which the document must be added - * @param string $typeId Object Type id for the document object being created - * @param array $properties Array of properties which must be applied to the created document object - * @param string $folderId The id of the folder which will be the parent of the created document object - * This parameter is optional IF unfilingCapability is supported - * @param contentStream $contentStream optional content stream data - * @param string $versioningState optional version state value: checkedout/major/minor - * @return string $objectId The id of the created folder object + * Moves a fileable object from one folder to another. + * + * @param object $repositoryId + * @param object $objectId + * @param object $changeToken [optional] + * @param object $targetFolderId + * @param object $sourceFolderId [optional] */ - // TODO throw ConstraintViolationException if: - // value of any of the properties violates the min/max/required/length constraints - // specified in the property definition in the Object-Type. - function createDocument($repositoryId, $typeId, $properties, $folderId = null, - $contentStream = null, $versioningState = null) + public function moveObject($repositoryId, $objectId, $changeToken = '', $targetFolderId, $sourceFolderId = null) { - $result = parent::createDocument($repositoryId, $typeId, $properties, $folderId, $contentStream, $versioningState); + $result = parent::moveObject($repositoryId, $objectId, $changeToken, $targetFolderId, $sourceFolderId); if ($result['status_code'] == 0) { return $result['results']; } else { - return $result; + return new PEAR_Error($result['message']); } } @@ -90,7 +111,7 @@ class ObjectService extends KTObjectService { */ // NOTE Invoking this service method on an object SHALL not delete the entire version series for a Document Object. // To delete an entire version series, use the deleteAllVersions() service - function deleteObject($repositoryId, $objectId, $changeToken = null) + public function deleteObject($repositoryId, $objectId, $changeToken = null) { $result = parent::deleteObject($repositoryId, $objectId, $changeToken);