Commit c0e37132f1dd78fe7331faba1c608b65c487faf8

Authored by Paul Barrett
1 parent 2a0d453f

KTS-4359. Added support for decoding of individual base64 encoded chunks concate…

…nated as a single string.  Also added a file write function to the upload manager code which accepts an already decoded stream as input

The CMISSpaces client appears to send document content in an unusual way, which needs to be catered for

Fixed

Committed by: Paul Barrett

Reviewed by: Mark Holtzhausen
ktwebservice/KTUploadManager.inc.php
@@ -134,6 +134,36 @@ class KTUploadManager @@ -134,6 +134,36 @@ class KTUploadManager
134 134
135 return $tempfilename; 135 return $tempfilename;
136 } 136 }
  137 +
  138 + /**
  139 + *
  140 + * @param string $content file content NOT base64 encoded (may be string, may be binary)
  141 + * @param string $prefix [optional]
  142 + * @return $tempfilename the name of the temporary file created
  143 + */
  144 + function store_file($content, $prefix= 'sa_')
  145 + {
  146 + $tempfilename = $this->get_temp_filename($prefix);
  147 + if (!is_writable($tempfilename))
  148 + {
  149 + return new PEAR_Error("Cannot write to file: $tempfilename");
  150 + }
  151 +
  152 + if (!$this->is_valid_temporary_file($tempfilename))
  153 + {
  154 + return new PEAR_Error("Invalid temporary file: $tempfilename. There is a problem with the temporary storage path: $this->temp_dir.");
  155 + }
  156 +
  157 + $fp=fopen($tempfilename, 'wb');
  158 + if ($fp === false)
  159 + {
  160 + return new PEAR_Error("Cannot write content to temporary file: $tempfilename.");
  161 + }
  162 + fwrite($fp, $content);
  163 + fclose($fp);
  164 +
  165 + return $tempfilename;
  166 + }
