ktapi = $ktapi; } /** * 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 string $contentStream optional content stream data - expected as a base64 encoded string * @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) { $objectId = null; // fetch type definition of supplied type and check for base type "document", if not true throw exception $RepositoryService = new CMISRepositoryService(); try { $typeDefinition = $RepositoryService->getTypeDefinition($repositoryId, $typeId); } // NOTE Not sure that we should throw this specific exception, maybe just let the underlying // exception propogate upward... // Alternatively: throw new exception with original exception message appended // NOTE The latter method has been adopted for the moment catch (Exception $e) { throw new ConstraintViolationException('Object base type could not be determined. ' . $e->getMessage()); } if ($typeDefinition['attributes']['baseType'] != 'document') { throw new ConstraintViolationException('Object is not of base type document'); } // if no $folderId submitted and repository does not support "unfiling" throw exception if (empty($folderId)) { $repositoryInfo = $RepositoryService->getRepositoryInfo($repositoryId); $capabilities = $repositoryInfo->getCapabilities(); if (!$capabilities->hasCapabilityUnfiling()) { throw new ConstraintViolationException('Repository does not support the Unfiling capability and no folder id was supplied'); } } // Attempt to decode $folderId, use as is if not detected as encoded $tmpObjectId = $folderId; $tmpObjectId = CMISUtil::decodeObjectId($tmpObjectId, $tmpTypeId); if ($tmpTypeId != 'Unknown') $folderId = $tmpObjectId; // if parent folder is not allowed to hold this type, throw exception $CMISFolder = new CMISFolderObject($folderId, $this->ktapi); $allowed = $CMISFolder->getProperty('AllowedChildObjectTypeIds'); $typeAllowed = false; if (is_array($allowed)) { foreach($allowed as $type) { if (strtolower($type) == strtolower($typeId)) { $typeAllowed = true; break; } } } if (!$typeAllowed) { throw new ConstraintViolationException('Parent folder may not hold objects of this type (' . $typeId . ')'); } // if content stream is required and no content stream is supplied, throw a ConstraintViolationException if (($typeDefinition['attributes']['contentStreamAllowed'] == 'required') && is_null($contentStream)) { throw new ConstraintViolationException('This repository requires a content stream for document creation. ' . 'Refusing to create an empty document'); } else if (($typeDefinition['attributes']['contentStreamAllowed'] == 'notAllowed') && !empty($contentStream)) { throw new StreamNotSupportedException('Content Streams are not supported'); } // if versionable attribute is set to false and versioningState is supplied, throw a ConstraintViolationException if (!$typeDefinition['attributes']['versionable'] && !empty($versioningState)) { throw new ConstraintViolationException('This repository does not support versioning'); } // TODO deal with $versioningState when supplied // set title and name identical if only one submitted if ($properties['title'] == '') { $properties['title'] = $properties['name']; } else if ($properties['name'] == '') { $properties['name'] = $properties['title']; } // if name is blank throw exception (check type) - using invalidArgument Exception for now if (trim($properties['name']) == '') { throw new InvalidArgumentException('Refusing to create an un-named document'); } // TODO also set to Default if a non-supported type is submitted if ($properties['type'] == '') { $properties['type'] = 'Default'; } // create the content stream from the supplied data // NOTE since the repository is set to require a content stream and we don't currently have another way to get the document data // this check isn't strictly necessary; however it is needed for a repository which does not support content streams if (!is_null($contentStream)) { $tempfilename = CMISUtil::createTemporaryFile($contentStream); // metadata $metadata = array(); $metaFields = array(); $sysdata = array(); if (!empty($properties['summary'])) { $metadata[] = array('fieldset' => 'Tag Cloud', 'fields' => array( array( 'name' => 'Tag', 'value' => $properties['summary'] ) ) ); } $user = $this->ktapi->get_user(); if (!PEAR::isError($user)) { $metaFields['General Information'][] = array( 'name' => 'Document Author', 'value' => $user->getName() ); } if (!empty($properties['category'])) { $category = $properties['category']; } else { $category = 'Miscellaneous'; } $metaFields['General Information'][] = array( 'name' => 'Category', 'value' => $category ); /** * Try to determine mime type which maps to one of the following KnowledgetTree document types: * * Audio * Image * Text * Video * * example mime types: * * text/plain * image/gif * application/x-dosexec * application/pdf * application/msword * audio/mpeg * application/octet-stream * application/zip */ // TODO check extension for types which are not obvious? e.g. wmv video returns application/octet-stream $mediatype = null; include_once(KT_LIB_DIR . '/mime.inc.php'); $KTMime = new KTMime(); $mimetype = $KTMime->getMimeTypeFromFile($tempfilename); preg_match('/^([^\/]*)\/([^\/]*)/', $mimetype, $matches); if (($matches[1] == 'text') || ($matches[1] == 'image') || ($matches[1] == 'audio')) { $mediatype = ucwords($matches[1]); } else if (($matches[2] == 'pdf') || ($matches[2] == 'msword')) { $mediatype = 'Text'; } if (!is_null($mediatype)) { $metaFields['General Information'][] = array( 'name' => 'Media Type', 'value' => $mediatype ); } if (count($metaFields['General Information']) > 0) { foreach($metaFields['General Information'] as $field) { $fields[] = $field; } $metadata[] = array('fieldset' => 'General Information', 'fields' => $fields); } $response = $this->ktapi->add_document_with_metadata((int)$folderId, $properties['title'], $properties['name'], $properties['type'], $tempfilename, $metadata, $sysdata); if ($response['status_code'] != 0) { throw new StorageException('The repository was unable to create the document. ' . $response['message']); } else { $objectId = CMISUtil::encodeObjectId('Document', $response['results']['document_id']); } // remove temporary file @unlink($tempfilename); } // else create the document object in the database but don't actually create any content since none was supplied // NOTE perhaps this content could be supplied in the $properties array? else { // TODO creation of document without content. leaving this for now as we require content streams and any code // here will therefore never be executed; if we implement some form of template based document creation // then we may have something else to do here; // for now we just throw a general RuntimeException, since we should not // actually reach this code unless something is wrong; this may be removed or replaced later throw new RuntimeException('Cannot create document without a content stream'); } return $objectId; } /** * Creates a new folder within the repository * * @param string $repositoryId The repository to which the folder must be added * @param string $typeId Object Type id for the folder object being created * @param array $properties Array of properties which must be applied to the created folder object * @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 */ // 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 createFolder($repositoryId, $typeId, $properties, $folderId) { $objectId = null; // fetch type definition of supplied type and check for base type "folder", if not true throw exception $RepositoryService = new CMISRepositoryService(); try { $typeDefinition = $RepositoryService->getTypeDefinition($repositoryId, $typeId); } // NOTE Not sure that we should throw this specific exception, maybe just let the underlying // exception propogate upward... // Alternatively: throw new exception with original exception message appended // NOTE The latter method has been adopted for the moment catch (Exception $e) { throw new ConstraintViolationException('Object is not of base type folder. ' . $e->getMessage()); } if ($typeDefinition['attributes']['baseType'] != 'folder') { throw new ConstraintViolationException('Object is not of base type folder'); } // Attempt to decode $folderId, use as is if not detected as encoded $tmpObjectId = $folderId; $tmpObjectId = CMISUtil::decodeObjectId($tmpObjectId, $tmpTypeId); if ($tmpTypeId != 'Unknown') $folderId = $tmpObjectId; // if parent folder is not allowed to hold this type, throw exception $CMISFolder = new CMISFolderObject($folderId, $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 . ')'); } // TODO if name is blank! throw another exception (check type) - using invalidArgument Exception for now if (trim($properties['name']) == '') { throw new InvalidArgumentException('Refusing to create an un-named folder'); } $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']); } else { $objectId = CMISUtil::encodeObjectId('Folder', $response['results']['id']); } return $objectId; } /** * Fetches the properties for the specified object * * @param string $repositoryId * @param string $objectId * @param boolean $includeAllowableActions * @param boolean $includeRelationships * @param boolean $returnVersion * @param string $filter * @return object CMIS object properties */ // TODO optional parameter support // TODO FilterNotValidException: The Repository SHALL throw this exception if this property filter input parameter is not valid public function getProperties($repositoryId, $objectId, $includeAllowableActions, $includeRelationships, $returnVersion = false, $filter = '') { $repository = new CMISRepository($repositoryId); // TODO a better default value? $properties = array(); $objectId = CMISUtil::decodeObjectId($objectId, $typeId); if ($typeId == 'Unknown') { throw new ObjectNotFoundException('The type of the requested object could not be determined'); } switch($typeId) { case 'Document': $CMISObject = new CMISDocumentObject($objectId, $this->ktapi, $repository->getRepositoryURI()); break; case 'Folder': $CMISObject = new CMISFolderObject($objectId, $this->ktapi, $repository->getRepositoryURI()); break; } // check that we were actually able to retrieve a real object $objectId = $CMISObject->getProperty('ObjectId'); if (empty($objectId)) { throw new ObjectNotFoundException('The requested object could not be found'); } $properties = $CMISObject->getProperties(); return $properties; } /** * Fetches the content stream data for an object * * @param string $repositoryId * @param string $objectId * @return string $contentStream (binary or text data) */ // NOTE streamNotSupportedException: The Repository SHALL throw this exception if the Object-Type definition // specified by the objectId parameter’s “contentStreamAllowed” attribute is set to “not allowed”. // function getContentStream($repositoryId, $objectId) { $contentStream = null; // decode $objectId $objectId = CMISUtil::decodeObjectId($objectId, $typeId); // unknown object type? if ($typeId == 'Unknown') { throw new ObjectNotFoundException('The type of the requested object could not be determined'); } // fetch type definition of supplied object type $objectClass = 'CMIS' . $typeId . 'Object'; $CMISObject = new $objectClass($objectId, $this->ktapi); // if content stream is not allowed for this object type definition, throw a ConstraintViolationException if (($CMISObject->getAttribute('contentStreamAllowed') == 'notAllowed')) { // NOTE spec version 0.61c specifies both a ConstraintViolationException and a StreamNotSupportedException // for this case. Choosing to throw StreamNotSupportedException until the specification is clarified // as it is a more specific exception throw new StreamNotSupportedException('Content Streams are not allowed for this object type'); } // now go on to fetching the content stream // TODO allow fetching of partial streams // from the CMIS specification (0.61): // "Each CMIS protocol binding SHALL provide a way for fetching a sub-range within a content stream, in a manner appropriate to that protocol." // steps to fetch content stream: // 1. find actual physical document (see zip/download code) // TODO move this into a ktapi function $document = $this->ktapi->get_document_by_id($objectId); $contentStream = $document->get_document_content(); return $contentStream; } /** * 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 * @param string $objectId * @param string $changeToken [optional] * @return boolean true on success (exception should be thrown otherwise) */ // 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 public function deleteObject($repositoryId, $objectId, $changeToken = null) { // 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') { $object = $this->ktapi->get_folder_by_id($objectId); if (PEAR::isError($object)) { $exists = false; } } else if ($typeId == 'Document') { $object = $this->ktapi->get_document_by_id($objectId); if (PEAR::isError($object)) { $exists = false; } // TODO check deleted status? } else { $exists = false; } if (!$exists) { throw new updateConflictException('Unable to delete the object as it cannot be found.'); } // throw ConstraintViolationException if method is invoked on a Folder object that contains one or more objects if ($typeId == 'Folder') { $folderContent = $object->get_listing(); if (!PEAR::isError($folderContent)) { if (count($folderContent) > 0) { throw new ConstraintViolationException('Unable to delete a folder with content. Use deleteTree instead.'); } } // now try the delete and throw an exception if there is an error // TODO add a default reason // TODO add the electronic signature capability $result = $this->ktapi->delete_folder($objectId, $reason, $sig_username, $sig_password); } else if ($typeId == 'Document') { // since we do not allow deleting of only the latest version we must throw an exception when this function is called on any document // which has more than one version. Okay to delete if there is only the one version. $versions = $object->get_version_history(); if (count($versions) > 1) { // NOTE possibly may want to just throw a RuntimeException rather than this CMIS specific exception. throw new ConstraintViolationException('This function may not be used to delete an object which has multiple versions. ' . 'Since the repository does not allow deleting of only the latest version, nothing can be deleted.'); } // do not allow deletion of a checked out document - this is actually handled by the ktapi code, // but is possibly slightly more efficient to check before trying to delete if ($object->is_checked_out()) { throw new RuntimeException('The object cannot be deleted as it is currently checked out'); } // now try the delete and throw an exception if there is an error // TODO add a default reason // TODO add the electronic signature capability $auth_sig = true; $result = $this->ktapi->delete_document($objectId, $reason, $auth_sig, $sig_username, $sig_password); } // if there was an error performing the delete, throw exception if ($result['status_code'] == 1) { throw new RuntimeException('There was an error deleting the object: ' . $result['message']); } } /** * Deletes an entire tree including all subfolders and other filed objects * * @param string $repositoryId * @param string $objectId * @param string $changeToken [optional] * @param boolean $unfileNonfolderObject [optional] - note that since KnowledgeTree does not allow unfiling this will be ignored * @param boolean $continueOnFailure [optional] - note that since KnowledgeTree does not allow continue on failure this will be ignored * @return array $failedToDelete A list of identifiers of objects in the folder tree that were not deleted. */ // NOTE • A Repository MAY attempt to delete child- and descendant-objects of the specified folder in any order. // • Any child- or descendant-object that the Repository cannot delete SHALL persist in a valid state in the CMIS domain model. // • This is not transactional. // • However, if DeleteSingleFiled is chosen and some objects fail to delete, then single-filed objects are either deleted or kept, // never just unfiled. This is so that a user can call this command again to recover from the error by using the same tree. public function deleteTree($repositoryId, $objectId, $changeToken = null, $unfileNonfolderObject = 'delete', $continueOnFailure = false) { // NOTE since we do not currently allow partial deletes this will always be empty // (unless there is a failure at the requested folder level - what do we do then? exception or array of all objects?) $failedToDelete = array(); // determine object type and internal id $objectId = CMISUtil::decodeObjectId($objectId, $typeId); // 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') { $object = $this->ktapi->get_folder_by_id($objectId); if (PEAR::isError($object)) { $exists = false; } } // if not of type folder then we have a general problem, throw exception else { throw new RuntimeException('Cannot call deleteTree on a non-folder object.'); } if (!$exists) { throw new updateConflictException('Unable to delete the object as it cannot be found.'); } // attempt to delete tree, throw RuntimeException if failed // TODO add a default reason // TODO add the electronic signature capability $result = $this->ktapi->delete_folder($objectId, $reason, $sig_username, $sig_password); // if there was an error performing the delete, throw exception // TODO list of objects which failed in $failedToDelete array; // since we do not delete the folder or any contents if anything cannot be deleted, this will contain the entire tree listing // NOTE once we do this we will need to deal with it externally as well, since we can no longer just catch an exception. if ($result['status_code'] == 1) { // TODO consider sending back full properties on each object? // Not sure yet what this output may be used for by a client, and the current specification (0.61c) says: // "A list of identifiers of objects in the folder tree that were not deleted", so let's leave it returning just ids for now. $failedToDelete[] = CMISUtil::encodeObjectId('Folder', $objectId); $folderContents = $object->get_full_listing(); foreach($folderContents as $folderObject) { if ($folderObject['item_type'] == 'F') $type = 'Folder'; else if ($folderObject['item_type'] == 'D') $type = 'Document'; // TODO deal with non-folder and non-document content else continue; // TODO find out whether this is meant to be a hierarchical list or simply a list. // for now we are just returning the list in non-hierarchical form // (seeing as we don't really know how CMIS AtomPub is planning to deal with hierarchies at this time.) $failedToDelete[] = CMISUtil::encodeObjectId($type, $folderObject['id']); } } return $failedToDelete; } // NOTE this function is presently incomplete and untested. Completion deferred to implementation of Checkout/Checkin // functionality // NOTE I am not sure yet when this function would ever be called - checkin would be able to update the content stream // already and the only easy method we have (via KTAPI as it stands) to update the content is on checkin anyway. // Additionally this function doesn't take a value for the versioning status (major/minor) and so cannot pass it // on to the ktapi checkin function. // I imagine this function may be called if we ever allow updating document content independent of checkin, // or if we change some of the underlying code and call direct to the document functions and not via KTAPI. /** * Sets the content stream data for an existing document * * if $overwriteFlag = TRUE, the new content stream is applied whether or not the document has an existing content stream * if $overwriteFlag = FALSE, the new content stream is applied only if the document does not have an existing content stream * * NOTE A Repository MAY automatically create new Document versions as part of this service method. * Therefore, the documentId output NEED NOT be identical to the documentId input. * * @param string $repositoryId * @param string $documentId * @param boolean $overwriteFlag * @param string $contentStream * @param string $changeToken * @return string $documentId */ // TODO exceptions: // updateConflictException: The operation is attempting to update an object that is no longer current // (as determined by the repository). // versioningException: The repository MAY throw this exception if the object is a non-current Document Version. public function setContentStream($repositoryId, $documentId, $overwriteFlag, $contentStream, $changeToken = null) { // if no document id was supplied, we are going to create the underlying physical document // NOTE while it might have been nice to keep this out of here, KTAPI has no method for creating a document without // a physical upload, so we cannot create the document first and then add the upload as a content stream, the // entire creation step needs to happen here. // Attempt to decode $documentId, use as is if not detected as encoded $tmpObjectId = $documentId; $tmpObjectId = CMISUtil::decodeObjectId($tmpObjectId, $tmpTypeId); if ($tmpTypeId != 'Unknown') $documentId = $tmpObjectId; // TODO deal with other types except documents // fetch type definition of supplied document $CMISDocument = new CMISDocumentObject($documentId, $this->ktapi); // if content stream is not allowed for this object type definition, throw a ConstraintViolationException if (($CMISDocument->getAttribute('contentStreamAllowed') == 'notAllowed')) { // NOTE spec version 0.61c specifies both a ConstraintViolationException and a StreamNotSupportedException // for this case. Choosing to throw StreamNotSupportedException until the specification is clarified // as it is a more specific exception throw new StreamNotSupportedException('Content Streams are not allowed for this object type'); } $csFileName = $CMISDocument->getProperty('ContentStreamFilename'); if (!empty($csFileName) && (!$overwriteFlag)) { throw new ContentAlreadyExistsException('Unable to overwrite existing content stream'); } $tempfilename = CMISUtil::createTemporaryFile($contentStream); // update the document content from this temporary file as per usual // TODO Use checkin_document_with_metadata instead if metadata content submitted || update metadata separately? $response = $this->ktapi->checkin_document($documentId, $csFileName, 'CMIS setContentStream action', $tempfilename, false); if ($response['status_code'] != 0) { throw new StorageException('Unable to update the content stream. ' . $response['message']); } // else // { // $objectId = CMISUtil::encodeObjectId('Document', $response['results']['id']); // } @unlink($csFile); // update the CMIS document object with the content stream information // $CMISDocument->reload($document['result']['document_id']); return $CMISDocument->getProperty('ObjectId'); } } ?>