Add privs check to metadata generator. Update unit tests.
[bookreader.git] / BookReaderIA / datanode / BookReaderMeta.inc.php
1 <?
2 /*
3
4 Builds metadata about a book on the Internet Archive in json(p) format so that the book
5 can be accessed by other software including the Internet Archive BookReader.
6
7 Michael Ang <http://github.com/mangtronix>
8
9 Copyright (c) 2008-2010 Internet Archive. Software license AGPL version 3.
10
11 This file is part of BookReader.
12
13     BookReader is free software: you can redistribute it and/or modify
14     it under the terms of the GNU Affero General Public License as published by
15     the Free Software Foundation, either version 3 of the License, or
16     (at your option) any later version.
17
18     BookReader is distributed in the hope that it will be useful,
19     but WITHOUT ANY WARRANTY; without even the implied warranty of
20     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
21     GNU Affero General Public License for more details.
22
23     You should have received a copy of the GNU Affero General Public License
24     along with BookReader.  If not, see <http://www.gnu.org/licenses/>.
25 */
26
27 class BookReaderMeta {
28
29     // Builds metadata object (to be encoded as JSON)
30     function buildMetadata($id, $itemPath, $bookId, $server) {
31     
32         $response = array();
33         
34         if (! $bookId) {
35             $bookId = $id;
36         }
37         $subItemPath = $itemPath . '/' . $bookId;
38         
39         if ("" == $id) {
40             $this->BRFatal("No identifier specified!");
41         }
42         
43         if ("" == $itemPath) {
44             $this->BRFatal("No itemPath specified!");
45         }
46         
47         if ("" == $server) {
48             $this->BRFatal("No server specified!");
49         }
50         
51         if (!preg_match("|^/\d+/items/{$id}$|", $itemPath)) {
52             $this->BRFatal("Bad id!");
53         }
54         
55         // XXX check here that subitem is okay
56         
57         $filesDataFile = "$itemPath/${id}_files.xml";
58         
59         if (file_exists($filesDataFile)) {
60             $filesData = simplexml_load_file("$itemPath/${id}_files.xml");
61         } else {
62             $this->BRfatal("File metadata not found!");
63         }
64         
65         $imageStackInfo = $this->findImageStack($bookId, $filesData);
66         if ($imageStackInfo['imageFormat'] == 'unknown') {
67             $this->BRfatal('Couldn\'t find image stack');
68         }
69         
70         $imageFormat = $imageStackInfo['imageFormat'];
71         $archiveFormat = $imageStackInfo['archiveFormat'];
72         $imageStackFile = $itemPath . "/" . $imageStackInfo['imageStackFile'];
73         
74         if ("unknown" == $imageFormat) {
75           $this->BRfatal("Unknown image format");
76         }
77         
78         if ("unknown" == $archiveFormat) {
79           $this->BRfatal("Unknown archive format");
80         }
81         
82         
83         $scanDataFile = "${subItemPath}_scandata.xml";
84         $scanDataZip  = "$itemPath/scandata.zip";
85         if (file_exists($scanDataFile)) {
86             $this->checkPrivs($scanDataFile);
87             $scanData = simplexml_load_file($scanDataFile);
88         } else if (file_exists($scanDataZip)) {
89             $this->checkPrivs($scanDataZip);
90             $cmd  = 'unzip -p ' . escapeshellarg($scanDataZip) . ' scandata.xml';
91             exec($cmd, $output, $retval);
92             if ($retval != 0) {
93                 $this->BRFatal("Could not unzip ScanData!");
94             }
95             
96             $dump = join("\n", $output);
97             $scanData = simplexml_load_string($dump);
98         } else if (file_exists("$itemPath/scandata.xml")) {
99             // For e.g. Scribe v.0 books!
100             $scanData = simplexml_load_file("$itemPath/scandata.xml");
101         } else {
102             $this->BRFatal("ScanData file not found!");
103         }
104         
105         $metaDataFile = "$itemPath/{$id}_meta.xml";
106         if (!file_exists($metaDataFile)) {
107             $this->BRFatal("MetaData file not found!");
108         }
109         
110         
111         $metaData = simplexml_load_file($metaDataFile);
112         
113         /* Find pages by type */
114         $titleLeaf = '';
115         $coverLeafs = array();
116         foreach ($scanData->pageData->page as $page) {
117             if (("Title Page" == $page->pageType) || ("Title" == $page->pageType)) {
118                 if ('' == $titleLeaf) {
119                     // not already set
120                     $titleLeaf = "{$page['leafNum']}";
121                 }
122             }
123             
124             if (('Cover' == $page->pageType) || ('Cover Page' == $page->pageType)) {
125                 array_push($coverLeafs, $page['leafNum']);
126             }
127         }
128         
129         // These arrays map accessible page index numbers to width, height, scanned leaf numbers
130         // and page number strings (NB: these may not be unique)
131         $pageWidths = array();
132         $pageHeights = array();
133         $leafNums = array();
134         $i=0;
135         $totalHeight = 0;
136         foreach ($scanData->pageData->page as $page) {
137             if ($this->shouldAddPage($page)) {
138                 $pageWidths[$i] = intval($page->cropBox->w);
139                 $pageHeights[$i] = intval($page->cropBox->h);
140                 $totalHeight += intval($page->cropBox->h/4) + 10;
141                 $leafNums[$i] = intval($page['leafNum']);
142                 $pageNums[$i] = $page->pageNumber . '';
143                 $i++;
144             }
145         }
146                 
147         # Load some values from meta.xml
148         $pageProgression = 'lr'; // default
149         if ('' != $metaData->{'page-progression'}) {
150           $pageProgression = $metaData->{"page-progression"};
151         }
152         
153         // General metadata
154         $response['title'] = $metaData->title . ''; // $$$ renamed
155         $response['numPages'] = count($pageNums); // $$$ renamed    
156         if ('' != $titleLeaf) {
157             $response['titleLeaf'] = $titleLeaf; // $$$ change to titleIndex - do leaf mapping here
158             $titleIndex = $this->indexForLeaf($titleLeaf, $leafNums);
159             if ($titleIndex !== NULL) {
160                 $response['titleIndex'] = intval($titleIndex);
161             }
162         }
163         $response['url'] = "http://www.archive.org/details/$id";
164         $response['pageProgression'] = $pageProgression . '';
165         $response['pageWidths'] = $pageWidths;
166         $response['pageHeights'] = $pageHeights;
167         $response['pageNums'] = $pageNums;
168         
169         // Internet Archive specific
170         $response['itemId'] = $id; // $$$ renamed
171         $response['bookId'] = $bookId;  // $$$ renamed
172         $response['itemPath'] = $itemPath;
173         $response['zip'] = $imageStackFile;
174         $response['server'] = $server;
175         $response['imageFormat'] = $imageFormat;
176         $response['archiveFormat'] = $archiveFormat;
177         $response['leafNums'] = $leafNums;
178         $response['previewImage'] = $this->previewURL('preview', $response);
179         
180         // URL to title image
181         if ('' != $titleLeaf) {
182             $response['titleImage'] = $this->previewURL('title', $response);
183         }
184         
185         if (count($coverLeafs) > 0) {
186             $coverIndices = array();
187             $coverImages = array();
188             foreach ($coverLeafs as $key => $leafNum) {
189                 array_push($coverIndices, $this->indexForLeaf($leafNum, $leafNums));
190                 // $$$ TODO use preview API once it supports multiple covers
191                 array_push($coverImages, $this->imageUrl($leafNum, $response));
192             }
193             
194             $response['coverIndices'] = $coverIndices;
195             $response['coverImages'] = $coverImages;
196         }
197                 
198         return $response;
199     }
200     
201     function emitResponse($metadata) {
202         $callback = $_REQUEST['callback'];
203         
204         $contentType = 'application/json'; // default
205         if ($callback) {
206             if (! $this->isValidCallback($callback) ) {
207                 $this->BRfatal("Invalid callback");
208             }
209             $contentType = 'text/javascript'; // JSONP is not JSON
210         }
211         
212         header('Content-type: ' . $contentType . ';charset=UTF-8');
213         header('Access-Control-Allow-Origin: *'); // allow cross-origin requests
214         
215         if ($callback) {
216             print $callback . '( ';
217         }
218         print json_encode($metadata);
219         if ($callback) {
220             print ' );';
221         }
222     }
223     
224     function BRFatal($string) {
225         // $$$ TODO log error
226         throw new Exception("Metadata error: $string");
227         //echo "alert('$string');\n";
228         //die(-1);
229     }
230     
231     // Returns true if a page should be added based on it's information in
232     // the metadata
233     function shouldAddPage($page) {
234         // Return false only if the page is marked addToAccessFormats false.
235         // If there is no assertion we assume it should be added.
236         if (isset($page->addToAccessFormats)) {
237             if ("false" == strtolower(trim($page->addToAccessFormats))) {
238                 return false;
239             }
240         }
241         
242         return true;
243     }
244     
245     // Returns { 'imageFormat' => , 'archiveFormat' => '} given a sub-item prefix and loaded xml data
246     function findImageStack($subPrefix, $filesData) {
247     
248         // $$$ The order of the image formats determines which will be returned first
249         $imageFormats = array('JP2' => 'jp2', 'TIFF' => 'tif', 'JPEG' => 'jpg');
250         $archiveFormats = array('ZIP' => 'zip', 'Tar' => 'tar');
251         $imageGroup = implode('|', array_keys($imageFormats));
252         $archiveGroup = implode('|', array_keys($archiveFormats));
253         // $$$ Currently only return processed images
254         $imageStackRegex = "/Single Page (Processed) (${imageGroup}) (${archiveGroup})/";
255             
256         foreach ($filesData->file as $file) {        
257             if (strpos($file['name'], $subPrefix) === 0) { // subprefix matches beginning
258                 if (preg_match($imageStackRegex, $file->format, $matches)) {
259                 
260                     // Make sure we have a regular image stack
261                     $imageFormat = $imageFormats[$matches[2]];
262                     if (strpos($file['name'], $subPrefix . '_' . $imageFormat) === 0) {            
263                         return array('imageFormat' => $imageFormat,
264                                      'archiveFormat' => $archiveFormats[$matches[3]],
265                                      'imageStackFile' => $file['name']);
266                     }
267                 }
268             }
269         }
270         
271         return array('imageFormat' => 'unknown', 'archiveFormat' => 'unknown', 'imageStackFile' => 'unknown');    
272     }
273     
274     function isValidCallback($identifier) {
275         $pattern = '/^[a-zA-Z_$][a-zA-Z0-9_$]*$/';
276         return preg_match($pattern, $identifier) == 1;
277     }
278     
279     function indexForLeaf($leafNum, $leafNums) {
280         $key = array_search($leafNum, $leafNums);
281         if ($key === FALSE) {
282             return NULL;
283         } else {
284             return $key;
285         }
286     }
287     
288     function leafForIndex($index, $leafNums) {
289         return $leafNums[$index]; // $$$ todo change to instance variables
290     }
291     
292     function imageURL($leafNum, $metadata, $scale, $rotate) {
293         // "Under the hood", non-public, dynamically changing (achtung!) image URLs currently look like:
294         // http://{server}/BookReader/BookReaderImages.php?zip={zipPath}&file={filePath}&scale={scale}&rotate={rotate}
295         // e.g. http://ia311213.us.archive.org/BookReader/BookReaderImages.php?zip=/0/items/coloritsapplicat00andriala/coloritsapplicat00andriala_jp2.zip&file=coloritsapplicat00andriala_jp2/coloritsapplicat00andriala_0009.jp2&scale=8&rotate=0
296         
297     
298         $filePath = $this->imageFilePath($leafNum, $metadata['bookId'], $metadata['imageFormat']);
299         $url = 'http://' . $metadata['server'] . '/BookReader/BookReaderImages.php?zip=' . $metadata['zip'] . '&file=' . $filePath;
300         
301         if (defined($scale)) {
302             $url .= '&scale=' . $scale;
303         }
304         if (defined($rotate)) {
305             $url .= '&rotate=' . $rotate;
306         }
307         
308         return $url;
309     }
310     
311     // $$$ move inside BookReaderPreview
312     function previewURL($page, $metadata) {
313         $query = array(
314             'id' => $metadata['itemId'],
315             'bookId' => $metadata['bookId'],
316             'itemPath' => $metadata['itemPath'],
317             'server' => $metadata['server'],
318             'page' => $page,
319         );
320         
321         return 'http://' . $metadata['server'] . '/BookReader/BookReaderPreview.php?' . http_build_query($query, '', '&');
322     }
323     
324     function imageFilePath($leafNum, $bookId, $format) {
325         return sprintf("%s_%s/%s_%04d.%s", $bookId, $format, $bookId, intval($leafNum), $format);
326     }
327     
328     function processRequest($requestEnv) {
329         $id = $requestEnv['itemId']; // $$$ renamed
330         $itemPath = $requestEnv['itemPath'];
331         $bookId = $requestEnv['bookId']; // $$$ renamed
332         $server = $requestEnv['server'];
333         
334         // Check if we're on a dev vhost and point to JSIA in the user's public_html on the datanode
335         // $$$ TODO consolidate this logic
336         if (strpos($_SERVER["REQUEST_URI"], "/~mang") === 0) { // Serving out of home dir
337             $server .= ':80/~mang';
338         } else if (strpos($_SERVER["REQUEST_URI"], "/~testflip") === 0) { // Serving out of home dir
339             $server .= ':80/~testflip';
340         }
341         
342         $this->emitResponse( $this->buildMetadata($id, $itemPath, $bookId, $server) );
343     }
344     
345     function checkPrivs($filename) {
346         if (!is_readable($filename)) {
347             header('HTTP/1.1 403 Forbidden');
348             exit(0);
349         }
350     }
351
352 }
353
354 ?>