/** * Check an addon's file to extract the addon information out of it. * * @param string $Path The path to the file. * @param bool $Fix Whether or not to fix files that have been zipped incorrectly. * @return array An array of addon information. */ public static function AnalyzeAddon($Path, $Fix = FALSE, $ThrowError = TRUE) { $Result = array(); // Extract the zip file so we can make sure it has appropriate information. $Zip = NULL; if (class_exists('ZipArchive', FALSE)) { $Zip = new ZipArchive(); $ZipOpened = $Zip->open($Path); if ($ZipOpened !== TRUE) { $Zip = NULL; } } if (!$Zip) { require_once PATH_LIBRARY . "/vendors/pclzip/class.pclzipadapter.php"; $Zip = new PclZipAdapter(); $ZipOpened = $Zip->open($Path); } if ($ZipOpened !== TRUE) { if ($ThrowError) { $Errors = array(ZIPARCHIVE::ER_EXISTS => 'ER_EXISTS', ZIPARCHIVE::ER_INCONS => 'ER_INCONS', ZIPARCHIVE::ER_INVAL => 'ER_INVAL', ZIPARCHIVE::ER_MEMORY => 'ER_MEMORY', ZIPARCHIVE::ER_NOENT => 'ER_NOENT', ZIPARCHIVE::ER_NOZIP => 'ER_NOZIP', ZIPARCHIVE::ER_OPEN => 'ER_OPEN', ZIPARCHIVE::ER_READ => 'ER_READ', ZIPARCHIVE::ER_SEEK => 'ER_SEEK'); throw new Exception(T('Could not open addon file. Addons must be zip files.') . ' (' . $Path . ' ' . GetValue($ZipOpened, $Errors, 'Unknown Error') . ')' . $Worked, 400); } return FALSE; } $Entries = array(); for ($i = 0; $i < $Zip->numFiles; $i++) { $Entries[] = $Zip->statIndex($i); } // Figure out which system files to delete. $Deletes = array(); foreach ($Entries as $Index => $Entry) { $Name = $Entry['name']; $Delete = strpos($Name, '__MACOSX') !== FALSE | strpos($Name, '.DS_Store') !== FALSE | strpos($Name, 'thumbs.db') !== FALSE | strpos($Name, '.gitignore') !== FALSE; if ($Delete) { $Deletes[] = $Entry; unset($Entries[$Index]); } } // Get a folder ready for checking the addon. $FolderPath = dirname($Path) . '/' . basename($Path, '.zip') . '/'; if (file_exists($FolderPath)) { Gdn_FileSystem::RemoveFolder($FolderPath); } // Figure out what kind of addon this is. $Root = ''; $NewRoot = ''; $Addon = FALSE; foreach ($Entries as $Entry) { $Name = '/' . ltrim($Entry['name'], '/'); $Filename = basename($Name); $Folder = substr($Name, 0, -strlen($Filename)); $NewRoot = ''; // Check to see if the entry is a plugin file. if ($Filename == 'default.php' || StringEndsWith($Filename, '.plugin.php')) { if (count(explode('/', $Folder)) > 3) { // The file is too deep to be a plugin file. continue; } // This could be a plugin file, but we have to examine its info array. $Zip->extractTo($FolderPath, $Entry['name']); $FilePath = CombinePaths(array($FolderPath, $Name)); $Info = self::ParseInfoArray($FilePath, 'PluginInfo'); Gdn_FileSystem::RemoveFolder(dirname($FilePath)); if (!is_array($Info) || !count($Info)) { continue; } // Check to see if the info array conforms to a plugin spec. $Key = key($Info); $Info = $Info[$Key]; $Root = trim($Folder, '/'); $Valid = TRUE; // Make sure the key matches the folder name. if ($Root && strcasecmp($Root, $Key) != 0) { $Result[] = "{$Name}: The plugin's key is not the same as its folder name."; $Valid = FALSE; } else { $NewRoot = $Root; } if (!GetValue('Description', $Info)) { $Result[] = $Name . ': ' . sprintf(T('ValidateRequired'), T('Description')); $Valid = FALSE; } if (!GetValue('Version', $Info)) { $Result[] = $Name . ': ' . sprintf(T('ValidateRequired'), T('Version')); $Valid = FALSE; } if ($Valid) { // The plugin was confirmed. $Addon = array('AddonKey' => $Key, 'AddonTypeID' => ADDON_TYPE_PLUGIN, 'Name' => GetValue('Name', $Info) ? $Info['Name'] : $Key, 'Description' => $Info['Description'], 'Version' => $Info['Version'], 'Path' => $Path); break; } continue; } // Check to see if the entry is an application file. if (StringEndsWith($Name, '/settings/about.php')) { if (count(explode('/', $Folder)) > 4) { $Result[] = "{$Name}: The application's info array was not in the correct location."; // The file is too deep to be a plugin file. continue; } // This could be a plugin file, but we have to examine its info array. $Zip->extractTo($FolderPath, $Entry['name']); $FilePath = CombinePaths(array($FolderPath, $Name)); $Info = self::ParseInfoArray($FilePath, 'ApplicationInfo'); Gdn_FileSystem::RemoveFolder(dirname($FilePath)); if (!is_array($Info) || !count($Info)) { $Result[] = "{$Name}: The application's info array could not be parsed."; continue; } $Key = key($Info); $Info = $Info[$Key]; $Root = trim(substr($Name, 0, -strlen('/settings/about.php')), '/'); $Valid = TRUE; // Make sure the key matches the folder name. if ($Root && strcasecmp($Root, $Key) != 0) { $Result[] = "{$Name}: The application's key is not the same as its folder name."; $Valid = FALSE; } else { $NewRoot = $Root; } if (!GetValue('Description', $Info)) { $Result[] = $Name . ': ' . sprintf(T('ValidateRequired'), T('Description')); $Valid = FALSE; } if (!GetValue('Version', $Info)) { $Result[] = $Name . ': ' . sprintf(T('ValidateRequired'), T('Version')); $Valid = FALSE; } if ($Valid) { // The application was confirmed. $Addon = array('AddonKey' => $Key, 'AddonTypeID' => ADDON_TYPE_APPLICATION, 'Name' => GetValue('Name', $Info) ? $Info['Name'] : $Key, 'Description' => $Info['Description'], 'Version' => $Info['Version'], 'Path' => $Path); break; } continue; } // Check to see if the entry is a theme file. if (StringEndsWith($Name, '/about.php')) { if (count(explode('/', $Folder)) > 3) { // The file is too deep to be a plugin file. continue; } // This could be a theme file, but we have to examine its info array. $Zip->extractTo($FolderPath, $Entry['name']); $FilePath = CombinePaths(array($FolderPath, $Name)); $Info = self::ParseInfoArray($FilePath, 'ThemeInfo'); Gdn_FileSystem::RemoveFolder(dirname($FilePath)); if (!is_array($Info) || !count($Info)) { continue; } $Key = key($Info); $Info = $Info[$Key]; $Valid = TRUE; $Root = trim(substr($Name, 0, -strlen('/about.php')), '/'); // Make sure the theme is at least one folder deep. if (strlen($Root) == 0) { $Result[] = $Name . ': The theme must be in a folder.'; $Valid = FALSE; } if (!GetValue('Description', $Info)) { $Result[] = $Name . ': ' . sprintf(T('ValidateRequired'), T('Description')); $Valid = FALSE; } if (!GetValue('Version', $Info)) { $Result[] = $Name . ': ' . sprintf(T('ValidateRequired'), T('Version')); $Valid = FALSE; } if ($Valid) { // The application was confirmed. $Addon = array('AddonKey' => $Key, 'AddonTypeID' => ADDON_TYPE_THEME, 'Name' => GetValue('Name', $Info) ? $Info['Name'] : $Key, 'Description' => $Info['Description'], 'Version' => $Info['Version'], 'Path' => $Path); break; } } if (StringEndsWith($Name, '/definitions.php')) { if (count(explode('/', $Folder)) > 3) { // The file is too deep to be a plugin file. continue; } // This could be a locale pack, but we have to examine its info array. $Zip->extractTo($FolderPath, $Entry['name']); $FilePath = CombinePaths(array($FolderPath, $Name)); $Info = self::ParseInfoArray($FilePath, 'LocaleInfo'); Gdn_FileSystem::RemoveFolder(dirname($FilePath)); if (!is_array($Info) || !count($Info)) { continue; } $Key = key($Info); $Info = $Info[$Key]; $Valid = TRUE; $Root = trim(substr($Name, 0, -strlen('/definitions.php')), '/'); // Make sure the locale is at least one folder deep. if ($Root != $Key) { $Result[] = $Name . ': The locale pack\'s key must be the same as its folder name.'; $Valid = FALSE; } if (!GetValue('Locale', $Info)) { $Result[] = $Name . ': ' . sprintf(T('ValidateRequired'), T('Locale')); $Valud = FALSE; } elseif (strcasecmp($Info['Locale'], $Key) == 0) { $Result[] = $Name . ': ' . T('The locale\'s key cannot be the same as the name of the locale.'); $Valid = FALSE; } if (!GetValue('Description', $Info)) { $Result[] = $Name . ': ' . sprintf(T('ValidateRequired'), T('Description')); $Valid = FALSE; } if (!GetValue('Version', $Info)) { $Result[] = $Name . ': ' . sprintf(T('ValidateRequired'), T('Version')); $Valid = FALSE; } if ($Valid) { // The locale pack was confirmed. $Addon = array('AddonKey' => $Key, 'AddonTypeID' => ADDON_TYPE_LOCALE, 'Name' => GetValue('Name', $Info) ? $Info['Name'] : $Key, 'Description' => $Info['Description'], 'Version' => $Info['Version'], 'Path' => $Path); break; } } // Check to see if the entry is a core file. if (StringEndsWith($Name, '/index.php')) { if (count(explode('/', $Folder)) != 3) { // The file is too deep to be the core's index.php continue; } // This could be a theme file, but we have to examine its info array. $Zip->extractTo($FolderPath, $Entry['name']); $FilePath = CombinePaths(array($FolderPath, $Name)); // Get the version number from the core. $Version = self::ParseCoreVersion($FilePath); if (!$Version) { continue; } // The application was confirmed. $Addon = array('AddonKey' => 'vanilla', 'AddonTypeID' => ADDON_TYPE_CORE, 'Name' => 'Vanilla', 'Description' => 'Vanilla is an open-source, standards-compliant, multi-lingual, fully extensible discussion forum for the web. Anyone who has web-space that meets the requirements can download and use Vanilla for free!', 'Version' => $Version, 'Path' => $Path); $Info = array(); break; } } if ($Addon) { // Add the requirements. $Requirements = ArrayTranslate($Info, array('RequiredApplications' => 'Applications', 'RequiredPlugins' => 'Plugins', 'RequiredThemes' => 'Themes')); foreach ($Requirements as $Type => $Items) { if (!is_array($Items)) { unset($Requirements[$Type]); } } $Addon['Requirements'] = serialize($Requirements); $Addon['Checked'] = TRUE; $UploadsPath = PATH_ROOT . '/uploads/'; if (StringBeginsWith($Addon['Path'], $UploadsPath)) { $Addon['File'] = substr($Addon['Path'], strlen($UploadsPath)); } if ($Fix) { // Delete extraneous files. foreach ($Deletes as $Delete) { $Zip->deleteName($Delete['name']); } } } $Zip->close(); if (file_exists($FolderPath)) { Gdn_FileSystem::RemoveFolder($FolderPath); } if ($Addon) { $Addon['MD5'] = md5_file($Path); return $Addon; } else { if ($ThrowError) { $Msg = implode("\n", $Result); throw new Exception($Msg, 400); } else { return FALSE; } } }