public function Analyze() { $getid3 = $this->getid3; // dependency $getid3->include_module('tag.id3v1'); if ($getid3->option_tags_images) { $getid3->include_module('lib.image_size'); } // Overall tag structure: // +-----------------------------+ // | Header (10 bytes) | // +-----------------------------+ // | Extended Header | // | (variable length, OPTIONAL) | // +-----------------------------+ // | Frames (variable length) | // +-----------------------------+ // | Padding | // | (variable length, OPTIONAL) | // +-----------------------------+ // | Footer (10 bytes, OPTIONAL) | // +-----------------------------+ // // Header // ID3v2/file identifier "ID3" // ID3v2 version $04 00 // ID3v2 flags (%ab000000 in v2.2, %abc00000 in v2.3, %abcd0000 in v2.4.x) // ID3v2 size 4 * %0xxxxxxx // shortcuts $getid3->info['id3v2']['header'] = true; $info_id3v2 =& $getid3->info['id3v2']; $info_id3v2['flags'] = array(); $info_id3v2_flags =& $info_id3v2['flags']; $this->fseek($this->option_starting_offset, SEEK_SET); $header = $this->fread(10); if (substr($header, 0, 3) == 'ID3' && strlen($header) == 10) { $info_id3v2['majorversion'] = ord($header[3]); $info_id3v2['minorversion'] = ord($header[4]); // shortcut $id3v2_major_version =& $info_id3v2['majorversion']; } else { unset($getid3->info['id3v2']); return false; } if ($id3v2_major_version > 4) { // this script probably won't correctly parse ID3v2.5.x and above (if it ever exists) throw new getid3_exception('this script only parses up to ID3v2.4.x - this tag is ID3v2.' . $id3v2_major_version . '.' . $info_id3v2['minorversion']); } $id3_flags = ord($header[5]); switch ($id3v2_major_version) { case 2: // %ab000000 in v2.2 $info_id3v2_flags['unsynch'] = (bool) ($id3_flags & 0x80); // a - Unsynchronisation $info_id3v2_flags['compression'] = (bool) ($id3_flags & 0x40); // b - Compression break; case 3: // %abc00000 in v2.3 $info_id3v2_flags['unsynch'] = (bool) ($id3_flags & 0x80); // a - Unsynchronisation $info_id3v2_flags['exthead'] = (bool) ($id3_flags & 0x40); // b - Extended header $info_id3v2_flags['experim'] = (bool) ($id3_flags & 0x20); // c - Experimental indicator break; case 4: // %abcd0000 in v2.4 $info_id3v2_flags['unsynch'] = (bool) ($id3_flags & 0x80); // a - Unsynchronisation $info_id3v2_flags['exthead'] = (bool) ($id3_flags & 0x40); // b - Extended header $info_id3v2_flags['experim'] = (bool) ($id3_flags & 0x20); // c - Experimental indicator $info_id3v2_flags['isfooter'] = (bool) ($id3_flags & 0x10); // d - Footer present break; } $info_id3v2['headerlength'] = getid3_lib::BigEndianSyncSafe2Int(substr($header, 6, 4)) + 10; // length of ID3v2 tag in 10-byte header doesn't include 10-byte header length $info_id3v2['tag_offset_start'] = $this->option_starting_offset; $info_id3v2['tag_offset_end'] = $info_id3v2['tag_offset_start'] + $info_id3v2['headerlength']; // Frames // All ID3v2 frames consists of one frame header followed by one or more // fields containing the actual information. The header is always 10 // bytes and laid out as follows: // // Frame ID $xx xx xx xx (four characters) // Size 4 * %0xxxxxxx // Flags $xx xx $size_of_frames = $info_id3v2['headerlength'] - 10; // not including 10-byte initial header if (@$info_id3v2['exthead']['length']) { $size_of_frames -= $info_id3v2['exthead']['length'] + 4; } if (@$info_id3v2_flags['isfooter']) { $size_of_frames -= 10; // footer takes last 10 bytes of ID3v2 header, after frame data, before audio } if ($size_of_frames > 0) { $frame_data = $this->fread($size_of_frames); // read all frames from file into $frame_data variable // if entire frame data is unsynched, de-unsynch it now (ID3v2.3.x) if (@$info_id3v2_flags['unsynch'] && $id3v2_major_version <= 3) { $frame_data = str_replace("ÿ", "ÿ", $frame_data); } // [in ID3v2.4.0] Unsynchronisation [S:6.1] is done on frame level, instead // of on tag level, making it easier to skip frames, increasing the streamability // of the tag. The unsynchronisation flag in the header [S:3.1] indicates that // there exists an unsynchronised frame, while the new unsynchronisation flag in // the frame header [S:4.1.2] indicates unsynchronisation. //$frame_data_offset = 10 + (@$info_id3v2['exthead']['length'] ? $info_id3v2['exthead']['length'] + 4 : 0); // how many bytes into the stream - start from after the 10-byte header (and extended header length+4, if present) $frame_data_offset = 10; // how many bytes into the stream - start from after the 10-byte header // Extended Header if (@$info_id3v2_flags['exthead']) { $extended_header_offset = 0; if ($id3v2_major_version == 3) { // v2.3 definition: //Extended header size $xx xx xx xx // 32-bit integer //Extended Flags $xx xx // %x0000000 %00000000 // v2.3 // x - CRC data present //Size of padding $xx xx xx xx $info_id3v2['exthead']['length'] = getid3_lib::BigEndian2Int(substr($frame_data, $extended_header_offset, 4), 0); $extended_header_offset += 4; $info_id3v2['exthead']['flag_bytes'] = 2; $info_id3v2['exthead']['flag_raw'] = getid3_lib::BigEndian2Int(substr($frame_data, $extended_header_offset, $info_id3v2['exthead']['flag_bytes'])); $extended_header_offset += $info_id3v2['exthead']['flag_bytes']; $info_id3v2['exthead']['flags']['crc'] = (bool) ($info_id3v2['exthead']['flag_raw'] & 0x8000); $info_id3v2['exthead']['padding_size'] = getid3_lib::BigEndian2Int(substr($frame_data, $extended_header_offset, 4)); $extended_header_offset += 4; if ($info_id3v2['exthead']['flags']['crc']) { $info_id3v2['exthead']['flag_data']['crc'] = getid3_lib::BigEndian2Int(substr($frame_data, $extended_header_offset, 4)); $extended_header_offset += 4; } $extended_header_offset += $info_id3v2['exthead']['padding_size']; } elseif ($id3v2_major_version == 4) { // v2.4 definition: //Extended header size 4 * %0xxxxxxx // 28-bit synchsafe integer //Number of flag bytes $01 //Extended Flags $xx // %0bcd0000 // v2.4 // b - Tag is an update // Flag data length $00 // c - CRC data present // Flag data length $05 // Total frame CRC 5 * %0xxxxxxx // d - Tag restrictions // Flag data length $01 $info_id3v2['exthead']['length'] = getid3_lib::BigEndian2Int(substr($frame_data, $extended_header_offset, 4), 1); $extended_header_offset += 4; $info_id3v2['exthead']['flag_bytes'] = 1; $info_id3v2['exthead']['flag_raw'] = getid3_lib::BigEndian2Int(substr($frame_data, $extended_header_offset, $info_id3v2['exthead']['flag_bytes'])); $extended_header_offset += $info_id3v2['exthead']['flag_bytes']; $info_id3v2['exthead']['flags']['update'] = (bool) ($info_id3v2['exthead']['flag_raw'] & 0x4000); $info_id3v2['exthead']['flags']['crc'] = (bool) ($info_id3v2['exthead']['flag_raw'] & 0x2000); $info_id3v2['exthead']['flags']['restrictions'] = (bool) ($info_id3v2['exthead']['flag_raw'] & 0x1000); if ($info_id3v2['exthead']['flags']['crc']) { $info_id3v2['exthead']['flag_data']['crc'] = getid3_lib::BigEndian2Int(substr($frame_data, $extended_header_offset, 5), 1); $extended_header_offset += 5; } if ($info_id3v2['exthead']['flags']['restrictions']) { // %ppqrrstt $restrictions_raw = getid3_lib::BigEndian2Int(substr($frame_data, $extended_header_offset, 1)); $extended_header_offset += 1; $info_id3v2['exthead']['flags']['restrictions']['tagsize'] = ($restrictions_raw && 0xc0) >> 6; // p - Tag size restrictions $info_id3v2['exthead']['flags']['restrictions']['textenc'] = ($restrictions_raw && 0x20) >> 5; // q - Text encoding restrictions $info_id3v2['exthead']['flags']['restrictions']['textsize'] = ($restrictions_raw && 0x18) >> 3; // r - Text fields size restrictions $info_id3v2['exthead']['flags']['restrictions']['imgenc'] = ($restrictions_raw && 0x4) >> 2; // s - Image encoding restrictions $info_id3v2['exthead']['flags']['restrictions']['imgsize'] = ($restrictions_raw && 0x3) >> 0; // t - Image size restrictions } } $frame_data_offset += $extended_header_offset; $frame_data = substr($frame_data, $extended_header_offset); } // end extended header while (isset($frame_data) && strlen($frame_data) > 0) { // cycle through until no more frame data is left to parse if (strlen($frame_data) <= ($id3v2_major_version == 2 ? 6 : 10)) { // insufficient room left in ID3v2 header for actual data - must be padding $info_id3v2['padding']['start'] = $frame_data_offset; $info_id3v2['padding']['length'] = strlen($frame_data); $info_id3v2['padding']['valid'] = true; for ($i = 0; $i < $info_id3v2['padding']['length']; $i++) { if ($frame_data[$i] != "") { $info_id3v2['padding']['valid'] = false; $info_id3v2['padding']['errorpos'] = $info_id3v2['padding']['start'] + $i; $getid3->warning('Invalid ID3v2 padding found at offset ' . $info_id3v2['padding']['errorpos'] . ' (the remaining ' . ($info_id3v2['padding']['length'] - $i) . ' bytes are considered invalid)'); break; } } break; // skip rest of ID3v2 header } if ($id3v2_major_version == 2) { // Frame ID $xx xx xx (three characters) // Size $xx xx xx (24-bit integer) // Flags $xx xx $frame_header = substr($frame_data, 0, 6); // take next 6 bytes for header $frame_data = substr($frame_data, 6); // and leave the rest in $frame_data $frame_name = substr($frame_header, 0, 3); $frame_size = getid3_lib::BigEndian2Int(substr($frame_header, 3, 3)); $frame_flags = 0; // not used for anything in ID3v2.2, just set to avoid E_NOTICEs } elseif ($id3v2_major_version > 2) { // Frame ID $xx xx xx xx (four characters) // Size $xx xx xx xx (32-bit integer in v2.3, 28-bit synchsafe in v2.4+) // Flags $xx xx $frame_header = substr($frame_data, 0, 10); // take next 10 bytes for header $frame_data = substr($frame_data, 10); // and leave the rest in $frame_data $frame_name = substr($frame_header, 0, 4); if ($id3v2_major_version == 3) { $frame_size = getid3_lib::BigEndian2Int(substr($frame_header, 4, 4)); // 32-bit integer } else { // ID3v2.4+ $frame_size = getid3_lib::BigEndianSyncSafe2Int(substr($frame_header, 4, 4)); // 32-bit synchsafe integer (28-bit value) } if ($frame_size < strlen($frame_data) + 4) { $nextFrameID = substr($frame_data, $frame_size, 4); if (getid3_id3v2::IsValidID3v2FrameName($nextFrameID, $id3v2_major_version)) { // next frame is OK } elseif ($frame_name == "" . 'MP3' || $frame_name == "" . 'MP' || $frame_name == ' MP3' || $frame_name == 'MP3e') { // MP3ext known broken frames - "ok" for the purposes of this test } elseif ($id3v2_major_version == 4 && getid3_id3v2::IsValidID3v2FrameName(substr($frame_data, getid3_lib::BigEndian2Int(substr($frame_header, 4, 4)), 4), 3)) { $getid3->warning('ID3v2 tag written as ID3v2.4, but with non-synchsafe integers (ID3v2.3 style). Older versions of (Helium2; iTunes) are known culprits of this. Tag has been parsed as ID3v2.3'); $id3v2_major_version = 3; $frame_size = getid3_lib::BigEndian2Int(substr($frame_header, 4, 4)); // 32-bit integer } } $frame_flags = getid3_lib::BigEndian2Int(substr($frame_header, 8, 2)); } if ($id3v2_major_version == 2 && $frame_name == "" || $frame_name == "") { // padding encountered $info_id3v2['padding']['start'] = $frame_data_offset; $info_id3v2['padding']['length'] = strlen($frame_header) + strlen($frame_data); $info_id3v2['padding']['valid'] = true; $len = strlen($frame_data); for ($i = 0; $i < $len; $i++) { if ($frame_data[$i] != "") { $info_id3v2['padding']['valid'] = false; $info_id3v2['padding']['errorpos'] = $info_id3v2['padding']['start'] + $i; $getid3->warning('Invalid ID3v2 padding found at offset ' . $info_id3v2['padding']['errorpos'] . ' (the remaining ' . ($info_id3v2['padding']['length'] - $i) . ' bytes are considered invalid)'); break; } } break; // skip rest of ID3v2 header } if ($frame_name == 'COM ') { $getid3->warning('error parsing "' . $frame_name . '" (' . $frame_data_offset . ' bytes into the ID3v2.' . $id3v2_major_version . ' tag). (ERROR: IsValidID3v2FrameName("' . str_replace("", ' ', $frame_name) . '", ' . $id3v2_major_version . '))). [Note: this particular error has been known to happen with tags edited by iTunes (versions "X v2.0.3", "v3.0.1" are known-guilty, probably others too)]'); $frame_name = 'COMM'; } if ($frame_size <= strlen($frame_data) && getid3_id3v2::IsValidID3v2FrameName($frame_name, $id3v2_major_version)) { unset($parsed_frame); $parsed_frame['frame_name'] = $frame_name; $parsed_frame['frame_flags_raw'] = $frame_flags; $parsed_frame['data'] = substr($frame_data, 0, $frame_size); $parsed_frame['datalength'] = (int) $frame_size; $parsed_frame['dataoffset'] = $frame_data_offset; $this->ParseID3v2Frame($parsed_frame); $info_id3v2[$frame_name][] = $parsed_frame; $frame_data = substr($frame_data, $frame_size); } else { // invalid frame length or FrameID if ($frame_size <= strlen($frame_data)) { if (getid3_id3v2::IsValidID3v2FrameName(substr($frame_data, $frame_size, 4), $id3v2_major_version)) { // next frame is valid, just skip the current frame $frame_data = substr($frame_data, $frame_size); $getid3->warning('Next ID3v2 frame is valid, skipping current frame.'); } else { // next frame is invalid too, abort processing throw new getid3_exception('Next ID3v2 frame is also invalid, aborting processing.'); } } elseif ($frame_size == strlen($frame_data)) { // this is the last frame, just skip $getid3->warning('This was the last ID3v2 frame.'); } else { // next frame is invalid too, abort processing $frame_data = null; $getid3->warning('Invalid ID3v2 frame size, aborting.'); } if (!getid3_id3v2::IsValidID3v2FrameName($frame_name, $id3v2_major_version)) { switch ($frame_name) { case "" . 'MP': case "" . 'MP3': case ' MP3': case 'MP3e': case "" . 'MP': case ' MP': case 'MP3': $getid3->warning('error parsing "' . $frame_name . '" (' . $frame_data_offset . ' bytes into the ID3v2.' . $id3v2_major_version . ' tag). (ERROR: !IsValidID3v2FrameName("' . str_replace("", ' ', $frame_name) . '", ' . $id3v2_major_version . '))). [Note: this particular error has been known to happen with tags edited by "MP3ext (www.mutschler.de/mp3ext/)"]'); break; default: $getid3->warning('error parsing "' . $frame_name . '" (' . $frame_data_offset . ' bytes into the ID3v2.' . $id3v2_major_version . ' tag). (ERROR: !IsValidID3v2FrameName("' . str_replace("", ' ', $frame_name) . '", ' . $id3v2_major_version . '))).'); break; } } elseif ($frame_size > strlen(@$frame_data)) { throw new getid3_exception('error parsing "' . $frame_name . '" (' . $frame_data_offset . ' bytes into the ID3v2.' . $id3v2_major_version . ' tag). (ERROR: $frame_size (' . $frame_size . ') > strlen($frame_data) (' . strlen($frame_data) . ')).'); } else { throw new getid3_exception('error parsing "' . $frame_name . '" (' . $frame_data_offset . ' bytes into the ID3v2.' . $id3v2_major_version . ' tag).'); } } $frame_data_offset += $frame_size + ($id3v2_major_version == 2 ? 6 : 10); } } // Footer // The footer is a copy of the header, but with a different identifier. // ID3v2 identifier "3DI" // ID3v2 version $04 00 // ID3v2 flags %abcd0000 // ID3v2 size 4 * %0xxxxxxx if (isset($info_id3v2_flags['isfooter']) && $info_id3v2_flags['isfooter']) { $footer = fread($getid3->fp, 10); if (substr($footer, 0, 3) == '3DI') { $info_id3v2['footer'] = true; $info_id3v2['majorversion_footer'] = ord($footer[3]); $info_id3v2['minorversion_footer'] = ord($footer[4]); } if ($info_id3v2['majorversion_footer'] <= 4) { $id3_flags = ord($footer[5]); $info_id3v2_flags['unsynch_footer'] = (bool) ($id3_flags & 0x80); $info_id3v2_flags['extfoot_footer'] = (bool) ($id3_flags & 0x40); $info_id3v2_flags['experim_footer'] = (bool) ($id3_flags & 0x20); $info_id3v2_flags['isfooter_footer'] = (bool) ($id3_flags & 0x10); $info_id3v2['footerlength'] = getid3_lib::BigEndianSyncSafe2Int(substr($footer, 6, 4)); } } // end footer if (isset($info_id3v2['comments']['genre'])) { foreach ($info_id3v2['comments']['genre'] as $key => $value) { unset($info_id3v2['comments']['genre'][$key]); $info_id3v2['comments'] = getid3_id3v2::array_merge_noclobber($info_id3v2['comments'], getid3_id3v2::ParseID3v2GenreString($value)); } } if (isset($info_id3v2['comments']['track'])) { foreach ($info_id3v2['comments']['track'] as $key => $value) { if (strstr($value, '/')) { list($info_id3v2['comments']['track'][$key], $info_id3v2['comments']['totaltracks'][$key]) = explode('/', $info_id3v2['comments']['track'][$key]); } } } // Use year from recording time if year not set if (!isset($info_id3v2['comments']['year']) && ereg('^([0-9]{4})', @$info_id3v2['comments']['recording_time'][0], $matches)) { $info_id3v2['comments']['year'] = array($matches[1]); } // Set avdataoffset $getid3->info['avdataoffset'] = $info_id3v2['headerlength']; if (isset($info_id3v2['footer'])) { $getid3->info['avdataoffset'] += 10; } return true; }
function GenerateID3v2Tag($noerrorsonly = true) { $this->ID3v2FrameIsAllowed(null, ''); // clear static array in case this isn't the first call to $this->GenerateID3v2Tag() $tagstring = ''; if (is_array($this->tag_data)) { foreach ($this->tag_data as $frame_name => $frame_rawinputdata) { foreach ($frame_rawinputdata as $irrelevantindex => $source_data_array) { if (getid3_id3v2::IsValidID3v2FrameName($frame_name, $this->majorversion)) { unset($frame_length); unset($frame_flags); $frame_data = false; if ($this->ID3v2FrameIsAllowed($frame_name, $source_data_array)) { if ($frame_data = $this->GenerateID3v2FrameData($frame_name, $source_data_array)) { $FrameUnsynchronisation = false; if ($this->majorversion >= 4) { // frame-level unsynchronisation $unsynchdata = $frame_data; if ($this->id3v2_use_unsynchronisation) { $unsynchdata = $this->Unsynchronise($frame_data); } if (strlen($unsynchdata) != strlen($frame_data)) { // unsynchronisation needed $FrameUnsynchronisation = true; $frame_data = $unsynchdata; if (isset($TagUnsynchronisation) && $TagUnsynchronisation === false) { // only set to true if ALL frames are unsynchronised } else { $TagUnsynchronisation = true; } } else { if (isset($TagUnsynchronisation)) { $TagUnsynchronisation = false; } } unset($unsynchdata); $frame_length = getid3_lib::BigEndian2String(strlen($frame_data), 4, true); } else { $frame_length = getid3_lib::BigEndian2String(strlen($frame_data), 4, false); } $frame_flags = $this->GenerateID3v2FrameFlags($this->ID3v2FrameFlagsLookupTagAlter($frame_name), $this->ID3v2FrameFlagsLookupFileAlter($frame_name), false, false, false, false, $FrameUnsynchronisation, false); } } else { $this->errors[] = 'Frame "' . $frame_name . '" is NOT allowed'; } if ($frame_data === false) { $this->errors[] = '$this->GenerateID3v2FrameData() failed for "' . $frame_name . '"'; if ($noerrorsonly) { return false; } else { unset($frame_name); } } } else { // ignore any invalid frame names, including 'title', 'header', etc $this->warnings[] = 'Ignoring invalid ID3v2 frame type: "' . $frame_name . '"'; unset($frame_name); unset($frame_length); unset($frame_flags); unset($frame_data); } if (isset($frame_name) && isset($frame_length) && isset($frame_flags) && isset($frame_data)) { $tagstring .= $frame_name . $frame_length . $frame_flags . $frame_data; } } } if (!isset($TagUnsynchronisation)) { $TagUnsynchronisation = false; } if ($this->majorversion <= 3 && $this->id3v2_use_unsynchronisation) { // tag-level unsynchronisation $unsynchdata = $this->Unsynchronise($tagstring); if (strlen($unsynchdata) != strlen($tagstring)) { // unsynchronisation needed $TagUnsynchronisation = true; $tagstring = $unsynchdata; } } while ($this->paddedlength < strlen($tagstring) + getid3_id3v2::ID3v2HeaderLength($this->majorversion)) { $this->paddedlength += 1024; } $footer = false; // ID3v2 footers not yet supported in getID3() if (!$footer && $this->paddedlength > strlen($tagstring) + getid3_id3v2::ID3v2HeaderLength($this->majorversion)) { // pad up to $paddedlength bytes if unpadded tag is shorter than $paddedlength // "Furthermore it MUST NOT have any padding when a tag footer is added to the tag." $tagstring .= @str_repeat("", $this->paddedlength - strlen($tagstring) - getid3_id3v2::ID3v2HeaderLength($this->majorversion)); } if ($this->id3v2_use_unsynchronisation && substr($tagstring, strlen($tagstring) - 1, 1) == "ÿ") { // special unsynchronisation case: // if last byte == $FF then appended a $00 $TagUnsynchronisation = true; $tagstring .= ""; } $tagheader = 'ID3'; $tagheader .= chr($this->majorversion); $tagheader .= chr($this->minorversion); $tagheader .= $this->GenerateID3v2TagFlags(array('unsynchronisation' => $TagUnsynchronisation)); $tagheader .= getid3_lib::BigEndian2String(strlen($tagstring), 4, true); return $tagheader . $tagstring; } $this->errors[] = 'tag_data is not an array in GenerateID3v2Tag()'; return false; }