Commit a83c2de9c6e334c34fdb017579ee5d421fee746f
1 parent
07800ae6
Added PermissionDeniedException class. Throw this on failed auth attempts (stil…
…l needed for service doc but in place for general services.) Added header for HTTP Basic Auth on failure. Committed by: Paul Barrett
Showing
10 changed files
with
313 additions
and
155 deletions
lib/api/ktcmis/exceptions/PermissionDeniedException.inc.php
0 → 100644
lib/api/ktcmis/ktcmis.inc.php
| ... | ... | @@ -52,12 +52,14 @@ require_once(realpath(dirname(__FILE__) . '/../../../config/dmsDefaults.php')); |
| 52 | 52 | require_once(KT_DIR . '/ktapi/ktapi.inc.php'); |
| 53 | 53 | |
| 54 | 54 | define ('CMIS_DIR', KT_LIB_DIR . '/api/ktcmis'); |
| 55 | +require_once(CMIS_DIR . '/exceptions/PermissionDeniedException.inc.php'); | |
| 55 | 56 | require_once(CMIS_DIR . '/services/CMISRepositoryService.inc.php'); |
| 56 | 57 | require_once(CMIS_DIR . '/services/CMISNavigationService.inc.php'); |
| 57 | 58 | require_once(CMIS_DIR . '/services/CMISObjectService.inc.php'); |
| 58 | 59 | require_once(CMIS_DIR . '/util/CMISUtil.inc.php'); |
| 59 | 60 | |
| 60 | 61 | /** |
| 62 | + * Base class for all KT CMIS classes | |
| 61 | 63 | * Handles authentication |
| 62 | 64 | */ |
| 63 | 65 | class KTCMISBase { |
| ... | ... | @@ -67,6 +69,11 @@ class KTCMISBase { |
| 67 | 69 | static protected $ktapi; |
| 68 | 70 | static protected $session; |
| 69 | 71 | |
| 72 | +// public function __construct($username = null, $password = null) | |
| 73 | +// { | |
| 74 | +// $this->startSession($username, $password); | |
| 75 | +// } | |
| 76 | + | |
| 70 | 77 | // TODO try to pick up existing session if possible, i.e. if the $session value is not empty |
| 71 | 78 | public function startSession($username, $password) |
| 72 | 79 | { |
| ... | ... | @@ -85,6 +92,12 @@ class KTCMISBase { |
| 85 | 92 | self::$ktapi = new KTAPI(); |
| 86 | 93 | self::$session =& self::$ktapi->start_session($username, $password); |
| 87 | 94 | } |
| 95 | + | |
| 96 | + // failed authentication? | |
| 97 | + if (PEAR::isError(self::$session)) | |
| 98 | + { | |
| 99 | + throw new PermissionDeniedException('You must be authenticated to perform this action'); | |
| 100 | + } | |
| 88 | 101 | |
| 89 | 102 | // print_r(self::$ktapi); |
| 90 | 103 | return self::$session; |
| ... | ... | @@ -181,7 +194,7 @@ class KTRepositoryService extends KTCMISBase { |
| 181 | 194 | * @param string $repositoryId |
| 182 | 195 | */ |
| 183 | 196 | public function getTypes($repositoryId, $typeId = '', $returnPropertyDefinitions = false, |
| 184 | - $maxItems = 0, $skipCount = 0, &$hasMoreItems = false) | |
| 197 | + $maxItems = 0, $skipCount = 0, &$hasMoreItems = false) | |
| 185 | 198 | { |
| 186 | 199 | try { |
| 187 | 200 | $repositoryObjectTypeResult = $this->RepositoryService->getTypes($repositoryId, $typeId, $returnPropertyDefinitions, | ... | ... |
lib/api/ktcmis/services/CMISObjectService.inc.php
| ... | ... | @@ -166,6 +166,12 @@ class CMISObjectService { |
| 166 | 166 | $properties['name'] = $properties['title']; |
| 167 | 167 | } |
| 168 | 168 | |
| 169 | + // TODO if name is blank! throw another exception (check type) - using invalidArgument Exception for now | |
| 170 | + if (trim($properties['name']) == '') | |
| 171 | + { | |
| 172 | + throw new InvalidArgumentException('Refusing to create an un-named document'); | |
| 173 | + } | |
| 174 | + | |
| 169 | 175 | // TODO also set to Default if a non-supported type is submitted |
| 170 | 176 | if ($properties['type'] == '') |
| 171 | 177 | { |
| ... | ... | @@ -262,10 +268,10 @@ class CMISObjectService { |
| 262 | 268 | throw new ConstraintViolationException('Parent folder may not hold objects of this type (' . $typeId . ')'); |
| 263 | 269 | } |
| 264 | 270 | |
| 265 | - // TODO if name is blank! throw another exception (check type) - using RuntimeException for now | |
| 271 | + // TODO if name is blank! throw another exception (check type) - using invalidArgument Exception for now | |
| 266 | 272 | if (trim($properties['name']) == '') |
| 267 | 273 | { |
| 268 | - throw new RuntimeException('Refusing to create an un-named folder'); | |
| 274 | + throw new InvalidArgumentException('Refusing to create an un-named folder'); | |
| 269 | 275 | } |
| 270 | 276 | |
| 271 | 277 | $response = $this->ktapi->create_folder((int)$folderId, $properties['name'], $sig_username = '', $sig_password = '', $reason = ''); | ... | ... |
webservice/atompub/cmis/KT_cmis_atom_server.services.inc.php
| ... | ... | @@ -5,7 +5,7 @@ include_once CMIS_ATOM_LIB_FOLDER . 'NavigationService.inc.php'; |
| 5 | 5 | include_once CMIS_ATOM_LIB_FOLDER . 'ObjectService.inc.php'; |
| 6 | 6 | include_once 'KT_cmis_atom_service_helper.inc.php'; |
| 7 | 7 | |
| 8 | -// TODO response if failed auth, need generic response which can be used by all code | |
| 8 | +// TODO auth failed response requires WWW-Authenticate: Basic realm="KnowledgeTree DMS" header | |
| 9 | 9 | |
| 10 | 10 | /** |
| 11 | 11 | * AtomPub Service: folder |
| ... | ... | @@ -13,12 +13,22 @@ include_once 'KT_cmis_atom_service_helper.inc.php'; |
| 13 | 13 | * Returns children, descendants (up to arbitrary depth) or detail for a particular folder |
| 14 | 14 | * |
| 15 | 15 | */ |
| 16 | -class KT_cmis_atom_service_folder extends KT_cmis_atom_service | |
| 17 | -{ | |
| 18 | - public function GET_action() | |
| 16 | +class KT_cmis_atom_service_folder extends KT_cmis_atom_service { | |
| 17 | + | |
| 18 | + public function GET_action() | |
| 19 | 19 | { |
| 20 | 20 | $RepositoryService = new RepositoryService(); |
| 21 | - $RepositoryService->startSession(self::$authData['username'], self::$authData['password']); | |
| 21 | + try { | |
| 22 | + $RepositoryService->startSession(self::$authData['username'], self::$authData['password']); | |
| 23 | + } | |
| 24 | + catch (Exception $e) | |
| 25 | + { | |
| 26 | + $this->headers[] = 'WWW-Authenticate: Basic realm="KnowledgeTree Secure Area"'; | |
| 27 | + $feed = KT_cmis_atom_service_helper::getErrorFeed($this, self::STATUS_NOT_AUTHENTICATED, $e->getMessage()); | |
| 28 | + $this->responseFeed = $feed; | |
| 29 | + return null; | |
| 30 | + } | |
| 31 | + | |
| 22 | 32 | $repositories = $RepositoryService->getRepositories(); |
| 23 | 33 | $repositoryId = $repositories[0]['repositoryId']; |
| 24 | 34 | |
| ... | ... | @@ -31,7 +41,6 @@ class KT_cmis_atom_service_folder extends KT_cmis_atom_service |
| 31 | 41 | $folderId = CMISUtil::encodeObjectId('Folder', 1); |
| 32 | 42 | $folderName = urldecode($this->params[0]); |
| 33 | 43 | } |
| 34 | - // this is a bit of a hack, but then it's to accomodate a bit of a hack to work with the knowledgetree/drupal cmis modules... | |
| 35 | 44 | else if ($this->params[0] == 'path') |
| 36 | 45 | { |
| 37 | 46 | $ktapi =& $RepositoryService->getInterface(); |
| ... | ... | @@ -41,43 +50,84 @@ class KT_cmis_atom_service_folder extends KT_cmis_atom_service |
| 41 | 50 | { |
| 42 | 51 | $folderId = $this->params[0]; |
| 43 | 52 | $ObjectService = new ObjectService(); |
| 44 | - $ObjectService->startSession(self::$authData['username'], self::$authData['password']); | |
| 53 | + | |
| 54 | + try { | |
| 55 | + $ObjectService->startSession(self::$authData['username'], self::$authData['password']); | |
| 56 | + } | |
| 57 | + catch (Exception $e) | |
| 58 | + { | |
| 59 | + $this->headers[] = 'WWW-Authenticate: Basic realm="KnowledgeTree Secure Area"'; | |
| 60 | + $feed = KT_cmis_atom_service_helper::getErrorFeed($this, self::STATUS_NOT_AUTHENTICATED, $e->getMessage()); | |
| 61 | + $this->responseFeed = $feed; | |
| 62 | + return null; | |
| 63 | + } | |
| 64 | + | |
| 45 | 65 | $cmisEntry = $ObjectService->getProperties($repositoryId, $folderId, false, false); |
| 46 | 66 | $folderName = $cmisEntry['properties']['Name']['value']; |
| 47 | -// $feed = $this->getFolderChildrenFeed($NavigationService, $repositoryId, $newObjectId, $cmisEntry['properties']['Name']['value']); | |
| 67 | + // $feed = $this->getFolderChildrenFeed($NavigationService, $repositoryId, $newObjectId, $cmisEntry['properties']['Name']['value']); | |
| 48 | 68 | } |
| 49 | 69 | |
| 50 | 70 | if (!empty($this->params[1]) && (($this->params[1] == 'children') || ($this->params[1] == 'descendants'))) |
| 51 | 71 | { |
| 52 | 72 | $NavigationService = new NavigationService(); |
| 53 | - $NavigationService->startSession(self::$authData['username'], self::$authData['password']); | |
| 73 | + | |
| 74 | + try { | |
| 75 | + $NavigationService->startSession(self::$authData['username'], self::$authData['password']); | |
| 76 | + } | |
| 77 | + catch (Exception $e) | |
| 78 | + { | |
| 79 | + $this->headers[] = 'WWW-Authenticate: Basic realm="KnowledgeTree Secure Area"'; | |
| 80 | + $feed = KT_cmis_atom_service_helper::getErrorFeed($this, self::STATUS_NOT_AUTHENTICATED, $e->getMessage()); | |
| 81 | + $this->responseFeed = $feed; | |
| 82 | + return null; | |
| 83 | + } | |
| 54 | 84 | |
| 55 | 85 | $feed = $this->getFolderChildrenFeed($NavigationService, $repositoryId, $folderId, $folderName, $this->params[1]); |
| 56 | 86 | } |
| 57 | 87 | else |
| 58 | 88 | { |
| 59 | 89 | $ObjectService = new ObjectService(); |
| 60 | - $ObjectService->startSession(self::$authData['username'], self::$authData['password']); | |
| 90 | + | |
| 91 | + try { | |
| 92 | + $ObjectService->startSession(self::$authData['username'], self::$authData['password']); | |
| 93 | + } | |
| 94 | + catch (Exception $e) | |
| 95 | + { | |
| 96 | + $this->headers[] = 'WWW-Authenticate: Basic realm="KnowledgeTree Secure Area"'; | |
| 97 | + $feed = KT_cmis_atom_service_helper::getErrorFeed($this, self::STATUS_NOT_AUTHENTICATED, $e->getMessage()); | |
| 98 | + $this->responseFeed = $feed; | |
| 99 | + return null; | |
| 100 | + } | |
| 61 | 101 | |
| 62 | 102 | $feed = $this->getFolderFeed($ObjectService, $repositoryId, $folderId); |
| 63 | 103 | } |
| 64 | 104 | |
| 65 | - //Expose the responseFeed | |
| 66 | - $this->responseFeed = $feed; | |
| 67 | - } | |
| 68 | - | |
| 69 | - public function POST_action() | |
| 105 | + //Expose the responseFeed | |
| 106 | + $this->responseFeed = $feed; | |
| 107 | + } | |
| 108 | + | |
| 109 | + public function POST_action() | |
| 70 | 110 | { |
| 71 | -// $username = $password = 'admin'; | |
| 72 | 111 | $RepositoryService = new RepositoryService(); |
| 73 | - $RepositoryService->startSession(self::$authData['username'], self::$authData['password']); | |
| 112 | + | |
| 113 | + try { | |
| 114 | + $RepositoryService->startSession(self::$authData['username'], self::$authData['password']); | |
| 115 | + } | |
| 116 | + catch (Exception $e) | |
| 117 | + { | |
| 118 | + $this->headers[] = 'WWW-Authenticate: Basic realm="KnowledgeTree Secure Area"'; | |
| 119 | + $feed = KT_cmis_atom_service_helper::getErrorFeed($this, self::STATUS_NOT_AUTHENTICATED, $e->getMessage()); | |
| 120 | + $this->responseFeed = $feed; | |
| 121 | + return null; | |
| 122 | + } | |
| 123 | + | |
| 74 | 124 | $repositories = $RepositoryService->getRepositories(); |
| 75 | 125 | $repositoryId = $repositories[0]['repositoryId']; |
| 76 | 126 | |
| 77 | 127 | $folderId = $this->params[0]; |
| 78 | 128 | $title = KT_cmis_atom_service_helper::getAtomValues($this->parsedXMLContent['@children'], 'title'); |
| 79 | 129 | $summary = KT_cmis_atom_service_helper::getAtomValues($this->parsedXMLContent['@children'], 'summary'); |
| 80 | - | |
| 130 | + | |
| 81 | 131 | $properties = array('name' => $title, 'summary' => $summary); |
| 82 | 132 | |
| 83 | 133 | // determine whether this is a folder or a document create |
| ... | ... | @@ -109,7 +159,18 @@ class KT_cmis_atom_service_folder extends KT_cmis_atom_service |
| 109 | 159 | [0]['@children']); |
| 110 | 160 | |
| 111 | 161 | $ObjectService = new ObjectService(); |
| 112 | - $ObjectService->startSession(self::$authData['username'], self::$authData['password']); | |
| 162 | + | |
| 163 | + try { | |
| 164 | + $ObjectService->startSession(self::$authData['username'], self::$authData['password']); | |
| 165 | + } | |
| 166 | + catch (Exception $e) | |
| 167 | + { | |
| 168 | + $this->headers[] = 'WWW-Authenticate: Basic realm="KnowledgeTree Secure Area"'; | |
| 169 | + $feed = KT_cmis_atom_service_helper::getErrorFeed($this, self::STATUS_NOT_AUTHENTICATED, $e->getMessage()); | |
| 170 | + $this->responseFeed = $feed; | |
| 171 | + return null; | |
| 172 | + } | |
| 173 | + | |
| 113 | 174 | if ($type == 'folder') |
| 114 | 175 | $newObjectId = $ObjectService->createFolder($repositoryId, ucwords($cmisObjectProperties['ObjectTypeId']), $properties, $folderId); |
| 115 | 176 | else |
| ... | ... | @@ -127,22 +188,30 @@ class KT_cmis_atom_service_folder extends KT_cmis_atom_service |
| 127 | 188 | else |
| 128 | 189 | { |
| 129 | 190 | $NavigationService = new NavigationService(); |
| 130 | - $NavigationService->startSession(self::$authData['username'], self::$authData['password']); | |
| 191 | + | |
| 192 | + try { | |
| 193 | + $NavigationService->startSession(self::$authData['username'], self::$authData['password']); | |
| 194 | + } | |
| 195 | + catch (Exception $e) | |
| 196 | + { | |
| 197 | + $this->headers[] = 'WWW-Authenticate: Basic realm="KnowledgeTree Secure Area"'; | |
| 198 | + $feed = KT_cmis_atom_service_helper::getErrorFeed($this, self::STATUS_NOT_AUTHENTICATED, $e->getMessage()); | |
| 199 | + $this->responseFeed = $feed; | |
| 200 | + return null; | |
| 201 | + } | |
| 202 | + | |
| 131 | 203 | $cmisEntry = $ObjectService->getProperties($repositoryId, $folderId, false, false); |
| 132 | 204 | $feed = $this->getFolderChildrenFeed($NavigationService, $repositoryId, $folderId, $cmisEntry['properties']['Name']['value']); |
| 133 | 205 | } |
| 134 | 206 | } |
| 135 | 207 | else |
| 136 | 208 | { |
| 137 | - $this->setStatus(self::STATUS_SERVER_ERROR); | |
| 138 | - $feed = new KT_cmis_atom_responseFeed(CMIS_APP_BASE_URI, 'Error: ' . self::STATUS_SERVER_ERROR); | |
| 139 | - $entry = $feed->newEntry(); | |
| 140 | - $feed->newField('error', $newObjectId['message'], $entry); | |
| 209 | + $feed = KT_cmis_atom_service_helper::getErrorFeed($this, self::STATUS_SERVER_ERROR, $newObjectId['message']); | |
| 141 | 210 | } |
| 142 | 211 | |
| 143 | 212 | //Expose the responseFeed |
| 144 | - $this->responseFeed = $feed; | |
| 145 | - } | |
| 213 | + $this->responseFeed = $feed; | |
| 214 | + } | |
| 146 | 215 | |
| 147 | 216 | /** |
| 148 | 217 | * Retrieves children/descendants of the specified folder |
| ... | ... | @@ -169,7 +238,7 @@ class KT_cmis_atom_service_folder extends KT_cmis_atom_service |
| 169 | 238 | } |
| 170 | 239 | |
| 171 | 240 | // $baseURI=NULL,$title=NULL,$link=NULL,$updated=NULL,$author=NULL,$id=NULL |
| 172 | - $feed = new KT_cmis_atom_responseFeed(CMIS_APP_BASE_URI, $folderName . ' ' . ucwords($feedType), null, null, null, | |
| 241 | + $feed = new KT_cmis_atom_responseFeed(CMIS_APP_BASE_URI, $folderName . ' ' . ucwords($feedType), null, null, null, | |
| 173 | 242 | 'urn:uuid:' . $folderName . '-' . $feedType); |
| 174 | 243 | |
| 175 | 244 | foreach($entries as $cmisEntry) |
| ... | ... | @@ -200,14 +269,15 @@ class KT_cmis_atom_service_folder extends KT_cmis_atom_service |
| 200 | 269 | |
| 201 | 270 | $feed = new KT_cmis_atom_responseFeed(CMIS_APP_BASE_URI, $cmisEntry['properties']['ObjectTypeId']['value'], null, null, null, |
| 202 | 271 | 'urn:uuid:' . $cmisEntry['properties']['ObjectId']['value']); |
| 203 | - | |
| 272 | + | |
| 204 | 273 | KT_cmis_atom_service_helper::createObjectEntry($feed, $cmisEntry, $folderName); |
| 205 | -// // <cmis:hasMoreItems>false</cmis:hasMoreItems> | |
| 206 | -// // global $folderFeed; | |
| 207 | -// // $outputs = | |
| 274 | + // // <cmis:hasMoreItems>false</cmis:hasMoreItems> | |
| 275 | + // // global $folderFeed; | |
| 276 | + // // $outputs = | |
| 208 | 277 | |
| 209 | 278 | return $feed; |
| 210 | 279 | } |
| 280 | + | |
| 211 | 281 | } |
| 212 | 282 | |
| 213 | 283 | /** |
| ... | ... | @@ -216,15 +286,22 @@ class KT_cmis_atom_service_folder extends KT_cmis_atom_service |
| 216 | 286 | * Returns a list of supported object types |
| 217 | 287 | * |
| 218 | 288 | */ |
| 219 | -class KT_cmis_atom_service_types extends KT_cmis_atom_service | |
| 220 | -{ | |
| 221 | - public function GET_action() | |
| 289 | +class KT_cmis_atom_service_types extends KT_cmis_atom_service { | |
| 290 | + | |
| 291 | + public function GET_action() | |
| 222 | 292 | { |
| 223 | -// $username = $password = 'admin'; | |
| 224 | 293 | $RepositoryService = new RepositoryService(); |
| 225 | - // technically do not need to log in to access this information | |
| 226 | - // TODO consider requiring authentication even to access basic repository information | |
| 227 | - $RepositoryService->startSession(self::$authData['username'], self::$authData['password']); | |
| 294 | + | |
| 295 | + try { | |
| 296 | + $RepositoryService->startSession(self::$authData['username'], self::$authData['password']); | |
| 297 | + } | |
| 298 | + catch (Exception $e) | |
| 299 | + { | |
| 300 | + $this->headers[] = 'WWW-Authenticate: Basic realm="KnowledgeTree Secure Area"'; | |
| 301 | + $feed = KT_cmis_atom_service_helper::getErrorFeed($this, self::STATUS_NOT_AUTHENTICATED, $e->getMessage()); | |
| 302 | + $this->responseFeed = $feed; | |
| 303 | + return null; | |
| 304 | + } | |
| 228 | 305 | |
| 229 | 306 | // fetch repository id |
| 230 | 307 | $repositories = $RepositoryService->getRepositories(); |
| ... | ... | @@ -234,9 +311,10 @@ class KT_cmis_atom_service_types extends KT_cmis_atom_service |
| 234 | 311 | $type = ((empty($this->params[0])) ? 'all' : $this->params[0]); |
| 235 | 312 | $feed = KT_cmis_atom_service_helper::getTypeFeed($type, $types); |
| 236 | 313 | |
| 237 | - //Expose the responseFeed | |
| 238 | - $this->responseFeed = $feed; | |
| 239 | - } | |
| 314 | + //Expose the responseFeed | |
| 315 | + $this->responseFeed = $feed; | |
| 316 | + } | |
| 317 | + | |
| 240 | 318 | } |
| 241 | 319 | |
| 242 | 320 | /** |
| ... | ... | @@ -245,42 +323,48 @@ class KT_cmis_atom_service_types extends KT_cmis_atom_service |
| 245 | 323 | * Returns the type defintion for the selected type |
| 246 | 324 | * |
| 247 | 325 | */ |
| 248 | -class KT_cmis_atom_service_type extends KT_cmis_atom_service | |
| 249 | -{ | |
| 250 | - public function GET_action() | |
| 326 | +class KT_cmis_atom_service_type extends KT_cmis_atom_service { | |
| 327 | + | |
| 328 | + public function GET_action() | |
| 251 | 329 | { |
| 252 | -// $username = $password = 'admin'; | |
| 253 | 330 | $RepositoryService = new RepositoryService(); |
| 254 | - // technically do not need to log in to access this information | |
| 255 | - // TODO consider requiring authentication even to access basic repository information | |
| 256 | - $RepositoryService->startSession(self::$authData['username'], self::$authData['password']); | |
| 331 | + | |
| 332 | + try { | |
| 333 | + $RepositoryService->startSession(self::$authData['username'], self::$authData['password']); | |
| 334 | + } | |
| 335 | + catch (Exception $e) | |
| 336 | + { | |
| 337 | + $this->headers[] = 'WWW-Authenticate: Basic realm="KnowledgeTree Secure Area"'; | |
| 338 | + $feed = KT_cmis_atom_service_helper::getErrorFeed($this, self::STATUS_NOT_AUTHENTICATED, $e->getMessage()); | |
| 339 | + $this->responseFeed = $feed; | |
| 340 | + return null; | |
| 341 | + } | |
| 257 | 342 | |
| 258 | 343 | // fetch repository id |
| 259 | 344 | $repositories = $RepositoryService->getRepositories(); |
| 260 | 345 | $repositoryId = $repositories[0]['repositoryId']; |
| 261 | - | |
| 262 | - if (!isset($this->params[1])) | |
| 263 | - { | |
| 264 | - // For easier return in the wanted format, we call getTypes instead of getTypeDefinition. | |
| 265 | - // Calling this with a single type specified returns an array containing the definition of | |
| 266 | - // just the requested type. | |
| 267 | - // NOTE could maybe be more efficient to call getTypeDefinition direct and then place in | |
| 268 | - // an array on this side? or directly expose the individual entry response code and | |
| 269 | - // call directly from here rather than via getTypeFeed. | |
| 346 | + | |
| 347 | + if (!isset($this->params[1])) { | |
| 348 | + // For easier return in the wanted format, we call getTypes instead of getTypeDefinition. | |
| 349 | + // Calling this with a single type specified returns an array containing the definition of | |
| 350 | + // just the requested type. | |
| 351 | + // NOTE could maybe be more efficient to call getTypeDefinition direct and then place in | |
| 352 | + // an array on this side? or directly expose the individual entry response code and | |
| 353 | + // call directly from here rather than via getTypeFeed. | |
| 270 | 354 | $type = ucwords($this->params[0]); |
| 271 | 355 | $types = $RepositoryService->getTypes($repositoryId, $type); |
| 272 | 356 | $feed = KT_cmis_atom_service_helper::getTypeFeed($type, $types); |
| 273 | 357 | } |
| 274 | - else | |
| 275 | - { | |
| 276 | - // TODO dynamic dates, as needed everywhere | |
| 277 | - // NOTE children of types not yet implemented and we don't support any non-basic types at this time | |
| 358 | + else { | |
| 359 | + // TODO dynamic dates, as needed everywhere | |
| 360 | + // NOTE children of types not yet implemented and we don't support any non-basic types at this time | |
| 278 | 361 | $feed = $this->getTypeChildrenFeed($this->params[1]); |
| 279 | 362 | } |
| 280 | 363 | |
| 281 | - //Expose the responseFeed | |
| 282 | - $this->responseFeed=$feed; | |
| 283 | - } | |
| 364 | + //Expose the responseFeed | |
| 365 | + $this->responseFeed=$feed; | |
| 366 | + } | |
| 367 | + | |
| 284 | 368 | /** |
| 285 | 369 | * Retrieves a list of child types for the supplied type |
| 286 | 370 | * |
| ... | ... | @@ -292,10 +376,10 @@ class KT_cmis_atom_service_type extends KT_cmis_atom_service |
| 292 | 376 | */ |
| 293 | 377 | private function getTypeChildrenFeed() |
| 294 | 378 | { |
| 295 | - //Create a new response feed | |
| 296 | - // $baseURI=NULL,$title=NULL,$link=NULL,$updated=NULL,$author=NULL,$id=NULL | |
| 297 | - $feed = new KT_cmis_atom_responseFeed(CMIS_APP_BASE_URI, 'Child Types of ' . ucwords($this->params[0]), null, null, null, | |
| 298 | - $this->params[0] . '-children'); | |
| 379 | + //Create a new response feed | |
| 380 | + // $baseURI=NULL,$title=NULL,$link=NULL,$updated=NULL,$author=NULL,$id=NULL | |
| 381 | + $feed = new KT_cmis_atom_responseFeed(CMIS_APP_BASE_URI, 'Child Types of ' . ucwords($this->params[0]), null, null, null, | |
| 382 | + $this->params[0] . '-children'); | |
| 299 | 383 | |
| 300 | 384 | // TODO actually fetch child types - to be implemented when we support child types in the API |
| 301 | 385 | |
| ... | ... | @@ -316,8 +400,8 @@ class KT_cmis_atom_service_type extends KT_cmis_atom_service |
| 316 | 400 | // TODO actual dynamic listing, currently we have no objects with which to test |
| 317 | 401 | |
| 318 | 402 | // TODO |
| 319 | -// <updated>2009-06-23T13:40:32.786+02:00</updated> | |
| 320 | -// <cmis:hasMoreItems>false</cmis:hasMoreItems> | |
| 403 | + // <updated>2009-06-23T13:40:32.786+02:00</updated> | |
| 404 | + // <cmis:hasMoreItems>false</cmis:hasMoreItems> | |
| 321 | 405 | /* |
| 322 | 406 | // TODO need to create this dynamically now, will no longer work with static output |
| 323 | 407 | $output = '<?xml version="1.0" encoding="UTF-8"?> |
| ... | ... | @@ -343,23 +427,31 @@ class KT_cmis_atom_service_type extends KT_cmis_atom_service |
| 343 | 427 | * |
| 344 | 428 | */ |
| 345 | 429 | // NOTE this is always an empty document, underlying API code still to be implemented |
| 346 | -class KT_cmis_atom_service_checkedout extends KT_cmis_atom_service | |
| 347 | -{ | |
| 348 | - public function GET_action() | |
| 430 | +class KT_cmis_atom_service_checkedout extends KT_cmis_atom_service { | |
| 431 | + | |
| 432 | + public function GET_action() | |
| 349 | 433 | { |
| 350 | -// $username = $password = 'admin'; | |
| 351 | 434 | $RepositoryService = new RepositoryService(); |
| 352 | 435 | $NavigationService = new NavigationService(); |
| 353 | 436 | |
| 354 | - $NavigationService->startSession(self::$authData['username'], self::$authData['password']); | |
| 437 | + try { | |
| 438 | + $NavigationService->startSession(self::$authData['username'], self::$authData['password']); | |
| 439 | + } | |
| 440 | + catch (Exception $e) | |
| 441 | + { | |
| 442 | + $this->headers[] = 'WWW-Authenticate: Basic realm="KnowledgeTree Secure Area"'; | |
| 443 | + $feed = KT_cmis_atom_service_helper::getErrorFeed($this, self::STATUS_NOT_AUTHENTICATED, $e->getMessage()); | |
| 444 | + $this->responseFeed = $feed; | |
| 445 | + return null; | |
| 446 | + } | |
| 355 | 447 | |
| 356 | 448 | $repositories = $RepositoryService->getRepositories(); |
| 357 | 449 | $repositoryId = $repositories[0]['repositoryId']; |
| 358 | 450 | |
| 359 | 451 | $checkedout = $NavigationService->getCheckedoutDocs($repositoryId); |
| 360 | 452 | |
| 361 | - //Create a new response feed | |
| 362 | - $feed = new KT_cmis_atom_responseFeed(CMIS_APP_BASE_URI, 'Checked out Documents', null, null, null, 'urn:uuid:checkedout'); | |
| 453 | + //Create a new response feed | |
| 454 | + $feed = new KT_cmis_atom_responseFeed(CMIS_APP_BASE_URI, 'Checked out Documents', null, null, null, 'urn:uuid:checkedout'); | |
| 363 | 455 | |
| 364 | 456 | foreach($checkedout as $document) |
| 365 | 457 | { |
| ... | ... | @@ -382,9 +474,9 @@ class KT_cmis_atom_service_checkedout extends KT_cmis_atom_service |
| 382 | 474 | $entry = null; |
| 383 | 475 | $feed->newField('hasMoreItems', 'false', $entry, true); |
| 384 | 476 | |
| 385 | - //Expose the responseFeed | |
| 386 | - $this->responseFeed = $feed; | |
| 387 | - } | |
| 477 | + //Expose the responseFeed | |
| 478 | + $this->responseFeed = $feed; | |
| 479 | + } | |
| 388 | 480 | |
| 389 | 481 | } |
| 390 | 482 | |
| ... | ... | @@ -394,35 +486,45 @@ class KT_cmis_atom_service_checkedout extends KT_cmis_atom_service |
| 394 | 486 | * Returns detail on a particular document |
| 395 | 487 | * |
| 396 | 488 | */ |
| 397 | -class KT_cmis_atom_service_document extends KT_cmis_atom_service | |
| 398 | -{ | |
| 399 | - public function GET_action() | |
| 489 | +class KT_cmis_atom_service_document extends KT_cmis_atom_service { | |
| 490 | + | |
| 491 | + public function GET_action() | |
| 400 | 492 | { |
| 401 | -// $username = $password = 'admin'; | |
| 402 | 493 | $RepositoryService = new RepositoryService(); |
| 403 | 494 | |
| 404 | 495 | $ObjectService = new ObjectService(); |
| 405 | - $ObjectService->startSession(self::$authData['username'], self::$authData['password']); | |
| 406 | - | |
| 496 | + | |
| 497 | + try { | |
| 498 | + $ObjectService->startSession(self::$authData['username'], self::$authData['password']); | |
| 499 | + } | |
| 500 | + catch (Exception $e) | |
| 501 | + { | |
| 502 | + $this->headers[] = 'WWW-Authenticate: Basic realm="KnowledgeTree Secure Area"'; | |
| 503 | + $feed = KT_cmis_atom_service_helper::getErrorFeed($this, self::STATUS_NOT_AUTHENTICATED, $e->getMessage()); | |
| 504 | + $this->responseFeed = $feed; | |
| 505 | + return null; | |
| 506 | + } | |
| 507 | + | |
| 407 | 508 | $repositories = $RepositoryService->getRepositories(); |
| 408 | 509 | $repositoryId = $repositories[0]['repositoryId']; |
| 409 | 510 | |
| 410 | 511 | $cmisEntry = $ObjectService->getProperties($repositoryId, $this->params[0], false, false); |
| 411 | 512 | |
| 412 | 513 | //Create a new response feed |
| 413 | - $feed = new KT_cmis_atom_responseFeed(CMIS_APP_BASE_URI, $cmisEntry['properties']['ObjectTypeId']['value'], null, null, null, | |
| 414 | - 'urn:uuid:' . $cmisEntry['properties']['ObjectId']['value']); | |
| 514 | + $feed = new KT_cmis_atom_responseFeed(CMIS_APP_BASE_URI, $cmisEntry['properties']['ObjectTypeId']['value'], null, null, null, | |
| 515 | + 'urn:uuid:' . $cmisEntry['properties']['ObjectId']['value']); | |
| 415 | 516 | |
| 416 | 517 | KT_cmis_atom_service_helper::createObjectEntry($feed, $cmisEntry, $cmisEntry['properties']['ParentId']['value']); |
| 417 | 518 | |
| 418 | 519 | // <cmis:hasMoreItems>false</cmis:hasMoreItems> |
| 419 | 520 | |
| 420 | -// global $docFeed; | |
| 421 | -// $output = $docFeed; | |
| 521 | + // global $docFeed; | |
| 522 | + // $output = $docFeed; | |
| 422 | 523 | |
| 423 | - //Expose the responseFeed | |
| 424 | - $this->responseFeed=$feed; | |
| 425 | - } | |
| 524 | + //Expose the responseFeed | |
| 525 | + $this->responseFeed=$feed; | |
| 526 | + } | |
| 527 | + | |
| 426 | 528 | } |
| 427 | 529 | |
| 428 | 530 | $childrenFeed[] = '<?xml version="1.0" encoding="utf-8"?> |
| ... | ... | @@ -532,7 +634,7 @@ $childrenFeed[] = '<?xml version="1.0" encoding="utf-8"?> |
| 532 | 634 | </entry> |
| 533 | 635 | </feed>'; |
| 534 | 636 | |
| 535 | - $childrenFeed[] = '<?xml version="1.0" encoding="UTF-8"?> | |
| 637 | +$childrenFeed[] = '<?xml version="1.0" encoding="UTF-8"?> | |
| 536 | 638 | <feed xmlns="http://www.w3.org/2005/Atom" xmlns:app="http://www.w3.org/2007/app" xmlns:cmis="http://www.cmis.org/2008/05" xmlns:alf="http://www.alfresco.org" xmlns:opensearch="http://a9.com/-/spec/opensearch/1.1/"> |
| 537 | 639 | <author><name>System</name></author> |
| 538 | 640 | <generator version="3.0.0 (Stable 1526)">Alfresco (Labs)</generator> | ... | ... |
webservice/atompub/cmis/KT_cmis_atom_service_helper.inc.php
| ... | ... | @@ -112,7 +112,7 @@ class KT_cmis_atom_service_helper { |
| 112 | 112 | } |
| 113 | 113 | |
| 114 | 114 | //Create a new response feed |
| 115 | - $feed = new KT_cmis_atom_responseFeed(CMIS_APP_BASE_URI, $typesHeading, null, null, null, 'urn:uuid:' . $typesString); | |
| 115 | + $feed = new KT_cmis_atom_responseFeed(CMIS_APP_BASE_URI, $typesHeading, null, null, null, 'urn:uuid:' . $typesString); | |
| 116 | 116 | |
| 117 | 117 | foreach($types as $type) |
| 118 | 118 | { |
| ... | ... | @@ -157,6 +157,16 @@ class KT_cmis_atom_service_helper { |
| 157 | 157 | return $feed; |
| 158 | 158 | } |
| 159 | 159 | |
| 160 | + static public function getErrorFeed(&$service, $status, $message) | |
| 161 | + { | |
| 162 | + $service->setStatus($status); | |
| 163 | + $feed = new KT_cmis_atom_responseFeed(CMIS_APP_BASE_URI, 'Error: ' . $status); | |
| 164 | + $entry = $feed->newEntry(); | |
| 165 | + $feed->newField('error', $message, $entry); | |
| 166 | + | |
| 167 | + return $feed; | |
| 168 | + } | |
| 169 | + | |
| 160 | 170 | /** |
| 161 | 171 | * Fetches the CMIS objectId based on the path |
| 162 | 172 | * | ... | ... |
webservice/classes/atompub/KT_atom_server.inc.php
| 1 | 1 | <?php |
| 2 | -class KT_atom_server{ | |
| 2 | +class KT_atom_server { | |
| 3 | + | |
| 4 | + public $output=''; | |
| 3 | 5 | protected $services=array(); |
| 4 | 6 | protected $workspaceDetail=array(); |
| 5 | 7 | protected $errors=array(); |
| 6 | - public $output=''; | |
| 7 | 8 | protected $queryArray=array(); |
| 8 | 9 | protected $serviceName=''; |
| 9 | 10 | protected $method=''; |
| 10 | 11 | protected $workspace=''; |
| 11 | - | |
| 12 | + protected $serviceObject = null; | |
| 12 | 13 | |
| 13 | 14 | public function __construct(){ |
| 14 | 15 | } | ... | ... |
webservice/classes/atompub/KT_atom_service.inc.php
| ... | ... | @@ -3,11 +3,11 @@ class KT_atom_service{ |
| 3 | 3 | const STATUS_OK = '200 OK'; |
| 4 | 4 | const STATUS_NOT_FOUND = '204 No Content'; |
| 5 | 5 | const STATUS_NOT_ALLOWED = '204 Not Allowed'; |
| 6 | - const STATUS_NOT_AUTHENTICATED = '204 Not Authenticated'; | |
| 7 | 6 | const STATUS_CREATED = '201 Created'; |
| 8 | 7 | const STATUS_UPDATED = '200 Updated'; |
| 9 | 8 | const STATUS_NOT_MODIFIED = '304 Not Modified'; // For use with ETag & If-None-Match headers. |
| 10 | 9 | const STATUS_BAD_REQUEST = '400 Bad Request'; // Client issued a wrongly constructed request |
| 10 | + const STATUS_NOT_AUTHENTICATED = '401 Not Authenticated'; | |
| 11 | 11 | const STATUS_PRECONDITION_FAILED = '412 Precondition Failed'; // Could not update document because another a newer version exist on the server than the one you are trying to update |
| 12 | 12 | const STATUS_SERVER_ERROR = '500 Internal Server Error'; // Server encountered an error processing the request |
| 13 | 13 | |
| ... | ... | @@ -56,6 +56,10 @@ class KT_atom_service{ |
| 56 | 56 | $this->setStatus(KT_atom_service::STATUS_NOT_FOUND ); |
| 57 | 57 | } |
| 58 | 58 | |
| 59 | + public function getHeaders() | |
| 60 | + { | |
| 61 | + return $this->headers; | |
| 62 | + } | |
| 59 | 63 | |
| 60 | 64 | public function render(){ |
| 61 | 65 | return $this->responseFeed->render(); | ... | ... |
webservice/classes/atompub/cmis/KT_cmis_atom_server.inc.php
| ... | ... | @@ -119,6 +119,24 @@ class KT_cmis_atom_server extends KT_atom_server { |
| 119 | 119 | return false; |
| 120 | 120 | } |
| 121 | 121 | |
| 122 | + // TODO we probably want this version in the base class for auth purposes | |
| 123 | + public function render() | |
| 124 | + { | |
| 125 | + ob_end_clean(); | |
| 126 | + // possible additional headers, e.g. basic auth request | |
| 127 | + // FIXME this won't work with the service document as no service object exists | |
| 128 | + if (!is_null($this->serviceObject)) | |
| 129 | + { | |
| 130 | + $headers = $this->serviceObject->getHeaders(); | |
| 131 | + foreach ($headers as $header) | |
| 132 | + { | |
| 133 | + header($header); | |
| 134 | + } | |
| 135 | + } | |
| 136 | + header('Content-type: text/xml'); | |
| 137 | + echo $this->output; | |
| 138 | + } | |
| 139 | + | |
| 122 | 140 | } |
| 123 | 141 | |
| 124 | 142 | ?> |
| 125 | 143 | \ No newline at end of file | ... | ... |
webservice/classes/atompub/cmis/KT_cmis_atom_service.inc.php
| ... | ... | @@ -10,17 +10,12 @@ class KT_cmis_atom_service extends KT_atom_service { |
| 10 | 10 | |
| 11 | 11 | protected function parseHeaders() |
| 12 | 12 | { |
| 13 | -// echo "PARSE HEADERS\n"; | |
| 14 | 13 | parent::parseHeaders(); |
| 15 | -// echo "CHECKING HEADERS\n"; | |
| 16 | -// print_r($this->headers); | |
| 17 | -// print_r($_SERVER); | |
| 18 | 14 | // attempt to fetch auth info from supplied headers |
| 19 | 15 | if (!empty($this->headers['Authorization'])) |
| 20 | 16 | { |
| 21 | 17 | $auth = base64_decode(preg_replace('/Basic */', '', $this->headers['Authorization'])); |
| 22 | 18 | $authData = explode(':', $auth); |
| 23 | -// print_r($authData); | |
| 24 | 19 | self::$authData['username'] = $authData[0]; |
| 25 | 20 | self::$authData['password'] = $authData[1]; |
| 26 | 21 | } | ... | ... |
webservice/classes/atompub/cmis/KT_cmis_atom_serviceDoc.inc.php
| ... | ... | @@ -32,7 +32,7 @@ |
| 32 | 32 | * logo is not reasonably feasible for technical reasons, the Appropriate Legal Notices |
| 33 | 33 | * must display the words "Powered by KnowledgeTree" and retain the original |
| 34 | 34 | * copyright notice. |
| 35 | - * Contributor( s): | |
| 35 | + * Contributor( s): | |
| 36 | 36 | * Mark Holtzhausen <mark@knowledgetree.com> |
| 37 | 37 | * Paul Barrett <paul@knowledgetree.com> |
| 38 | 38 | * |
| ... | ... | @@ -47,13 +47,13 @@ include_once(KT_ATOM_LIB_FOLDER.'KT_atom_serviceDoc.inc.php'); |
| 47 | 47 | |
| 48 | 48 | class KT_cmis_atom_serviceDoc extends KT_atom_serviceDoc { |
| 49 | 49 | |
| 50 | - // override and extend as needed | |
| 50 | +// override and extend as needed | |
| 51 | 51 | |
| 52 | 52 | public $repositoryInfo = array(); |
| 53 | 53 | |
| 54 | 54 | public function __construct($baseURI = NULL) |
| 55 | 55 | { |
| 56 | - parent::__construct(); | |
| 56 | + parent::__construct(); | |
| 57 | 57 | |
| 58 | 58 | // get repositoryInfo |
| 59 | 59 | // NOTE currently we only support one repository, which will be the first one found in the repositories.xml config |
| ... | ... | @@ -61,63 +61,65 @@ class KT_cmis_atom_serviceDoc extends KT_atom_serviceDoc { |
| 61 | 61 | |
| 62 | 62 | include 'services/cmis/RepositoryService.inc.php'; |
| 63 | 63 | $RepositoryService = new RepositoryService(); |
| 64 | + // TODO add auth requirement here, don't want to even supply service doc without auth | |
| 65 | +// $RepositoryService->startSession(); | |
| 64 | 66 | |
| 65 | 67 | // fetch data for response |
| 66 | 68 | $repositories = $RepositoryService->getRepositories(); |
| 67 | 69 | // fetch for default first repo; NOTE that this will probably have to change at some point, quick and dirty for now |
| 68 | 70 | $this->repositoryInfo = $RepositoryService->getRepositoryInfo($repositories[0]['repositoryId']); |
| 69 | - } | |
| 70 | - | |
| 71 | - protected function constructServiceDocumentHeaders() | |
| 71 | + } | |
| 72 | + | |
| 73 | + protected function constructServiceDocumentHeaders() | |
| 72 | 74 | { |
| 73 | - $service = $this->newElement('service'); | |
| 74 | - $service->appendChild($this->newAttr('xmlns', 'http://www.w3.org/2007/app')); | |
| 75 | - $service->appendChild($this->newAttr('xmlns:atom', 'http://www.w3.org/2005/Atom')); | |
| 75 | + $service = $this->newElement('service'); | |
| 76 | + $service->appendChild($this->newAttr('xmlns', 'http://www.w3.org/2007/app')); | |
| 77 | + $service->appendChild($this->newAttr('xmlns:atom', 'http://www.w3.org/2005/Atom')); | |
| 76 | 78 | $service->appendChild($this->newAttr('xmlns:cmis', 'http://www.cmis.org/2008/05')); |
| 77 | 79 | $this->service =& $service; |
| 78 | - $this->DOM->appendChild($this->service); | |
| 79 | - } | |
| 80 | - | |
| 81 | - public function &newCollection($url = NULL, $title = NULL, $cmisCollectionType = NULL, &$ws = NULL) | |
| 80 | + $this->DOM->appendChild($this->service); | |
| 81 | + } | |
| 82 | + | |
| 83 | + public function &newCollection($url = NULL, $title = NULL, $cmisCollectionType = NULL, &$ws = NULL) | |
| 82 | 84 | { |
| 83 | - $collection=$this->newElement('collection'); | |
| 84 | - $collection->appendChild($this->newAttr('href', $url)); | |
| 85 | - $collection->appendChild($this->newAttr('cmis:collectionType', $cmisCollectionType)); | |
| 86 | - $collection->appendChild($this->newElement('atom:title', $title)); | |
| 87 | - if(isset($ws))$ws->appendChild($collection); | |
| 88 | - return $collection; | |
| 89 | - } | |
| 90 | - | |
| 85 | + $collection=$this->newElement('collection'); | |
| 86 | + $collection->appendChild($this->newAttr('href', $url)); | |
| 87 | + $collection->appendChild($this->newAttr('cmis:collectionType', $cmisCollectionType)); | |
| 88 | + $collection->appendChild($this->newElement('atom:title', $title)); | |
| 89 | + if(isset($ws))$ws->appendChild($collection); | |
| 90 | + return $collection; | |
| 91 | + } | |
| 92 | + | |
| 91 | 93 | } |
| 92 | 94 | |
| 93 | 95 | /** |
| 94 | -<?xml version="1.0" encoding="utf-8"?> | |
| 95 | -<service xmlns="http://www.w3.org/2007/app" xmlns:atom="http://www.w3.org/2005/Atom"> | |
| 96 | - <workspace> | |
| 97 | - <atom:title>Main Site</atom:title> | |
| 98 | - <collection href="http://example.org/blog/main" > | |
| 99 | - <atom:title>My Blog Entries</atom:title> | |
| 100 | - <categories href="http://example.com/cats/forMain.cats" /> | |
| 101 | - </collection> | |
| 102 | - <collection href="http://example.org/blog/pic" > | |
| 103 | - <atom:title>Pictures</atom:title> | |
| 104 | - <accept>image/png</accept> | |
| 105 | - <accept>image/jpeg</accept> | |
| 106 | - <accept>image/gif</accept> | |
| 107 | - </collection> | |
| 108 | - </workspace> | |
| 109 | - <workspace> | |
| 110 | - <atom:title>Sidebar Blog</atom:title> | |
| 111 | - <collection href="http://example.org/sidebar/list" > | |
| 112 | - <atom:title>Remaindered Links</atom:title> | |
| 113 | - <accept>application/atom+xml;type=entry</accept> | |
| 114 | - <categories fixed="yes"> | |
| 115 | - <atom:category scheme="http://example.org/extra-cats/" term="joke" /> | |
| 116 | - <atom:category scheme="http://example.org/extra-cats/" term="serious" /> | |
| 117 | - </categories> | |
| 118 | - </collection> | |
| 119 | - </workspace> | |
| 120 | -</service> | |
| 96 | + <?xml version="1.0" encoding="utf-8"?> | |
| 97 | + <service xmlns="http://www.w3.org/2007/app" xmlns:atom="http://www.w3.org/2005/Atom"> | |
| 98 | + <workspace> | |
| 99 | + <atom:title>Main Site</atom:title> | |
| 100 | + <collection href="http://example.org/blog/main" > | |
| 101 | + <atom:title>My Blog Entries</atom:title> | |
| 102 | + <categories href="http://example.com/cats/forMain.cats" /> | |
| 103 | + </collection> | |
| 104 | + <collection href="http://example.org/blog/pic" > | |
| 105 | + <atom:title>Pictures</atom:title> | |
| 106 | + <accept>image/png</accept> | |
| 107 | + <accept>image/jpeg</accept> | |
| 108 | + <accept>image/gif</accept> | |
| 109 | + </collection> | |
| 110 | + </workspace> | |
| 111 | + <workspace> | |
| 112 | + <atom:title>Sidebar Blog</atom:title> | |
| 113 | + <collection href="http://example.org/sidebar/list" > | |
| 114 | + <atom:title>Remaindered Links</atom:title> | |
| 115 | + <accept>application/atom+xml;type=entry</accept> | |
| 116 | + <categories fixed="yes"> | |
| 117 | + <atom:category scheme="http://example.org/extra-cats/" term="joke" /> | |
| 118 | + <atom:category scheme="http://example.org/extra-cats/" term="serious" /> | |
| 119 | + </categories> | |
| 120 | + </collection> | |
| 121 | + </workspace> | |
| 122 | + </service> | |
| 121 | 123 | |
| 122 | 124 | */ |
| 123 | 125 | ... | ... |