Skip to content

Commit

Permalink
Move My Publications into My Library [schema change]
Browse files Browse the repository at this point in the history
API support for zotero/zotero@5ff2a59f87

Schema change:

CREATE TABLE `publicationsItems` (
  `itemID` int(10) unsigned NOT NULL,
  PRIMARY KEY (`itemID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

ALTER TABLE `publicationsItems`
  ADD CONSTRAINT `publicationsItems_ibfk_1` FOREIGN KEY (`itemID`) REFERENCES `items` (`itemID`) ON DELETE CASCADE;
  • Loading branch information
dstillman committed Apr 14, 2017
1 parent aafda6d commit 18a93e7
Show file tree
Hide file tree
Showing 17 changed files with 1,044 additions and 285 deletions.
67 changes: 49 additions & 18 deletions controllers/ApiController.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ class ApiController extends Controller {
protected $objectName;
protected $subset;
protected $singleObject;
protected $publications = false;
protected $fileMode;
protected $fileView;
protected $httpAuth = false;
Expand Down Expand Up @@ -338,33 +339,52 @@ public function init($extra) {
$apiVersion = 1;
}

// For publications URLs (e.g., /users/:userID/publications/items), swap in
// objectLibraryID of user's publications library
if (!empty($extra['publications'])) {
// Query parameters not yet parsed, so check version parameter
if (($apiVersion && $apiVersion < 3)
|| (!empty($_REQUEST['v']) && $_REQUEST['v'] < 3)
|| (!empty($_REQUEST['version']) && $_REQUEST['version'] == 1)) {
$this->e404();
}
$userLibraryID = $this->objectLibraryID;
$this->objectLibraryID = Zotero_Users::getLibraryIDFromUserID(
$this->objectUserID, 'publications'
);
// If one doesn't exist, for write requests create a library if the key
// has write permission to the user library. For read requests, just
// return a 404.
if (!$this->objectLibraryID) {
if ($this->isWriteMethod()) {
if (!$this->permissions->canAccess($userLibraryID)
|| !$this->permissions->canWrite($userLibraryID)) {
$this->e403();
}
$this->objectLibraryID = Zotero_Publications::add($this->objectUserID);

if ($this->isWriteMethod()) {
$this->e405("Please upgrade to the latest Zotero 5.0 beta to update My Publications.", Z_ERROR_INVALID_INPUT);
}

$this->permissions->setPublications();
// Added to queryParams below
$this->publications = true;

// If no publications items in main user library, see if there's a legacy publications library
if (!Zotero_Users::hasPublicationsInUserLibrary($this->objectUserID)) {
$publicationsLibraryID = Zotero_Users::getLibraryIDFromUserID(
$this->objectUserID, 'publications'
);
if ($publicationsLibraryID) {
$this->objectLibraryID = $publicationsLibraryID;
}
else {
$this->objectLibraryID = 0;
}

// TEMP: Remove after integrated publications upgrade
if ($this->action == 'settings') {
// If publications in either legacy library or user library, show upgrade error
if (Zotero_Users::hasPublicationsInUserLibrary($this->objectUserID)
|| Zotero_Users::hasPublicationsInLegacyLibrary($this->objectUserID)) {
$this->e400("Please upgrade to the latest Zotero 5.0 beta to continue syncing My Publications.", Z_ERROR_INVALID_INPUT);
}
$this->apiVersion = 3;
header("Total-Results: 0");
$this->libraryVersion = Zotero_Libraries::getUpdatedVersion($this->objectLibraryID);
$this->queryParams['format'] = 'json';
echo json_encode([]);
$this->end();
}
else if ($this->action == 'deleted') {
$this->apiVersion = 3;
$this->libraryVersion = Zotero_Libraries::getUpdatedVersion($this->objectLibraryID);
$this->queryParams['format'] = 'json';
echo json_encode(new stdClass);
$this->end();
}
}

Expand Down Expand Up @@ -413,6 +433,11 @@ public function init($extra) {
$apiVersion,
$atomAccepted
);
if ($this->publications) {
$this->queryParams['publications'] = true;
// Don't show trashed items in publications view
$this->queryParams['includeTrashed'] = false;
}

// Sorting by Item Type or Added By currently require writing to shard tables, so don't
// send those to the read replicas
Expand Down Expand Up @@ -882,6 +907,7 @@ public function __call($name, $arguments) {
if ($matches[1] == "4" || $matches[1] == "5") {
if (!$this->libraryVersionOnFailure) {
$this->libraryVersion = null;
$this->etag = null;
}
Zotero_DB::rollback(true);
}
Expand Down Expand Up @@ -936,6 +962,7 @@ protected function redirect($url, $httpCode=302) {
}

$this->libraryVersion = null;
$this->etag = null;
$this->responseXML = null;

$this->responseCode = $httpCode;
Expand Down Expand Up @@ -1020,6 +1047,10 @@ protected function end() {
}
}

if (isset($this->etag)) {
header("ETag: " . $this->etag);
}

if ($this->responseXML instanceof SimpleXMLElement) {
if (!$this->responseCode) {
$updated = (string) $this->responseXML->updated;
Expand Down
7 changes: 0 additions & 7 deletions controllers/FullTextController.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,6 @@ public function fulltext() {
$this->e403();
}

// Default empty library
if ($this->objectLibraryID === 0) {
$this->libraryVersion = 0;
echo Zotero_Utilities::formatJSON(new stdClass);
$this->end();
}

// Multi-item write
if ($this->isWriteMethod()) {
if ($this->apiVersion < 3) {
Expand Down
50 changes: 38 additions & 12 deletions controllers/ItemsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,6 @@

class ItemsController extends ApiController {
public function items() {
// Check for general library access
if (!$this->permissions->canAccess($this->objectLibraryID)) {
$this->e403();
}

if ($this->isWriteMethod()) {
// Check for library write access
if (!$this->permissions->canWrite($this->objectLibraryID)) {
Expand Down Expand Up @@ -73,20 +68,26 @@ public function items() {
$this->allowMethods(array('HEAD', 'GET', 'PUT', 'PATCH', 'DELETE'));
}

if (!$this->objectLibraryID || !Zotero_ID::isValidKey($this->objectKey)) {
if (!Zotero_ID::isValidKey($this->objectKey)) {
$this->e404();
}

$item = Zotero_Items::getByLibraryAndKey($this->objectLibraryID, $this->objectKey);
if ($item) {
// If no access to the note, don't show that it exists
if ($item->isNote() && !$this->permissions->canAccess($this->objectLibraryID, 'notes')) {
// If no access to the item, don't show that it exists
if (!$this->permissions->canAccessObject($item)) {
$this->e404();
}

// Don't show an item in publications that doesn't belong there, even if user has
// access to it
if ($this->publications && (!$item->inPublications || $item->deleted)) {
$this->e404();
}

// Make sure URL libraryID matches item libraryID
if ($this->objectLibraryID != $item->libraryID) {
$this->e404("Item does not exist");
$this->e404();
}

// File access mode
Expand Down Expand Up @@ -186,7 +187,7 @@ public function items() {
break;

default:
$export = Zotero_Translate::doExport(array($item), $this->queryParams['format']);
$export = Zotero_Translate::doExport([$item], $this->queryParams);
$this->queryParams['format'] = null;
header("Content-Type: " . $export['mimeType']);
echo $export['body'];
Expand All @@ -200,7 +201,27 @@ public function items() {
else {
$this->allowMethods(array('HEAD', 'GET', 'POST', 'DELETE'));

$this->libraryVersion = Zotero_Libraries::getUpdatedVersion($this->objectLibraryID);
// Check for general library access
if (!$this->publications && !$this->permissions->canAccess($this->objectLibraryID)) {
$this->e403();
}

if ($this->publications) {
// Include ETag in My Publications (or, in the future, public collections)
$this->etag = Zotero_Publications::getETag($this->objectUserID);

// Return 304 if ETag matches
if (!empty($_SERVER['HTTP_IF_NONE_MATCH']) && $_SERVER['HTTP_IF_NONE_MATCH'] == $this->etag) {
$this->e304();
}

// TEMP: Remove after integrated publications upgrade
$this->libraryVersion = Zotero_Libraries::getUpdatedVersion($this->objectLibraryID);
}
// Last-Modified-Version otherwise
else {
$this->libraryVersion = Zotero_Libraries::getUpdatedVersion($this->objectLibraryID);
}

$includeTrashed = $this->queryParams['includeTrashed'];

Expand Down Expand Up @@ -336,6 +357,11 @@ public function items() {
$this->e404("Item not found");
}

// Don't show child items in publications mode of an item not in publications
if ($this->publications && !$item->inPublications) {
$this->e404("Item not found");
}

if ($item->isAttachment()) {
$this->e400("/children cannot be called on attachment items");
}
Expand Down Expand Up @@ -630,7 +656,7 @@ private function generateMultiResponse($results, $title='') {
if ($this->method == 'HEAD') {
break;
}
$export = Zotero_Translate::doExport($results['results'], $this->queryParams['format']);
$export = Zotero_Translate::doExport($results['results'], $this->queryParams);
$this->queryParams['format'] = null;
header("Content-Type: " . $export['mimeType']);
echo $export['body'];
Expand Down
19 changes: 10 additions & 9 deletions include/config/routes.inc.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@

// Attachment files
$router->map('/users/i:objectUserID/laststoragesync', array('controller' => 'Storage', 'action' => 'laststoragesync', 'extra' => array('auth' => true)));
$router->map('/users/i:objectUserID/publications/laststoragesync', ['controller' => 'Storage', 'action' => 'laststoragesync', 'extra' => ['auth' => true, 'publications' => true]]);
$router->map('/groups/i:objectGroupID/laststoragesync', array('controller' => 'Storage', 'action' => 'laststoragesync', 'extra' => array('auth' => true)));
$router->map('/users/i:objectUserID/storageadmin', array('controller' => 'Storage', 'action' => 'storageadmin'));
$router->map('/storagepurge', array('controller' => 'Storage', 'action' => 'storagepurge'));
Expand All @@ -38,10 +37,10 @@

// Full-text content
$router->map('/users/i:objectUserID/items/:objectKey/fulltext', array('controller' => 'FullText', 'action' => 'itemContent'));
$router->map('/users/i:objectUserID/publications/items/:objectKey/fulltext', ['controller' => 'FullText', 'action' => 'itemContent', 'extra' => ['publications' => true]]);
//$router->map('/users/i:objectUserID/publications/items/:objectKey/fulltext', ['controller' => 'FullText', 'action' => 'itemContent', 'extra' => ['publications' => true]]);
$router->map('/groups/i:objectGroupID/items/:objectKey/fulltext', array('controller' => 'FullText', 'action' => 'itemContent'));
$router->map('/users/i:objectUserID/fulltext', array('controller' => 'FullText', 'action' => 'fulltext'));
$router->map('/users/i:objectUserID/publications/fulltext', ['controller' => 'FullText', 'action' => 'fulltext', 'extra' => ['publications' => true]]);
//$router->map('/users/i:objectUserID/publications/fulltext', ['controller' => 'FullText', 'action' => 'fulltext', 'extra' => ['publications' => true]]);
$router->map('/groups/i:objectGroupID/fulltext', array('controller' => 'FullText', 'action' => 'fulltext'));

// All trashed items
Expand All @@ -59,11 +58,11 @@
$router->map('/users/i:objectUserID/tags/:scopeObjectName/items/:objectName/:subset', array('controller' => 'Items', 'extra' => array('scopeObject' => 'tags')));
$router->map('/groups/i:objectGroupID/tags/:scopeObjectName/items/:objectName/:subset', array('controller' => 'Items', 'extra' => array('scopeObject' => 'tags')));
$router->map('/users/i:objectUserID/tags/:objectName/:subset', array('controller' => 'Tags'));
$router->map('/users/i:objectUserID/publications/tags/:objectName/:subset', ['controller' => 'Tags', 'extra' => ['publications' => true]]);
//$router->map('/users/i:objectUserID/publications/tags/:objectName/:subset', ['controller' => 'Tags', 'extra' => ['publications' => true]]);
$router->map('/groups/i:objectGroupID/tags/:objectName/:subset', array('controller' => 'Tags'));

// Tags within something else
$router->map('/users/i:objectUserID/publications/items/:scopeObjectKey/tags/:objectKey/:subset', ['controller' => 'Tags', 'extra' => ['publications']]);
//$router->map('/users/i:objectUserID/publications/items/:scopeObjectKey/tags/:objectKey/:subset', ['controller' => 'Tags', 'extra' => ['publications']]);
$router->map('/users/i:objectUserID/:scopeObject/:scopeObjectKey/tags/:objectKey/:subset', array('controller' => 'Tags'));
$router->map('/groups/i:objectGroupID/:scopeObject/:scopeObjectKey/tags/:objectKey/:subset', array('controller' => 'Tags'));

Expand All @@ -80,18 +79,20 @@
$router->map('/users/i:objectUserID/keys/:objectName', array('controller' => 'Keys'));

// User/library settings
$router->map('/users/i:objectUserID/publications/settings/:objectKey', ['controller' => 'settings', 'extra' => ['publications' => true]]);
$router->map('/users/i:objectUserID/settings/:objectKey', array('controller' => 'settings'));
$router->map('/groups/i:objectGroupID/settings/:objectKey', array('controller' => 'settings'));

// Clear (for testing)
$router->map('/users/i:objectUserID/clear', array('controller' => 'Api', 'action' => 'clear'));
$router->map('/users/i:objectUserID/publications/clear', ['controller' => 'Api', 'action' => 'clear', 'extra' => ['publications' => true]]);
$router->map('/groups/i:objectGroupID/clear', array('controller' => 'Api', 'action' => 'clear'));

// My Publications items
$router->map('/users/i:objectUserID/publications/settings', ['controller' => 'settings', 'extra' => ['publications' => true]]); // TEMP
$router->map('/users/i:objectUserID/publications/deleted', ['controller' => 'deleted', 'extra' => ['publications' => true]]); // TEMP
$router->map('/users/i:objectUserID/publications/items/:objectKey/children', ['controller' => 'Items', 'extra' => ['publications' => true, 'subset' => 'children']]);
$router->map('/users/i:objectUserID/publications/items/:objectKey', ['controller' => 'Items', 'extra' => ['publications' => true]]);

// Other top-level URLs, with an optional key and subset
$router->map('/users/i:objectUserID/publications/items/:objectKey/:subset', ['controller' => 'Items', 'extra' => ['publications' => true]]); // Just items for now
$router->map('/users/i:objectUserID/publications/deleted', ['controller' => 'Deleted', 'extra' => ['publications' => true]]);
$router->map('/users/i:objectUserID/:controller/:objectKey/:subset');
$router->map('/groups/i:objectGroupID/:controller/:objectKey/:subset');

Expand Down
8 changes: 8 additions & 0 deletions misc/shard.sql
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,11 @@ CREATE TABLE `groupItems` (
) ENGINE=InnoDB DEFAULT CHARSET=utf8;


CREATE TABLE `publicationsItems` (
`itemID` int(10) unsigned NOT NULL,
PRIMARY KEY (`itemID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;


CREATE TABLE `itemAttachments` (
`itemID` int(10) unsigned NOT NULL,
Expand Down Expand Up @@ -350,6 +355,9 @@ ALTER TABLE `deletedItems`
ALTER TABLE `groupItems`
ADD CONSTRAINT `groupItems_ibfk_1` FOREIGN KEY (`itemID`) REFERENCES `items` (`itemID`) ON DELETE CASCADE;

ALTER TABLE `publicationsItems`
ADD CONSTRAINT `publicationsItems_ibfk_1` FOREIGN KEY (`itemID`) REFERENCES `items` (`itemID`) ON DELETE CASCADE;

ALTER TABLE `itemAttachments`
ADD CONSTRAINT `itemAttachments_ibfk_1` FOREIGN KEY (`itemID`) REFERENCES `items` (`itemID`) ON DELETE CASCADE,
ADD CONSTRAINT `itemAttachments_ibfk_2` FOREIGN KEY (`sourceItemID`) REFERENCES `items` (`itemID`) ON DELETE SET NULL;
Expand Down
8 changes: 4 additions & 4 deletions model/API.inc.php
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,8 @@ class Zotero_API {

// For internal use only
'emptyFirst' => false,
'uncached' => false
'uncached' => false,
'publications' => false
];


Expand Down Expand Up @@ -563,7 +564,6 @@ public static function buildQueryString($apiVersion, $action, $params, $excludeP
public static function buildLinks($action, $path, $totalResults, $queryParams, $nonDefaultParams, $excludeParams=[]) {
$apiVersion = $queryParams['v'];
$baseURI = Zotero_API::getBaseURI() . substr($path, 1);
$alternateBaseURI = Zotero_URI::getBaseWWWURI() . substr($path, 1);

$links = [];

Expand Down Expand Up @@ -630,7 +630,7 @@ public static function buildLinks($action, $path, $totalResults, $queryParams, $
}
}

$links['alternate'] = $alternateBaseURI;
$links['alternate'] = Zotero_URI::getBaseWWWURI() . substr($path, 1);

return $links;
}
Expand Down Expand Up @@ -812,7 +812,7 @@ private static function removePrivateParams($params) {


private static function getPrivateParams() {
$params = ['emptyFirst'];
$params = ['emptyFirst', 'publications'];
if (!Z_CONFIG::$TESTING_SITE) {
$params[] = 'uncached';
}
Expand Down
23 changes: 16 additions & 7 deletions model/Atom.inc.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,20 +50,29 @@ public static function createAtomFeed($action, $title, $url, $entries, $totalRes

$path = parse_url($url, PHP_URL_PATH);

// Generate canonical URI
$zoteroURI = Zotero_URI::getBaseURI() . substr($path, 1)
. Zotero_API::buildQueryString($queryParams['v'], $action, $nonDefaultParams, ['v']);
$baseURI = Zotero_API::getBaseURI() . substr($path, 1);
$xml->id = Zotero_URI::getBaseURI()
. substr($path, 1)
. Zotero_API::buildQueryString(
$queryParams['v'],
$action,
$nonDefaultParams,
['format', 'v']
);

// API version isn't included in URLs (as with the API key)
//
// It could alternatively be made a private parameter so that it didn't appear
// in the Link header either, but for now it's still there.
$excludeParams = ['v'];

$links = Zotero_API::buildLinks($action, $path, $totalResults, $queryParams, $nonDefaultParams, $excludeParams);

$xml->id = $zoteroURI;
$links = Zotero_API::buildLinks(
$action,
$path,
$totalResults,
$queryParams,
$nonDefaultParams,
$excludeParams
);

$link = $xml->addChild("link");
$link['rel'] = "self";
Expand Down
Loading

0 comments on commit 18a93e7

Please sign in to comment.