PageRenderTime 53ms CodeModel.GetById 14ms RepoModel.GetById 0ms app.codeStats 0ms

/php-pdb/php-pdb.inc

#
PHP | 1135 lines | 598 code | 184 blank | 353 comment | 121 complexity | 71b2dde192300c18c60f67ecf7cad688 MD5 | raw file
Possible License(s): LGPL-2.1
  1. <?PHP
  2. /* PHP-PDB -- PHP class to write PalmOS databases.
  3. *
  4. * Copyright (C) 2001 - PHP-PDB development team
  5. * Licensed under the GNU LGPL software license.
  6. * See the doc/LEGAL file for more information
  7. * See http://php-pdb.sourceforge.net/ for more information about the library
  8. *
  9. *
  10. * As a note, storing all of the information as hexadecimal kinda sucks,
  11. * but it is tough to store and properly manipulate a binary string in
  12. * PHP. We double the size of the data but decrease the difficulty level
  13. * immensely.
  14. */
  15. /*
  16. * Define constants
  17. */
  18. // Sizes
  19. define('PDB_HEADER_SIZE', 72); // Size of the database header
  20. define('PDB_INDEX_HEADER_SIZE', 6); // Size of the record index header
  21. define('PDB_RECORD_HEADER_SIZE', 8); // Size of the record index entry
  22. define('PDB_RESOURCE_SIZE', 10); // Size of the resource index entry
  23. define('PDB_EPOCH_1904', 2082844800); // Difference between Palm's time and Unix
  24. // Attribute Flags
  25. define('PDB_ATTRIB_RESOURCE', 0x01);
  26. define('PDB_ATTRIB_READ_ONLY', 0x02);
  27. define('PDB_ATTRIB_APPINFO_DIRTY', 0x04);
  28. define('PDB_ATTRIB_BACKUP', 0x08);
  29. define('PDB_ATTRIB_OK_NEWER', 0x10);
  30. define('PDB_ATTRIB_RESET', 0x20);
  31. define('PDB_ATTRIB_OPEN', 0x40);
  32. // Where are 0x80 and 0x100?
  33. define('PDB_ATTRIB_LAUNCHABLE', 0x200);
  34. // Record Flags
  35. // The first nibble is reserved for the category number
  36. // See PDB_CATEGORY_MASK
  37. define('PDB_RECORD_ATTRIB_ARCHIVE', 0x08); // Special -- see below
  38. define('PDB_RECORD_ATTRIB_PRIVATE', 0x10);
  39. define('PDB_RECORD_ATTRIB_DELETED', 0x20);
  40. define('PDB_RECORD_ATTRIB_DIRTY', 0x40);
  41. define('PDB_RECORD_ATTRIB_EXPUNGED', 0x80);
  42. define('PDB_RECORD_ATTRIB_DEL_EXP', 0xA0); // Mask for easier use
  43. define('PDB_RECORD_ATTRIB_MASK', 0xF0); // The 4 bytes for the attributes
  44. define('PDB_RECORD_ATTRIB_CATEGORY_MASK', 0xFF); // 1 byte
  45. /* The archive bit should only be used when the record is deleted or
  46. * expunged.
  47. *
  48. * if ($attr & PDB_RECORD_ATTRIB_DEL_EXP) {
  49. * // Lower 3 bits (0x07) should be 0
  50. * if ($attr & PDB_RECORD_ATTRIB_ARCHIVE)
  51. * echo "Record is deleted/expunged and should be archived.\n";
  52. * else
  53. * echo "Record is deleted/expunged and should not be archived.\n";
  54. * } else {
  55. * // Lower 4 bits are the category
  56. * echo "Record is not deleted/expunged.\n";
  57. * echo "Record's category # is " . ($attr & PDB_CATEGORY_MASK) . "\n";
  58. * }
  59. */
  60. // Category support
  61. define('PDB_CATEGORY_NUM', 16); // Number of categories
  62. define('PDB_CATEGORY_NAME_LENGTH', 16); // Bytes allocated for name
  63. define('PDB_CATEGORY_SIZE', 276); // 2 + (num * length) + num + 1 + 1
  64. define('PDB_CATEGORY_MASK', 0x0f); // Bitmask -- use with attribute of record
  65. // to get the category ID
  66. // Double conversion
  67. define('PDB_DOUBLEMETHOD_UNTESTED', 0);
  68. define('PDB_DOUBLEMETHOD_NORMAL', 1);
  69. define('PDB_DOUBLEMETHOD_REVERSE', 2);
  70. define('PDB_DOUBLEMETHOD_BROKEN', 3);
  71. /*
  72. * PalmDB Class
  73. *
  74. * Contains all of the required methods and variables to write a pdb file.
  75. * Extend this class to provide functionality for memos, addresses, etc.
  76. */
  77. class PalmDB {
  78. var $Records = array(); // All of the data from the records is here
  79. // Key = record ID
  80. var $RecordAttrs = array(); // And their attributes are here
  81. var $CurrentRecord = 1; // Which record we are currently editing
  82. var $Name = ''; // Name of the PDB file
  83. var $TypeID = ''; // The 'Type' of the file (4 chars)
  84. var $CreatorID = ''; // The 'Creator' of the file (4 chars)
  85. var $Attributes = 0; // Attributes (bitmask)
  86. var $Version = 0; // Version of the file
  87. var $ModNumber = 0; // Modification number
  88. var $CreationTime = 0; // Stored in unix time (Jan 1, 1970)
  89. var $ModificationTime = 0; // Stored in unix time (Jan 1, 1970)
  90. var $BackupTime = 0; // Stored in unix time (Jan 1, 1970)
  91. var $AppInfo = ''; // Basic AppInfo block
  92. var $SortInfo = ''; // Basic SortInfo block
  93. var $DoubleMethod = PDB_DOUBLEMETHOD_UNTESTED;
  94. // What method to use for converting doubles
  95. var $CategoryList = array(); // Category data (not used by default --
  96. // See "Category Support" comment below)
  97. // Creates a new database class
  98. function PalmDB($Type = '', $Creator = '', $Name = '') {
  99. $this->TypeID = $Type;
  100. $this->CreatorID = $Creator;
  101. $this->Name = $Name;
  102. $this->CreationTime = time();
  103. $this->ModificationTime = time();
  104. }
  105. /*
  106. * Data manipulation functions
  107. *
  108. * These convert various numbers and strings into the hexadecimal
  109. * format that is used internally to construct the file. We use hex
  110. * encoded strings since that is a lot easier to work with than binary
  111. * data in strings, and we can easily tell how big the true value is.
  112. * B64 encoding does some odd stuff, so we just make the memory
  113. * consumption grow tremendously and the complexity level drops
  114. * considerably.
  115. */
  116. // Converts a byte and returns the value
  117. function Int8($value) {
  118. $value &= 0xFF;
  119. return sprintf("%02x", $value);
  120. }
  121. // Loads a single byte as a number from the file
  122. // Use if you want to make your own ReadFile function
  123. function LoadInt8($file) {
  124. if (is_resource($file))
  125. $string = fread($file, 1);
  126. else
  127. $string = $file;
  128. return ord($string[0]);
  129. }
  130. // Converts an integer (two bytes) and returns the value
  131. function Int16($value) {
  132. $value &= 0xFFFF;
  133. return sprintf("%02x%02x", $value / 256, $value % 256);
  134. }
  135. // Loads two bytes as a number from the file
  136. // Use if you want to make your own ReadFile function
  137. function LoadInt16($file) {
  138. if (is_resource($file))
  139. $string = fread($file, 2);
  140. else
  141. $string = $file;
  142. return ord($string[0]) * 256 + ord($string[1]);
  143. }
  144. // Converts an integer (three bytes) and returns the value
  145. function Int24($value) {
  146. $value &= 0xFFFFFF;
  147. return sprintf("%02x%02x%02x", $value / 65536,
  148. ($value / 256) % 256, $value % 256);
  149. }
  150. // Loads three bytes as a number from the file
  151. // Use if you want to make your own ReadFile function
  152. function LoadInt24($file) {
  153. if (is_resource($file))
  154. $string = fread($file, 3);
  155. else
  156. $string = $file;
  157. return ord($string[0]) * 65536 + ord($string[1]) * 256 +
  158. ord($string[2]);
  159. }
  160. // Converts an integer (four bytes) and returns the value
  161. // 32-bit integers have problems with PHP when they are bigger than
  162. // 0x80000000 (about 2 billion) and that's why I don't use pack() here
  163. function Int32($value) {
  164. $negative = false;
  165. if ($value < 0) {
  166. $negative = true;
  167. $value = - $value;
  168. }
  169. $big = $value / 65536;
  170. settype($big, 'integer');
  171. $little = $value - ($big * 65536);
  172. if ($negative) {
  173. // Little must contain a value
  174. $little = - $little;
  175. // Big might be zero, and should be 0xFFFF if that is the case.
  176. $big = 0xFFFF - $big;
  177. }
  178. $value = PalmDB::Int16($big) . PalmDB::Int16($little);
  179. return $value;
  180. }
  181. // Loads a four-byte string from a file into a number
  182. // Use if you want to make your own ReadFile function
  183. function LoadInt32($file) {
  184. if (is_resource($file))
  185. $string = fread($file, 4);
  186. else
  187. $string = $file;
  188. $value = 0;
  189. $i = 0;
  190. while ($i < 4) {
  191. $value *= 256;
  192. $value += ord($string[$i]);
  193. $i ++;
  194. }
  195. return $value;
  196. }
  197. // Returns the method used for generating doubles
  198. function GetDoubleMethod() {
  199. if ($this->DoubleMethod != PDB_DOUBLEMETHOD_UNTESTED)
  200. return $this->DoubleMethod;
  201. $val = bin2hex(pack('d', 10.53));
  202. $val = strtolower($val);
  203. if (substr($val, 0, 4) == '8fc2')
  204. $this->DoubleMethod = PDB_DOUBLEMETHOD_REVERSE;
  205. if (substr($val, 0, 4) == '4025')
  206. $this->DoubleMethod = PDB_DOUBLEMETHOD_NORMAL;
  207. if ($this->DoubleMethod == PDB_DOUBLEMETHOD_UNTESTED)
  208. $this->DoubleMethod = PDB_DOUBLEMETHOD_BROKEN;
  209. return $this->DoubleMethod;
  210. }
  211. // Converts the number into a double and returns the encoded value
  212. // Not sure if this will work on all platforms.
  213. // Double(10.53) should return "40250f5c28f5c28f"
  214. function Double($value) {
  215. $method = $this->GetDoubleMethod();
  216. if ($method == PDB_DOUBLEMETHOD_BROKEN)
  217. return '0000000000000000';
  218. $value = bin2hex(pack('d', $value));
  219. if ($method == PDB_DOUBLEMETHOD_REVERSE)
  220. $value = substr($value, 14, 2) . substr($value, 12, 2) .
  221. substr($value, 10, 2) . substr($value, 8, 2) .
  222. substr($value, 6, 2) . substr($value, 4, 2) .
  223. substr($value, 2, 2) . substr($value, 0, 2);
  224. return $value;
  225. }
  226. // The reverse? May not work on your platform.
  227. // Use if you want to make your own ReadFile function
  228. function LoadDouble($file) {
  229. $method = $this->GetDoubleMethod();
  230. if ($method == PDB_DOUBLEMETHOD_BROKEN)
  231. return 0.0;
  232. if (is_resource($file))
  233. $string = fread($file, 8);
  234. else
  235. $string = $file;
  236. // Reverse the bytes... this might not be nessesary
  237. // if PHP is running on a big-endian server
  238. if ($method == PDB_DOUBLEMETHOD_REVERSE)
  239. $string = substr($string, 7, 1) . substr($string, 6, 1) .
  240. substr($string, 5, 1) . substr($string, 4, 1) .
  241. substr($string, 3, 1) . substr($string, 2, 1) .
  242. substr($string, 1, 1) . substr($string, 0, 1);
  243. // Back to binary
  244. $dnum = unpack('d', $string);
  245. return $dnum[''];
  246. }
  247. // Converts a date string ( YYYY-MM-DD )( "2001-10-31" )
  248. // into bitwise ( YYYY YYYM MMMD DDDD )
  249. // Should only be used when saving
  250. function DateToInt16($date) {
  251. $YMD = explode('-', $date);
  252. settype($YMD[0], 'integer');
  253. settype($YMD[1], 'integer');
  254. settype($YMD[2], 'integer');
  255. return ((($YMD[0] - 1904) & 0x7F) << 9) |
  256. (($YMD[1] & 0x0f) << 5) |
  257. ($YMD[2] & 0x1f);
  258. }
  259. // Converts a bitwise date ( YYYY YYYM MMMD DDDD )
  260. // Into the human readable date string ( YYYY-MM-DD )( "2001-2-28" )
  261. // Should only be used when loading
  262. function Int16ToDate($number) {
  263. settype($number, 'integer');
  264. $year = ($number >> 9) & 0x7F;
  265. $year += 1904;
  266. $month = ($number >> 5) & 0xF;
  267. $day = $number & 0x1F;
  268. return $year . '-' . $month . '-' . $day;
  269. }
  270. // Converts a string into hexadecimal.
  271. // If $maxLen is specified and is greater than zero, the string is
  272. // trimmed and will contain up to $maxLen characters.
  273. // String("abcd", 2) will return "ab" hex encoded (a total of 4
  274. // resulting bytes, but 2 encoded characters).
  275. // Returned string is *not* NULL-terminated.
  276. function String($value, $maxLen = false) {
  277. $value = bin2hex($value);
  278. if ($maxLen !== false && $maxLen > 0)
  279. $value = substr($value, 0, $maxLen * 2);
  280. return $value;
  281. }
  282. // Pads a hex-encoded value (typically a string) to a fixed size.
  283. // May grow too long if $value starts too long
  284. // $value = hex encoded value
  285. // $minLen = Append nulls to $value until it reaches $minLen
  286. // $minLen is the desired size of the string, unencoded.
  287. // PadString('6162', 3) results in '616200' (remember the hex encoding)
  288. function PadString($value, $minLen) {
  289. $PadBytes = '00000000000000000000000000000000';
  290. $PadMe = $minLen - (strlen($value) / 2);
  291. while ($PadMe > 0) {
  292. if ($PadMe > 16)
  293. $value .= $PadBytes;
  294. else
  295. return $value . substr($PadBytes, 0, $PadMe * 2);
  296. $PadMe = $minLen - (strlen($value) / 2);
  297. }
  298. return $value;
  299. }
  300. /*
  301. * Record manipulation functions
  302. */
  303. // Sets the current record pointer to the new record number if an
  304. // argument is passed in.
  305. // Returns the old record number (just in case you want to jump back)
  306. // Does not do basic record initialization if we are going to a new
  307. // record.
  308. function GoToRecord($num = false) {
  309. if ($num === false)
  310. return $this->CurrentRecord;
  311. if (gettype($num) == 'string' && ($num[0] == '+' || $num[0] == '-'))
  312. $num = $this->CurrentRecord + $num;
  313. $oldRecord = $this->CurrentRecord;
  314. $this->CurrentRecord = $num;
  315. return $oldRecord;
  316. }
  317. // Returns the size of the current record if no arguments.
  318. // Returns the size of the specified record if arguments.
  319. function GetRecordSize($num = false) {
  320. if ($num === false)
  321. $num = $this->CurrentRecord;
  322. if (! isset($this->Records[$num]))
  323. return 0;
  324. return strlen($this->Records[$num]) / 2;
  325. }
  326. // Adds to the current record. The input data must be already
  327. // hex encoded. Initializes the record if it doesn't exist.
  328. function AppendCurrent($value) {
  329. if (! isset($this->Records[$this->CurrentRecord]))
  330. $this->Records[$this->CurrentRecord] = '';
  331. $this->Records[$this->CurrentRecord] .= $value;
  332. }
  333. // Adds a byte to the current record
  334. function AppendInt8($value) {
  335. $this->AppendCurrent($this->Int8($value));
  336. }
  337. // Adds an integer (2 bytes) to the current record
  338. function AppendInt16($value) {
  339. $this->AppendCurrent($this->Int16($value));
  340. }
  341. // Adds an integer (4 bytes) to the current record
  342. function AppendInt32($value) {
  343. $this->AppendCurrent($this->Int32($value));
  344. }
  345. // Adds a double to the current record
  346. function AppendDouble($value) {
  347. $this->AppendCurrent($this->Double($value));
  348. }
  349. // Adds a string (not NULL-terminated)
  350. function AppendString($value, $maxLen = false) {
  351. $this->AppendCurrent($this->String($value, $maxLen));
  352. }
  353. // Returns true if the specified/current record exists and is set
  354. function RecordExists($Rec = false) {
  355. if ($Rec === false)
  356. $Rec = $this->CurrentRecord;
  357. if (isset($this->Records[$Rec]))
  358. return true;
  359. return false;
  360. }
  361. // Returns the hex-encoded data for the specified record or the current
  362. // record if not specified
  363. // This is nearly identical to GetRecordRaw except that this function
  364. // may be overridden by classes (see modules/doc.inc) and that there
  365. // should always be a function that will return the raw data of the
  366. // Records array.
  367. function GetRecord($Rec = false) {
  368. if ($Rec === false)
  369. $Rec = $this->CurrentRecord;
  370. if (isset($this->Records[$Rec]))
  371. return $this->Records[$Rec];
  372. return '';
  373. }
  374. // Returns the attributes for the specified record or the current
  375. // record if not specified.
  376. function GetRecordAttrib($Rec = false) {
  377. if ($Rec === false)
  378. $Rec = $this->CurrentRecord;
  379. if (isset($this->RecordAttrs[$Rec]))
  380. return $this->RecordAttrs[$Rec] & PDB_RECORD_ATTRIB_CATEGORY_MASK;
  381. return 0;
  382. }
  383. // Returns the raw data inside the current/specified record. Use this
  384. // for odd record types (like a Datebook record). Also, use this
  385. // instead of just using $PDB->Records[] directly.
  386. // Please do not override this function.
  387. function GetRecordRaw($Rec = false) {
  388. if ($Rec === false)
  389. $Rec = $this->CurrentRecord;
  390. if (isset($this->Records[$Rec]))
  391. return $this->Records[$Rec];
  392. return false;
  393. }
  394. // Sets the hex-encoded data (or whatever) for the current record
  395. // Use this instead of the Append* functions if you have an odd
  396. // type of record (like a Datebook record).
  397. // Also, use this instead of just setting $PDB->Records[]
  398. // directly.
  399. // SetRecordRaw('data');
  400. // SetRecordRaw(24, 'data'); (specifying the record num)
  401. function SetRecordRaw($A, $B = false) {
  402. if ($B === false) {
  403. $B = $A;
  404. $A = $this->CurrentRecord;
  405. }
  406. $this->Records[$A] = $B;
  407. }
  408. // Sets the attributes for the specified record or the current
  409. // record if not specified.
  410. // Note: The 'attributes' byte also sets the category.
  411. // SetRecordAttrib($attr);
  412. // SetRecordAttrib($RecNo, $attr);
  413. function SetRecordAttrib($A, $B = false) {
  414. if ($B === false) {
  415. $B = $A;
  416. $A = $this->CurrentRecord;
  417. }
  418. $this->RecordAttrs[$A] = $B & PDB_RECORD_ATTRIB_CATEGORY_MASK;
  419. }
  420. // Deletes the specified record or the current record if not specified.
  421. // If you delete the current record and then use an append function, the
  422. // record will be recreated.
  423. function DeleteRecord($RecNo = false) {
  424. if ($RecNo === false) {
  425. $RecNo = $this->CurrentRecord;
  426. }
  427. if (isset($this->Records[$RecNo]))
  428. unset($this->Records[$RecNo]);
  429. if (isset($this->RecordAttrs[$RecNo]))
  430. unset($this->RecordAttrs[$RecNo]);
  431. }
  432. // Returns an array of available record IDs in the order they should
  433. // be written.
  434. // Probably should only be called within the class.
  435. function GetRecordIDs() {
  436. $keys = array_keys($this->Records);
  437. if (! is_array($keys) || count($keys) < 1)
  438. return array();
  439. sort($keys, SORT_NUMERIC);
  440. return $keys;
  441. }
  442. // Returns the number of records. This should match the number of
  443. // keys returned by GetRecordIDs().
  444. function GetRecordCount() {
  445. return count($this->Records);
  446. }
  447. // Returns the size of the AppInfo block.
  448. // Used only for writing
  449. function GetAppInfoSize() {
  450. if (! isset($this->AppInfo))
  451. return 0;
  452. return strlen($this->AppInfo) / 2;
  453. }
  454. // Returns the AppInfo block (hex encoded)
  455. // Used only for writing
  456. function GetAppInfo() {
  457. if (! isset($this->AppInfo))
  458. return 0;
  459. return $this->AppInfo;
  460. }
  461. // Returns the size of the SortInfo block
  462. // Used only for writing
  463. function GetSortInfoSize() {
  464. if (! isset($this->SortInfo))
  465. return 0;
  466. return strlen($this->SortInfo) / 2;
  467. }
  468. // Returns the SortInfo block (hex encoded)
  469. // Used only for writing
  470. function GetSortInfo() {
  471. if (! isset($this->SortInfo))
  472. return 0;
  473. return $this->SortInfo;
  474. }
  475. /*
  476. * Category Support
  477. *
  478. * If you plan on using categories in your module, you will have to use
  479. * these next four functions.
  480. *
  481. * In your LoadAppInfo(), you should have something like this ...
  482. * function LoadAppInfo($fileData) {
  483. * $this->LoadCategoryData($fileData);
  484. * $fileData = substr($fileData, PDB_CATEGORY_SIZE);
  485. * // .....
  486. * }
  487. *
  488. * In your GetAppInfo() function, you need to output the categories ...
  489. * function GetAppInfo() {
  490. * $AppInfo = $this->CreateCategoryData();
  491. * // .....
  492. * return $AppInfo;
  493. * }
  494. *
  495. * To change the category data, use SetCategoryList() and GetCategoryList()
  496. * helper functions.
  497. */
  498. // Returns the category data. See SetCategoryList() for a description
  499. // of the format of the array returned.
  500. function GetCategoryList() {
  501. return $this->CategoryList;
  502. }
  503. // Sets the categories to what you specified.
  504. //
  505. // Data format: (easy way)
  506. // $categoryArray[###] = name
  507. // Or: (how it is stored in the class)
  508. // $categoryArray[###]['Name'] = name
  509. // $categoryArray[###]['Renamed'] = true / false
  510. // $categoryArray[###]['ID'] = number from 0 to 255
  511. //
  512. // Tips:
  513. // * The number for the key of $categoryArray is from 0-15, specifying
  514. // the order that the category is written in the AppInfo block.
  515. // * I'd suggest numbering your categories sequentially
  516. // * ID numbers must be unique. If they are not, a new arbitrary number
  517. // will be assigned.
  518. // * $categoryArray[0] is reserved for the 'Unfiled' category. It's
  519. // ID is 0. Do not use 0 as an index for the array. Do not use 0 as
  520. // an ID number. This function will enforce this rule.
  521. // * There is a maximum of 16 categories, including 'Unfiled'. This
  522. // means that you only have 15 left to play with.
  523. //
  524. // Category 0 is reserved for 'Unfiled'
  525. // Categories 1-127 are used for handheld ID numbers
  526. // Categories 128-255 are used for desktop ID numbers
  527. // Do not let category numbers be created larger than 255 -- this function
  528. // will erase categories with an ID larger than 255
  529. function SetCategoryList($list) {
  530. $usedCheck = 0;
  531. $usedList = array();
  532. // Clear out old category list
  533. $this->CategoryList = array();
  534. // Force category ID 0 to be "Unfiled"
  535. $list[0] = array('Name' => 'Unfiled', 'Renamed' => false, 'ID' => 0);
  536. $keys = array_keys($list);
  537. // Loop through the array
  538. $CatsWritten = 0;
  539. foreach ($keys as $key) {
  540. // If there is room for more categories ...
  541. if ($CatsWritten < PDB_CATEGORY_NUM && $key <= 15 && $key >= 0) {
  542. if (is_array($list[$key]) && isset($list[$key]['ID']))
  543. $id = $list[$key]['ID'];
  544. else
  545. $id = $key;
  546. if ($id > 255 || isset($usedList[$id])) {
  547. // Find a new arbitrary number for this category
  548. $usedCheck ++;
  549. while (isset($usedList[$usedCheck]))
  550. $usedCheck ++;
  551. $id = $usedCheck;
  552. }
  553. $CatsWritten ++;
  554. // Set the "Renamed" flag if available
  555. // By default, the Renamed flag is false
  556. $RenamedFlag = false;
  557. if (is_array($list[$key]) && isset($list[$key]['Renamed']) &&
  558. $list[$key]['Renamed'])
  559. $RenamedFlag = true;
  560. // Set the name of the category
  561. $name = '';
  562. if (is_array($list[$key])) {
  563. if (isset($list[$key]['Name']))
  564. $name = $list[$key]['Name'];
  565. } else {
  566. $name = $list[$key];
  567. }
  568. $this->CategoryList[$key] = array('Name' => $name,
  569. 'Renamed' => $RenamedFlag,
  570. 'ID' => $id);
  571. }
  572. }
  573. }
  574. // Creates the hex-encoded data to be stuck in the AppInfo
  575. // block if the database supports categories.
  576. //
  577. // See SetCategoryList() for the format of $CategoryArray
  578. function CreateCategoryData() {
  579. $UsedIds = array();
  580. $UsedIdCheck = 0;
  581. // Force category data to be valid and in a specific format
  582. $this->SetCategoryList($this->CategoryList);
  583. $RenamedFlags = 0;
  584. $CategoryStr = '';
  585. $IdStr = '';
  586. $LastID = 0;
  587. foreach ($this->CategoryList as $key => $data) {
  588. $UsedIds[$data['ID']] = true;
  589. if ($data['ID'] > $LastID)
  590. $LastID = $data['ID'];
  591. }
  592. // Loop through the array
  593. for ($key = 0; $key < 16; $key ++) {
  594. if (isset($this->CategoryList[$key])) {
  595. $RenamedFlags *= 2;
  596. // Set the "Renamed" flag if available
  597. // By default, the Renamed flag is false
  598. if ($this->CategoryList[$key]['Renamed'])
  599. $RenamedFlags += 1;
  600. // Set the name of the category
  601. $name = $this->CategoryList[$key]['Name'];
  602. $name = $this->String($name, PDB_CATEGORY_NAME_LENGTH);
  603. $CategoryStr .= $this->PadString($name,
  604. PDB_CATEGORY_NAME_LENGTH);
  605. $IdStr .= $this->Int8($this->CategoryList[$key]['ID']);
  606. } else {
  607. // Add blank categories where they are missing
  608. $UsedIdCheck ++;
  609. while (isset($UsedIds[$UsedIdCheck]))
  610. $UsedIdCheck ++;
  611. $RenamedFlags *= 2;
  612. $CategoryStr .= $this->PadString('', PDB_CATEGORY_NAME_LENGTH);
  613. $IdStr .= $this->Int8($UsedIdCheck);
  614. }
  615. }
  616. // According to the docs, this is just the last ID written. It doesn't
  617. // say whether this is the last one written by the palm, last one
  618. // written by the desktop, highest one written, or the ID for number
  619. // 15.
  620. $TrailingBytes = $this->Int8($LastID);
  621. $TrailingBytes .= $this->Int8(0);
  622. return $this->Int16($RenamedFlags) . $CategoryStr . $IdStr .
  623. $TrailingBytes;
  624. }
  625. // This should be called by other subclasses that use category support
  626. // It returns a category array. Each element in the array is another
  627. // array with the key 'Name' set to the name of the category and
  628. // the key 'Renamed' set to the renamed flag for that category.
  629. function LoadCategoryData($fileData) {
  630. $key = 0;
  631. $RenamedFlags = $this->LoadInt16(substr($fileData, 0, 2));
  632. $Offset = 2;
  633. $StartingFlag = 65536;
  634. $Categories = array();
  635. while ($StartingFlag > 1) {
  636. $StartingFlag /= 2;
  637. $Name = substr($fileData, $Offset, PDB_CATEGORY_NAME_LENGTH);
  638. $i = 0;
  639. while ($i < PDB_CATEGORY_NAME_LENGTH && $Name[$i] != "\0")
  640. $i ++;
  641. if ($i == 0)
  642. $Name = '';
  643. elseif ($i < PDB_CATEGORY_NAME_LENGTH)
  644. $Name = substr($Name, 0, $i);
  645. if ($RenamedFlags & $StartingFlag)
  646. $RenamedFlag = true;
  647. else
  648. $RenamedFlag = false;
  649. $Categories[$key] = array('Name' => $Name, 'Renamed' => $RenamedFlag);
  650. $Offset += PDB_CATEGORY_NAME_LENGTH;
  651. $key ++;
  652. }
  653. $CategoriesParsed = array();
  654. for ($key = 0; $key < 16; $key ++) {
  655. $UID = $this->LoadInt8(substr($fileData, $Offset, 1));
  656. $Offset ++;
  657. $CategoriesParsed[$key] = array('Name' => $Categories[$key]['Name'],
  658. 'Renamed' => $Categories[$key]['Renamed'],
  659. 'ID' => $UID);
  660. }
  661. // Ignore the last ID. Maybe it should be preserved?
  662. $this->CategoryList = $CategoriesParsed;
  663. }
  664. /*
  665. * Database Writing Functions
  666. */
  667. // *NEW*
  668. // Takes a hex-encoded string and makes sure that when decoded, the data
  669. // lies on a four-byte boundary. If it doesn't, it pads the string with
  670. // NULLs
  671. /*
  672. * Commented out because we don't use this function currently.
  673. * It is part of a test to see what is needed to get files to sync
  674. * properly with Desktop 4.0
  675. *
  676. function PadTo4ByteBoundary($string) {
  677. while ((strlen($string)/2) % 4) {
  678. $string .= '00';
  679. }
  680. return $string;
  681. }
  682. *
  683. */
  684. // Returns the hex encoded header of the pdb file
  685. // Header = name, attributes, version, creation/modification/backup
  686. // dates, modification number, some offsets, record offsets,
  687. // record attributes, appinfo block, sortinfo block
  688. // Shouldn't be called from outside the class
  689. function MakeHeader() {
  690. // 32 bytes = name, but only 31 available (one for null)
  691. $header = $this->String($this->Name, 31);
  692. $header = $this->PadString($header, 32);
  693. // Attributes & version fields
  694. $header .= $this->Int16($this->Attributes);
  695. $header .= $this->Int16($this->Version);
  696. // Creation, modification, and backup date
  697. if ($this->CreationTime != 0)
  698. $header .= $this->Int32($this->CreationTime + PDB_EPOCH_1904);
  699. else
  700. $header .= $this->Int32(time() + PDB_EPOCH_1904);
  701. if ($this->ModificationTime != 0)
  702. $header .= $this->Int32($this->ModificationTime + PDB_EPOCH_1904);
  703. else
  704. $header .= $this->Int32(time() + PDB_EPOCH_1904);
  705. if ($this->BackupTime != 0)
  706. $header .= $this->Int32($this->BackupTime + PDB_EPOCH_1904);
  707. else
  708. $header .= $this->Int32(0);
  709. // Calculate the initial offset
  710. $Offset = PDB_HEADER_SIZE + PDB_INDEX_HEADER_SIZE;
  711. $Offset += PDB_RECORD_HEADER_SIZE * count($this->GetRecordIDs());
  712. // Modification number, app information id, sort information id
  713. $header .= $this->Int32($this->ModNumber);
  714. $AppInfo_Size = $this->GetAppInfoSize();
  715. if ($AppInfo_Size > 0) {
  716. $header .= $this->Int32($Offset);
  717. $Offset += $AppInfo_Size;
  718. } else
  719. $header .= $this->Int32(0);
  720. $SortInfo_Size = $this->GetSortInfoSize();
  721. if ($SortInfo_Size > 0) {
  722. $header .= $this->Int32($Offset);
  723. $Offset += $SortInfo_Size;
  724. } else
  725. $header .= $this->Int32(0);
  726. // Type, creator
  727. $header .= $this->String($this->TypeID, 4);
  728. $header .= $this->String($this->CreatorID, 4);
  729. // Unique ID seed
  730. $header .= $this->Int32(0);
  731. // next record list
  732. $header .= $this->Int32(0);
  733. // Number of records
  734. $header .= $this->Int16($this->GetRecordCount());
  735. // Compensate for the extra 2 NULL characters in the $Offset
  736. $Offset += 2;
  737. // Dump each record
  738. if ($this->GetRecordCount() != 0) {
  739. $keys = $this->GetRecordIDs();
  740. sort($keys, SORT_NUMERIC);
  741. foreach ($keys as $index) {
  742. $header .= $this->Int32($Offset);
  743. $header .= $this->Int8($this->GetRecordAttrib($index));
  744. // The unique id is just going to be the record number
  745. $header .= $this->Int24($index);
  746. $Offset += $this->GetRecordSize($index);
  747. // *new* method 3
  748. //$Mod4 = $Offset % 4;
  749. //if ($Mod4)
  750. // $Offset += 4 - $Mod4;
  751. }
  752. }
  753. // AppInfo and SortInfo blocks go here
  754. if ($AppInfo_Size > 0)
  755. // *new* method 1
  756. $header .= $this->GetAppInfo();
  757. //$header .= $this->PadTo4ByteBoundary($this->GetAppInfo());
  758. if ($SortInfo_Size > 0)
  759. // *new* method 2
  760. $header .= $this->GetSortInfo();
  761. //$header .= $this->PadTo4ByteBoundary($this->GetSortInfo());
  762. // These are the mysterious two NULL characters that we need
  763. $header .= $this->Int16(0);
  764. return $header;
  765. }
  766. // Writes the database to the file handle specified.
  767. // Use this function like this:
  768. // $file = fopen("output.pdb", "wb");
  769. // // "wb" = write binary for non-Unix systems
  770. // if (! $file) {
  771. // echo "big problem -- can't open file";
  772. // exit;
  773. // }
  774. // $pdb->WriteToFile($file);
  775. // fclose($file);
  776. function WriteToFile($file) {
  777. $header = $this->MakeHeader();
  778. fwrite($file, pack('H*', $header), strlen($header) / 2);
  779. $keys = $this->GetRecordIDs();
  780. sort($keys, SORT_NUMERIC);
  781. foreach ($keys as $index) {
  782. // *new* method 3
  783. //$data = $this->PadTo4ByteBoundary($this->GetRecord($index));
  784. $data = $this->GetRecord($index);
  785. fwrite($file, pack('H*', $data), strlen($data) / 2);
  786. }
  787. fflush($file);
  788. }
  789. // Writes the database to the standard output (like echo).
  790. // Can be trapped with output buffering
  791. function WriteToStdout() {
  792. // You'd think these three lines would work.
  793. // If someone can figure out why they don't, please tell me.
  794. //
  795. // $fp = fopen('php://stdout', 'wb');
  796. // $this->WriteToFile($fp);
  797. // fclose($fp);
  798. $header = $this->MakeHeader();
  799. echo pack("H*", $header);
  800. $keys = $this->GetRecordIDs();
  801. sort($keys, SORT_NUMERIC);
  802. foreach ($keys as $index) {
  803. // *new* method 3
  804. $data = $this->GetRecord($index);
  805. //$data = $this->PadTo4ByteBoundary($this->GetRecord($index));
  806. echo pack("H*", $data);
  807. }
  808. }
  809. // Writes the database to the standard output (like echo) but also
  810. // writes some headers so that the browser should prompt to save the
  811. // file properly.
  812. //
  813. // Use this only if you didn't send any content and you only want the
  814. // PHP script to output the PDB file and nothing else. An example
  815. // would be if you wanted to have 'download' link so the user can
  816. // stick the information they are currently viewing and transfer
  817. // it easily into their handheld.
  818. //
  819. // $filename is the desired filename to download the database as.
  820. // For example, DownloadPDB('memos.pdb');
  821. function DownloadPDB($filename) {
  822. global $HTTP_USER_AGENT;
  823. // Alter the filename to only allow certain characters.
  824. // Some platforms and some browsers don't respond well if
  825. // there are illegal characters (such as spaces) in the name of
  826. // the file being downloaded.
  827. $filename = preg_replace('/[^-a-zA-Z0-9\\.]/', '_', $filename);
  828. if (strstr($HTTP_USER_AGENT, 'compatible; MSIE ') !== false &&
  829. strstr($HTTP_USER_AGENT, 'Opera') === false) {
  830. // IE doesn't properly download attachments. This should work
  831. // pretty well for IE 5.5 SP 1
  832. header("Content-Disposition: inline; filename=$filename");
  833. header("Content-Type: application/download; name=\"$filename\"");
  834. } else {
  835. // Use standard headers for Netscape, Opera, etc.
  836. header("Content-Disposition: attachment; filename=\"$filename\"");
  837. header("Content-Type: application/x-pilot; name=\"$filename\"");
  838. }
  839. $this->WriteToStdout();
  840. }
  841. /*
  842. * Loading in a database
  843. */
  844. // Reads data from the file and tries to load it properly
  845. // $file is the already-opened file handle.
  846. // Returns false if no error
  847. function ReadFile($file) {
  848. // 32 bytes = name, but only 31 available
  849. $this->Name = fread($file, 32);
  850. $i = 0;
  851. while ($i < 32 && $this->Name[$i] != "\0")
  852. $i ++;
  853. $this->Name = substr($this->Name, 0, $i);
  854. $this->Attributes = $this->LoadInt16($file);
  855. $this->Version = $this->LoadInt16($file);
  856. $this->CreationTime = $this->LoadInt32($file);
  857. if ($this->CreationTime != 0)
  858. $this->CreationTime -= PDB_EPOCH_1904;
  859. if ($this->CreationTime < 0)
  860. $this->CreationTime = 0;
  861. $this->ModificationTime = $this->LoadInt32($file);
  862. if ($this->ModificationTime != 0)
  863. $this->ModificationTime -= PDB_EPOCH_1904;
  864. if ($this->ModificationTime < 0)
  865. $this->ModificationTime = 0;
  866. $this->BackupTime = $this->LoadInt32($file);
  867. if ($this->BackupTime != 0)
  868. $this->BackupTime -= PDB_EPOCH_1904;
  869. if ($this->BackupTime < 0)
  870. $this->BackupTime = 0;
  871. // Modification number
  872. $this->ModNumber = $this->LoadInt32($file);
  873. // AppInfo and SortInfo size
  874. $AppInfoOffset = $this->LoadInt32($file);
  875. $SortInfoOffset = $this->LoadInt32($file);
  876. // Type, creator
  877. $this->TypeID = fread($file, 4);
  878. $this->CreatorID = fread($file, 4);
  879. // Skip unique ID seed
  880. fread($file, 4);
  881. // skip next record list (hope that's ok)
  882. fread($file, 4);
  883. $RecCount = $this->LoadInt16($file);
  884. $RecordData = array();
  885. while ($RecCount > 0) {
  886. $RecCount --;
  887. $Offset = $this->LoadInt32($file);
  888. $Attrs = $this->LoadInt8($file);
  889. $UID = $this->LoadInt24($file);
  890. $RecordData[] = array('Offset' => $Offset, 'Attrs' => $Attrs,
  891. 'UID' => $UID);
  892. }
  893. // Create the offset list
  894. if ($AppInfoOffset != 0)
  895. $OffsetList[$AppInfoOffset] = 'AppInfo';
  896. if ($SortInfoOffset != 0)
  897. $OffsetList[$SortInfoOffset] = 'SortInfo';
  898. foreach ($RecordData as $data)
  899. $OffsetList[$data['Offset']] = array('Record', $data);
  900. fseek($file, 0, SEEK_END);
  901. $OffsetList[ftell($file)] = 'EOF';
  902. // Parse each chunk
  903. ksort($OffsetList);
  904. $Offsets = array_keys($OffsetList);
  905. while (count($Offsets) > 1) {
  906. // Don't use the EOF (which should be the last offset)
  907. $ThisOffset = $Offsets[0];
  908. $NextOffset = $Offsets[1];
  909. if ($OffsetList[$ThisOffset] == 'EOF')
  910. // Messed up file. Stop here.
  911. return true;
  912. $FuncName = 'Load';
  913. if (is_array($OffsetList[$ThisOffset])) {
  914. $FuncName .= $OffsetList[$ThisOffset][0];
  915. $extraData = $OffsetList[$ThisOffset][1];
  916. } else {
  917. $FuncName .= $OffsetList[$ThisOffset];
  918. $extraData = false;
  919. }
  920. fseek($file, $ThisOffset);
  921. $fileData = fread($file, $NextOffset - $ThisOffset);
  922. if ($this->$FuncName($fileData, $extraData))
  923. return -2;
  924. array_shift($Offsets);
  925. }
  926. return false;
  927. }
  928. // Generic function to load the AppInfo block into $this->AppInfo
  929. // Should only be called within this class
  930. // Return false to signal no error
  931. function LoadAppInfo($fileData) {
  932. $this->AppInfo = bin2hex($fileData);
  933. return false;
  934. }
  935. // Generic function to load the SortInfo block into $this->SortInfo
  936. // Should only be called within this class
  937. // Return false to signal no error
  938. function LoadSortInfo($fileData) {
  939. $this->SortInfo = bin2hex($fileData);
  940. return false;
  941. }
  942. // Generic function to load a record
  943. // Should only be called within this class
  944. // Return false to signal no error
  945. function LoadRecord($fileData, $recordInfo) {
  946. $this->Records[$recordInfo['UID']] = bin2hex($fileData);
  947. $this->RecordAttrs[$recordInfo['UID']] = $recordInfo['Attrs'];
  948. return false;
  949. }
  950. }
  951. ?>