Better preview handling. Start of image options parsing. Test harness.
[bookreader.git] / BookReaderIA / inc / BookReader.inc
1 <?
2
3 class BookReader
4 {
5
6   // Operators recognized in BookReader download URLs
7   public static $downloadOperators = array('page');
8
9   // Returns true if can display the book in item with a given prefix (typically the item identifier)
10   public static function canDisplay($item, $prefix, $checkOldScandata = false)
11   {
12     
13     // A "book" is an image stack and scandata.
14     // 1. Old items may have scandata.xml or scandata.zip and itemid_{imageformat}.{zip,tar}
15     // 2. Newer items may have multiple {arbitraryname}_scandata.xml and {arbitraryname}_{imageformat}.{zip,tar}
16         
17     $foundScandata = false;
18     $foundImageStack = false;
19     
20     $targetScandata = $prefix . "_scandata.xml";
21         
22     // $$$ TODO add support for jpg and tar stacks
23     // https://bugs.edge.launchpad.net/gnubook/+bug/323003
24     // https://bugs.edge.launchpad.net/gnubook/+bug/385397
25     $imageFormatRegex = '@' . preg_quote($prefix, '@') . '_(jp2|tif|jpg)\.(zip|tar)$@';
26     
27     $baseLength = strlen($item->metadataGrabber->mainDir . '/');
28     foreach ($item->getFiles() as $location => $fileInfo) {
29         $filename = substr($location, $baseLength);
30         
31         if ($checkOldScandata) {
32             if ($filename == 'scandata.xml' || $filename == 'scandata.zip') {
33                 $foundScandata = $filename;
34             }
35         }
36         
37         if ($filename == $targetScandata) {
38             $foundScandata = $filename;
39         }
40         
41         if (preg_match($imageFormatRegex, $filename)) {
42             $foundImageStack = $filename;
43         }
44     }
45     
46     if ($foundScandata && $foundImageStack) {
47         return true;
48     }
49     
50     return false;
51   }
52   
53   // Finds the prefix to use for the book given the part of the URL trailing after /stream/
54   public static function findPrefix($urlPortion)
55   {
56     if (!preg_match('#[^/&?]+#', $urlPortion, $matches)) {
57         // URL portion was empty or started with /, &, or ? -- no item identifier
58         return false;
59     }
60     
61     $prefix = $matches[0]; // item identifier
62     
63     // $$$ Currently swallows the rest of the URL.
64     //     If we want to support e.g. /stream/itemid/subdir/prefix/page/23 will need to adjust.
65     if (preg_match('#[^/&?]+/([^&?]+)#', $urlPortion, $matches)) {
66         // Match is everything after item identifier and slash, up to end or ? or &
67         // e.g. itemid/{match/these/parts}?foo=bar
68         $prefix = $matches[1]; // sub prefix -- 
69     }
70     
71     return $prefix;
72   }
73
74   // $$$ would be cleaner to use different templates instead of the uiMode param
75   // 
76   // @param subprefix Optional prefix to display a book inside an item (e.g. if does not match identifier)
77   public static function draw($server, $mainDir, $identifier, $subPrefix, $title,
78                               $coverLeaf=null, $titleStart='Internet Archive', $uiMode='full')
79   {
80     // Set title to default if not set
81     if (!$title) {
82         $title = 'BookReader';
83     }
84     
85     $id = $identifier;
86     
87     // manually update with Launchpad version number at each checkin so that browsers
88     // do not use old cached version
89     // see https://bugs.launchpad.net/gnubook/+bug/330748
90     $version = "r26a";
91     
92     if (BookReader::getDevHost($server)) {
93         // on dev host - add time to force reload
94         $version .= '_' . time();
95     }
96     
97     if ("" == $id) {
98         echo "No identifier specified!";
99         die(-1);
100     }
101     
102     $metaURL = BookReader::jsMetadataURL($server, $identifier, $mainDir, $subPrefix);
103     
104 ?>
105 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
106 <html>
107 <head>
108     <meta name="viewport" content="width=device-width, maximum-scale=1.0" />
109     <meta name="apple-mobile-web-app-capable" content="yes" />
110     <title><? echo $title; ?></title>
111 <!--[if lte IE 6]>
112     <meta http-equiv="refresh" content="2; URL=/bookreader/browserunsupported.php?id=<? echo($id); ?>">
113 <![endif]-->
114     <link rel="stylesheet" type="text/css" href="/bookreader/BookReader.css?v=<? echo($version); ?>">
115 <? if ($uiMode == "embed") { ?>
116     <link rel="stylesheet" type="text/css" href="/bookreader/BookReaderEmbed.css?v=<? echo($version); ?>">
117 <? } elseif ($uiMode == "touch") { ?>
118     <link rel="stylesheet" type="text/css" href="/bookreader/touch/BookReaderTouch.css?v=<? echo($version); ?>">
119 <? } /* uiMode */ ?>
120     <script src="/includes/jquery-1.4.2.min.js" type="text/javascript"></script>
121     <script type="text/javascript" src="/bookreader/jquery-ui-1.8.1.custom.min.js?v=<? echo($version); ?>"></script>
122     <script type="text/javascript" src="/bookreader/dragscrollable.js?v=<? echo($version); ?>"></script>
123     <script type="text/javascript" src="/bookreader/BookReader.js?v=<? echo($version); ?>"></script>
124 </head>
125 <body style="background-color: #FFFFFF;">
126
127 <? if ($uiMode == 'full') { ?>
128 <div id="BookReader" style="left:10px; right:200px; top:10px; bottom:2em;">Internet Archive BookReader <noscript>requires JavaScript to be enabled.</noscript></div>
129 <? } else { ?>
130 <div id="BookReader" style="left:0; right:0; top:0; bottom:0; border:0">Internet Archive Bookreader <noscript>requires JavaScript to be enabled.</noscript></div>
131 <? } /* uiMode*/ ?>
132
133 <script type="text/javascript">
134   // Set some config variables -- $$$ NB: Config object format has not been finalized
135   var brConfig = {};
136 <? if ($uiMode == 'embed') { ?>
137   brConfig["mode"] = 1;
138   brConfig["reduce"] = 8;
139   brConfig["ui"] = "embed";
140 <? } else { ?>
141   brConfig["mode"] = 2;
142 <? } ?>
143 </script>
144 <!-- The script included below is dynamically generated JavaScript that includes the book metadata and page image access functions -->
145 <script type="text/javascript" src="<? echo($metaURL); ?>"></script>
146
147 <? if ($uiMode == 'full') { ?>
148 <div id="BookReaderSearch" style="width:190px; right:0px; top:10px; bottom:2em;">
149     <form action='javascript:' onsubmit="br.search($('#BookReaderSearchBox').val());">
150         <p style="display: inline">
151             <input id="BookReaderSearchBox" type="text" size="20" value="search..." onfocus="if('search...'==this.value)this.value='';" /><input type="submit" value="go" />
152         </p>
153     </form>
154     <div id="BookReaderSearchResults">
155         Search results
156     </div>
157 </div>
158
159
160 <div id="BRfooter">
161     <div class="BRlogotype">
162         <a href="http://archive.org/" class="BRblack">Internet Archive</a>
163     </div>
164     <div class="BRnavlinks">
165         <!-- <a class="BRblack" href="http://openlibrary.org/dev/docs/bookreader">About the Bookreader</a> | -->
166         <a class="BRblack" href="http://www.archive.org/about/faqs.php#Report_Item">Content Problems</a> |
167         <a class="BRblack" href="https://bugs.launchpad.net/bookreader/+filebug">Report Bugs</a> |
168         <a class="BRblack" href="http://www.archive.org/details/texts">Texts Collection</a> |
169         <a class="BRblack" href="http://www.archive.org/about/contact.php">Contact Us</a>
170     </div>
171 </div>
172 <? } /* uiMode */ ?>
173
174 <script type="text/javascript">
175     // $$$ hack to workaround sizing bug when starting in two-up mode
176     $(document).ready(function() {
177         $(window).trigger('resize');
178     });
179 </script>
180   <?
181     exit;
182   }
183   
184   // Returns the user part of dev host from URL, or null
185   public static function getDevHost($server)
186   {
187       if (preg_match("/^www-(\w+)/", $_SERVER["SERVER_NAME"], $match)) {
188         return $match[1];
189       }
190       
191       return null;
192   }
193
194   
195   public static function serverBaseURL($server)
196   {
197       // Check if we're on a dev vhost and point to JSIA in the user's public_html
198       // on the datanode
199       // $$$ the remapping isn't totally automatic yet and requires user to
200       //     ln -s ~/petabox/www/datanode/BookReader ~/public_html/BookReader
201       //     so we enable it only for known hosts
202       $devhost = BookReader::getDevHost($server);
203       $devhosts = array('mang', 'testflip', 'rkumar');
204       if (in_array($devhost, $devhosts)) {
205         $server = $server . "/~" . $devhost;
206       }
207       return $server;
208   }
209   
210   
211   public static function jsMetadataURL($server, $identifier, $mainDir, $subPrefix = '')
212   {
213     $serverBaseURL = BookReader::serverBaseURL($server);
214
215     $params = array( 'id' => $identifier, 'itemPath' => $mainDir, 'server' => $server );
216     if ($subPrefix) {
217         $params['subPrefix'] = $subPrefix;
218     }
219     
220     $keys = array_keys($params);
221     $lastParam = end($keys);
222     $url = "http://{$serverBaseURL}/BookReader/BookReaderJSIA.php?";
223     foreach($params as $param=>$value) {
224         $url .= $param . '=' . $value;
225         if ($param != $lastParam) {
226             $url .= '&';
227         }
228     }
229     
230     return $url;
231   }
232   
233   // Return the URL for the requested /download/$path, or null
234   public static function getURL($path, $item) {
235     // $path should look like {itemId}/{operator}/{filename}
236     // Other operators may be added
237     
238     $urlParts = BookReader::parsePath($path);
239     
240     // Check for non-handled cases
241     $required = array('identifier', 'operator', 'operand');
242     foreach ($required as $key) {
243         if (!array_key_exists($key, $urlParts)) {
244             return null;
245         }
246     }
247     
248     $identifier = $urlParts['identifier'];
249     $operator = $urlParts['operator'];
250     $filename = $urlParts['operand'];
251     $subPrefix = $urlParts['subPrefix'];
252     
253     $serverBaseURL = BookReader::serverBaseURL($item->getServer());
254     
255     // Baseline query params
256     $query = array(
257         'id' => $identifier,
258         'itemPath' => $item->getMainDir(),
259         'server' => $serverBaseURL
260     );
261     if ($subPrefix) {
262         $query['subPrefix'] = $subPrefix;
263     }
264     
265     switch ($operator) {
266         case 'page':
267             
268             // Look for old-style preview request
269             // $$$ currently ignoring file extension
270             if (preg_match('/^(.*)_(cover|title|preview)/', $filename, $matches) === 1) {
271                 // Serve preview image
272                 $page = $matches[2];
273                 $query['page'] = $page;
274                 return 'http://' . $serverBaseURL . '/BookReader/BookReaderPreview.php?' . http_build_query($query, '', '&');
275             }
276             
277             // Asking for a non-preview page
278             $query['page'] = $filename;
279             return 'http://' . $serverBaseURL . '/BookReader/BookReaderImages.php?' . http_build_query($query, '', '&');
280         
281         default:
282             // Unknown operator
283             return null;            
284     }
285       
286     return null; // was not handled
287   }
288   
289   public static function browserFromUserAgent($userAgent) {
290       $browserPatterns = array(
291           'ipad' => '/iPad/',
292           'iphone' => '/iPhone/', // Also cover iPod Touch
293           'android' => '/Android/',
294       );
295       
296       foreach ($browserPatterns as $browser => $pattern) {
297           if (preg_match($pattern, $userAgent)) {
298               return $browser;
299           }
300       }
301       return null;
302   }
303
304   
305   // $$$ Ideally we will not rely on user agent, but for the moment we do
306   public static function paramsFromUserAgent($userAgent) {
307       // $$$ using 'embed' here for devices with assumed small screens -- really should just use CSS3 media queries
308       $browserParams = array(
309           'ipad' => array( 'ui' => 'touch' ),
310           'iphone' => array( 'ui' => 'embed', 'mode' => '1up' ),
311           'android' => array( 'ui' => 'embed', 'mode' => '1up' ),
312       );
313   
314       $browser = BookReader::browserFromUserAgent($userAgent);
315       if ($browser) {
316           return $browserParams[$browser];
317       }
318       return array();
319   }
320   
321   public static function parsePath($path) {
322     // Parse the BookReader path and return the parts
323     // e.g. itemid/some/sub/dir/page/cover.jpg -> array( 'identifier' => 'itemid', 'subPrefix' => 'some/sub/dir',
324     //            'operator' => 'page', 'filename' => 'cover.jpg')
325     
326     $parts = array();
327     
328     // Pull off query
329     if (preg_match('#(.*?)(\?.*)#', $path, $matches) === 1) {
330         $parts['query'] = $matches[2];
331         $path = $matches[1];
332     }
333     
334     // Pull off identifier
335     if (preg_match('#[^/&?]+#', $path, $matches) === 0) {
336         // no match
337         return $parts;
338     }
339     $parts['identifier'] = $matches[0];
340     $path = substr($path, strlen($matches[0]));
341     
342     // Look for operators
343     // The sub-prefix can be arbitrary, so we match up until the first operator
344     $operators = '(' . join('|', self::$downloadOperators) . ')';
345     $pattern = '#/(?P<subPrefix>.*?)/(?P<operator>' . $operators . ')/(?P<operand>.*)#';
346     if (preg_match($pattern, $path, $matches) === 1) {
347         $parts['subPrefix'] = $matches['subPrefix'];
348         $parts['operator'] = $matches['operator'];
349         $parts['operand'] = $matches['operand'];
350     } else {
351         $parts['subPrefix'] = $path;
352     }
353     
354     return $parts;
355   }
356     
357 }
358
359 ?>