public function save($userID = false) { if (!$this->libraryID) { trigger_error("Library ID must be set before saving", E_USER_ERROR); } Zotero_Items::editCheck($this); if (!$this->hasChanged()) { Z_Core::debug("Item {$this->id} has not changed"); return false; } // Make sure there are no gaps in the creator indexes $creators = $this->getCreators(); $lastPos = -1; foreach ($creators as $pos => $creator) { if ($pos != $lastPos + 1) { trigger_error("Creator index {$pos} out of sequence for item {$this->id}", E_USER_ERROR); } $lastPos++; } $shardID = Zotero_Shards::getByLibraryID($this->libraryID); Zotero_DB::beginTransaction(); try { // // New item, insert and return id // if (!$this->id || !$this->exists()) { Z_Core::debug('Saving data for new item to database'); $isNew = true; $sqlColumns = array(); $sqlValues = array(); // // Primary fields // $itemID = $this->id ? $this->id : Zotero_ID::get('items'); $key = $this->key ? $this->key : $this->generateKey(); $sqlColumns = array('itemID', 'itemTypeID', 'libraryID', 'key', 'dateAdded', 'dateModified', 'serverDateModified'); $timestamp = Zotero_DB::getTransactionTimestamp(); $sqlValues = array($itemID, $this->itemTypeID, $this->libraryID, $key, $this->dateAdded ? $this->dateAdded : $timestamp, $this->dateModified ? $this->dateModified : $timestamp, $timestamp); // // Primary fields // $sql = 'INSERT INTO items (`' . implode('`, `', $sqlColumns) . '`) VALUES ('; // Insert placeholders for bind parameters for ($i = 0; $i < sizeOf($sqlValues); $i++) { $sql .= '?, '; } $sql = substr($sql, 0, -2) . ')'; // Save basic data to items table $insertID = Zotero_DB::query($sql, $sqlValues, $shardID); if (!$this->id) { if (!$insertID) { throw new Exception("Item id not available after INSERT"); } $itemID = $insertID; $this->serverDateModified = $timestamp; } // Group item data if (Zotero_Libraries::getType($this->libraryID) == 'group' && $userID) { $sql = "INSERT INTO groupItems VALUES (?, ?, ?)"; Zotero_DB::query($sql, array($itemID, $userID, null), $shardID); } // // ItemData // if ($this->changed['itemData']) { // Use manual bound parameters to speed things up $origInsertSQL = "INSERT INTO itemData (itemID, fieldID, value) VALUES "; $insertSQL = $origInsertSQL; $insertParams = array(); $insertCounter = 0; $maxInsertGroups = 40; $max = Zotero_Items::$maxDataValueLength; $fieldIDs = array_keys($this->changed['itemData']); foreach ($fieldIDs as $fieldID) { $value = $this->getField($fieldID, true, false, true); if ($value == 'CURRENT_TIMESTAMP' && Zotero_ItemFields::getID('accessDate') == $fieldID) { $value = Zotero_DB::getTransactionTimestamp(); } // Check length if (strlen($value) > $max) { $fieldName = Zotero_ItemFields::getLocalizedString($this->itemTypeID, $fieldID); throw new Exception("={$fieldName} field " . "'" . substr($value, 0, 50) . "...' too long"); } if ($insertCounter < $maxInsertGroups) { $insertSQL .= "(?,?,?),"; $insertParams = array_merge($insertParams, array($itemID, $fieldID, $value)); } if ($insertCounter == $maxInsertGroups - 1) { $insertSQL = substr($insertSQL, 0, -1); $stmt = Zotero_DB::getStatement($insertSQL, true, $shardID); Zotero_DB::queryFromStatement($stmt, $insertParams); $insertSQL = $origInsertSQL; $insertParams = array(); $insertCounter = -1; } $insertCounter++; } if ($insertCounter > 0 && $insertCounter < $maxInsertGroups) { $insertSQL = substr($insertSQL, 0, -1); $stmt = Zotero_DB::getStatement($insertSQL, true, $shardID); Zotero_DB::queryFromStatement($stmt, $insertParams); } } // // Creators // if ($this->changed['creators']) { $indexes = array_keys($this->changed['creators']); // TODO: group queries $sql = "INSERT INTO itemCreators\n\t\t\t\t\t\t\t\t(itemID, creatorID, creatorTypeID, orderIndex) VALUES "; $placeholders = array(); $sqlValues = array(); $cacheRows = array(); foreach ($indexes as $orderIndex) { Z_Core::debug('Adding creator in position ' . $orderIndex, 4); $creator = $this->getCreator($orderIndex); if (!$creator) { continue; } if ($creator['ref']->hasChanged()) { Z_Core::debug("Auto-saving changed creator {$creator['ref']->id}"); $creator['ref']->save(); } $placeholders[] = "(?, ?, ?, ?)"; array_push($sqlValues, $itemID, $creator['ref']->id, $creator['creatorTypeID'], $orderIndex); $cacheRows[] = array('creatorID' => $creator['ref']->id, 'creatorTypeID' => $creator['creatorTypeID'], 'orderIndex' => $orderIndex); } if ($sqlValues) { $sql = $sql . implode(',', $placeholders); Zotero_DB::query($sql, $sqlValues, $shardID); } } // Deleted item if ($this->changed['deleted']) { $deleted = $this->getDeleted(); if ($deleted) { $sql = "REPLACE INTO deletedItems (itemID) VALUES (?)"; } else { $sql = "DELETE FROM deletedItems WHERE itemID=?"; } Zotero_DB::query($sql, $itemID, $shardID); } // Note if ($this->isNote() || $this->changed['note']) { $noteIsSanitized = false; // If we don't have a sanitized note, generate one if (is_null($this->noteTextSanitized)) { $noteTextSanitized = Zotero_Notes::sanitize($this->noteText); // But if the same as original, just use reference if ($this->noteText == $noteTextSanitized) { $this->noteTextSanitized =& $this->noteText; $noteIsSanitized = true; } else { $this->noteTextSanitized = $noteTextSanitized; } } // If note is sanitized already, store empty string // If not, store sanitized version $noteTextSanitized = $noteIsSanitized ? '' : $this->noteTextSanitized; $title = Zotero_Notes::noteToTitle($this->noteTextSanitized); $sql = "INSERT INTO itemNotes\n\t\t\t\t\t\t\t(itemID, sourceItemID, note, noteSanitized, title, hash)\n\t\t\t\t\t\t\tVALUES (?,?,?,?,?,?)"; $parent = $this->isNote() ? $this->getSource() : null; $hash = $this->noteText ? md5($this->noteText) : ''; $bindParams = array($itemID, $parent ? $parent : null, $this->noteText ? $this->noteText : '', $noteTextSanitized, $title, $hash); try { Zotero_DB::query($sql, $bindParams, $shardID); } catch (Exception $e) { if (strpos($e->getMessage(), "Incorrect string value") !== false) { throw new Exception("=Invalid character in note '" . Zotero_Utilities::ellipsize($title, 70) . "'", Z_ERROR_INVALID_INPUT); } throw $e; } Zotero_Notes::updateNoteCache($this->libraryID, $itemID, $this->noteText); Zotero_Notes::updateHash($this->libraryID, $itemID, $hash); } // Attachment if ($this->isAttachment()) { $sql = "INSERT INTO itemAttachments\n\t\t\t\t\t\t\t(itemID, sourceItemID, linkMode, mimeType, charsetID, path, storageModTime, storageHash)\n\t\t\t\t\t\t\tVALUES (?,?,?,?,?,?,?,?)"; $parent = $this->getSource(); if ($parent) { $parentItem = Zotero_Items::get($this->libraryID, $parent); if (!$parentItem) { throw new Exception("Parent item {$parent} not found"); } if ($parentItem->getSource()) { trigger_error("Parent item cannot be a child attachment", E_USER_ERROR); } } $linkMode = $this->attachmentLinkMode; $charsetID = Zotero_CharacterSets::getID($this->attachmentCharset); $path = $this->attachmentPath; $storageModTime = $this->attachmentStorageModTime; $storageHash = $this->attachmentStorageHash; $bindParams = array($itemID, $parent ? $parent : null, $linkMode + 1, $this->attachmentMIMEType, $charsetID ? $charsetID : null, $path ? $path : '', $storageModTime ? $storageModTime : null, $storageHash ? $storageHash : null); Zotero_DB::query($sql, $bindParams, $shardID); } // Sort fields $sortTitle = Zotero_Items::getSortTitle($this->getDisplayTitle(true)); if (mb_substr($sortTitle, 0, 5) == mb_substr($this->getField('title', false, true), 0, 5)) { $sortTitle = null; } $creatorSummary = $this->isRegularItem() ? mb_strcut($this->getCreatorSummary(), 0, Zotero_Creators::$creatorSummarySortLength) : ''; $sql = "INSERT INTO itemSortFields (itemID, sortTitle, creatorSummary) VALUES (?, ?, ?)"; Zotero_DB::query($sql, array($itemID, $sortTitle, $creatorSummary), $shardID); // // Source item id // if ($sourceItemID = $this->getSource()) { $newSourceItem = Zotero_Items::get($this->libraryID, $sourceItemID); if (!$newSourceItem) { throw new Exception("Cannot set source to invalid item"); } switch (Zotero_ItemTypes::getName($this->itemTypeID)) { case 'note': $newSourceItem->incrementNoteCount(); break; case 'attachment': $newSourceItem->incrementAttachmentCount(); break; } } // Related items if (!empty($this->changed['relatedItems'])) { $removed = array(); $newids = array(); $currentIDs = $this->relatedItems; if (!$currentIDs) { $currentIDs = array(); } foreach ($this->previousData['relatedItems'] as $id) { if (!in_array($id, $currentIDs)) { $removed[] = $id; } } foreach ($currentIDs as $id) { if (in_array($id, $this->previousData['relatedItems'])) { continue; } $newids[] = $id; } if ($removed) { $sql = "DELETE FROM itemRelated WHERE itemID=?\n\t\t\t\t\t\t\t\tAND linkedItemID IN ("; $sql .= implode(', ', array_fill(0, sizeOf($removed), '?')) . ")"; Zotero_DB::query($sql, array_merge(array($this->id), $removed), $shardID); } if ($newids) { $sql = "INSERT INTO itemRelated (itemID, linkedItemID)\n\t\t\t\t\t\t\t\tVALUES (?,?)"; $insertStatement = Zotero_DB::getStatement($sql, false, $shardID); foreach ($newids as $linkedItemID) { $insertStatement->execute(array($itemID, $linkedItemID)); } } } // Remove from delete log if it's there $sql = "DELETE FROM syncDeleteLogKeys WHERE libraryID=? AND objectType='item' AND `key`=?"; Zotero_DB::query($sql, array($this->libraryID, $key), $shardID); } else { Z_Core::debug('Updating database with new item data', 4); $isNew = false; // // Primary fields // $sql = "UPDATE items SET "; $sqlValues = array(); $timestamp = Zotero_DB::getTransactionTimestamp(); $updateFields = array('itemTypeID', 'libraryID', 'key', 'dateAdded', 'dateModified'); foreach ($updateFields as $updateField) { if (in_array($updateField, $this->changed['primaryData'])) { $sql .= "`{$updateField}`=?, "; $sqlValues[] = $this->{$updateField}; } else { if ($updateField == 'dateModified') { $sql .= "`{$updateField}`=?, "; $sqlValues[] = $timestamp; } } } $sql .= "serverDateModified=?, version=IF(version = 65535, 0, version + 1) WHERE itemID=?"; array_push($sqlValues, $timestamp, $this->id); Zotero_DB::query($sql, $sqlValues, $shardID); $this->serverDateModified = $timestamp; // Group item data if (Zotero_Libraries::getType($this->libraryID) == 'group' && $userID) { $sql = "INSERT INTO groupItems VALUES (?, ?, ?)\n\t\t\t\t\t\t\t\tON DUPLICATE KEY UPDATE lastModifiedByUserID=?"; Zotero_DB::query($sql, array($this->id, null, $userID, $userID), $shardID); } // // ItemData // if ($this->changed['itemData']) { $del = array(); $origReplaceSQL = "REPLACE INTO itemData (itemID, fieldID, value) VALUES "; $replaceSQL = $origReplaceSQL; $replaceParams = array(); $replaceCounter = 0; $maxReplaceGroups = 40; $max = Zotero_Items::$maxDataValueLength; $fieldIDs = array_keys($this->changed['itemData']); foreach ($fieldIDs as $fieldID) { $value = $this->getField($fieldID, true, false, true); // If field changed and is empty, mark row for deletion if ($value === "") { $del[] = $fieldID; continue; } if ($value == 'CURRENT_TIMESTAMP' && Zotero_ItemFields::getID('accessDate') == $fieldID) { $value = Zotero_DB::getTransactionTimestamp(); } // Check length if (strlen($value) > $max) { $fieldName = Zotero_ItemFields::getLocalizedString($this->itemTypeID, $fieldID); throw new Exception("={$fieldName} field " . "'" . substr($value, 0, 50) . "...' too long"); } if ($replaceCounter < $maxReplaceGroups) { $replaceSQL .= "(?,?,?),"; $replaceParams = array_merge($replaceParams, array($this->id, $fieldID, $value)); } if ($replaceCounter == $maxReplaceGroups - 1) { $replaceSQL = substr($replaceSQL, 0, -1); $stmt = Zotero_DB::getStatement($replaceSQL, true, $shardID); Zotero_DB::queryFromStatement($stmt, $replaceParams); $replaceSQL = $origReplaceSQL; $replaceParams = array(); $replaceCounter = -1; } $replaceCounter++; } if ($replaceCounter > 0 && $replaceCounter < $maxReplaceGroups) { $replaceSQL = substr($replaceSQL, 0, -1); $stmt = Zotero_DB::getStatement($replaceSQL, true, $shardID); Zotero_DB::queryFromStatement($stmt, $replaceParams); } // Update memcached with used fields $fids = array(); foreach ($this->itemData as $fieldID => $value) { if ($value !== false && $value !== null) { $fids[] = $fieldID; } } // Delete blank fields if ($del) { $sql = 'DELETE from itemData WHERE itemID=? AND fieldID IN ('; $sqlParams = array($this->id); foreach ($del as $d) { $sql .= '?, '; $sqlParams[] = $d; } $sql = substr($sql, 0, -2) . ')'; Zotero_DB::query($sql, $sqlParams, $shardID); } } // // Creators // if ($this->changed['creators']) { $indexes = array_keys($this->changed['creators']); $sql = "INSERT INTO itemCreators\n\t\t\t\t\t\t\t\t(itemID, creatorID, creatorTypeID, orderIndex) VALUES "; $placeholders = array(); $sqlValues = array(); $cacheRows = array(); foreach ($indexes as $orderIndex) { Z_Core::debug('Creator in position ' . $orderIndex . ' has changed', 4); $creator = $this->getCreator($orderIndex); $sql2 = 'DELETE FROM itemCreators WHERE itemID=? AND orderIndex=?'; Zotero_DB::query($sql2, array($this->id, $orderIndex), $shardID); if (!$creator) { continue; } if ($creator['ref']->hasChanged()) { Z_Core::debug("Auto-saving changed creator {$creator['ref']->id}"); $creator['ref']->save(); } $placeholders[] = "(?, ?, ?, ?)"; array_push($sqlValues, $this->id, $creator['ref']->id, $creator['creatorTypeID'], $orderIndex); } if ($sqlValues) { $sql = $sql . implode(',', $placeholders); Zotero_DB::query($sql, $sqlValues, $shardID); } } // Deleted item if ($this->changed['deleted']) { $deleted = $this->getDeleted(); if ($deleted) { $sql = "REPLACE INTO deletedItems (itemID) VALUES (?)"; } else { $sql = "DELETE FROM deletedItems WHERE itemID=?"; } Zotero_DB::query($sql, $this->id, $shardID); } // In case this was previously a standalone item, // delete from any collections it may have been in if ($this->changed['source'] && $this->getSource()) { $sql = "DELETE FROM collectionItems WHERE itemID=?"; Zotero_DB::query($sql, $this->id, $shardID); } // // Note or attachment note // if ($this->changed['note']) { $noteIsSanitized = false; // If we don't have a sanitized note, generate one if (is_null($this->noteTextSanitized)) { $noteTextSanitized = Zotero_Notes::sanitize($this->noteText); // But if the same as original, just use reference if ($this->noteText == $noteTextSanitized) { $this->noteTextSanitized =& $this->noteText; $noteIsSanitized = true; } else { $this->noteTextSanitized = $noteTextSanitized; } } // If note is sanitized already, store empty string // If not, store sanitized version $noteTextSanitized = $noteIsSanitized ? '' : $this->noteTextSanitized; $title = Zotero_Notes::noteToTitle($this->noteTextSanitized); // Only record sourceItemID in itemNotes for notes if ($this->isNote()) { $sourceItemID = $this->getSource(); } $sourceItemID = !empty($sourceItemID) ? $sourceItemID : null; $hash = $this->noteText ? md5($this->noteText) : ''; $sql = "INSERT INTO itemNotes\n\t\t\t\t\t\t\t(itemID, sourceItemID, note, noteSanitized, title, hash)\n\t\t\t\t\t\t\tVALUES (?,?,?,?,?,?)\n\t\t\t\t\t\t\tON DUPLICATE KEY UPDATE sourceItemID=?, note=?, noteSanitized=?, title=?, hash=?"; $bindParams = array($this->id, $sourceItemID, $this->noteText ? $this->noteText : '', $noteTextSanitized, $title, $hash, $sourceItemID, $this->noteText ? $this->noteText : '', $noteTextSanitized, $title, $hash); Zotero_DB::query($sql, $bindParams, $shardID); Zotero_Notes::updateNoteCache($this->libraryID, $this->id, $this->noteText); Zotero_Notes::updateHash($this->libraryID, $this->id, $hash); // TODO: handle changed source? } // Attachment if ($this->changed['attachmentData']) { $sql = "REPLACE INTO itemAttachments\n\t\t\t\t\t\t(itemID, sourceItemID, linkMode, mimeType, charsetID, path, storageModTime, storageHash)\n\t\t\t\t\t\tVALUES (?,?,?,?,?,?,?,?)"; $parent = $this->getSource(); if ($parent) { $parentItem = Zotero_Items::get($this->libraryID, $parent); if (!$parentItem) { throw new Exception("Parent item {$parent} not found"); } if ($parentItem->getSource()) { trigger_error("Parent item cannot be a child attachment", E_USER_ERROR); } } $linkMode = $this->attachmentLinkMode; $charsetID = Zotero_CharacterSets::getID($this->attachmentCharset); $path = $this->attachmentPath; $storageModTime = $this->attachmentStorageModTime; $storageHash = $this->attachmentStorageHash; $bindParams = array($this->id, $parent ? $parent : null, $linkMode + 1, $this->attachmentMIMEType, $charsetID ? $charsetID : null, $path ? $path : '', $storageModTime ? $storageModTime : null, $storageHash ? $storageHash : null); Zotero_DB::query($sql, $bindParams, $shardID); } // Sort fields if (!empty($this->changed['primaryData']['itemTypeID']) || $this->changed['itemData'] || $this->changed['creators']) { $sql = "UPDATE itemSortFields SET sortTitle=?"; $params = array(); $sortTitle = Zotero_Items::getSortTitle($this->getDisplayTitle(true)); if (mb_substr($sortTitle, 0, 5) == mb_substr($this->getField('title', false, true), 0, 5)) { $sortTitle = null; } $params[] = $sortTitle; if ($this->changed['creators']) { $creatorSummary = mb_strcut($this->getCreatorSummary(), 0, Zotero_Creators::$creatorSummarySortLength); $sql .= ", creatorSummary=?"; $params[] = $creatorSummary; } $sql .= " WHERE itemID=?"; $params[] = $this->id; Zotero_DB::query($sql, $params, $shardID); } // // Source item id // if ($this->changed['source']) { $type = Zotero_ItemTypes::getName($this->itemTypeID); $Type = ucwords($type); // Update DB, if not a note or attachment we already changed above if (!$this->changed['attachmentData'] && (!$this->changed['note'] || !$this->isNote())) { $sql = "UPDATE item" . $Type . "s SET sourceItemID=? WHERE itemID=?"; $parent = $this->getSource(); $bindParams = array($parent ? $parent : null, $this->id); Zotero_DB::query($sql, $bindParams, $shardID); } } if (false && $this->changed['source']) { trigger_error("Unimplemented", E_USER_ERROR); $newItem = Zotero_Items::get($this->libraryID, $sourceItemID); // FK check if ($newItem) { if ($sourceItemID) { } else { trigger_error("Cannot set {$type} source to invalid item {$sourceItemID}", E_USER_ERROR); } } $oldSourceItemID = $this->getSource(); if ($oldSourceItemID == $sourceItemID) { Z_Core::debug("{$Type} source hasn't changed", 4); } else { $oldItem = Zotero_Items::get($this->libraryID, $oldSourceItemID); if ($oldSourceItemID && $oldItem) { } else { //$oldItemNotifierData = null; Z_Core::debug("Old source item {$oldSourceItemID} didn't exist in setSource()", 2); } // If this was an independent item, remove from any collections where it // existed previously and add source instead if there is one if (!$oldSourceItemID) { $sql = "SELECT collectionID FROM collectionItems WHERE itemID=?"; $changedCollections = Zotero_DB::query($sql, $itemID, $shardID); if ($changedCollections) { trigger_error("Unimplemented", E_USER_ERROR); if ($sourceItemID) { $sql = "UPDATE OR REPLACE collectionItems " . "SET itemID=? WHERE itemID=?"; Zotero_DB::query($sql, array($sourceItemID, $this->id), $shardID); } else { $sql = "DELETE FROM collectionItems WHERE itemID=?"; Zotero_DB::query($sql, $this->id, $shardID); } } } $sql = "UPDATE item{$Type}s SET sourceItemID=?\n\t\t\t\t\t\t\t\tWHERE itemID=?"; $bindParams = array($sourceItemID ? $sourceItemID : null, $itemID); Zotero_DB::query($sql, $bindParams, $shardID); //Zotero.Notifier.trigger('modify', 'item', $this->id, notifierData); // Update the counts of the previous and new sources if ($oldItem) { /* switch ($type) { case 'note': $oldItem->decrementNoteCount(); break; case 'attachment': $oldItem->decrementAttachmentCount(); break; } */ //Zotero.Notifier.trigger('modify', 'item', oldSourceItemID, oldItemNotifierData); } if ($newItem) { /* switch ($type) { case 'note': $newItem->incrementNoteCount(); break; case 'attachment': $newItem->incrementAttachmentCount(); break; } */ //Zotero.Notifier.trigger('modify', 'item', sourceItemID, newItemNotifierData); } } } // Related items if (!empty($this->changed['relatedItems'])) { $removed = array(); $newids = array(); $currentIDs = $this->relatedItems; if (!$currentIDs) { $currentIDs = array(); } foreach ($this->previousData['relatedItems'] as $id) { if (!in_array($id, $currentIDs)) { $removed[] = $id; } } foreach ($currentIDs as $id) { if (in_array($id, $this->previousData['relatedItems'])) { continue; } $newids[] = $id; } if ($removed) { $sql = "DELETE FROM itemRelated WHERE itemID=?\n\t\t\t\t\t\t\t\tAND linkedItemID IN ("; $q = array_fill(0, sizeOf($removed), '?'); $sql .= implode(', ', $q) . ")"; Zotero_DB::query($sql, array_merge(array($this->id), $removed), $shardID); } if ($newids) { $sql = "INSERT INTO itemRelated (itemID, linkedItemID)\n\t\t\t\t\t\t\t\tVALUES (?,?)"; $insertStatement = Zotero_DB::getStatement($sql, false, $shardID); foreach ($newids as $linkedItemID) { $insertStatement->execute(array($this->id, $linkedItemID)); } } } } Zotero_DB::commit(); } catch (Exception $e) { Zotero_DB::rollback(); throw $e; } if (!$this->id) { $this->id = $itemID; } if (!$this->key) { $this->key = $key; } if ($isNew) { Zotero_Items::cache($this); Zotero_Items::cacheLibraryKeyID($this->libraryID, $key, $itemID); } // TODO: invalidate memcache Zotero_Items::reload($this->libraryID, $this->id); if ($isNew) { //Zotero.Notifier.trigger('add', 'item', $this->getID()); return $this->id; } //Zotero.Notifier.trigger('modify', 'item', $this->getID(), { old: $this->_preChangeArray }); return true; }
/** * Set an item note * * Note: This can only be called on notes and attachments **/ public function setNote($text) { if (!$this->isNote() && !$this->isAttachment()) { trigger_error("setNote() can only be called on notes and attachments", E_USER_ERROR); } if (!is_string($text)) { $text = ''; } if (mb_strlen($text) > Zotero_Notes::$MAX_NOTE_LENGTH) { // UTF-8 (0xC2 0xA0) isn't trimmed by default $whitespace = chr(0x20) . chr(0x09) . chr(0x0A) . chr(0x0D) . chr(0x00) . chr(0x0B) . chr(0xC2) . chr(0xA0); $excerpt = iconv( "UTF-8", "UTF-8//IGNORE", Zotero_Notes::noteToTitle(trim($text), true) ); $excerpt = trim($excerpt, $whitespace); // If tag-stripped version is empty, just return raw HTML if ($excerpt == '') { $excerpt = iconv( "UTF-8", "UTF-8//IGNORE", preg_replace( '/\s+/', ' ', mb_substr(trim($text), 0, Zotero_Notes::$MAX_TITLE_LENGTH) ) ); $excerpt = html_entity_decode($excerpt); $excerpt = trim($excerpt, $whitespace); } $msg = "=Note '" . $excerpt . "...' too long"; if ($this->key) { $msg .= " for item '" . $this->libraryID . "/" . $this->key . "'"; } throw new Exception($msg, Z_ERROR_NOTE_TOO_LONG); } $sanitizedText = Zotero_Notes::sanitize($text); if ($sanitizedText === $this->getNote(true)) { Z_Core::debug("Note text hasn't changed in setNote()"); return; } $this->noteText = $text; // If sanitized version is the same as original, store empty string if ($text === $sanitizedText) { $this->noteTextSanitized = ''; } else { $this->noteTextSanitized = $sanitizedText; } $this->changed['note'] = true; }
/** * Handle uploaded data, overwriting existing data */ public function upload() { $this->sessionCheck(); // Another session is either queued or writing — upload data won't be valid, // so client should wait and return to /updated with 'upload' flag Zotero_DB::beginTransaction(); if (Zotero_Sync::userIsReadLocked($this->userID) || Zotero_Sync::userIsWriteLocked($this->userID)) { Zotero_DB::commit(); $locked = $this->responseXML->addChild('locked'); $locked['wait'] = $this->getWaitTime($this->sessionID); $this->end(); } Zotero_DB::commit(); $this->clearWaitTime($this->sessionID); if (empty($_REQUEST['updateKey'])) { $this->error(400, 'INVALID_UPLOAD_DATA', 'Update key not provided'); } if ($_REQUEST['updateKey'] != Zotero_Users::getUpdateKey($this->userID)) { $this->e409("Server data has changed since last retrieval"); } // TODO: change to POST if (empty($_REQUEST['data'])) { $this->error(400, 'MISSING_UPLOAD_DATA', 'Uploaded data not provided'); } $xmldata =& $_REQUEST['data']; try { $doc = new DOMDocument(); $doc->loadXML($xmldata, LIBXML_PARSEHUGE); // For huge uploads, make sure notes aren't bigger than SimpleXML can parse if (strlen($xmldata) > 7000000) { $xpath = new DOMXPath($doc); $results = $xpath->query('/data/items/item/note[string-length(text()) > ' . Zotero_Notes::$MAX_NOTE_LENGTH . ']'); if ($results->length) { $noteElem = $results->item(0); $text = $noteElem->textContent; $libraryID = $noteElem->parentNode->getAttribute('libraryID'); $key = $noteElem->parentNode->getAttribute('key'); // UTF-8 (0xC2 0xA0) isn't trimmed by default $whitespace = chr(0x20) . chr(0x9) . chr(0xa) . chr(0xd) . chr(0x0) . chr(0xb) . chr(0xc2) . chr(0xa0); $excerpt = iconv("UTF-8", "UTF-8//IGNORE", Zotero_Notes::noteToTitle(trim($text), true)); $excerpt = trim($excerpt, $whitespace); // If tag-stripped version is empty, just return raw HTML if ($excerpt == '') { $excerpt = iconv("UTF-8", "UTF-8//IGNORE", preg_replace('/\\s+/', ' ', mb_substr(trim($text), 0, Zotero_Notes::$MAX_TITLE_LENGTH))); $excerpt = html_entity_decode($excerpt); $excerpt = trim($excerpt, $whitespace); } $msg = "=Note '" . $excerpt . "...' too long"; if ($key) { $msg .= " for item '" . $libraryID . "/" . $key . "'"; } throw new Exception($msg, Z_ERROR_NOTE_TOO_LONG); } } } catch (Exception $e) { $this->handleUploadError($e, $xmldata); } function relaxNGErrorHandler($errno, $errstr) { //Z_Core::logError($errstr); } set_error_handler('relaxNGErrorHandler'); set_time_limit(60); if (!$doc->relaxNGValidate(Z_ENV_MODEL_PATH . 'relax-ng/upload.rng')) { $id = substr(md5(uniqid(rand(), true)), 0, 10); $str = date("D M j G:i:s T Y") . "\n"; $str .= "IP address: " . $_SERVER['REMOTE_ADDR'] . "\n"; if (isset($_SERVER['HTTP_X_ZOTERO_VERSION'])) { $str .= "Version: " . $_SERVER['HTTP_X_ZOTERO_VERSION'] . "\n"; } $str .= "Error: RELAX NG validation failed\n\n"; $str .= $xmldata; if (!file_put_contents(Z_CONFIG::$SYNC_ERROR_PATH . $id, $str)) { error_log("Unable to save error report to " . Z_CONFIG::$SYNC_ERROR_PATH . $id); } $this->error(500, 'INVALID_UPLOAD_DATA', "Uploaded data not well-formed (Report ID: {$id})"); } restore_error_handler(); try { $xml = simplexml_import_dom($doc); $queue = true; if (Z_ENV_TESTING_SITE && !empty($_GET['noqueue'])) { $queue = false; } if ($queue) { $affectedLibraries = Zotero_Sync::parseAffectedLibraries($xmldata); // Relations-only uploads don't have affected libraries if (!$affectedLibraries) { $affectedLibraries = array(Zotero_Users::getLibraryIDFromUserID($this->userID)); } Zotero_Sync::queueUpload($this->userID, $this->sessionID, $xmldata, $affectedLibraries); try { Zotero_Processors::notifyProcessors('upload'); Zotero_Processors::notifyProcessors('error'); usleep(750000); } catch (Exception $e) { Z_Core::logError($e); } // Give processor a chance to finish while we're still here $this->uploadstatus(); } else { set_time_limit(210); $timestamp = Zotero_Sync::processUpload($this->userID, $xml); $this->responseXML['timestamp'] = $timestamp; $this->responseXML->addChild('uploaded'); $this->end(); } } catch (Exception $e) { $this->handleUploadError($e, $xmldata); } }