TypeID = $Type; $this->CreatorID = $Creator; $this->Name = $Name; $this->CreationTime = time(); $this->ModificationTime = time(); } /* * Data manipulation functions * * These convert various numbers and strings into the hexadecimal * format that is used internally to construct the file. We use hex * encoded strings since that is a lot easier to work with than binary * data in strings, and we can easily tell how big the true value is. * B64 encoding does some odd stuff, so we just make the memory * consumption grow tremendously and the complexity level drops * considerably. */ // Converts a byte and returns the value function Int8($value) { $value &= 0xFF; return sprintf("%02x", $value); } // Loads a single byte as a number from the file // Use if you want to make your own ReadFile function function LoadInt8($file) { if (is_resource($file)) $string = fread($file, 1); else $string = $file; return ord($string[0]); } // Converts an integer (two bytes) and returns the value function Int16($value) { $value &= 0xFFFF; return sprintf("%02x%02x", $value / 256, $value % 256); } // Loads two bytes as a number from the file // Use if you want to make your own ReadFile function function LoadInt16($file) { if (is_resource($file)) $string = fread($file, 2); else $string = $file; return ord($string[0]) * 256 + ord($string[1]); } // Converts an integer (three bytes) and returns the value function Int24($value) { $value &= 0xFFFFFF; return sprintf("%02x%02x%02x", $value / 65536, ($value / 256) % 256, $value % 256); } // Loads three bytes as a number from the file // Use if you want to make your own ReadFile function function LoadInt24($file) { if (is_resource($file)) $string = fread($file, 3); else $string = $file; return ord($string[0]) * 65536 + ord($string[1]) * 256 + ord($string[2]); } // Converts an integer (four bytes) and returns the value // 32-bit integers have problems with PHP when they are bigger than // 0x80000000 (about 2 billion) and that's why I don't use pack() here function Int32($value) { $negative = false; if ($value < 0) { $negative = true; $value = - $value; } $big = $value / 65536; settype($big, 'integer'); $little = $value - ($big * 65536); if ($negative) { // Little must contain a value $little = - $little; // Big might be zero, and should be 0xFFFF if that is the case. $big = 0xFFFF - $big; } $value = PalmDB::Int16($big) . PalmDB::Int16($little); return $value; } // Loads a four-byte string from a file into a number // Use if you want to make your own ReadFile function function LoadInt32($file) { if (is_resource($file)) $string = fread($file, 4); else $string = $file; $value = 0; $i = 0; while ($i < 4) { $value *= 256; $value += ord($string[$i]); $i ++; } return $value; } // Converts the number into a double and returns the encoded value // Not sure if this will work on all platforms. // Double(10.53) should return "40250f5c28f5c28f" function Double($value) { if ($this->DoubleMethod == PDB_DOUBLEMETHOD_UNTESTED) { $val = bin2hex(pack('d', 10.53)); $val = strtolower($val); if (substr($val, 0, 4) == '8fc2') $this->DoubleMethod = PDB_DOUBLEMETHOD_REVERSE; if (substr($val, 0, 4) == '4025') $this->DoubleMethod = PDB_DOUBLEMETHOD_NORMAL; if ($this->DoubleMethod == PDB_DOUBLEMETHOD_UNTESTED) $this->DoubleMethod = PDB_DOUBLEMETHOD_BROKEN; } if ($this->DoubleMethod == PDB_DOUBLEMETHOD_BROKEN) return '0000000000000000'; $value = bin2hex(pack('d', $value)); if ($this->DoubleMethod == PDB_DOUBLEMETHOD_REVERSE) $value = substr($value, 14, 2) . substr($value, 12, 2) . substr($value, 10, 2) . substr($value, 8, 2) . substr($value, 6, 2) . substr($value, 4, 2) . substr($value, 2, 2) . substr($value, 0, 2); return $value; } // The reverse? Not coded yet. // Use if you want to make your own ReadFile function function LoadDouble($file) { if (is_resource($file)) $string = fread($file, 8); else $string = $file; return 0; } // Converts a string into hexadecimal. // If $maxLen is specified and is greater than zero, the string is // trimmed and will contain up to $maxLen characters. // String("abcd", 2) will return "ab" hex encoded (a total of 4 // resulting bytes, but 2 encoded characters). // Returned string is *not* NULL-terminated. function String($value, $maxLen = false) { $value = bin2hex($value); if ($maxLen !== false && $maxLen > 0) $value = substr($value, 0, $maxLen * 2); return $value; } // Pads a hex-encoded value (typically a string) to a fixed size. // May grow too long if $value starts too long // $value = hex encoded value // $minLen = Append nulls to $value until it reaches $minLen // $minLen is the desired size of the string, unencoded. // PadString('6162', 3) results in '616200' (remember the hex encoding) function PadString($value, $minLen) { $PadBytes = '00000000000000000000000000000000'; $PadMe = $minLen - (strlen($value) / 2); while ($PadMe > 0) { if ($PadMe > 16) $value .= $PadBytes; else return $value . substr($PadBytes, 0, $PadMe * 2); $PadMe = $minLen - (strlen($value) / 2); } return $value; } /* * Record manipulation functions */ // Sets the current record pointer to the new record number if an // argument is passed in. // Returns the old record number (just in case you want to jump back) // Does not do basic record initialization if we are going to a new // record. function GoToRecord($num = false) { if ($num === false) return $this->CurrentRecord; if (gettype($num) == 'string' && ($num[0] == '+' || $num[0] == '-')) $num = $this->CurrentRecord + $num; $oldRecord = $this->CurrentRecord; $this->CurrentRecord = $num; return $oldRecord; } // Returns the size of the current record if no arguments. // Returns the size of the specified record if arguments. function GetRecordSize($num = false) { if ($num === false) $num = $this->CurrentRecord; if (! isset($this->Records[$num])) return 0; return strlen($this->Records[$num]) / 2; } // Adds to the current record. The input data must be already // hex encoded. Initializes the record if it doesn't exist. function AppendCurrent($value) { if (! isset($this->Records[$this->CurrentRecord])) $this->Records[$this->CurrentRecord] = ''; $this->Records[$this->CurrentRecord] .= $value; } // Adds a byte to the current record function AppendInt8($value) { $this->AppendCurrent($this->Int8($value)); } // Adds an integer (2 bytes) to the current record function AppendInt16($value) { $this->AppendCurrent($this->Int16($value)); } // Adds an integer (4 bytes) to the current record function AppendInt32($value) { $this->AppendCurrent($this->Int32($value)); } // Adds a double to the current record function AppendDouble($value) { $this->AppendCurrent($this->Double($value)); } // Adds a string (not NULL-terminated) function AppendString($value, $maxLen = false) { $this->AppendCurrent($this->String($value, $maxLen)); } // Returns true if the specified/current record exists and is set function RecordExists($Rec = false) { if ($Rec === false) $Rec = $this->CurrentRecord; if (isset($this->Records[$Rec])) return true; return false; } // Returns the hex-encoded data for the specified record or the current // record if not specified // This is nearly identical to GetRecordRaw except that this function // may be overridden by classes (see modules/doc.inc) and that there // should always be a function that will return the raw data of the // Records array. function GetRecordAttr($Rec = false) { if ($Rec === false) $Rec = $this->CurrentRecord; if (isset($this->RecordAttrs[$Rec])) { return $this->RecordAttrs[$Rec]; } return ''; } function CleanRecord($Rec = false) { if ($Rec === false) $Rec = $this->CurrentRecord; if (isset($this->RecordAttrs[$Rec])) { $this->RecordAttrs[$Rec] = $this->RecordAttrs[$Rec] & !PDB_RECORD_ATTRIB_DIRTY; } return ''; } function GetRecord($Rec = false) { if ($Rec === false) $Rec = $this->CurrentRecord; if (isset($this->Records[$Rec])) return $this->Records[$Rec]; return ''; } // Returns the raw data inside the current/specified record. Use this // for odd record types (like a Datebook record). Also, use this // instead of just using $PDB->Records[] directly. // Please do not override this function. function GetRecordRaw($Rec = false) { if ($Rec === false) $Rec = $this->CurrentRecord; if (isset($this->Records[$Rec])) return $this->Records[$Rec]; return false; } // Sets the hex-encoded data (or whatever) for the current record // Use this instead of the Append* functions if you have an odd // type of record (like a Datebook record). // Also, use this instead of just setting $PDB->Records[] // directly. // SetRecordRaw('data'); // SetRecordRaw(24, 'data'); (specifying the record num) function SetRecordRaw($A, $B = false, $C = 0) { if ($B === false) { $B = $A; $A = $this->CurrentRecord; } $this->Records[$A] = $B; $this->RecordAttrs[$A] = $C; } // Deletes the current record // You are urged to use GoToRecord() and jump to an existing record // after this function call so that the deleted record doesn't // get accidentally recreated/used -- all append functions will // create a new, empty record if the current record doesn't exist. function DeleteCurrentRecord() { if (isset($this->Records[$this->CurrentRecord])) unset($this->Records[$this->CurrentRecord]); if (isset($this->RecordAttrs[$this->CurrentRecord])) unset($this->RecordAttrs[$this->CurrentRecord]); } // Deletes the record // And sets current record to 0 // You are urged to use GoToRecord() and jump to an existing record // after this function call so that the deleted record doesn't // get accidentally recreated/used -- all append functions will // create a new, empty record if the current record doesn't exist. function DeleteRecord($Rec = false) { if ($Rec === false) $Rec = $this->CurrentRecord; if (isset($this->Records[$Rec])) unset($this->Records[$Rec]); if (isset($this->RecordAttrs[$Rec])) unset($this->RecordAttrs[$Rec]); $this->GoToRecord(0); } // Returns an array of available record IDs in the order they should // be written. // Probably should only be called within the class. function GetRecordIDs() { $keys = array_keys($this->Records); if (! is_array($keys) || count($keys) < 1) return array(); sort($keys, SORT_NUMERIC); return $keys; } // Returns the number of records. This should match the number of // keys returned by GetRecordIDs(). function GetRecordCount() { return count($this->Records); } // Returns the size of the AppInfo block. // Used only for writing function GetAppInfoSize() { if (! isset($this->AppInfo)) return 0; return strlen($this->AppInfo) / 2; } // Returns the AppInfo block (hex encoded) // Used only for writing function GetAppInfo() { if (! isset($this->AppInfo)) return 0; return $this->AppInfo; } // Returns the size of the SortInfo block // Used only for writing function GetSortInfoSize() { if (! isset($this->SortInfo)) return 0; return strlen($this->SortInfo) / 2; } // Returns the SortInfo block (hex encoded) // Used only for writing function GetSortInfo() { if (! isset($this->SortInfo)) return 0; return $this->SortInfo; } /* * Category Support */ // Creates the hex-encoded data to be stuck in the AppInfo // block if the database supports categories. // // Data format: // $categoryArray[id#] = name // Or: // $categoryArray[id#]['name'] = name // $categoryArray[id#]['renamed'] = true / false // // Tips: // * I'd suggest numbering your categories sequentially // * Do not have a category 0. It must always be 'Unfiled'. This // function will overwrite any category with the ID of 0. // * There is a maximum of 16 categories, including 'Unfiled'. // // Category 0 is reserved for 'Unfiled' // Categories 1-127 are used for handheld ID numbers // Categories 128-255 are used for desktop ID numbers // Do not let category numbers be created larger than 255 function CreateCategoryData($CategoryArray) { $CategoryArray[0] = array('Name' => 'Unfiled', 'Renamed' => false); $CatsWritten = 0; $LastIdWritten = 0; $RenamedFlags = 0; $CategoryStr = ''; $IdStr = ''; $keys = array_keys($CategoryArray); sort($keys); foreach ($keys as $id) { if ($CatsWritten < PDB_CATEGORY_NUM) { $CatsWritten ++; $LastIdWritten = $id; $RenamedFlags *= 2; if (is_array($CategoryArray[$id]) && isset($CategoryArray[$id]['Renamed']) && $CategoryArray[$id]['Renamed']) $RenamedFlags += 1; $name = ''; if (is_array($CategoryArray[$id])) { if (isset($CategoryArray[$id]['Name'])) $name = $CategoryArray[$id]['Name']; } else $name = $CategoryArray[$id]; $name = $this->String($name, PDB_CATEGORY_NAME_LENGTH); $CategoryStr .= $this->PadString($name, PDB_CATEGORY_NAME_LENGTH); $IdStr .= $this->Int8($id); } } while ($CatsWritten < PDB_CATEGORY_NUM) { $CatsWritten ++; $LastIdWritten ++; $RenamedFlags *= 2; $CategoryStr .= $this->PadString('', PDB_CATEGORY_NAME_LENGTH); $IdStr .= $this->Int8($LastIdWritten); } $TrailingBytes = $this->Int8($LastIdWritten); $TrailingBytes .= $this->Int8(0); // Error checking if ($LastIdWritten >= 256) return $this->PadString('', PDB_CATEGORY_SIZE); return $this->Int16($RenamedFlags) . $CategoryStr . $IdStr . $TrailingBytes; } // This should be called by other subclasses that use category support // It returns a category array. Each element in the array is another // array with the key 'name' set to the name of the category and // the key 'renamed' set to the renamed flag for that category. function LoadCategoryData($fileData) { $RenamedFlags = $this->LoadInt16(substr($fileData, 0, 2)); $Offset = 2; $StartingFlag = 65536; $Categories = array(); while ($StartingFlag > 1) { $StartingFlag /= 2; $Name = substr($fileData, $Offset, PDB_CATEGORY_NAME_LENGTH); $i = 0; while ($i < PDB_CATEGORY_NAME_LENGTH && $Name[$i] != "\0") $i ++; if ($i == 0) $Name = ''; elseif ($i < PDB_CATEGORY_NAME_LENGTH) $Name = substr($Name, 0, $i); if ($RenamedFlags & $StartingFlag) $RenamedFlag = true; else $RenamedFlag = false; $Categories[] = array('Name' => $Name, 'Renamed' => $RenamedFlag); $Offset += PDB_CATEGORY_NAME_LENGTH; } $CategoriesParsed = array(); foreach ($Categories as $CategoryData) { $UID = $this->LoadInt8(substr($fileData, $Offset, 1)); $Offset ++; if ($CategoryData['Name'] != '') $CategoriesParsed[$UID] = $CategoryData; } // Ignore the last ID return $CategoriesParsed; } /* * Database Writing Functions */ // *NEW* // Takes a hex-encoded string and makes sure that when decoded, the data // lies on a four-byte boundary. If it doesn't, it pads the string with // NULLs /* * Commented out because we don't use this function currently. * It is part of a test to see what is needed to get files to sync * properly with Desktop 4.0 * function PadTo4ByteBoundary($string) { while ((strlen($string)/2) % 4) { $string .= '00'; } return $string; } * */ // Returns the hex encoded header of the pdb file // Header = name, attributes, version, creation/modification/backup // dates, modification number, some offsets, record offsets, // record attributes, appinfo block, sortinfo block // Shouldn't be called from outside the class function MakeHeader() { // 32 bytes = name, but only 31 available (one for null) $header = $this->String($this->Name, 31); $header = $this->PadString($header, 32); // Attributes & version fields $header .= $this->Int16($this->Attributes); $header .= $this->Int16($this->Version); // Creation, modification, and backup date if ($this->CreationTime != 0) $header .= $this->Int32($this->CreationTime + PDB_EPOCH_1904); else $header .= $this->Int32(time() + PDB_EPOCH_1904); if ($this->ModificationTime != 0) $header .= $this->Int32($this->ModificationTime + PDB_EPOCH_1904); else $header .= $this->Int32(time() + PDB_EPOCH_1904); if ($this->BackupTime != 0) $header .= $this->Int32($this->BackupTime + PDB_EPOCH_1904); else $header .= $this->Int32(0); // Calculate the initial offset $Offset = PDB_HEADER_SIZE + PDB_INDEX_HEADER_SIZE; $Offset += PDB_RECORD_HEADER_SIZE * count($this->GetRecordIDs()); // Modification number, app information id, sort information id $header .= $this->Int32($this->ModNumber); $AppInfo_Size = $this->GetAppInfoSize(); if ($AppInfo_Size > 0) { $header .= $this->Int32($Offset); $Offset += $AppInfo_Size; } else $header .= $this->Int32(0); $SortInfo_Size = $this->GetSortInfoSize(); if ($SortInfo_Size > 0) { $header .= $this->Int32($Offset); $Offset += $SortInfo_Size; } else $header .= $this->Int32(0); // Type, creator $header .= $this->String($this->TypeID, 4); $header .= $this->String($this->CreatorID, 4); // Unique ID seed $header .= $this->Int32(0); // next record list $header .= $this->Int32(0); // Number of records $header .= $this->Int16($this->GetRecordCount()); // Compensate for the extra 2 NULL characters in the $Offset $Offset += 2; // Dump each record if ($this->GetRecordCount() != 0) { $keys = $this->GetRecordIDs(); sort($keys, SORT_NUMERIC); foreach ($keys as $index) { $header .= $this->Int32($Offset); if (isset($this->RecordAttrs[$index])) $header .= $this->Int8($this->RecordAttrs[$index]); else $header .= $this->Int8(0); // The unique id is just going to be the record number $header .= $this->Int24($index); $Offset += $this->GetRecordSize($index); // *new* method 3 //$Mod4 = $Offset % 4; //if ($Mod4) // $Offset += 4 - $Mod4; } } // AppInfo and SortInfo blocks go here if ($AppInfo_Size > 0) // *new* method 1 $header .= $this->GetAppInfo(); //$header .= $this->PadTo4ByteBoundary($this->GetAppInfo()); if ($SortInfo_Size > 0) // *new* method 2 $header .= $this->GetSortInfo(); //$header .= $this->PadTo4ByteBoundary($this->GetSortInfo()); // These are the mysterious two NULL characters that we need $header .= $this->Int16(0); return $header; } // Writes the database to the file handle specified. // Use this function like this: // $file = fopen("output.pdb", "wb"); // // "wb" = write binary for non-Unix systems // if (! $file) { // echo "big problem -- can't open file"; // exit; // } // $pdb->WriteToFile($file); // fclose($file); function WriteToFile($file) { $header = $this->MakeHeader(); fwrite($file, pack('H*', $header), strlen($header) / 2); $keys = $this->GetRecordIDs(); sort($keys, SORT_NUMERIC); foreach ($keys as $index) { // *new* method 3 //$data = $this->PadTo4ByteBoundary($this->GetRecord($index)); $data = $this->GetRecordRaw($index); //echo $data . '
'; fwrite($file, pack('H*', $data), strlen($data) / 2); } fflush($file); } // Writes the database to the standard output (like echo). // Can be trapped with output buffering function WriteToStdout() { // You'd think these three lines would work. // If someone can figure out why they don't, please tell me. // // $fp = fopen('php://stdout', 'wb'); // $this->WriteToFile($fp); // fclose($fp); $header = $this->MakeHeader(); echo pack("H*", $header); $keys = $this->GetRecordIDs(); sort($keys, SORT_NUMERIC); foreach ($keys as $index) { // *new* method 3 $data = $this->GetRecord($index); //$data = $this->PadTo4ByteBoundary($this->GetRecord($index)); echo pack("H*", $data); } } // Writes the database to the standard output (like echo) but also // writes some headers so that the browser should prompt to save the // file properly. // // Use this only if you didn't send any content and you only want the // PHP script to output the PDB file and nothing else. An example // would be if you wanted to have 'download' link so the user can // stick the information they are currently viewing and transfer // it easily into their handheld. // // $filename is the desired filename to download the database as. // For example, DownloadPDB('memos.pdb'); function DownloadPDB($filename) { global $HTTP_USER_AGENT; // Alter the filename to only allow certain characters. // Some platforms and some browsers don't respond well if // there are illegal characters (such as spaces) in the name of // the file being downloaded. $filename = preg_replace('/[^-a-zA-Z0-9\\.]/', '_', $filename); if (strstr($HTTP_USER_AGENT, 'compatible; MSIE ') !== false && strstr($HTTP_USER_AGENT, 'Opera') === false) { // IE doesn't properly download attachments. This should work // pretty well for IE 5.5 SP 1 header("Content-Disposition: inline; filename=$filename"); header("Content-Type: application/download; name=\"$filename\""); } else { // Use standard headers for Netscape, Opera, etc. header("Content-Disposition: attachment; filename=\"$filename\""); header("Content-Type: application/x-pilot; name=\"$filename\""); } $this->WriteToStdout(); } /* * Loading in a database */ // Reads data from the file and tries to load it properly // $file is the already-opened file handle. // Returns false if no error function ReadFile($file, $headeronly=0) { // 32 bytes = name, but only 31 available $this->Name = fread($file, 32); $i = 0; while ($i < 32 && $this->Name[$i] != "\0") $i ++; $this->Name = substr($this->Name, 0, $i); $this->Attributes = $this->LoadInt16($file); $this->Version = $this->LoadInt16($file); $this->CreationTime = $this->LoadInt32($file); if ($this->CreationTime != 0) $this->CreationTime -= PDB_EPOCH_1904; if ($this->CreationTime < 0) $this->CreationTime = 0; $this->ModificationTime = $this->LoadInt32($file); if ($this->ModificationTime != 0) $this->ModificationTime -= PDB_EPOCH_1904; if ($this->ModificationTime < 0) $this->ModificationTime = 0; $this->BackupTime = $this->LoadInt32($file); if ($this->BackupTime != 0) $this->BackupTime -= PDB_EPOCH_1904; if ($this->BackupTime < 0) $this->BackupTime = 0; // Modification number $this->ModNumber = $this->LoadInt32($file); // AppInfo and SortInfo size $AppInfoOffset = $this->LoadInt32($file); $SortInfoOffset = $this->LoadInt32($file); // Type, creator $this->TypeID = fread($file, 4); $this->CreatorID = fread($file, 4); // Skip unique ID seed fread($file, 4); // skip next record list (hope that's ok) fread($file, 4); $RecCount = $this->LoadInt16($file); $RecordData = array(); while ($RecCount > 0) { $RecCount --; $Offset = $this->LoadInt32($file); $Attrs = $this->LoadInt8($file); $UID = $this->LoadInt24($file); if($headeronly!=0) { // laduje tylko artybuty, nic wiêcej $this->Records[$UID] = ''; $this->RecordAttrs[$UID] = $Attrs; } else { $RecordData[] = array('Offset' => $Offset, 'Attrs' => $Attrs, 'UID' => $UID); } } if($headeronly!=0) return false; // Create the offset list if ($AppInfoOffset != 0) $OffsetList[$AppInfoOffset] = 'AppInfo'; if ($SortInfoOffset != 0) $OffsetList[$SortInfoOffset] = 'SortInfo'; foreach ($RecordData as $data) { $OffsetList[$data['Offset']] = array('Record', $data); // echo $OffsetList[$data['Offset']][0] . ' ' .$OffsetList[$data['Offset']][1] . '
'; } fseek($file, 0, SEEK_END); $OffsetList[ftell($file)] = 'EOF'; // Parse each chunk ksort($OffsetList); $Offsets = array_keys($OffsetList); while (count($Offsets) > 1) { // Don't use the EOF (which should be the last offset) $ThisOffset = $Offsets[0]; $NextOffset = $Offsets[1]; if ($OffsetList[$ThisOffset] == 'EOF') // Messed up file. Stop here. return true; $FuncName = 'Load'; if (is_array($OffsetList[$ThisOffset])) { $FuncName .= $OffsetList[$ThisOffset][0]; $extraData = $OffsetList[$ThisOffset][1]; // echo $FuncName . ' ' . $extraData . '
' ; } else { $FuncName .= $OffsetList[$ThisOffset]; $extraData = false; } fseek($file, $ThisOffset); $fileData = fread($file, $NextOffset - $ThisOffset); if ($this->$FuncName($fileData, $extraData)) return -2; array_shift($Offsets); } return false; } // Generic function to load the AppInfo block into $this->AppInfo // Should only be called within this class // Return false to signal no error function LoadAppInfo($fileData) { $this->AppInfo = bin2hex($fileData); return false; } // Generic function to load the SortInfo block into $this->SortInfo // Should only be called within this class // Return false to signal no error function LoadSortInfo($fileData) { $this->SortInfo = bin2hex($fileData); return false; } // Generic function to load a record // Should only be called within this class // Return false to signal no error function LoadRecord($fileData, $recordInfo) { $this->Records[$recordInfo['UID']] = bin2hex($fileData); $this->RecordAttrs[$recordInfo['UID']] = $recordInfo['Attrs']; //echo $recordInfo['UID'] . ' ' . $this->Records[$recordInfo['UID']] . ' ' . $this->RecordAttrs[$recordInfo['UID']] . '
'; return false; } } ?>