Permalink
| <?php | |
| namespace Jackalope\Transport\Jackrabbit; | |
| use DOMDocument; | |
| use DOMElement; | |
| use DOMXPath; | |
| use LogicException; | |
| use InvalidArgumentException; | |
| use PHPCR\CredentialsInterface; | |
| use PHPCR\ItemExistsException; | |
| use PHPCR\Query\InvalidQueryException; | |
| use PHPCR\RepositoryInterface; | |
| use PHPCR\SimpleCredentials; | |
| use PHPCR\PropertyType; | |
| use PHPCR\SessionInterface; | |
| use PHPCR\RepositoryException; | |
| use PHPCR\UnsupportedRepositoryOperationException; | |
| use PHPCR\ItemNotFoundException; | |
| use PHPCR\PathNotFoundException; | |
| use PHPCR\LoginException; | |
| use PHPCR\Query\QueryInterface; | |
| use PHPCR\Observation\EventFilterInterface; | |
| use PHPCR\Util\PathHelper; | |
| use Jackalope\Transport\BaseTransport; | |
| use Jackalope\Transport\QueryInterface as QueryTransport; | |
| use Jackalope\Transport\PermissionInterface; | |
| use Jackalope\Transport\WritingInterface; | |
| use Jackalope\Transport\VersioningInterface; | |
| use Jackalope\Transport\NodeTypeCndManagementInterface; | |
| use Jackalope\Transport\LockingInterface; | |
| use Jackalope\Transport\ObservationInterface; | |
| use Jackalope\Transport\WorkspaceManagementInterface; | |
| use Jackalope\NotImplementedException; | |
| use Jackalope\Node; | |
| use Jackalope\Property; | |
| use Jackalope\Query\Query; | |
| use Jackalope\NodeType\NodeTypeManager; | |
| use Jackalope\Lock\Lock; | |
| use Jackalope\FactoryInterface; | |
| use PHPCR\Util\ValueConverter; | |
| use PHPCR\ValueFormatException; | |
| use PHPCR\Version\LabelExistsVersionException; | |
| /** | |
| * Connection to one Jackrabbit server. | |
| * | |
| * This class handles the communication between Jackalope and Jackrabbit over | |
| * Davex. Once the login method has been called, the workspace is set and can | |
| * not be changed anymore. | |
| * | |
| * We make one exception to the rule that nothing may be cached in the | |
| * transport: Repository descriptors are considered immutable and cached | |
| * (because they are also used in startup to check the backend version is | |
| * compatible). | |
| * | |
| * @license http://www.apache.org/licenses Apache License Version 2.0, January 2004 | |
| * @license http://opensource.org/licenses/MIT MIT License | |
| * * | |
| * @author Christian Stocker <chregu@liip.ch> | |
| * @author David Buchmann <david@liip.ch> | |
| * @author Tobias Ebnöther <ebi@liip.ch> | |
| * @author Roland Schilter <roland.schilter@liip.ch> | |
| * @author Uwe Jäger <uwej711@googlemail.com> | |
| * @author Lukas Kahwe Smith <smith@pooteeweet.org> | |
| * @author Daniel Barsotti <daniel.barsotti@liip.ch> | |
| * @author Markus Schmucker <markus.sr@gmx.net> | |
| */ | |
| class Client extends BaseTransport implements JackrabbitClientInterface | |
| { | |
| /** | |
| * minimal version needed for the backend server | |
| */ | |
| const VERSION = "2.3.6"; | |
| /** | |
| * Description of the namspace to be used for communication with the server. | |
| * @var string | |
| */ | |
| const NS_DCR = 'http://www.day.com/jcr/webdav/1.0'; | |
| /** | |
| * Identifier of the used namespace. | |
| * @var string | |
| */ | |
| const NS_DAV = 'DAV:'; | |
| /** | |
| * Value of the <timeout> tag for infinite timeout (since jackrabbit 2.4) | |
| */ | |
| const JCR_INFINITE = 'Infinite'; | |
| /** | |
| * Jackrabbit 2.3.6+2.3.7 return this weird number to say its an infinite lock | |
| * This has been fixed in 2.4 | |
| */ | |
| const JCR_INFINITE_LOCK_TIMEOUT = 2147483; | |
| /** | |
| * The path to request to get the Jackrabbit event journal | |
| */ | |
| const JCR_JOURNAL_PATH = '?type=journal'; | |
| /** | |
| * The factory to instantiate objects | |
| * @var FactoryInterface | |
| */ | |
| protected $factory; | |
| /** @var ValueConverter */ | |
| protected $valueConverter; | |
| /** | |
| * Server url including protocol. | |
| * | |
| * i.e http://localhost:8080/server/ | |
| * constructor ensures the trailing slash / | |
| * | |
| * @var string | |
| */ | |
| protected $server; | |
| /** | |
| * Workspace name the transport is bound to | |
| * | |
| * Set once login() has been executed and may not be changed later on. | |
| * | |
| * @var string | |
| */ | |
| protected $workspace; | |
| /** | |
| * Identifier of the workspace including the used protocol and server name. | |
| * | |
| * "$server/$workspace" without trailing slash | |
| * | |
| * @var string | |
| */ | |
| protected $workspaceUri; | |
| /** | |
| * Root node path with server domain without trailing slash. | |
| * | |
| * "$server/$workspace/jcr%3aroot | |
| * (make sure you never hardcode the jcr%3aroot, its ugly) | |
| * @todo apparently, jackrabbit handles the root node by name - it is invisible everywhere for the api, | |
| * but needed when talking to the backend... could that name change? | |
| * | |
| * @var string | |
| */ | |
| protected $workspaceUriRoot; | |
| /** | |
| * Set of credentials necessary to connect to the server. | |
| * | |
| * Set once login() has been executed and may not be changed later on. | |
| * | |
| * @var SimpleCredentials | |
| */ | |
| protected $credentials; | |
| /** | |
| * The cURL resource handle | |
| * @var curl | |
| */ | |
| protected $curl = null; | |
| /** | |
| * A list of additional HTTP headers to be sent on each request | |
| * @var array[]string | |
| */ | |
| protected $defaultHeaders = array(); | |
| /** | |
| * @var bool Send Expect: 100-continue header | |
| */ | |
| protected $sendExpect = false; | |
| /** | |
| * @var \Jackalope\NodeType\NodeTypeXmlConverter | |
| */ | |
| protected $typeXmlConverter; | |
| /** | |
| * @var NodeTypeManager | |
| */ | |
| protected $nodeTypeManager; | |
| /** | |
| * Check if an initial PROPFIND should be send to check if repository exists | |
| * This is according to the JCR specifications and set to true by default | |
| * @see setCheckLoginOnServer | |
| * @var bool | |
| */ | |
| protected $checkLoginOnServer = true; | |
| /** | |
| * Cached result of the repository descriptors. | |
| * | |
| * This is our exception to the rule that nothing may be cached in transport. | |
| * | |
| * @var array of strings as returned by getRepositoryDescriptors | |
| */ | |
| protected $descriptors = null; | |
| protected $jsopBody = array(); | |
| protected $userData; | |
| /** | |
| * Global curl-options used in each request. | |
| * | |
| * @var array | |
| */ | |
| private $curlOptions = array(); | |
| /** | |
| * Create a transport pointing to a server url. | |
| * | |
| * @param FactoryInterface $factory the object factory | |
| * @param string $serverUri location of the server | |
| */ | |
| public function __construct(FactoryInterface $factory, $serverUri) | |
| { | |
| $this->factory = $factory; | |
| $this->valueConverter = $this->factory->get('PHPCR\Util\ValueConverter'); | |
| // append a slash if not there | |
| if ('/' !== substr($serverUri, -1)) { | |
| $serverUri .= '/'; | |
| } | |
| $this->server = $serverUri; | |
| } | |
| /** | |
| * Tidies up the current cUrl connection. | |
| */ | |
| public function __destruct() | |
| { | |
| $this->logout(); | |
| } | |
| /** | |
| * {@inheritDoc} | |
| */ | |
| public function addDefaultHeader($header) | |
| { | |
| $this->defaultHeaders[] = $header; | |
| } | |
| /** | |
| * {@inheritDoc} | |
| */ | |
| public function sendExpect($send = true) | |
| { | |
| $this->sendExpect = $send; | |
| } | |
| /** | |
| * {@inheritDoc} | |
| */ | |
| public function forceHttpVersion10($forceHttpVersion10 = true) | |
| { | |
| if ($forceHttpVersion10) { | |
| $this->addCurlOptions(array(CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_0)); | |
| } else { | |
| unset($this->curlOptions[CURLOPT_HTTP_VERSION]); | |
| } | |
| } | |
| /** | |
| * {@inheritDoc} | |
| */ | |
| public function addCurlOptions(array $options) | |
| { | |
| return $this->curlOptions += $options; | |
| } | |
| /** | |
| * Makes sure there is an open curl connection. | |
| * | |
| * @return Request The Request | |
| */ | |
| protected function getRequest($method, $uri, $addWorkspacePathToUri = true) | |
| { | |
| $uri = (array) $uri; | |
| $curl = $this->getCurl(); | |
| if ($addWorkspacePathToUri) { | |
| foreach ($uri as $key => $row) { | |
| $uri[$key] = $this->addWorkspacePathToUri($row); | |
| } | |
| } | |
| $request = $this->factory->get('Transport\\Jackrabbit\\Request', array($this, $curl, $method, $uri)); | |
| $request->setCredentials($this->credentials); | |
| if (null !== $this->userData) { | |
| $request->addUserData($this->userData); | |
| } | |
| foreach ($this->defaultHeaders as $header) { | |
| $request->addHeader($header); | |
| } | |
| if (!$this->sendExpect) { | |
| $request->addHeader("Expect:"); | |
| } | |
| $request->addCurlOptions($this->curlOptions); | |
| return $request; | |
| } | |
| protected function getCurl() | |
| { | |
| if (is_null($this->curl)) { | |
| // lazy init curl | |
| $this->curl = new curl(); | |
| } elseif ($this->curl === false) { | |
| // but do not re-connect, rather report the error if trying to access a closed connection | |
| throw new LogicException('Tried to start a request on a closed transport.'); | |
| } | |
| return $this->curl; | |
| } | |
| /** | |
| * {@inheritDoc} | |
| */ | |
| public function getWorkspaceUri() | |
| { | |
| return $this->workspaceUri; | |
| } | |
| // CoreInterface // | |
| /** | |
| * {@inheritDoc} | |
| */ | |
| public function login(CredentialsInterface $credentials = null, $workspaceName = null) | |
| { | |
| if ($this->credentials) { | |
| throw new RepositoryException( | |
| 'Do not call login twice. Rather instantiate a new Transport object '. | |
| 'to log in as different user or for a different workspace.' | |
| ); | |
| } | |
| if (!$credentials instanceof SimpleCredentials) { | |
| $hint = is_null($credentials) | |
| ? 'jackalope-jackrabbit does not support "null" credentials' | |
| : 'Only SimpleCredentials are supported. Unkown credentials type: '.get_class($credentials); | |
| throw new LoginException($hint); | |
| } | |
| $this->credentials = $credentials; | |
| if (! $workspaceName) { | |
| $request = $this->getRequest(Request::PROPFIND, $this->server); | |
| $request->setBody($this->buildPropfindRequest(array('dcr:workspaceName'))); | |
| $dom = $request->executeDom(); | |
| $answer = $dom->getElementsByTagNameNS(self::NS_DCR, 'workspaceName'); | |
| $workspaceName = $answer->item(0)->textContent; | |
| } | |
| $this->workspace = $workspaceName; | |
| $this->workspaceUri = $this->server . $workspaceName; | |
| $this->workspaceUriRoot = $this->workspaceUri . "/jcr:root"; | |
| if (!$this->checkLoginOnServer) { | |
| return $workspaceName; | |
| } | |
| $request = $this->getRequest(Request::PROPFIND, $this->workspaceUri); | |
| $request->setBody($this->buildPropfindRequest(array('D:workspace', 'dcr:workspaceName'))); | |
| $dom = $request->executeDom(); | |
| $set = $dom->getElementsByTagNameNS(self::NS_DCR, 'workspaceName'); | |
| if ($set->length != 1) { | |
| throw new RepositoryException('Unexpected answer from server: '.$dom->saveXML()); | |
| } | |
| if ($set->item(0)->textContent != $this->workspace) { | |
| throw new RepositoryException('Wrong workspace in answer from server: '.$dom->saveXML()); | |
| } | |
| return $workspaceName; | |
| } | |
| /** | |
| * {@inheritDoc} | |
| */ | |
| public function logout() | |
| { | |
| if (!empty($this->curl)) { | |
| $this->curl->close(); | |
| } | |
| $this->curl = false; | |
| } | |
| /** | |
| * {@inheritDoc} | |
| */ | |
| public function setCheckLoginOnServer($bool) | |
| { | |
| $this->checkLoginOnServer = $bool; | |
| } | |
| /** | |
| * {@inheritDoc} | |
| */ | |
| public function getRepositoryDescriptors() | |
| { | |
| if (null == $this->descriptors) { | |
| $request = $this->getRequest(Request::REPORT, $this->server); | |
| $request->setBody($this->buildReportRequest('dcr:repositorydescriptors')); | |
| $dom = $request->executeDom(); | |
| if ($dom->firstChild->localName != 'repositorydescriptors-report' | |
| || $dom->firstChild->namespaceURI != self::NS_DCR | |
| ) { | |
| throw new RepositoryException('Error talking to the backend. '.$dom->saveXML()); | |
| } | |
| $descs = $dom->getElementsByTagNameNS(self::NS_DCR, 'descriptor'); | |
| $this->descriptors = array(); | |
| foreach ($descs as $desc) { | |
| $name = $desc->getElementsByTagNameNS(self::NS_DCR, 'descriptorkey')->item(0)->textContent; | |
| $values = array(); | |
| $valuenodes = $desc->getElementsByTagNameNS(self::NS_DCR, 'descriptorvalue'); | |
| foreach ($valuenodes as $value) { | |
| $values[] = $value->textContent; | |
| } | |
| if ($valuenodes->length == 1) { | |
| //there was one type and one value => this is a single value property | |
| //TODO: is this the correct assumption? or should the backend tell us specifically? | |
| $this->descriptors[$name] = $values[0]; | |
| } else { | |
| $this->descriptors[$name] = $values; | |
| } | |
| } | |
| // Supported by Jackrabbit, but not supported by this client | |
| $this->descriptors[RepositoryInterface::NODE_TYPE_MANAGEMENT_SAME_NAME_SIBLINGS_SUPPORTED] = false; | |
| $this->descriptors[RepositoryInterface::QUERY_CANCEL_SUPPORTED] = false; | |
| if (! isset($this->descriptors['jcr.repository.version'])) { | |
| throw new UnsupportedRepositoryOperationException("The backend at {$this->server} does not provide the jcr.repository.version descriptor"); | |
| } | |
| if (! version_compare(self::VERSION, $this->descriptors['jcr.repository.version'], '<=')) { | |
| throw new UnsupportedRepositoryOperationException("The backend at {$this->server} is an unsupported version of jackrabbit: \"". | |
| $this->descriptors['jcr.repository.version']. | |
| '". Need at least "'.self::VERSION.'"'); | |
| } | |
| } | |
| return $this->descriptors; | |
| } | |
| /** | |
| * {@inheritDoc} | |
| */ | |
| public function getAccessibleWorkspaceNames() | |
| { | |
| $request = $this->getRequest(Request::PROPFIND, $this->server); | |
| $request->setBody($this->buildPropfindRequest(array('D:workspace'))); | |
| $request->setDepth(1); | |
| $dom = $request->executeDom(); | |
| $workspaces = array(); | |
| foreach ($dom->getElementsByTagNameNS(self::NS_DAV, 'workspace') as $value) { | |
| if (!empty($value->nodeValue)) { | |
| $workspaces[] = substr(trim($value->nodeValue), strlen($this->server), -1); | |
| } | |
| } | |
| return array_unique($workspaces); | |
| } | |
| /** | |
| * {@inheritDoc} | |
| */ | |
| public function getNode($path) | |
| { | |
| $path = $this->encodeAndValidatePathForDavex($path); | |
| $path .= '.'.$this->getFetchDepth().'.json'; | |
| $request = $this->getRequest(Request::GET, $path); | |
| try { | |
| return $request->executeJson(); | |
| } catch (PathNotFoundException $e) { | |
| throw new ItemNotFoundException($e->getMessage(), $e->getCode(), $e); | |
| } | |
| } | |
| /** | |
| * {@inheritDoc} | |
| */ | |
| public function getNodes($paths, $query = ':include') | |
| { | |
| if (count($paths) == 0) { | |
| return array(); | |
| } | |
| if (count($paths) == 1) { | |
| $url = array_shift($paths); | |
| try { | |
| return array($url => $this->getNode($url)); | |
| } catch (ItemNotFoundException $e) { | |
| return array(); | |
| } | |
| } | |
| $body = array(); | |
| $url = '/.'.$this->getFetchDepth().'.json'; | |
| foreach ($paths as $path) { | |
| $body[] = http_build_query(array($query => $path)); | |
| } | |
| $body = implode('&', $body); | |
| $request = $this->getRequest(Request::POST, $url); | |
| $request->setBody($body); | |
| $request->setContentType('application/x-www-form-urlencoded; charset=utf-8'); | |
| try { | |
| $data = $request->executeJson(); | |
| return (array) $data->nodes; | |
| } catch (PathNotFoundException $e) { | |
| throw new ItemNotFoundException($e->getMessage(), $e->getCode(), $e); | |
| } | |
| } | |
| /** | |
| * {@inheritDoc} | |
| */ | |
| public function getNodesByIdentifier($identifiers) | |
| { | |
| // OPTIMIZE get paths for UUID's via a single query | |
| // or get the data directly | |
| // return $this->getNodes($identifiers, ':id'); | |
| $paths = array(); | |
| foreach ($identifiers as $key => $identifier) { | |
| try { | |
| $paths[$key] = $this->getNodePathForIdentifier($identifier); | |
| } catch (ItemNotFoundException $e) { | |
| // ignore | |
| } | |
| } | |
| return $this->getNodes($paths); | |
| } | |
| /** | |
| * {@inheritDoc} | |
| */ | |
| public function getNodeByIdentifier($uuid) | |
| { | |
| // OPTIMIZE get nodes directly by uuid from backend. needs implementation on jackrabbit | |
| $path = $this->getNodePathForIdentifier($uuid); | |
| $data = $this->getNode($path); | |
| $data->{':jcr:path'} = $path; | |
| return $data; | |
| } | |
| /** | |
| * {@inheritDoc} | |
| */ | |
| public function getNodePathForIdentifier($uuid, $workspace = null) | |
| { | |
| if (null !== $workspace && $workspace != $this->workspace) { | |
| $client = new Client($this->factory, $this->server); | |
| $client->login($this->credentials, $workspace); | |
| return $client->getNodePathForIdentifier($uuid); | |
| } | |
| $request = $this->getRequest(Request::REPORT, $this->workspaceUri); | |
| $request->setBody($this->buildLocateRequest($uuid)); | |
| $dom = $request->executeDom(); | |
| /* answer looks like | |
| <D:multistatus xmlns:D="DAV:"> | |
| <D:response> | |
| <D:href>http://localhost:8080/server/tests/jcr%3aroot/tests_level1_access_base/idExample/</D:href> | |
| </D:response> | |
| </D:multistatus> | |
| */ | |
| $set = $dom->getElementsByTagNameNS(self::NS_DAV, 'href'); | |
| if ($set->length != 1) { | |
| throw new RepositoryException('Unexpected answer from server: '.$dom->saveXML()); | |
| } | |
| $fullPath = $set->item(0)->textContent; | |
| if (strncmp($this->workspaceUriRoot, $fullPath, strlen($this->workspaceUri))) { | |
| throw new RepositoryException( | |
| "Server answered a path that is not in the current workspace: uuid=$uuid, path=$fullPath, workspace=". | |
| $this->workspaceUriRoot | |
| ); | |
| } | |
| return $this->stripServerRootFromUri(substr(urldecode($fullPath), 0, -1)); | |
| } | |
| /** | |
| * {@inheritDoc} | |
| */ | |
| public function getProperty($path) | |
| { | |
| throw new NotImplementedException(); | |
| /* | |
| * TODO: implement | |
| * jackrabbit: instead of fetching the node, we could make Transport provide it with a | |
| * GET /server/tests/jcr%3aroot/tests_level1_access_base/multiValueProperty/jcr%3auuid | |
| * (davex getItem uses json, which is not applicable to properties) | |
| */ | |
| } | |
| /** | |
| * {@inheritDoc} | |
| */ | |
| public function getBinaryStream($path) | |
| { | |
| $path = $this->encodeAndValidatePathForDavex($path); | |
| $request = $this->getRequest(Request::GET, $path); | |
| $curl = $request->execute(true); | |
| switch ($curl->getHeader('Content-Type')) { | |
| case 'text/xml; charset=utf-8': | |
| case 'text/xml;charset=utf-8': | |
| return $this->decodeBinaryDom($curl->getResponse()); | |
| case 'jcr-value/binary;charset=utf-8': | |
| case 'jcr-value/binary; charset=utf-8': | |
| // TODO: OPTIMIZE stream handling! | |
| $stream = fopen('php://memory', 'rwb+'); | |
| fwrite($stream, $curl->getResponse()); | |
| rewind($stream); | |
| return $stream; | |
| } | |
| throw new RepositoryException('Unknown encoding of binary data: '.$curl->getHeader('Content-Type')); | |
| } | |
| /** | |
| * parse the multivalue binary response (a list of base64 encoded values) | |
| * | |
| * <dcr:values xmlns:dcr="http://www.day.com/jcr/webdav/1.0"> | |
| * <dcr:value dcr:type="Binary">aDEuIENoYXB0ZXIgMSBUaXRsZQoKKiBmb28KKiBiYXIKKiogZm9vMgoqKiBmb28zCiogZm9vMAoKfHwgaGVhZGVyIHx8IGJhciB8fAp8IGggfCBqIHwKCntjb2RlfQpoZWxsbyB3b3JsZAp7Y29kZX0KCiMgZm9vCg==</dcr:value> | |
| * <dcr:value dcr:type="Binary">aDEuIENoYXB0ZXIgMSBUaXRsZQoKKiBmb28KKiBiYXIKKiogZm9vMgoqKiBmb28zCiogZm9vMAoKfHwgaGVhZGVyIHx8IGJhciB8fAp8IGggfCBqIHwKCntjb2RlfQpoZWxsbyB3b3JsZAp7Y29kZX0KCiMgZm9vCg==</dcr:value> | |
| * </dcr:values> | |
| * | |
| * @param string $xml the xml as returned by jackrabbit | |
| * | |
| * @return array of stream resources | |
| * | |
| * @throws RepositoryException if the xml is invalid or any value is not of type binary | |
| */ | |
| private function decodeBinaryDom($xml) | |
| { | |
| $dom = new DOMDocument(); | |
| if (! $dom->loadXML($xml)) { | |
| throw new RepositoryException("Failed to load xml data:\n\n$xml"); | |
| } | |
| $ret = array(); | |
| foreach ($dom->getElementsByTagNameNS(self::NS_DCR, 'values') as $node) { | |
| foreach ($node->getElementsByTagNameNS(self::NS_DCR, 'value') as $value) { | |
| if ($value->getAttributeNS(self::NS_DCR, 'type') != PropertyType::TYPENAME_BINARY) { | |
| throw new RepositoryException('Expected binary value but got '.$value->getAttributeNS(self::NS_DCR, 'type')); | |
| } | |
| // TODO: OPTIMIZE stream handling! | |
| $stream = fopen('php://memory', 'rwb+'); | |
| fwrite($stream, base64_decode($value->textContent)); | |
| rewind($stream); | |
| $ret[] = $stream; | |
| } | |
| } | |
| return $ret; | |
| } | |
| /** | |
| * {@inheritDoc} | |
| */ | |
| public function getReferences($path, $name = null) | |
| { | |
| return $this->getNodeReferences($path, $name); | |
| } | |
| /** | |
| * {@inheritDoc} | |
| */ | |
| public function getWeakReferences($path, $name = null) | |
| { | |
| return $this->getNodeReferences($path, $name, true); | |
| } | |
| /** | |
| * @param string $path the path for which we need the references | |
| * @param string $name the name of the referencing properties or null for all | |
| * @param bool $weak_reference whether to get weak or strong references | |
| * | |
| * @return array list of paths to nodes that reference $path | |
| */ | |
| protected function getNodeReferences($path, $name = null, $weak_reference = false) | |
| { | |
| $path = $this->encodeAndValidatePathForDavex($path); | |
| $identifier = $weak_reference ? 'weakreferences' : 'references'; | |
| $request = $this->getRequest(Request::PROPFIND, $path); | |
| $request->setBody($this->buildPropfindRequest(array('dcr:'.$identifier))); | |
| $request->setDepth(0); | |
| $dom = $request->executeDom(); | |
| $references = array(); | |
| foreach ($dom->getElementsByTagNameNS(self::NS_DCR, $identifier) as $node) { | |
| foreach ($node->getElementsByTagNameNS(self::NS_DAV, 'href') as $ref) { | |
| $refpath = str_replace($this->workspaceUriRoot, '', urldecode($ref->textContent)); | |
| $refpath = $this->removeTrailingSlash($refpath); | |
| if (null === $name || PathHelper::getNodeName($refpath) === $name) { | |
| $references[] = $refpath; | |
| } | |
| } | |
| } | |
| return $references; | |
| } | |
| /** | |
| * Remove the trailing slash if present. Used for backend responses when | |
| * jackrabbit is sloppy | |
| * | |
| * @param string $path a path with potentially a trailing slash | |
| * | |
| * @return string the path guaranteed to not have a trailing slash | |
| */ | |
| private function removeTrailingSlash($path) | |
| { | |
| if (strlen($path) <= 1) { | |
| return '/'; | |
| } | |
| if ('/' !== $path[strlen($path) - 1]) { | |
| // no trailing slash | |
| return $path; | |
| } | |
| return substr($path, 0, strlen($path) - 1); | |
| } | |
| // VersioningInterface // | |
| /** | |
| * {@inheritDoc} | |
| */ | |
| public function addVersionLabel($versionPath, $label, $moveLabel) | |
| { | |
| $versionPath = $this->encodeAndValidatePathForDavex($versionPath); | |
| $action = 'add'; | |
| if ($moveLabel) { | |
| $action = 'set'; | |
| } | |
| $body = '<D:label xmlns:D="DAV:"><D:'.$action.'><D:label-name>'.$label.'</D:label-name></D:'.$action.'></D:label>'; | |
| $request = $this->getRequest(Request::LABEL, $versionPath); | |
| $request->setBody($body); | |
| try { | |
| $request->execute(); // errors are checked in request | |
| } catch (HTTPErrorException $e) { | |
| if ($e->getCode() == 409) { | |
| throw new LabelExistsVersionException($e->getMessage()); | |
| } else { | |
| throw new RepositoryException($e->getMessage()); | |
| } | |
| } | |
| return; | |
| } | |
| /** | |
| * {@inheritDoc} | |
| */ | |
| public function removeVersionLabel($versionPath, $label) | |
| { | |
| $versionPath = $this->encodeAndValidatePathForDavex($versionPath); | |
| $body = '<D:label xmlns:D="DAV:"><D:remove><D:label-name>'.$label.'</D:label-name></D:remove></D:label>'; | |
| $request = $this->getRequest(Request::LABEL, $versionPath); | |
| $request->setBody($body); | |
| $request->execute(); | |
| return; | |
| } | |
| /** | |
| * {@inheritDoc} | |
| */ | |
| public function checkinItem($path) | |
| { | |
| $path = $this->encodeAndValidatePathForDavex($path); | |
| try { | |
| $request = $this->getRequest(Request::CHECKIN, $path); | |
| $curl = $request->execute(true); | |
| if ($curl->getHeader("Location")) { | |
| return $this->removeTrailingSlash( | |
| $this->stripServerRootFromUri(urldecode($curl->getHeader("Location"))) | |
| ); | |
| } | |
| } catch (HTTPErrorException $e) { | |
| if ($e->getCode() == 405) { | |
| throw new UnsupportedRepositoryOperationException(); | |
| } | |
| throw new RepositoryException($e->getMessage()); | |
| } | |
| throw new RepositoryException(); | |
| } | |
| /** | |
| * {@inheritDoc} | |
| */ | |
| public function checkoutItem($path) | |
| { | |
| $path = $this->encodeAndValidatePathForDavex($path); | |
| try { | |
| $request = $this->getRequest(Request::CHECKOUT, $path); | |
| $request->execute(); | |
| } catch (HTTPErrorException $e) { | |
| if ($e->getCode() == 405) { | |
| // TODO: when checking out a non-versionable node, we get here too. in that case the exception is very wrong | |
| throw new UnsupportedRepositoryOperationException($e->getMessage()); | |
| } | |
| throw new RepositoryException($e->getMessage()); | |
| } | |
| return; | |
| } | |
| /** | |
| * {@inheritDoc} | |
| */ | |
| public function restoreItem($removeExisting, $versionPath, $path) | |
| { | |
| $path = $this->encodeAndValidatePathForDavex($path); | |
| $body = | |
| '<D:update xmlns:D="DAV:"> | |
| <D:version> | |
| <D:href>'.$this->addWorkspacePathToUri($versionPath).'</D:href> | |
| </D:version> | |
| '; | |
| if ($removeExisting) { | |
| $body .= '<dcr:removeexisting xmlns:dcr="http://www.day.com/jcr/webdav/1.0" />'; | |
| } | |
| $body .= '</D:update>'; | |
| $request = $this->getRequest(Request::UPDATE, $path); | |
| $request->setBody($body); | |
| $request->execute(); // errors are checked in request | |
| } | |
| /** | |
| * {@inheritDoc} | |
| */ | |
| public function removeVersion($versionPath, $versionName) | |
| { | |
| $path = $this->encodeAndValidatePathForDavex($versionPath . '/' . $versionName); | |
| $request = $this->getRequest(Request::DELETE, $path); | |
| $resp = $request->execute(); | |
| return $resp; | |
| } | |
| // QueryTransport // | |
| /** | |
| * {@inheritDoc} | |
| */ | |
| public function query(Query $query) | |
| { | |
| // TODO handle bind variables | |
| $querystring = $query->getStatement(); | |
| $limit = $query->getLimit(); | |
| $offset = $query->getOffset(); | |
| $language = $query->getLanguage(); | |
| switch ($language) { | |
| case QueryInterface::JCR_JQOM: | |
| // for JQOM, fall through to SQL2 | |
| case QueryInterface::JCR_SQL2: | |
| $ns = ''; | |
| $langElement = 'JCR-SQL2'; | |
| break; | |
| case QueryInterface::XPATH: | |
| $langElement = 'dcr:xpath'; | |
| $ns = 'xmlns:dcr="http://www.day.com/jcr/webdav/1.0"'; | |
| break; | |
| case QueryInterface::SQL: | |
| $langElement = 'dcr:sql'; | |
| $ns = 'xmlns:dcr="http://www.day.com/jcr/webdav/1.0"'; | |
| break; | |
| default: | |
| // this should be impossible as we check on creation already | |
| throw new InvalidQueryException("Unsupported query language: $language"); | |
| } | |
| $body ='<D:searchrequest ' . $ns . ' xmlns:D="DAV:"><'.$langElement.'><![CDATA['.$querystring.']]></'.$langElement.'>'; | |
| if (null !== $limit || null !== $offset) { | |
| $body .= '<D:limit>'; | |
| if (null !== $limit) { | |
| $body .= '<D:nresults>'.(int) $limit.'</D:nresults>'; | |
| } | |
| if (null !== $offset) { | |
| $body .= '<offset>'.(int) $offset.'</offset>'; | |
| } | |
| $body .= '</D:limit>'; | |
| } | |
| $body .= '</D:searchrequest>'; | |
| $path = $this->addWorkspacePathToUri('/'); | |
| $request = $this->getRequest(Request::SEARCH, $path); | |
| $request->setBody($body); | |
| $rawData = $request->execute(); | |
| $dom = new DOMDocument(); | |
| $dom->loadXML($rawData); | |
| $domXpath = new DOMXPath($dom); | |
| $rows = array(); | |
| foreach ($domXpath->query('D:response') as $row) { | |
| $columns = array(); | |
| foreach ($row->getElementsByTagName('column') as $column) { | |
| $sets = array(); | |
| foreach ($column->childNodes as $childNode) { | |
| if ('dcr:value' == $childNode->tagName) { | |
| $value = $this->getDcrValue($childNode); | |
| // TODO if this bug is fixed, spaces may be urlencoded instead of the escape sequence: https://issues.apache.org/jira/browse/JCR-2997 | |
| // the following line fails for nodes with "_x0020 " in their name, changing that part to " x0020_" | |
| // other characters like < and > are urlencoded, which seems to be handled by dom already. | |
| if (is_string($value)) { | |
| $value = str_replace('_x0020_', ' ', $value); | |
| } | |
| } else { | |
| $value = $childNode->nodeValue; | |
| } | |
| $sets[$childNode->tagName] = $value; | |
| } | |
| if (! isset($sets['dcr:value'])) { | |
| $sets['dcr:value'] = null; | |
| } | |
| $columns[] = $sets; | |
| } | |
| $rows[] = $columns; | |
| } | |
| return $rows; | |
| } | |
| /** | |
| * {@inheritDoc} | |
| */ | |
| public function getSupportedQueryLanguages() | |
| { | |
| return array( | |
| QueryInterface::JCR_SQL2, | |
| QueryInterface::JCR_JQOM, | |
| QueryInterface::XPATH, | |
| QueryInterface::SQL, | |
| ); | |
| } | |
| /** | |
| * Get the value of a dcr:value node in the right format specified by the | |
| * dcr type. | |
| * | |
| * This uses PropertyType but takes into account the special case that | |
| * boolean false is encoded as string "false" which is otherwise true in php. | |
| * | |
| * <dcr:value dcr:type="Boolean">false</dcr:value> | |
| * | |
| * @param DOMElement $node a dcr:value xml element | |
| * @param string $attribute the attribute name | |
| * | |
| * @return mixed the node value converted to the specified type. | |
| */ | |
| private function getDcrValue(DOMElement $node) | |
| { | |
| $type = $node->getAttribute('dcr:type'); | |
| if (PropertyType::TYPENAME_BOOLEAN == $type && 'false' == $node->nodeValue) { | |
| return false; | |
| } | |
| return $this->valueConverter->convertType($node->nodeValue, PropertyType::valueFromName($type)); | |
| } | |
| // WritingInterface // | |
| /** | |
| * {@inheritDoc} | |
| */ | |
| public function deleteNodes(array $operations) | |
| { | |
| // Reverse sort the batch; work-around for problem with | |
| // deleting same-name siblings. Not guaranteed to work | |
| // across multiple calls to deleteNodes(). | |
| usort($operations, function ($a, $b) { | |
| $aParts = array(); | |
| $bParts = array(); | |
| $regex = '/^(.+?)(?:\[(\d+)])?$/'; | |
| preg_match($regex, $a->srcPath, $aParts); | |
| preg_match($regex, $b->srcPath, $bParts); | |
| $aPath = $aParts[1]; | |
| $bPath = $bParts[1]; | |
| if ($aPath != $bPath) { | |
| return strcmp($bPath, $aPath); | |
| } else { | |
| $aIndex = isset($aParts[2]) ? $aParts[2] : 1; | |
| $bIndex = isset($bParts[2]) ? $bParts[2] : 1; | |
| return $bIndex - $aIndex; | |
| } | |
| }); | |
| foreach ($operations as $operation) { | |
| $this->deleteItem($operation->srcPath); | |
| } | |
| } | |
| /** | |
| * {@inheritDoc} | |
| */ | |
| public function deleteProperties(array $operations) | |
| { | |
| foreach ($operations as $operation) { | |
| $this->deleteItem($operation->srcPath); | |
| } | |
| } | |
| /** | |
| * {@inheritDoc} | |
| */ | |
| public function deleteNodeImmediately($path) | |
| { | |
| $this->prepareSave(); | |
| $this->deleteItem($path); | |
| $this->finishSave(); | |
| } | |
| /** | |
| * {@inheritDoc} | |
| */ | |
| public function deletePropertyImmediately($path) | |
| { | |
| $this->prepareSave(); | |
| $this->deleteItem($path); | |
| $this->finishSave(); | |
| } | |
| /** | |
| * Record that we need to delete the item at $path | |
| * | |
| * @param string $path path to node or property | |
| */ | |
| protected function deleteItem($path) | |
| { | |
| PathHelper::assertValidAbsolutePath($path); | |
| $this->setJsopBody("-" . $path . " : "); | |
| } | |
| /** | |
| * {@inheritDoc} | |
| */ | |
| public function copyNode($srcAbsPath, $dstAbsPath, $srcWorkspace = null) | |
| { | |
| if ($srcWorkspace) { | |
| $this->copyNodeOtherWorkspace($srcAbsPath, $dstAbsPath, $srcWorkspace); | |
| } else { | |
| $this->copyNodeSameWorkspace($srcAbsPath, $dstAbsPath); | |
| } | |
| } | |
| /** | |
| * For copy within the same workspace, this is a COPY request. | |
| * | |
| * @param string $srcAbsPath Absolute source path to the node | |
| * @param string $destAbsPath Absolute destination path including the new | |
| * node name | |
| */ | |
| private function copyNodeSameWorkspace($srcAbsPath, $dstAbsPath) | |
| { | |
| $srcAbsPath = $this->encodeAndValidatePathForDavex($srcAbsPath); | |
| $dstAbsPath = $this->encodeAndValidatePathForDavex($dstAbsPath); | |
| $request = $this->getRequest(Request::COPY, $srcAbsPath); | |
| $request->setDepth(Request::INFINITY); | |
| $request->addHeader('Destination: '.$this->addWorkspacePathToUri($dstAbsPath)); | |
| $request->execute(); | |
| } | |
| /** | |
| * For copy from a different workspace, needs to be a JSOP. | |
| * | |
| * As seen with jackrabbit 2.6 | |
| * | |
| * @param string $srcAbsPath Absolute source path to the node | |
| * @param string $destAbsPath Absolute destination path including the new | |
| * node name | |
| * @param string $srcWorkspace The workspace where the source node can be | |
| * found or null for current workspace | |
| */ | |
| private function copyNodeOtherWorkspace($srcAbsPath, $dstAbsPath, $srcWorkspace) | |
| { | |
| $request = $this->getRequest(Request::POST, $this->workspaceUri); | |
| $request->setContentType("application/x-www-form-urlencoded; charset=utf-8"); | |
| $request->setBody(urlencode(':copy') . '=' . urlencode($srcWorkspace . ',' . $srcAbsPath . ',' . $dstAbsPath)); | |
| $request->execute(); | |
| } | |
| /** | |
| * {@inheritDoc} | |
| */ | |
| public function moveNodes(array $operations) | |
| { | |
| foreach ($operations as $operation) { | |
| PathHelper::assertValidAbsolutePath($operation->srcPath); | |
| PathHelper::assertValidAbsolutePath($operation->dstPath); | |
| $this->setJsopBody(">" . $operation->srcPath . " : " . $operation->dstPath); | |
| } | |
| } | |
| /** | |
| * {@inheritDoc} | |
| */ | |
| public function moveNodeImmediately($srcAbsPath, $dstAbsPath) | |
| { | |
| $request = $this->getRequest(Request::MOVE, $srcAbsPath); | |
| $request->setDepth(Request::INFINITY); | |
| $request->addHeader('Destination: ' . $this->addWorkspacePathToUri($dstAbsPath)); | |
| $request->execute(); | |
| } | |
| /** | |
| * {@inheritDoc} | |
| */ | |
| public function reorderChildren(Node $node) | |
| { | |
| $reorders = $node->getOrderCommands(); | |
| if (count($reorders) == 0) { | |
| // should not happen but safe is safe | |
| return; | |
| } | |
| $body = ''; | |
| $path = $node->getPath(); | |
| foreach ($reorders as $child => $destination) { | |
| if (is_null($destination)) { | |
| $body .= ">$path/$child : #last\r"; | |
| } else { | |
| $body .= ">$path/$child : $destination#before\r"; | |
| } | |
| } | |
| $this->setJsopBody(trim($body)); | |
| } | |
| /** | |
| * {@inheritDoc} | |
| */ | |
| public function cloneFrom($srcWorkspace, $srcAbsPath, $destAbsPath, $removeExisting) | |
| { | |
| $srcAbsPath = $this->encodeAndValidatePathForDavex($srcAbsPath); | |
| $destAbsPath = $this->encodeAndValidatePathForDavex($destAbsPath); | |
| // avoid creating a same name sibling as we don't handle them but jackrabbit does | |
| $this->checkForExistingNode($srcWorkspace, $srcAbsPath, $destAbsPath); | |
| $body = urlencode(':clone') . '=' | |
| . urlencode($srcWorkspace . ',' . $srcAbsPath . ',' . $destAbsPath . ',' . ($removeExisting ? 'true' : 'false')); | |
| $request = $this->getRequest(Request::POST, $this->workspaceUri); | |
| $request->setBody($body); | |
| $request->setContentType('application/x-www-form-urlencoded'); | |
| $request->execute(); | |
| } | |
| /** | |
| * Prevent accidental creation of same name siblings during clone operation. | |
| * | |
| * Jackrabbit supports them, but jackalope does not. | |
| * | |
| * @param $srcWorkspace | |
| * @param $srcAbsPath | |
| * @param $destAbsPath | |
| * | |
| * @throws \PHPCR\ItemExistsException | |
| */ | |
| protected function checkForExistingNode($srcWorkspace, $srcAbsPath, $destAbsPath) | |
| { | |
| try { | |
| $existingNode = $this->getNode($destAbsPath); | |
| } catch (ItemNotFoundException $exception) { | |
| return; | |
| } | |
| if (empty($existingNode->{'jcr:uuid'})) { | |
| throw new ItemExistsException('A node already exists at the destination path'); | |
| } else { | |
| $existingNodeUuid = $existingNode->{'jcr:uuid'}; | |
| try { | |
| $correspondingPath = $this->getNodePathForIdentifier($existingNodeUuid, $srcWorkspace); | |
| } catch (ItemNotFoundException $exception) { | |
| $correspondingPath = null; | |
| } | |
| if ($correspondingPath != $srcAbsPath) { | |
| throw new ItemExistsException( | |
| 'A node already exists at the destination path that does not correspond to the source node' | |
| ); | |
| } | |
| } | |
| } | |
| /** | |
| * {@inheritDoc} | |
| */ | |
| public function updateNode(Node $node, $srcWorkspace) | |
| { | |
| $path = $this->encodeAndValidatePathForDavex($node->getPath()); | |
| $srcWorkspaceUri = $this->server . $srcWorkspace; | |
| $body = ' | |
| <D:update xmlns:D="DAV:"> | |
| <D:workspace> | |
| ' . $srcWorkspaceUri . ' | |
| <D:href> | |
| ' . $srcWorkspaceUri . ' | |
| </D:href> | |
| </D:workspace> | |
| </D:update> | |
| '; | |
| $request = $this->getRequest(Request::UPDATE, $path, true); | |
| $request->setBody($body); | |
| $request->execute(); | |
| } | |
| /** | |
| * {@inheritDoc} | |
| */ | |
| public function storeNodes(array $operations) | |
| { | |
| /** @var $operation \Jackalope\Transport\AddNodeOperation */ | |
| foreach ($operations as $operation) { | |
| if ($operation->node->isDeleted()) { | |
| $properties = $operation->node->getPropertiesForStoreDeletedNode(); | |
| } else { | |
| $properties = $operation->node->getProperties(); | |
| } | |
| $this->createNodeJsop($operation->srcPath, $properties); | |
| } | |
| } | |
| /** | |
| * @param Property $property | |
| */ | |
| private function storeProperty(Property $property) | |
| { | |
| $path = $property->getPath(); | |
| $typeid = $property->getType(); | |
| $nativeValue = $property->getValueForStorage(); | |
| if ($typeid === PropertyType::STRING) { | |
| foreach ((array) $nativeValue as $string) { | |
| if (!$this->isStringValid($string)) { | |
| throw new ValueFormatException('Invalid character found in property "'.$property->getName().'". Are you passing a valid string?'); | |
| } | |
| } | |
| } | |
| $value = $this->propertyToJsopString($property); | |
| if (!$value) { | |
| $this->setJsopBody($nativeValue, $path, $typeid); | |
| if (is_array($nativeValue)) { | |
| $this->setJsopBody('^' . $path . ' : []'); | |
| } else { | |
| $this->setJsopBody('^' . $path . ' : '); | |
| } | |
| } else { | |
| $encoded = json_encode($value); | |
| if (PropertyType::DOUBLE == $property->getType() | |
| && !strpos($encoded, '.') | |
| ) { | |
| $encoded .= '.0'; | |
| } | |
| $this->setJsopBody('^' . $path . ' : ' . $encoded); | |
| } | |
| } | |
| /** | |
| * Checks for occurrence of invalid UTF characters, that can not occur in valid XML document. | |
| * If occurrence is found, returns false, otherwise true. | |
| * Invalid characters were taken from this list: http://en.wikipedia.org/wiki/Valid_characters_in_XML#XML_1.0 | |
| * | |
| * Uses regexp mentioned here: http://stackoverflow.com/a/961504 | |
| * | |
| * @param $string string value | |
| * @return bool true if string is OK, false otherwise. | |
| */ | |
| protected function isStringValid($string) | |
| { | |
| $regex = '/[^\x{9}\x{a}\x{d}\x{20}-\x{D7FF}\x{E000}-\x{FFFD}]+/u'; | |
| return (preg_match($regex, $string, $matches) === 0); | |
| } | |
| /** | |
| * {@inheritDoc} | |
| */ | |
| public function updateProperties(Node $node) | |
| { | |
| $this->updateLastModified($node); | |
| foreach ($node->getProperties() as $property) { | |
| /** @var $property Property */ | |
| if ($property->isModified() || $property->isNew()) { | |
| $this->storeProperty($property); | |
| } | |
| } | |
| } | |
| /** | |
| * Update the lastModified fields if they where not set manually. | |
| * | |
| * Note that we can drop this if this jackrabbit issue ever gets | |
| * implemented https://issues.apache.org/jira/browse/JCR-2233 | |
| * | |
| * @param Node $node | |
| */ | |
| protected function updateLastModified(Node $node) | |
| { | |
| if (!$this->getAutoLastModified() || !$node->isNodeType('mix:lastModified')) { | |
| return; | |
| } | |
| if ($node->hasProperty('jcr:lastModified') | |
| && !$node->getProperty('jcr:lastModified')->isModified() | |
| && !$node->getProperty('jcr:lastModified')->isNew() | |
| ) { | |
| $node->setProperty('jcr:lastModified', new \DateTime()); | |
| } | |
| if ($node->hasProperty('jcr:lastModifiedBy') | |
| && !$node->getProperty('jcr:lastModifiedBy')->isModified() | |
| && !$node->getProperty('jcr:lastModifiedBy')->isNew() | |
| ) { | |
| $node->setProperty('jcr:lastModifiedBy', $this->credentials->getUserID()); | |
| } | |
| } | |
| /** | |
| * create the node markup and a list of value dispatches for multivalue properties | |
| * | |
| * @param string $path path to the current node with the last path segment | |
| * being the node name | |
| * @param array $properties of this node | |
| * | |
| * @return string the xml for the node | |
| */ | |
| protected function createNodeJsop($path, $properties) | |
| { | |
| $body = '+' . $path . ' : {'; | |
| $binaries = array(); | |
| // first do the main properties, so they are certainly in the beginning | |
| $nodeCreationProperties = array("jcr:primaryType", "jcr:mixinTypes"); | |
| foreach ($nodeCreationProperties as $name) { | |
| if (isset($properties[$name])) { | |
| $body .= json_encode($name) . ':' . json_encode($properties[$name]->getValueForStorage()) . ","; | |
| } | |
| } | |
| foreach ($properties as $name => $property) { | |
| if (in_array($name, $nodeCreationProperties)) { | |
| continue; | |
| } | |
| $value = $this->propertyToJsopString($property); | |
| if (!$value) { | |
| $binaries[] = $property; | |
| } else { | |
| $body .= json_encode($name) . ':' . json_encode($value) . ","; | |
| } | |
| } | |
| $body .= "}"; | |
| $this->setJsopBody($body); | |
| foreach ($binaries as $binary) { | |
| $this->storeProperty($binary); | |
| } | |
| } | |
| /** | |
| * This method is used when building a JSOP of the properties | |
| * | |
| * @param $value | |
| * @param $type | |
| * @return mixed|string | |
| */ | |
| protected function propertyToJsopString(Property $property) | |
| { | |
| switch ($property->getType()) { | |
| case PropertyType::DECIMAL: | |
| return null; | |
| case PropertyType::DOUBLE: | |
| return $this->valueConverter->convertType($property->getValueForStorage(), PropertyType::DOUBLE); | |
| case PropertyType::LONG: | |
| return $this->valueConverter->convertType($property->getValueForStorage(), PropertyType::LONG); | |
| case PropertyType::DATE: | |
| case PropertyType::WEAKREFERENCE: | |
| case PropertyType::REFERENCE: | |
| case PropertyType::BINARY: | |
| case PropertyType::PATH: | |
| case PropertyType::URI: | |
| return null; | |
| case PropertyType::NAME: | |
| if ($property->getName() != 'jcr:primaryType') { | |
| return null; | |
| } | |
| break; | |
| } | |
| return $property->getValueForStorage(); | |
| } | |
| /** | |
| * {@inheritDoc} | |
| */ | |
| public function getNamespaces() | |
| { | |
| $request = $this->getRequest(Request::REPORT, $this->workspaceUri); | |
| $request->setBody($this->buildReportRequest('dcr:registerednamespaces')); | |
| $dom = $request->executeDom(); | |
| if ($dom->firstChild->localName != 'registerednamespaces-report' | |
| || $dom->firstChild->namespaceURI != self::NS_DCR | |
| ) { | |
| throw new RepositoryException('Error talking to the backend. '.$dom->saveXML()); | |
| } | |
| $mappings = array(); | |
| $namespaces = $dom->getElementsByTagNameNS(self::NS_DCR, 'namespace'); | |
| foreach ($namespaces as $elem) { | |
| $mappings[$elem->firstChild->textContent] = $elem->lastChild->textContent; | |
| } | |
| return $mappings; | |
| } | |
| /** | |
| * {@inheritDoc} | |
| */ | |
| public function registerNamespace($prefix, $uri) | |
| { | |
| // seems jackrabbit always expects full list of namespaces | |
| $namespaces = $this->getNamespaces(); | |
| // check if prefix is already mapped | |
| if (isset($namespaces[$prefix])) { | |
| if ($namespaces[$prefix] == $uri) { | |
| // nothing to do, we already have the mapping | |
| return; | |
| } | |
| // unregister old mapping | |
| throw new UnsupportedRepositoryOperationException("Trying to set existing prefix $prefix from ".$namespaces[$prefix]." to different uri $uri, but unregistering namespace is not supported by jackrabbit backend. You can move the old namespace to a different prefix before adding this prefix to work around this issue."); | |
| } | |
| // if target uri already exists elsewhere, do not re-send or result is random | |
| /* weird: we can not unset this or we get the unregister not | |
| * supported exception. but we can send two mappings and | |
| * jackrabbit does the right guess what we want and moves the | |
| * namespace to the new prefix | |
| if (false !== $expref = array_search($uri, $namespaces)) { | |
| unset($namespaces[$expref]); | |
| } | |
| */ | |
| $request = $this->getRequest(Request::PROPPATCH, $this->workspaceUri); | |
| $namespaces[$prefix] = $uri; | |
| $request->setBody($this->buildRegisterNamespaceRequest($namespaces)); | |
| $request->execute(); | |
| } | |
| /** | |
| * {@inheritDoc} | |
| */ | |
| public function unregisterNamespace($prefix) | |
| { | |
| throw new UnsupportedRepositoryOperationException('Unregistering namespace not supported by jackrabbit backend'); | |
| /* | |
| * TODO: could look a bit like the following if the backend would support it | |
| $request = $this->getRequest(Request::PROPPATCH, $this->workspaceUri); | |
| // seems jackrabbit always expects full list of namespaces | |
| $namespaces = $this->getNamespaces(); | |
| unset($namespaces[$prefix]); | |
| $request->setBody($this->buildRegisterNamespaceRequest($namespaces)); | |
| $request->execute(); | |
| */ | |
| } | |
| /** | |
| * {@inheritDoc} | |
| */ | |
| public function getNodeTypes($nodeTypes = array()) | |
| { | |
| $request = $this->getRequest(Request::REPORT, $this->workspaceUriRoot); | |
| $request->setBody($this->buildNodeTypesRequest($nodeTypes)); | |
| $dom = $request->executeDom(); | |
| if ($dom->firstChild->localName != 'nodeTypes') { | |
| throw new RepositoryException('Error talking to the backend. '.$dom->saveXML()); | |
| } | |
| if ($this->typeXmlConverter === null) { | |
| $this->typeXmlConverter = $this->factory->get('NodeType\\NodeTypeXmlConverter'); | |
| } | |
| return $this->typeXmlConverter->getNodeTypesFromXml($dom); | |
| } | |
| // NodeTypeCndManagementInterface // | |
| /** | |
| * {@inheritDoc} | |
| */ | |
| public function registerNodeTypesCnd($cnd, $allowUpdate) | |
| { | |
| $request = $this->getRequest(Request::PROPPATCH, $this->workspaceUri); | |
| $request->setBody($this->buildRegisterNodeTypeRequest($cnd, $allowUpdate)); | |
| $request->execute(); | |
| } | |
| // PermissionInterface // | |
| /** | |
| * {@inheritDoc} | |
| */ | |
| public function getPermissions($path) | |
| { | |
| // TODO: OPTIMIZE - once we have ACL this might be done without any server request | |
| $body = '<?xml version="1.0" encoding="UTF-8"?>' . | |
| '<dcr:privileges xmlns:dcr="http://www.day.com/jcr/webdav/1.0">' . | |
| '<D:href xmlns:D="DAV:">'.$this->addWorkspacePathToUri($path).'</D:href>' . | |
| '</dcr:privileges>'; | |
| $valid_permissions = array( | |
| SessionInterface::ACTION_ADD_NODE, | |
| SessionInterface::ACTION_READ, | |
| SessionInterface::ACTION_REMOVE, | |
| SessionInterface::ACTION_SET_PROPERTY); | |
| $result = array(); | |
| $request = $this->getRequest(Request::REPORT, $this->workspaceUri); | |
| $request->setBody($body); | |
| $dom = $request->executeDom(); | |
| foreach ($dom->getElementsByTagNameNS(self::NS_DAV, 'current-user-privilege-set') as $node) { | |
| foreach ($node->getElementsByTagNameNS(self::NS_DAV, 'privilege') as $privilege) { | |
| foreach ($privilege->childNodes as $child) { | |
| $permission = str_replace('dcr:', '', $child->tagName); | |
| if (! in_array($permission, $valid_permissions)) { | |
| throw new RepositoryException("Invalid permission '$permission'"); | |
| } | |
| $result[] = $permission; | |
| } | |
| } | |
| } | |
| return $result; | |
| } | |
| /** | |
| * {@inheritDoc} | |
| */ | |
| public function lockNode($absPath, $isDeep, $isSessionScoped, $timeoutHint = PHP_INT_MAX, $ownerInfo = null) | |
| { | |
| $timeout = $timeoutHint === PHP_INT_MAX ? 'infinite' : $timeoutHint; | |
| $ownerInfo = (null === $ownerInfo) ? $this->credentials->getUserID() : (string) $ownerInfo; | |
| $depth = $isDeep ? Request::INFINITY : 0; | |
| $lockScope = $isSessionScoped ? '<dcr:exclusive-session-scoped xmlns:dcr="http://www.day.com/jcr/webdav/1.0"/>' : '<D:exclusive/>'; | |
| $request = $this->getRequest(Request::LOCK, $absPath); | |
| $request->addHeader('Timeout: Second-' . $timeout); | |
| $request->setDepth($depth); | |
| $request->setBody('<?xml version="1.0" encoding="utf-8"?>'. | |
| '<D:lockinfo xmlns:D="' . self::NS_DAV . '">'. | |
| ' <D:lockscope>' . $lockScope . '</D:lockscope>'. | |
| ' <D:locktype><D:write/></D:locktype>'. | |
| ' <D:owner>' . $ownerInfo . '</D:owner>' . | |
| '</D:lockinfo>'); | |
| $dom = $request->executeDom(); | |
| return $this->generateLockFromDavResponse($dom, true, $absPath); | |
| } | |
| /** | |
| * {@inheritDoc} | |
| */ | |
| public function isLocked($absPath) | |
| { | |
| $request = $this->getRequest(Request::PROPFIND, $absPath); | |
| $request->setBody($this->buildPropfindRequest(array('D:lockdiscovery'))); | |
| $request->setDepth(0); | |
| $dom = $request->executeDom(); | |
| $lockInfo = $this->getRequiredDomElementByTagNameNS($dom, self::NS_DAV, 'lockdiscovery'); | |
| return $lockInfo->childNodes->length > 0; | |
| } | |
| /** | |
| * {@inheritDoc} | |
| */ | |
| public function unlock($absPath, $lockToken) | |
| { | |
| $request = $this->getRequest(Request::UNLOCK, $absPath); | |
| $request->setLockToken($lockToken); | |
| $request->execute(); | |
| } | |
| /** | |
| * {@inheritDoc} | |
| */ | |
| public function getEvents($date, EventFilterInterface $filter, SessionInterface $session) | |
| { | |
| return $this->factory->get('Jackalope\Transport\Jackrabbit\EventBuffer', array( | |
| $filter, | |
| $this, | |
| $this->nodeTypeManager, | |
| str_replace('jcr:root', 'jcr%3aroot', $this->workspaceUriRoot), | |
| $this->fetchEventData($date) | |
| )); | |
| } | |
| /** | |
| * {@inheritDoc} | |
| */ | |
| public function fetchEventData($date) | |
| { | |
| $path = $this->workspaceUri . self::JCR_JOURNAL_PATH; | |
| $request = $this->getRequest(Request::GET, $path, false); | |
| $request->addHeader(sprintf('If-None-Match: "%s"', base_convert($date, 10, 16))); | |
| $curl = $request->execute(true); | |
| // create new DOMDocument and load the response text. | |
| $dom = new DOMDocument(); | |
| $dom->loadXML($curl->getResponse()); | |
| $next = base_convert(trim($curl->getHeader('ETag'), '"'), 16, 10); | |
| if ($next == $date) { | |
| // no more events | |
| $next = false; | |
| } | |
| return array( | |
| 'data' => $dom, | |
| 'nextMillis' => $next, | |
| ); | |
| } | |
| /** | |
| * {@inheritDoc} | |
| */ | |
| public function setUserData($userData) | |
| { | |
| $this->userData = $userData; | |
| } | |
| /** | |
| * {@inheritDoc} | |
| */ | |
| public function getUserData() | |
| { | |
| return $this->userData; | |
| } | |
| /** | |
| * {@inheritDoc} | |
| */ | |
| public function createWorkspace($name, $srcWorkspace = null) | |
| { | |
| if (null != $srcWorkspace) { | |
| // https://issues.apache.org/jira/browse/JCR-3144 | |
| throw new UnsupportedRepositoryOperationException('Can not create a workspace from a source workspace as we neither implemented clone nor have native support for this'); | |
| } | |
| $curl = $this->getCurl(); | |
| $uri = $this->server . $name; | |
| $request = $this->factory->get('Transport\\Jackrabbit\\Request', array($this, $curl, Request::MKWORKSPACE, $uri)); | |
| $request->setCredentials($this->credentials); | |
| foreach ($this->defaultHeaders as $header) { | |
| $request->addHeader($header); | |
| } | |
| if (!$this->sendExpect) { | |
| $request->addHeader("Expect:"); | |
| } | |
| $request->execute(); | |
| } | |
| /** | |
| * {@inheritDoc} | |
| */ | |
| public function deleteWorkspace($name) | |
| { | |
| // https://issues.apache.org/jira/browse/JCR-3144 | |
| throw new UnsupportedRepositoryOperationException("Can not delete a workspace as jackrabbit can not do it. Find the jackrabbit folder and look for workspaces/$name and delete that folder"); | |
| } | |
| // protected helper methods // | |
| /** | |
| * Build the xml required to register node types | |
| * | |
| * @param string $cnd the node type definition | |
| * @return string XML with register request | |
| * | |
| * @author david at liip.ch | |
| */ | |
| protected function buildRegisterNodeTypeRequest($cnd, $allowUpdate) | |
| { | |
| $cnd = '<dcr:cnd>'.str_replace(array('<', '>'), array('<', '>'), $cnd).'</dcr:cnd>'; | |
| $cnd .= '<dcr:allowupdate>'.($allowUpdate ? 'true' : 'false').'</dcr:allowupdate>'; | |
| return '<?xml version="1.0" encoding="UTF-8" standalone="no"?><D:propertyupdate xmlns:D="DAV:"><D:set><D:prop><dcr:nodetypes-cnd xmlns:dcr="http://www.day.com/jcr/webdav/1.0">'.$cnd.'</dcr:nodetypes-cnd></D:prop></D:set></D:propertyupdate>'; | |
| } | |
| /** | |
| * Build the xml to update the namespaces | |
| * | |
| * You need to repeat all existing node type plus add your new ones | |
| * | |
| * @param array $mappings hashmap of prefix => uri for all existing and new namespaces | |
| */ | |
| protected function buildRegisterNamespaceRequest($mappings) | |
| { | |
| $ns = ''; | |
| foreach ($mappings as $prefix => $uri) { | |
| $ns .= "<dcr:namespace><dcr:prefix>$prefix</dcr:prefix><dcr:uri>$uri</dcr:uri></dcr:namespace>"; | |
| } | |
| return '<?xml version="1.0" encoding="UTF-8"?><D:propertyupdate xmlns:D="DAV:"><D:set><D:prop><dcr:namespaces xmlns:dcr="http://www.day.com/jcr/webdav/1.0">'. | |
| $ns . | |
| '</dcr:namespaces></D:prop></D:set></D:propertyupdate>'; | |
| } | |
| /** | |
| * Returns the XML required to request nodetypes | |
| * | |
| * @param array $nodesType The list of nodetypes you want to request for. | |
| * @return string XML with the request information. | |
| */ | |
| protected function buildNodeTypesRequest(array $nodeTypes) | |
| { | |
| $xml = '<?xml version="1.0" encoding="utf-8" ?>'. | |
| '<jcr:nodetypes xmlns:jcr="http://www.day.com/jcr/webdav/1.0">'; | |
| if (empty($nodeTypes)) { | |
| $xml .= '<jcr:all-nodetypes/>'; | |
| } else { | |
| foreach ($nodeTypes as $nodetype) { | |
| $xml .= '<jcr:nodetype><jcr:nodetypename>'.$nodetype.'</jcr:nodetypename></jcr:nodetype>'; | |
| } | |
| } | |
| $xml .='</jcr:nodetypes>'; | |
| return $xml; | |
| } | |
| /** | |
| * Build PROPFIND request XML for the specified property names | |
| * | |
| * @param array $properties names of the properties to search for | |
| * @return string XML to post in the body | |
| */ | |
| protected function buildPropfindRequest($properties) | |
| { | |
| $xml = '<?xml version="1.0" encoding="UTF-8"?>'. | |
| '<D:propfind xmlns:D="DAV:" xmlns:dcr="http://www.day.com/jcr/webdav/1.0"><D:prop>'; | |
| if (!is_array($properties)) { | |
| $properties = array($properties); | |
| } | |
| foreach ($properties as $property) { | |
| $xml .= '<'. $property . '/>'; | |
| } | |
| $xml .= '</D:prop></D:propfind>'; | |
| return $xml; | |
| } | |
| /** | |
| * Build a REPORT XML request string | |
| * | |
| * @param string $name Name of the resource to be requested. | |
| * @return string XML string representing the head of the request. | |
| */ | |
| protected function buildReportRequest($name) | |
| { | |
| return '<?xml version="1.0" encoding="UTF-8"?><' . | |
| $name . | |
| ' xmlns:dcr="http://www.day.com/jcr/webdav/1.0"/>'; | |
| } | |
| /** | |
| * Build REPORT XML request for locating a node path by uuid | |
| * | |
| * @param string $uuid Unique identifier of the node to be asked for. | |
| * @return string XML sring representing the content of the request. | |
| */ | |
| protected function buildLocateRequest($uuid) | |
| { | |
| return '<?xml version="1.0" encoding="UTF-8"?>'. | |
| '<dcr:locate-by-uuid xmlns:dcr="http://www.day.com/jcr/webdav/1.0">'. | |
| '<D:href xmlns:D="DAV:">' . | |
| $uuid . | |
| '</D:href></dcr:locate-by-uuid>'; | |
| } | |
| /** | |
| * {@inheritDoc} | |
| */ | |
| public function setNodeTypeManager($nodeTypeManager) | |
| { | |
| $this->nodeTypeManager = $nodeTypeManager; | |
| } | |
| /** | |
| * Checks if the path is absolute and valid, and properly urlencodes special characters | |
| * | |
| * This is to be used in the Davex headers. The XML requests can cope with unencoded stuff | |
| * | |
| * @param string $path to check | |
| * | |
| * @return string the cleaned path | |
| * | |
| * @throws RepositoryException If path is not absolute or invalid | |
| */ | |
| protected function encodeAndValidatePathForDavex($path) | |
| { | |
| PathHelper::assertValidAbsolutePath($path); | |
| $path = rawurlencode($path); | |
| // we encoded the whole path, need to rebuild slashes and parenthesis | |
| // this will not collide with %2F as % was encoded by rawurlencode | |
| $path = str_replace(array('%2F', '%5B', '%5D'), array('/', '[', ']'), $path); | |
| return $path; | |
| } | |
| /** | |
| * remove the server and workspace part from an uri, leaving the absolute | |
| * path inside the current workspace | |
| * | |
| * @param string $uri a full uri including the server path, workspace and jcr%3aroot | |
| * | |
| * @return string absolute path in the current work space | |
| */ | |
| protected function stripServerRootFromUri($uri) | |
| { | |
| return substr($uri, strlen($this->workspaceUriRoot)); | |
| } | |
| /** | |
| * Prepends the workspace root to the uris that contain an absolute path | |
| * | |
| * @param string $uri The absolute path in the current workspace or server uri | |
| * @return string The server uri with this path | |
| * @throws RepositoryException If workspaceUri is missing (not logged in) | |
| */ | |
| protected function addWorkspacePathToUri($uri) | |
| { | |
| if (empty($uri) || '/' === $uri[0]) { | |
| if (empty($this->workspaceUri)) { | |
| throw new RepositoryException("Implementation error: Please login before accessing content"); | |
| } | |
| $uri = $this->workspaceUriRoot . $uri; | |
| } | |
| return $uri; | |
| } | |
| /** | |
| * Extract the information from a LOCK DAV response and create the | |
| * corresponding Lock object. | |
| * | |
| * @param \DOMElement $response | |
| * @param bool $sessionOwning whether the current session is owning the lock (aka | |
| * we created it in this request) | |
| * @param string $path the owning node path, if we created this node | |
| * | |
| * @return \Jackalope\Lock\Lock | |
| */ | |
| protected function generateLockFromDavResponse($response, $sessionOwning = false, $path = null) | |
| { | |
| $lock = new Lock(); | |
| $lockDom = $this->getRequiredDomElementByTagNameNS($response, self::NS_DAV, 'activelock', "No lock received"); | |
| // Check this is not a transaction lock | |
| $type = $this->getRequiredDomElementByTagNameNS($lockDom, self::NS_DAV, 'locktype', 'No lock type received'); | |
| if (!$type->childNodes->length) { | |
| $tagName = $type->childNodes->item(0)->localName; | |
| if ($tagName !== 'write') { | |
| throw new RepositoryException("Invalid lock type '$tagName'"); | |
| } | |
| } | |
| // Extract the lock scope | |
| $scopeDom = $this->getRequiredDomElementByTagNameNS($lockDom, self::NS_DAV, 'lockscope', 'No lock scope in the received lock'); | |
| if ($this->getRequiredDomElementByTagNameNS($scopeDom, self::NS_DCR, 'exclusive-session-scoped')) { | |
| $lock->setIsSessionScoped(true); | |
| } elseif ($this->getRequiredDomElementByTagNameNS($scopeDom, self::NS_DAV, 'exclusive')) { | |
| $lock->setIsSessionScoped(false); | |
| } else { | |
| // Unknown XML found in the <D:lockscope> tag | |
| throw new RepositoryException('Invalid lock scope received: ' . $response->saveHTML($scopeDom)); | |
| } | |
| // Extract the depth | |
| $depthDom = $this->getRequiredDomElementByTagNameNS($lockDom, self::NS_DAV, 'depth', 'No depth in the received lock'); | |
| $lock->setIsDeep($depthDom->textContent === 'infinity'); | |
| // Extract the owner | |
| $ownerDom = $this->getRequiredDomElementByTagNameNS($lockDom, self::NS_DAV, 'owner', 'No owner in the received lock'); | |
| $lock->setLockOwner($ownerDom->textContent); | |
| // Extract the lock token | |
| $tokenDom = $this->getRequiredDomElementByTagNameNS($lockDom, self::NS_DAV, 'href', 'No lock token in the received lock'); | |
| $lock->setLockToken($tokenDom->textContent); | |
| // Extract the timeout | |
| $timeoutDom = $this->getRequiredDomElementByTagNameNS($lockDom, self::NS_DAV, 'timeout', 'No lock timeout in the received lock'); | |
| $lock->setExpireTime($this->parseTimeout($timeoutDom->nodeValue)); | |
| $lock->setIsLockOwningSession($sessionOwning); | |
| if (null !== $path) { | |
| $lock->setNodePath($path); | |
| } else { | |
| throw new NotImplementedException('get the lock owning node or provide Lock with info so it can get it when requested'); | |
| // TODO: get the lock owning node | |
| // Note that $n->getLock()->getNode() (where $n is a locked node) will only | |
| // return $n if $n is the lock holder. If $n is in the subgraph of the lock | |
| // holder, $h, then this call will return $h. | |
| } | |
| return $lock; | |
| } | |
| /** | |
| * Retrieve a child DOM element from a DOM element. | |
| * If the element is not found and $errorMessage is set, then a RepositoryException is thrown. | |
| * If the element is not found and $errorMessage is empty, then false is returned. | |
| * | |
| * @throws \PHPCR\RepositoryException When the element is not found and an $errorMessage is set | |
| * | |
| * @param \DOMNode $dom The DOM element which content should be searched | |
| * @param string $namespace The namespace of the searched element | |
| * @param string $element The name of the searched element | |
| * @param string $errorMessage The error message in case the element is not found | |
| * @return bool|\DOMNode | |
| */ | |
| protected function getRequiredDomElementByTagNameNS($dom, $namespace, $element, $errorMessage = '') | |
| { | |
| $list = $dom->getElementsByTagNameNS($namespace, $element); | |
| if (!$list->length) { | |
| if ($errorMessage) { | |
| throw new RepositoryException($errorMessage); | |
| } | |
| return false; | |
| } | |
| return $list->item(0); | |
| } | |
| /** | |
| * Parse the timeout value from a WebDAV response and calculate the expire | |
| * timestamp. | |
| * | |
| * The timeout value follows the syntax defined in RFC2518: Timeout Header. | |
| * Here we just parse the values in the form "Second-XXXX" or "Infinite". | |
| * Any other value will produce an error. | |
| * | |
| * The function returns the unix epoch timestamp for the second when this | |
| * lock will expire in case of normal timeout, or PHP_INT_MAX in case of an | |
| * "Infinite" timeout. | |
| * | |
| * @param string $timeoutValue The timeout in seconds or PHP_INT_MAX for infinite | |
| * | |
| * @return int the expire timestamp to be used with Lock::setExpireTime, | |
| * that is when this lock expires in seconds since 1970 or null for inifinite | |
| * | |
| * @throws InvalidArgumentException if the timeout value can not be parsed | |
| */ | |
| protected function parseTimeout($timeoutValue) | |
| { | |
| if (self::JCR_INFINITE == $timeoutValue) { | |
| return null; | |
| } | |
| if (! preg_match('/Second\-([\d]+)/', $timeoutValue, $matches)) { | |
| throw new RepositoryException("Unexpected response on lock from the backend, could not parse seconds out of '$timeoutValue'"); | |
| } | |
| $time = $matches[1]; | |
| // keep this hack for jackrabbit 2.3.7 for now. it reported a bogous value for the timeout | |
| if (self::JCR_INFINITE_LOCK_TIMEOUT == $time || self::JCR_INFINITE_LOCK_TIMEOUT - 1 == $time) { | |
| // prevent glitches due to second boundary during request | |
| return null; | |
| } | |
| return time() + $time; | |
| } | |
| protected function setJsopBody($value, $key = ":diff", $type = null) | |
| { | |
| if ($type) { | |
| $this->jsopBody[$key] = array($value,$type); | |
| } else { | |
| if (!isset($this->jsopBody[$key])) { | |
| $this->jsopBody[$key] = ""; | |
| } else { | |
| $this->jsopBody[$key] .= "\r"; | |
| } | |
| $this->jsopBody[$key] .= $value; | |
| } | |
| } | |
| /** | |
| * {@inheritDoc} | |
| */ | |
| public function prepareSave() | |
| { | |
| } | |
| /** | |
| * {@inheritDoc} | |
| */ | |
| public function finishSave() | |
| { | |
| if (count($this->jsopBody) > 0) { | |
| $request = $this->getRequest(Request::POST, "/"); | |
| $body = ''; | |
| if (count($this->jsopBody) > 1 || !isset($this->jsopBody[':diff'])) { | |
| $mime_boundary = md5(mt_rand()); | |
| //do the diffs at last | |
| $diff = null; | |
| if (isset($this->jsopBody[':diff'])) { | |
| $diff = $this->jsopBody[':diff']; | |
| unset($this->jsopBody[':diff']); | |
| } | |
| foreach ($this->jsopBody as $n => $v) { | |
| $body .= $this->getMimePart($n, $v, $mime_boundary); | |
| } | |
| if ($diff) { | |
| $body .= $this->getMimePart(":diff", $diff, $mime_boundary); | |
| } | |
| $body .= "--" . $mime_boundary . "--". "\r\n\r\n" ; // finish with two eol's!! | |
| $request->setContentType("multipart/form-data; boundary=$mime_boundary"); | |
| } else { | |
| $body = urlencode(":diff")."=". urlencode($this->jsopBody[':diff']); | |
| $request->setContentType("application/x-www-form-urlencoded; charset=utf-8"); | |
| } | |
| try { | |
| $request->setBody($body); | |
| $request->execute(); | |
| } catch (HTTPErrorException $e) { | |
| // TODO: can we throw any other more specific errors here? | |
| throw new RepositoryException('Something went wrong while saving nodes', $e->getCode(), $e); | |
| } | |
| } | |
| $this->jsopBody = array(); | |
| } | |
| /** | |
| * {@inheritDoc} | |
| */ | |
| public function rollbackSave() | |
| { | |
| } | |
| protected function getMimePart($name, $value, $mime_boundary) | |
| { | |
| $data = ''; | |
| $eol = "\r\n"; | |
| if (is_array($value)) { | |
| if (is_array($value[0])) { | |
| foreach ($value[0] as $v) { | |
| $data .= $this->getMimePart($name, array($v, $value[1]), $mime_boundary); | |
| } | |
| return $data; | |
| } | |
| $data .= '--' . $mime_boundary . $eol ; | |
| if (is_resource(($value[0]))) { | |
| $data .= 'Content-Disposition: form-data; name="' . $name . '"; filename="' . $name . '"' . $eol; | |
| $data .= 'Content-Type: jcr-value/'. strtolower(PropertyType::nameFromValue($value[1])) .'; charset=UTF-8'. $eol; | |
| $data .= 'Content-Transfer-Encoding: binary'. $eol.$eol; | |
| $data .= stream_get_contents($value[0]) . $eol; | |
| fclose($value[0]); | |
| } else { | |
| $data .= 'Content-Disposition: form-data; name="' . $name . '"' . $eol; | |
| $data .= 'Content-Type: jcr-value/'. strtolower(PropertyType::nameFromValue($value[1])) .'; charset=UTF-8'. $eol; | |
| $data .= 'Content-Transfer-Encoding: 8bit'. $eol.$eol; | |
| switch ($value[1]) { | |
| case PropertyType::DATE: | |
| $data .= $this->valueConverter->convertType($value[0], PropertyType::STRING); | |
| break; | |
| default: | |
| $data .= $value[0]; | |
| } | |
| $data .= $eol; | |
| } | |
| } else { | |
| if (is_array($value)) { | |
| foreach ($value as $v) { | |
| $data .= $this->getMimePart($name, $v, $mime_boundary); | |
| } | |
| return $data; | |
| } | |
| $data .= '--' . $mime_boundary . $eol ; | |
| $data .= 'Content-Disposition: form-data; name="'.$name.'"'. $eol; | |
| $data .= 'Content-Type: text/plain; charset=UTF-8'. $eol; | |
| $data .= 'Content-Transfer-Encoding: 8bit'. $eol. $eol; | |
| //$data .= '--' . $mime_boundary . $eol; | |
| $data .= $value . $eol; | |
| } | |
| return $data; | |
| } | |
| } |