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?>