137 167
138 /** 168 /**
139 * This tells the manager to manage a file that has been uploaded. 169 * This tells the manager to manage a file that has been uploaded.
lib/api/ktcmis/services/CMISObjectService.inc.php
@@ -80,16 +80,16 @@ class CMISObjectService { @@ -80,16 +80,16 @@ class CMISObjectService {
80 * @param array $properties Array of properties which must be applied to the created document object 80 * @param array $properties Array of properties which must be applied to the created document object
81 * @param string $folderId The id of the folder which will be the parent of the created document object 81 * @param string $folderId The id of the folder which will be the parent of the created document object
82 * This parameter is optional IF unfilingCapability is supported 82 * This parameter is optional IF unfilingCapability is supported
83 - * @param contentStream $contentStream optional content stream data 83 + * @param string $contentStream optional content stream data - expected as a base64 encoded string
84 * @param string $versioningState optional version state value: checkedout/major/minor 84 * @param string $versioningState optional version state value: checkedout/major/minor
85 * @return string $objectId The id of the created folder object 85 * @return string $objectId The id of the created folder object
86 */ 86 */
87 // TODO throw ConstraintViolationException if: 87 // TODO throw ConstraintViolationException if:
88 // value of any of the properties violates the min/max/required/length constraints 88 // value of any of the properties violates the min/max/required/length constraints
89 - // specified in the property definition in the Object-Type. 89 + // specified in the property definition in the Object-Type.
90 function createDocument($repositoryId, $typeId, $properties, $folderId = null, 90 function createDocument($repositoryId, $typeId, $properties, $folderId = null,
91 $contentStream = null, $versioningState = null) 91 $contentStream = null, $versioningState = null)
92 - { 92 + {
93 $objectId = null; 93 $objectId = null;
94 94
95 // fetch type definition of supplied type and check for base type "document", if not true throw exception 95 // fetch type definition of supplied type and check for base type "document", if not true throw exception
@@ -196,11 +196,17 @@ class CMISObjectService { @@ -196,11 +196,17 @@ class CMISObjectService {
196 // this check isn't strictly necessary; however it is needed for a repository which does not support content streams 196 // this check isn't strictly necessary; however it is needed for a repository which does not support content streams
197 if (!empty($contentStream)) 197 if (!empty($contentStream))
198 { 198 {
  199 + // TODO consider checking whether content is encoded (currently we expect encoded)
  200 + // TODO choose between this and the alternative decode function (see CMISUtil class)
  201 + // The current one appears to be miles better (1/0/3 vs 14/4/57 on respective test files)
  202 + $contentStream = CMISUtil::decodeChunkedContentStream($contentStream);
  203 +
199 // NOTE There is a function in CMISUtil to do this, written for the unit tests but since KTUploadManager exists 204 // NOTE There is a function in CMISUtil to do this, written for the unit tests but since KTUploadManager exists
200 // and has more functionality which could come in useful at some point I decided to go with that instead 205 // and has more functionality which could come in useful at some point I decided to go with that instead
201 // (did not know this existed when I wrote the CMISUtil function) 206 // (did not know this existed when I wrote the CMISUtil function)
202 $uploadManager = new KTUploadManager(); 207 $uploadManager = new KTUploadManager();
203 - $tempfilename = $uploadManager->store_base64_file($contentStream, 'cmis_'); 208 + // assumes already decoded from base64, should use store_base64_file if not
  209 + $tempfilename = $uploadManager->store_file($contentStream, 'cmis_');
204 210
205 // metadata 211 // metadata
206 $metadata = array(); 212 $metadata = array();
lib/api/ktcmis/util/CMISUtil.inc.php
@@ -298,10 +298,6 @@ class CMISUtil { @@ -298,10 +298,6 @@ class CMISUtil {
298 $object['properties']['ObjectId'] = array('type' => $properties->getFieldType('ObjectId'), 298 $object['properties']['ObjectId'] = array('type' => $properties->getFieldType('ObjectId'),
299 'value' => $properties->getValue('ObjectId')); 299 'value' => $properties->getValue('ObjectId'));
300 300
301 -  
302 -  
303 -  
304 -  
305 if (strtolower($properties->getValue('ObjectTypeId')) == 'document') 301 if (strtolower($properties->getValue('ObjectTypeId')) == 'document')
306 { 302 {
307 303
@@ -400,6 +396,117 @@ class CMISUtil { @@ -400,6 +396,117 @@ class CMISUtil {
400 396
401 return $temp; 397 return $temp;
402 } 398 }
  399 +
  400 + // TODO run evaluations on each of the following two functions and determine which
  401 + // is generally more efficienct
  402 +
  403 + /**
  404 + * Alternative function for decoding chunked streams, this will decode in blocks of 4.
  405 + * Not sure which method is more efficient, this or the function below (this does not
  406 + * re-encode but I am working on removing that step for the other function.)
  407 + *
  408 + * NOTE The current one appears to be much slower (14/4/57 vs 1/0/3 on respective test files)
  409 + *
  410 + * @param string $contentStream the base64 encoded content stream
  411 + * @return string $decoded the decoded content stream
  412 + */
  413 + static public function decodeContentStream($contentStream)
  414 + {
  415 + $decoded = '';
  416 +
  417 + $contentStream = preg_replace('/\r?\n+/', '', $contentStream);
  418 +
  419 + // decode in chunks or 4 chars at a time
  420 + for($i = 0, $len = strlen($contentStream); $i < $len; $i += 4) {
  421 + $decoded .= base64_decode(substr($contentStream, $i, 4));
  422 + }
  423 +
  424 + return $decoded;
  425 + }
  426 +
  427 + /**
  428 + * Checks the contentStream and ensures that it is a correct base64 string;
  429 + * This is purely for clients such as CMISSpaces breaking the content into
  430 + * chunks before base64 encoding.
  431 + *
  432 + * If the stream is chunked, it is decoded in chunks and sent back as a single stream.
  433 + * If it is not chunked it is decoded as is and sent back as a single stream.
  434 + *
  435 + * NOTE this function and the above need to be checked for efficiency.
  436 + * The current one appears to be miles better (1/0/3 vs 14/4/57 on respective test files)
  437 + *
  438 + * @param object $contentStream
  439 + * @return string decoded
  440 + */
  441 + static public function decodeChunkedContentStream($contentStream)
  442 + {
  443 + // check the content stream for any lines of unusual length (except the last line, which can be any length)
  444 + $count = -1;
  445 + $length = 0;
  446 + $b64String = '';
  447 + $outputStream = '';
  448 + $decode = array();
  449 + $chunks = 1;
  450 + $decoded = '';
  451 + $chunked = '';
  452 +
  453 + $splitStream = explode("\n", $contentStream);
  454 + foreach ($splitStream as $line)
  455 + {
  456 + $curlen = strlen($line);
  457 +
  458 + if ($length == 0) {
  459 + $length = $curlen;
  460 + }
  461 +
  462 + // if we find one we know that we must split the line here and end the previous base64 string
  463 + if ($curlen > $length)
  464 + {
  465 + // check for a new chunk
  466 + // either we have an equals sign (or two)
  467 + if (preg_match('/([^=]*={0,2})(.*)/', $line, $matches))
  468 + {
  469 + $lastChunk = $matches[1];
  470 + $nextChunk = $matches[2];
  471 + }
  472 + // or we need to try by line length
  473 + else {
  474 + $lastChunk = substr($line, 0, $curlen - $length);
  475 + $nextChunk = substr($line, $curlen - $length);
  476 + }
  477 +
  478 + $decode[++$count] = $b64String . $lastChunk;
  479 +
  480 + $b64String = $nextChunk . "\n";
  481 + $length = strlen($nextChunk);
  482 +
  483 + ++$chunks;
  484 + }
  485 + else {
  486 + $b64String .= $line . "\n";
  487 + }
  488 + }
  489 +
  490 + // anything left over
  491 + if (!empty($b64String)) {
  492 + $decode[] = $b64String;
  493 + }
  494 +
  495 + if ($chunks > 1)
  496 + {
  497 + foreach($decode as $code) {
  498 + // decode, append to output to be re-encoded
  499 + $chunked .= base64_decode($code);
  500 + }
  501 +
  502 + $decoded = $chunked;
  503 + }
  504 + else {
  505 + $decoded = base64_decode($decode[0]);
  506 + }
  507 +
  508 + return $decoded;
  509 + }
403 510
404 } 511 }
405 512
webservice/atompub/cmis/KT_cmis_atom_server.services.inc.php
@@ -103,10 +103,17 @@ class KT_cmis_atom_service_folder extends KT_atom_service { @@ -103,10 +103,17 @@ class KT_cmis_atom_service_folder extends KT_atom_service {
103 // determine whether this is a folder or a document create 103 // determine whether this is a folder or a document create
104 // document create will have a content tag <atom:content> or <content> containing base64 encoding of the document 104 // document create will have a content tag <atom:content> or <content> containing base64 encoding of the document
105 $content = KT_cmis_atom_service_helper::getAtomValues($this->parsedXMLContent['@children'], 'content'); 105 $content = KT_cmis_atom_service_helper::getAtomValues($this->parsedXMLContent['@children'], 'content');
106 - if (is_null($content)) 106 +
  107 + // check content for weird chars
  108 + $matches = array();
  109 + preg_match('/[^\w\d\/\+\n]*/', $content, $matches);
  110 +
  111 + if (is_null($content)) {
107 $type = 'folder'; 112 $type = 'folder';
108 - else 113 + }
  114 + else {
109 $type = 'document'; 115 $type = 'document';
  116 + }
110 117
111 $cmisObjectProperties = KT_cmis_atom_service_helper::getCmisProperties($this->parsedXMLContent['@children']['cmis:object'] 118 $cmisObjectProperties = KT_cmis_atom_service_helper::getCmisProperties($this->parsedXMLContent['@children']['cmis:object']
112 [0]['@children']['cmis:properties'] 119 [0]['@children']['cmis:properties']
@@ -124,20 +131,17 @@ class KT_cmis_atom_service_folder extends KT_atom_service { @@ -124,20 +131,17 @@ class KT_cmis_atom_service_folder extends KT_atom_service {
124 131
125 if ($typeId != 'Unknown') 132 if ($typeId != 'Unknown')
126 { 133 {
  134 + /*$f = fopen('c:\kt-stuff\here.txt', 'w');
  135 + fwrite($f, 'fgfgfgfg');
  136 + fclose($f);*/
127 $this->setStatus(self::STATUS_CREATED); 137 $this->setStatus(self::STATUS_CREATED);
128 -// if ($type == 'folder')  
129 -// {  
130 - $feed = KT_cmis_atom_service_helper::getObjectFeed($ObjectService, $repositoryId, $newObjectId, 'POST');  
131 -// }  
132 -// else  
133 -// {  
134 -// $NavigationService = new NavigationService(KT_cmis_atom_service_helper::getKt());  
135 -// $cmisEntry = $ObjectService->getProperties($repositoryId, $folderId, false, false);  
136 -// $feed = $this->getFolderChildrenFeed($NavigationService, $repositoryId, $folderId, $cmisEntry['properties']['Name']['value'], 'POST');  
137 -// } 138 + $feed = KT_cmis_atom_service_helper::getObjectFeed($ObjectService, $repositoryId, $newObjectId, 'POST');
138 } 139 }
139 else 140 else
140 { 141 {
  142 + /*$f = fopen('c:\kt-stuff\failed.txt', 'w');
  143 + fwrite($f, 'fgfgfgfg');
  144 + fclose($f);*/
141 $feed = KT_cmis_atom_service_helper::getErrorFeed($this, self::STATUS_SERVER_ERROR, $newObjectId['message']); 145 $feed = KT_cmis_atom_service_helper::getErrorFeed($this, self::STATUS_SERVER_ERROR, $newObjectId['message']);
142 } 146 }
143 147
webservice/atompub/cmis/index.php
@@ -72,11 +72,6 @@ if(!KT_atom_HTTPauth::isLoggedIn()) { @@ -72,11 +72,6 @@ if(!KT_atom_HTTPauth::isLoggedIn()) {
72 KT_atom_HTTPauth::login('KnowledgeTree DMS', 'You must authenticate to enter this realm'); 72 KT_atom_HTTPauth::login('KnowledgeTree DMS', 'You must authenticate to enter this realm');
73 } 73 }
74 74
75 -//$username = $_SERVER['PHP_AUTH_USER'];  
76 -//$password = $_SERVER['PHP_AUTH_PW'];  
77 -//// fetch user name and password (check auth include for proper method)  
78 -//KT_cmis_atom_service_helper::login($username, $password);  
79 -  
80 //Start the AtomPubProtocol Routing Engine 75 //Start the AtomPubProtocol Routing Engine
81 $APP = new KT_cmis_atom_server(); 76 $APP = new KT_cmis_atom_server();
82 77