1<?php
2
3/**
4 * Driver class for Net_CDDB_Client/Server, query the local filesystem in FreeDB database dump format
5 *
6 * @see Net_CDDB_Client
7 * @see Net_CDDB_Server
8 * @see Net_CDDB_CDDBP
9 * @see Net_CDDB_HTTP
10 *
11 * @author Keith Palmer <Keith@UglySlug.com>
12 * @category Net
13 * @package Net_CDDB
14 * @license http://www.opensource.org/licenses/bsd-license.php BSD License
15 */
16
17/**
18 * Require the utilities class, needed for calculating disc ids
19 */
20require_once 'Net/CDDB/Utilities.php';
21
22/**
23 * We need the constants from the Net_CDDB base file
24 */
25require_once 'Net/CDDB.php';
26
27/**
28 * All protocols extend the Net_CDDB_Protocol base class
29 */
30require_once 'Net/CDDB/Protocol.php';
31
32/**
33 * Connection protocol for querying local filesystem in FreeDB database dump format
34 *
35 * The FreeDB.org project provides database dumps of the entire CDDB/FreeDB
36 * database. The database downloads are usually provided in the following
37 * formats:
38 * 	- .tar.bz2 (tarred and bzipped)
39 * 	- .tar.7z (tarred and 7-zipped)
40 * 	- .torrent (Bittorrent download)
41 *
42 * In order to use the database dumps (and thus this protocol) you need to
43 * download and extract one of the database dumps. The DSN provided to the
44 * {@link Net_CDDB_Client} should point to the directory of the database dumps:
45 * <code>
46 * 	$proto = 'filesystem:///usr/home/keith/FreeDB Database/';
47 * 	$client = new Net_CDDB_Client($proto, 'cddiscid:///dev/acd0');
48 * </code>
49 *
50 * You probably don't want to use this protocol unless you have a very specific
51 * need for it. This protocol doesn't support a large chunk of the CDDB protocol
52 * and chances are your local CDDB data won't be as up-to-date as the FreeDB.org
53 * database servers. On the other hand, if you're without a network connection
54 * or need faster access to CDDB data than slow socket connections can provide,
55 * this protocol might work well for you.
56 *
57 * One thing you should watch out for: The 'stat' command
58 * {@link Net_CDDB_Client::statistics()} can be extremelly slow if you set the
59 * 'use_stat_file' option to 'false' or don't provide a valid 'stat.db' file to
60 * the 'stat_file' option. This is because it needs to count the number of files
61 * in each of the CDDB category directories (i.e.: count about 1.8 to 2-million
62 * files). These two parameters default to:
63 * 	- use_stat_file = true
64 * 	- stat_file = 'stat.db'
65 *
66 * When you first start using this protocol, you should create a 'stat.db' file
67 * in the CDDB database dump directory and chmod it so that its read/write by
68 * whatever user will be using the {@link Net_CDDB_Client} class. Run the 'stat'
69 * command immediately to build the 'stat.db' file so that the next 'stat'
70 * request doesn't need to count all of the files next time (it just reads the
71 * 'stat.db' file instead counters instead).
72 *
73 * @todo Finish implementing all of the CDDB commands
74 * @todo Sites file
75 * @todo Support more than one protocol level (and the proto command...?)
76 *
77 * @package Net_CDDB
78 */
79class Net_CDDB_Protocol_Filesystem extends Net_CDDB_Protocol
80{
81	/**
82	 * The directory the FreeDB database dump is stored in
83	 *
84	 * @var string
85	 * @access protected
86	 */
87	var $_dir;
88
89	/**
90	 * Whether or not to use a 'Message of the Day' file for the 'motd' command
91	 *
92	 * @var boolean
93	 * @access protected
94	 */
95	var $_use_motd_file;
96
97	/**
98	 * The filename of the message of the day message (i.e.: motd.txt, which should be located in the FreeDB database dump directory)
99	 *
100	 * @var string
101	 * @access protected
102	 */
103	var $_motd_file;
104
105	/**
106	 * Whether or not to use a cached statistics file for the 'stat' command
107	 *
108	 * If you don't use a the cached statistics file, then the number of files
109	 * in each of the CDDB category directories needs to be counted each time
110	 * you issue a 'stat' command. For a full dump of the database, this can
111	 * take 10 or 20 *minutes*.
112	 *
113	 * @var boolean
114	 * @access protected
115	 */
116	var $_use_stat_file;
117
118	/**
119	 * The name of the statistics file (defaults to 'stat.db' in the CDDB database directory)
120	 *
121	 * @var string
122	 * @access protected
123	 */
124	var $_stat_file;
125
126	/**
127	 *
128	 * @todo Implement...
129	 * @var boolean
130	 * @access protected
131	 */
132	var $_use_sites_file;
133
134	/**
135	 * The filename of a file containing mirror site entries (i.e.: sites.txt)
136	 *
137	 * @var string
138	 * @access protected
139	 */
140	var $_sites_file;
141
142	/**
143	 * String buffer containing protocol data
144	 *
145	 * @var string
146	 * @access protected
147	 */
148	var $_buffer;
149
150	/**
151	 * Integer buffer containing protocol status information
152	 *
153	 * @var integer
154	 * @access protected
155	 */
156	var $_status_buffer;
157
158	/**
159	 * Constructor (PHP v4.x)
160	 *
161	 * @access public
162	 *
163	 * @see Net_CDDB_Protocol_Filesystem::__construct()
164	 */
165	function Net_CDDB_Protocol_Filesystem($dsn = 'filesystem:///FreeDB', $options)
166	{
167		$this->__construct($dsn, $options);
168	}
169
170	/**
171	 * Constructor (PHP v5.x)
172	 *
173	 * @access public
174	 *
175	 * @param string $dsn
176	 * @param array $options
177	 */
178	function __construct($dsn = 'filesystem:///FreeDB', $options)
179	{
180		$dsn_params = $this->_parseDsn($dsn);
181
182		// Directory where the FreeDB database is stored
183		$this->_dir = $dsn_params['path'];
184
185		// Default parameter values
186		$defaults = array(
187			'use_motd_file'		=> true,
188			'motd_file'			=> 'motd.txt',
189			'use_stat_file'		=> true,
190			'stat_file'			=> 'stat.db',
191			'use_sites_file'	=> false,
192			'sites_file' 		=> 'sites.db',
193			);
194
195		$defaults = array_merge($defaults, $options);
196
197		$this->_use_motd_file = (boolean) $defaults['use_motd_file'];
198		$this->_motd_file = $defaults['motd_file'];
199
200		$this->_use_stat_file = (boolean) $defaults['use_stat_file'];
201		$this->_stat_file = $defaults['stat_file'];
202
203		$this->_use_sites_file = (boolean) $defaults['use_sites_file'];
204		$this->_sites_file = $defaults['sites_file'];
205
206		// Initialize buffer and status buffer
207		$this->_buffer = '';
208		$this->_status_buffer = NET_CDDB_RESPONSE_ERROR_SYNTAX;
209	}
210
211	/**
212	 * Pretend to connect to a remote server while we actually just check if the database directory is readable
213	 *
214	 * Function will return false if either the filesystem directory does not
215	 * exist or if the directory is not readable.
216	 *
217	 * @access public
218	 *
219	 * @return boolean
220	 */
221	function connect()
222	{
223		if (is_dir($this -> _dir) and is_readable($this -> _dir)) {
224			return true;
225		} else {
226			return false;
227		}
228	}
229
230	/**
231	 * Pretend to check if we are connected to a server
232	 *
233	 * @access public
234	 *
235	 * @return boolean
236	 */
237	function connected()
238	{
239		return $this->connect();
240	}
241
242	/**
243	 * Send a query to the Net_CDDB_Protocol_Filesystem object, the query will be parsed and the buffers will be filled with the response
244	 *
245	 * Not all CDDB commands are implemented for this protocol, some don't make
246	 * sense in the context of a filesystem protocol and some just havn't been
247	 * implemented yet.
248	 *
249	 * This method basically parses and dispatches the query to other protected
250	 * methods of the class for further processing.
251	 *
252	 * @access public
253	 * @see Net_CDDB_Protocol_Filesystem::recieve()
254	 *
255	 * @param string $query
256	 * @return void
257	 */
258	function send($query)
259	{
260		// First, break the query up into two parts, $cmd and $query
261		//	- $cmd holds the base command (i.e.: cddb read)
262		//	- $query holds the command parameters (i.e.: rock 7709a259)
263		$cmd = trim($query);
264
265		if (current($explode = explode(' ', $query)) == 'cddb') {
266			$cmd = current($explode) . ' ' . next($explode);
267		} else {
268			$cmd = current($explode);
269		}
270
271		$query = trim(substr($query, strlen($cmd)));
272
273		// Initial buffers
274		$this->_buffer = '';
275		$this->_status_buffer = NET_CDDB_RESPONSE_ERROR_SYNTAX; // 500 error by default
276
277		$impl_cmds = array(
278			'cddb read'		=> '_cddbRead',
279			'cddb lscat'	=> '_cddbLscat',
280			'cddb query'	=> '_cddbQuery',
281			'discid'		=> '_discid',
282			'ver'			=> '_ver',
283			'cddb hello'	=> '_cddbHello',
284			//'help'		=> '_help',
285			'motd'			=> '_motd',
286			//'proto'		=> '_proto',
287			'quit'			=> '_quit',
288			//'sites'		=> '_sites',
289			'stat'			=> '_stat',
290			//'whom'		=> '_whom',
291			);
292
293		if (isset($impl_cmds[$cmd]) and method_exists($this, $impl_cmds[$cmd])) {
294			$this->{$impl_cmds[$cmd]}($cmd, $query);
295			return;
296		} else {
297			return;
298		}
299	}
300
301	/**
302	 * Handle a CDDB 'discid' (Calculate a Disc ID) query and fill the buffer with a response
303	 *
304	 * @access protected
305	 *
306	 * @param string $cmd
307	 * @param string $query
308	 * @return void
309	 */
310	function _discid($cmd, $query)
311	{
312		$track_offsets = explode(' ', $query);
313		array_pop($track_offsets);
314		array_shift($track_offsets);
315
316		$this->_buffer = 'Disc ID is ' . Net_CDDB_Utilities::calculateDiscId($track_offsets);
317		$this->_status_buffer = NET_CDDB_RESPONSE_OK;
318	}
319
320	/**
321	 * Handle a 'cddb query' (Find possible disc matches by disc id) and fill the buffer with a response
322	 *
323	 * @access protected
324	 *
325	 * @param string $cmd
326	 * @param string $query
327	 * @return void
328	 */
329	function _cddbQuery($cmd, $query)
330	{
331		// 200	Found exact match
332		// 211	Found inexact matches, list follows (until terminating marker)
333		// 202	No match found
334
335		/*
336		211 Found inexact matches, list follows (until terminating `.')
337		reggae d50dd30f Various / Ska Island
338		misc d50dd30f Various / Ska Island
339		.
340
341		200 jazz 820e770a Joshua Redman / Wish 1993
342		 */
343
344		$matches = 0;
345
346		if ($dh = opendir($this->_dir)) {
347
348			while ($dir = readdir($dh)) {
349
350				$file = current(explode(' ', $query));
351				$path = $this->_dir . '/' . $dir . '/' . $file;
352
353				if (is_file($path)) {
354					$this->_buffer .= $dir . ' ' . $file . ' ' . Net_CDDB_Utilities::parseFieldFromRecord(file_get_contents($path), NET_CDDB_FIELD_DISC_TITLE) . "\n";
355					$matches++;
356				}
357			}
358
359			if ($matches > 1) {
360				$this->_status_buffer = NET_CDDB_RESPONSE_OK_INEXACT; // 211
361			} else if ($matches == 1) {
362				$this->_status_buffer = NET_CDDB_RESPONSE_OK; // 200 OK status
363			} else {
364				$this->_status_buffer = NET_CDDB_RESPONSE_OK_NOMATCH; // 202
365			}
366
367		} else {
368			$this->_status_buffer = NET_CDDB_RESPONSE_SERVER_CORRUPT; // 403, Couldn't open directory...?
369		}
370	}
371
372	/**
373	 * Handle a 'cddb lscat' (Display disc categories) query and fill the buffer with a response
374	 *
375	 * @access protected
376	 *
377	 * @param string $cmd
378	 * @param string $query
379	 * @return void
380	 */
381	function _cddbLscat($cmd, $query)
382	{
383		if ($dh = opendir($this->_dir)) {
384
385			while ($dir = readdir($dh)) {
386				if (is_dir($this -> _dir . "/" . $dir) and $dir != "." and $dir != "..") {
387					$this -> _buffer = $this -> _buffer . $dir . "\n";
388				}
389			}
390
391			$this->_buffer = trim($this->_buffer);
392
393			$this->_status_buffer = NET_CDDB_RESPONSE_OK_FOLLOWS; // OK status
394
395		} else {
396			$this->_status_buffer = NET_CDDB_RESPONSE_SERVER_CORRUPT; // Couldn't open directory...?
397		}
398	}
399
400	/**
401	 * Handle a 'cddb read ...' (read a complete disc entry) query and fill the buffer with a response
402	 *
403	 * @access protected
404	 *
405	 * @param string $cmd
406	 * @param string $query
407	 * @return void
408	 */
409	function _cddbRead($cmd, $query)
410	{
411		/*
412		210	OK, CDDB database entry follows (until terminating marker)
413		401	Specified CDDB entry not found.
414		402	Server error.
415		403	Database entry is corrupt.
416		409	No handshake.
417		 */
418
419		if (count($parts = explode(' ', $query)) == 2 and is_dir($this->_dir . '/' . $parts[0])) {
420
421			$path = $this->_dir . '/' . $parts[0] . '/' . $parts[1];
422			if (file_exists($path) and $contents = file_get_contents($path)) {
423				$this->_status_buffer = NET_CDDB_RESPONSE_OK_FOLLOWS; // OK, record follows
424				$this->_buffer = $contents;
425			} else {
426				$this->_status_buffer = NET_CDDB_RESPONSE_SERVER_UNAVAIL; // Entry does not exist
427			}
428
429		} else {
430			$this->_status_buffer = NET_CDDB_RESPONSE_SERVER_ERROR; // Server error, bad parameters
431		}
432	}
433
434	/**
435	 * Handle a 'motd' (Message Of The Day) query and fill the buffers with the repsonse
436	 *
437	 * @access protected
438	 *
439	 * @param string $cmd
440	 * @param string $query
441	 * @return void
442	 */
443	function _motd($cmd, $query)
444	{
445		/*
446		210	Last modified: 05/31/96 06:31:14 MOTD follows (until terminating marker)
447		401	No message of the day available
448		*/
449
450		$path = $this->_dir . '/' . $this->_motd_file;
451		if ($this->_use_motd_file and file_exists($path) and $contents = file_get_contents($path)) {
452			$this->_buffer = $contents;
453			$this->_status_buffer = NET_CDDB_RESPONSE_OK_FOLLOWS;
454		} else {
455			$this->_status_buffer = NET_CDDB_RESPONSE_SERVER_UNAVAIL;
456		}
457	}
458
459	/**
460	 * Handle a 'ver' (get CDDB server version) command and fill the buffers with a response
461	 *
462	 * @access protected
463	 *
464	 * @param string $cmd
465	 * @param string $query
466	 * @return void
467	 */
468	function _ver($cmd, $query)
469	{
470		$this->_status_buffer = NET_CDDB_RESPONSE_OK;
471		$this->_buffer = 'PHP/PEAR/' . get_class($this) . ' v' . NET_CDDB_VERSION . ' Copyright (c) 2006-' . date('Y') . ' Keith Palmer Jr.';
472	}
473
474	/**
475	 * Count the number of database entries in a given category (directory)
476	 *
477	 * @access protected
478	 *
479	 * @param string $category
480	 * @return integer
481	 */
482	function _countDatabaseEntries($category)
483	{
484		$count = 0;
485		if ($dh = opendir($this->_dir . '/' . $category)) {
486			while (false !== ($file = readdir($dh))) {
487				$count++;
488			}
489			closedir($dh);
490		}
491
492		return $count - 2; // Two too many because of '.' and '..' entries
493	}
494
495	/**
496	 * Write the 'stat' file
497	 *
498	 * @access protected
499	 *
500	 * @param array $arr
501	 * @return boolean
502	 */
503	function _writeStatFile($arr)
504	{
505		$bytes = 0;
506		$fp = fopen($this->_dir . '/' . $this->_stat_file, 'w');
507		foreach ($arr as $key => $value) {
508			$bytes = fwrite($fp, $key . '=' . (int) $value . "\r\n");
509		}
510		fclose($fp);
511		return $bytes > 0;
512	}
513
514	/**
515	 * Read the 'stat' file to determine how many database entries are in each CDDB category
516	 *
517	 * This method performs a check to make sure every CDDB category has a
518	 * corresponding, valid entry in the 'stat' file. If you want to clear the
519	 * 'stat' file, just truncate it to 0 characters at the command prompt.
520	 *
521	 * @access protected
522	 *
523	 * @return array Returns an array with CDDB categories as keys and the number of entries in the category as values
524	 */
525	function _readStatFile()
526	{
527		if ($dh = opendir($this->_dir) and is_file($this->_dir . '/' . $this->_stat_file)) {
528			$defaults = array();
529
530			// Get a list of all of the CDDB categories (directories)
531			while (false !== ($dir = readdir($dh))) {
532				if (is_dir($this->_dir . '/' . $dir)) {
533					$defaults[$dir] = -1;
534				}
535			}
536
537			$stats = array_merge($defaults, @parse_ini_file($this->_dir . '/' . $this->_stat_file));
538
539			// Sanity check, make sure that counts from stat file are correct
540			foreach ($stats as $key => $value) {
541				if ($value < 0) {
542					return false;
543				}
544			}
545
546			return $stats;
547
548		} else {
549			return false;
550		}
551	}
552
553	/**
554	 * @todo Implement this
555	 */
556	function _readSitesFile()
557	{
558		return false;
559	}
560
561	/**
562	 * Handle a cddb 'stat' (get server statistics) and fill the buffers with the response
563	 *
564	 * @todo Possibly have an option to not use a 'stat' file *and* not count the entries in the directory
565	 *
566	 * @access protected
567	 * @uses Net_CDDB_Protocol_Filesystem::_countDatabaseEntries()
568	 * @uses Net_CDDB_Protocol_Filesystem::_writeStatFile()
569	 * @uses Net_CDDB_Protocol_Filesystem::_readStatFile()
570	 *
571	 * @param string $cmd
572	 * @param string $query
573	 * @return void
574	 */
575	function _stat($cmd, $query)
576	{
577		$entry_counts = array();
578		$total = 0;
579
580		if ($this->_use_stat_file and $entry_counts = $this->_readStatFile()) {
581			;
582		} else {
583
584			// Keep on counting even if the user aborts the script/connection, just so we can write the 'stat' file
585			if ($this->_use_stat_file) {
586				ignore_user_abort(true);
587			}
588
589			if ($dh = opendir($this->_dir)) {
590				while (false !== ($dir = readdir($dh))) {
591					if (is_dir($this->_dir . '/' . $dir) and $dir != '.' and $dir != '..') {
592						$entry_counts[$dir] = $this->_countDatabaseEntries($dir);
593					}
594				}
595			}
596
597			if ($this->_use_stat_file) {
598				$this->_writeStatFile($entry_counts);
599			}
600		}
601
602		$total = array_sum($entry_counts);
603
604		$str = '';
605		$str .= 'Server status:' . "\n";
606		$str .= '    current proto: ' . NET_CDDB_PROTO_LEVEL . "\n";
607		$str .= '    max proto: ' . NET_CDDB_PROTO_LEVEL . "\n";
608		$str .= '    interface: Filesystem' . "\n";
609		$str .= '    gets: no' . "\n";
610		$str .= '    puts: no' . "\n";
611		$str .= '    updates: no' . "\n";
612		$str .= '    posting: no' . "\n";
613		$str .= '    validation: accepted' . "\n";
614		$str .= '    quotes: no' . "\n";
615		$str .= '    strip ext: no' . "\n";
616		$str .= '    secure: yes' . "\n";
617		$str .= '    current users: 1' . "\n";
618		$str .= '    max users: 100' . "\n";
619		$str .= 'Database entries: ' . $total . "\n";
620		$str .= 'Database entries by category:' . "\n";
621
622		foreach ($entry_counts as $category => $count) {
623			$str .= '    ' . $category . ': ' . $count . "\n";
624		}
625
626		$this->_buffer = $str;
627		$this->_status_buffer = NET_CDDB_RESPONSE_OK_FOLLOWS;
628	}
629
630	/**
631	 * Read data from the protocol buffer
632	 *
633	 * @access public
634	 *
635	 * @return string
636	 */
637	function recieve()
638	{
639		return $this->_buffer;
640	}
641
642	/**
643	 * Read the status of the last executed command from the protocol buffer
644	 *
645	 * @access public
646	 *
647	 * @return int
648	 */
649	function status()
650	{
651		return $this->_status_buffer;
652	}
653
654	/**
655	 * Pretend to disconnect (doesn't actually do anything because you don't need to disconnect from the local filesystem)
656	 *
657	 * @access public
658	 *
659	 * @return void
660	 */
661	function disconnect()
662	{
663		return;
664	}
665
666	/**
667	 * Report this class as *not* accessing remote resources for protocol output
668	 *
669	 * @see Net_CDDB_Protocol::remote()
670	 * @access public
671	 *
672	 * @return boolean
673	 */
674	function remote()
675	{
676		return false;
677	}
678}
679
680?>