diff options
Diffstat (limited to 'site/app/controllers/components/developers.php')
-rw-r--r-- | site/app/controllers/components/developers.php | 1251 |
1 files changed, 1251 insertions, 0 deletions
diff --git a/site/app/controllers/components/developers.php b/site/app/controllers/components/developers.php new file mode 100644 index 0000000..15ae6b8 --- /dev/null +++ b/site/app/controllers/components/developers.php @@ -0,0 +1,1251 @@ +<?php +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is addons.mozilla.org site. + * + * The Initial Developer of the Original Code is + * Justin Scott <fligtar@gmail.com>. + * Portions created by the Initial Developer are Copyright (C) 2006 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Frederic Wenzel <fwenzel@mozilla.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ +class DevelopersComponent extends Object { + var $controller; + var $imageExtensions = array('.png', '.jpg', '.gif'); + + /** + * Save a reference to the controller on startup + * @param object &$controller the controller using this component + */ + function startup(&$controller) { + $this->controller =& $controller; + } + + /** + * Make sure at least one but no more than 5 tags selected + * @param array $tags post data of selected tags + */ + function validateTags($tags) { + $errors =& $this->controller->Error; + + //Must have at least one tag selected, but no more than 5 + if (empty($tags)) { + $errors->addError(_('devcp_error_one_category'), 'Tag/Tag'); + $this->controller->Tag->invalidate('Tag'); + return false; + } + elseif (count($tags) > 5) { + $errors->addError(_('devcp_error_five_categories'), 'Tag/Tag'); + $this->controller->Tag->invalidate('Tag'); + return false; + } + + return true; + } + + /** + * Remove duplicates and make sure at least one user selected + * @param array &$users post data of selected users + */ + function validateUsers(&$users) { + $errors =& $this->controller->Error; + + //Remove deleted and duplicate entries from authors + $authors = array(); + if (!empty($users)) { + foreach($users as $user) { + if ($user != "" && !in_array($user, $authors)) { + $authors[] = $user; + } + } + } + $users = $authors; + + //Make sure there is at least one author + if (empty($users)) { + $errors->addError(_('devcp_error_one_user'), 'User/User'); + $this->controller->User->invalidate('User'); + return false; + } + + return true; + } + + /** + * Save users to addons_users table. For some reason, Cake refuses to save + * this properly the normal way. + * @param array $users The users + */ + function saveUsers($users) { + $this->controller->User->execute("DELETE FROM addons_users WHERE addon_id={$this->controller->Addon->id}"); + + if (!empty($users)) { + foreach ($users as $user) { + $this->controller->User->execute("INSERT INTO addons_users (addon_id, user_id) VALUES({$this->controller->Addon->id}, {$user})"); + } + } + + return true; + } + + /** + * Get all tags for an addontype + * @param int $addontypeId the addontype ID + * @param array $applicationIds the ids of supported applications + * @return array $tags the tags + */ + function getTags($addontypeId, $applicationIds) { + //Get tags based on addontype + $applicationIdQry = !empty($applicationIds) ? "Tag.application_id IN (".implode(', ', $applicationIds).") OR" : ''; + + // Override for search engines. They have no application restrictions (bug 417727) + if ($addontypeId == ADDON_SEARCH) { + $applicationIdQry = 'Tag.application_id IS NOT NULL OR'; + } + + $tagsQry = $this->controller->Tag->findAll("Tag.addontype_id='{$addontypeId}' AND ({$applicationIdQry} Tag.application_id IS NULL)"); + + if ($tagsQry) { + // show (APP) behind name? + $add_apps = (is_array($applicationIds) && count($applicationIds)>1); + if ($add_apps) { + global $app_shortnames, $app_prettynames; + $appnames = array(); + foreach($applicationIds as $app) { + $sn = array_search($app,$app_shortnames); + if ($sn!==false) + $appnames[$app] = $app_prettynames[$sn]; + } + } + + foreach ($tagsQry as $k => $v) { + $tags['names'][$v['Tag']['id']] = $v['Translation']['name']['string']; + if ($add_apps && !is_null($v['Tag']['application_id'])) + $tags['names'][$v['Tag']['id']] .= " ({$appnames[$v['Tag']['application_id']]})"; + $tags['descriptions'][] = $v['Tag']['id'].': "'.addslashes($v['Translation']['description']['string']).'"'; + } + } + + if (!empty($tags)) { + asort($tags['names']); + return $tags; + } + + return array(); + } + + /** + * Get all selected tags in order of post data, existing data, default + * @param array $currentTags currently selected tags + * @return array $selectedTags the selected tags + */ + function getSelectedTags($currentTags) { + //post data + if (!empty($this->controller->data['Tag']['Tag'])) { + foreach($this->controller->data['Tag']['Tag'] as $tag) { + $selectedTags[] = $tag; + } + } + //current data + elseif (!empty($currentTags)) { + foreach ($currentTags as $tag) { + $selectedTags[] = $tag['id']; + } + } + //default data + else { + $selectedTags = array(); + } + + return $selectedTags; + } + + /** + * Get authors in order of post data, existing data, default + * @param array $currentUsers currently selected users + * @param boolean $defaultToSession whether to default to current user + * @return array $authors the authors + */ + function getAuthors($currentUsers, $defaultToSession = true) { + //post data + if (!empty($this->controller->data['User']['User']) && !in_array('0', $this->controller->data['User']['User'])) { + foreach($this->controller->data['User']['User'] as $user) { + if ($user != "") { + $this->controller->User->id = $user; + $userinfo = $this->controller->User->read(); + $authors[$user] = $userinfo['User']['firstname'].' '.$userinfo['User']['lastname'].' ['.$userinfo['User']['email'].']'; + } + } + } + //current users + elseif (!empty($currentUsers)) { + foreach ($currentUsers as $user) { + $authors[$user['id']] = $user['firstname'].' '.$user['lastname'].' ['.$user['email'].']'; + } + } + //default to logged in + elseif ($defaultToSession == true) { + //default to current user + $session = $this->controller->Session->read('User'); + $authors[$session['id']] = $session['firstname'].' '.$session['lastname'].' ['.$session['email'].']'; + } + //default to empty + else { + $authors = array(); + } + + return $authors; + } + + /** + * Detect addontype based on file information + * @param array $file array of PHP file info + * @return int addontype id + */ + function detectAddontype($file) { + $extension = substr($file['name'], strrpos($file['name'], '.')); + switch ($extension) { + case '.xpi': + // Dictionaries have a .dic file in the dictionaries directory + $zip = new Archive_Zip($file['tmp_name']); + $dicFile = $zip->extract(array('extract_as_string' => true, 'by_preg' => "/dictionaries\/.+\.dic/i")); + + // if the .dic file is present, it is a dictionary, otherwise it's an extension + if (count($dicFile) > 0) { + return ADDON_DICT; + } + else { + return ADDON_EXTENSION; + } + break; + + case '.jar': + return ADDON_THEME; + break; + + case '.xml': + return ADDON_SEARCH; + break; + + default: + return 0; + break; + } + } + + /** + * Validate the uploaded files + * @deprecated + */ + function validateFiles() { + $errors =& $this->controller->Error; + + //Make sure the first file was uploaded + if (empty($this->controller->data['File']['file1']['name'])) { + $errors->addError(_('devcp_error_upload_file'), 'File/file1'); + $this->controller->File->invalidate('file1'); + return false; + } + + $errorCount = 0; + + //Loop through the files + for ($f = 1; $f <= 4; $f++) { + $file = (!empty($this->controller->data['File']['file'.$f])) ? $this->controller->data['File']['file'.$f] : array(); + + if (!empty($file['name'])) { + //File 4 is the icon file + if ($f != 4) { + //Make sure platform selected + if (empty($this->controller->data['File']['platform_id'.$f])) { + $errors->addError(_('devcp_error_no_platform'), 'File/platform_id'.$f); + $this->controller->File->invalidate('platform_id'.$f); + $errorCount++; + } + + //Validate the file + $files[$f] = $this->validateFile($this->controller->data['File']['file'.$f], $this->controller->data); + } + else { + //Validate the image + $files[$f] = $this->validateIcon($this->controller->data['File']['file'.$f]); + } + + //If an array is not returned, an error occurred + if (!is_array($files[$f])) { + $errors->addError($files[$f], 'File/file'.$f); + $this->controller->File->invalidate('file'.$f); + $errorCount++; + } + else { + if ($f != 4) { + $files[$f]['platform_id'] = $this->controller->data['File']['platform_id'.$f]; + } + } + } + } + + //Collect all file errors before returning + if ($errorCount > 0) { + return false; + } + else { + foreach ($files as $f => $fileInfo) { + $this->controller->addVars['file'.$f] = $fileInfo; + } + return true; + } + } + + /** + * Validate a file for basic problems with the upload + * @param array $file PHP info on the file + * @param array $addon data already saved about the add-on + * @return array if no errors + * @return string if error + */ + function validateFile($file, $addon) { + $uploadErrors = array( + '1' => _('devcp_error_http_maxupload'), + '2' => _('devcp_error_http_maxupload'), + '3' => _('devcp_error_http_incomplete'), + '4' => _('devcp_error_http_nofile') + ); + $allowedExtensions = $this->getAllowedExtensions($addon['Addon']['addontype_id']); + + // Check for file upload errors + if (!empty($file['error'])) { + return $uploadErrors[$file['error']]; + } + + // Set file properties to be used later + $fileInfo['filename'] = $file['name']; + $fileInfo['size'] = round($file['size']/1024, 0); // in KB + $fileInfo['extension'] = substr($file['name'], strrpos($file['name'], '.')); + $fileInfo['hash'] = 'sha256:'.hash_file("sha256", $file['tmp_name']); + + // Check for file extension match + if (!in_array(strtolower($fileInfo['extension']), $allowedExtensions)) { + return sprintf(_('devcp_error_file_extension'), $fileInfo['extension'], implode(', ', $allowedExtensions)); + } + + // Move temporary file to repository + $uploadedFile = $this->controller->Amo->unclean($file['tmp_name']); + $tempLocation = REPO_PATH.'/temp/'.$fileInfo['filename']; + + // Make sure file doesn't overwrite anything + if (file_exists($tempLocation)) { + $fileInfo['filename'] = str_replace('0.', '', microtime()).$fileInfo['extension']; + $tempLocation = REPO_PATH.'/temp/'.$fileInfo['filename']; + } + + if (move_uploaded_file($uploadedFile, $tempLocation)) { + chmod($tempLocation, 0644); + $fileInfo['path'] = $tempLocation; + } + else { + return _('devcp_error_move_file'); + } + + return $fileInfo; + } + + /** + * Validate the icon file + * @param array $file the icon file array + * @param array $fileErrors the HTTP file error associations + * @param array $allowedExtensions allowed file extensions + */ + function validateIcon($icon) { + $uploadErrors = array( + '1' => _('devcp_error_http_maxupload'), + '2' => _('devcp_error_http_maxupload'), + '3' => _('devcp_error_http_incomplete'), + '4' => _('devcp_error_http_nofile') + ); + + // Check for file upload errors + if (!empty($icon['error'])) { + return $uploadErrors[$icon['error']]; + } + + // Check for file extension match + $extension = substr($icon['name'], strrpos($icon['name'], '.')); + if (!in_array(strtolower($extension), $this->imageExtensions)) { + return sprintf(_('devcp_error_icon_extension'), $extension, implode(', ', $this->imageExtensions)); + } + + $fileInfo['icondata'] = file_get_contents($icon['tmp_name']); + $fileInfo['icontype'] = $icon['type']; + + // Get icon size + list($sourceWidth, $sourceHeight) = getimagesize($icon['tmp_name']); + + // Resize to 32x32 + $fileInfo['icondata'] = $this->resizeImage($fileInfo['icondata'], $sourceWidth, $sourceHeight, 32, 32); + + return $fileInfo; + } + + /** + * Resizes an image to specified size + * @param string $sourceData the image data + * @param int $sourceWidth original width + * @param int $sourceHeight original height + * @param int $newWidth width of the new image + * @param int $newHeight height of the new image + * @return string new image data + */ + function resizeImage($sourceData, $sourceWidth, $sourceHeight, $newWidth = 200, $newHeight = 150) { + $sourceImage = imagecreatefromstring($sourceData); + imagesavealpha($sourceImage, true); + + // Determine width/height aspect ratio + $sourceRatio = $sourceWidth / $sourceHeight; + $newRatio = $newWidth / $newHeight; + + if ($newRatio > $sourceRatio) { + $newWidth = $newHeight * $sourceRatio; + } + else { + $newHeight = $newWidth / $sourceRatio; + } + + // Only make image smaller, not larger + if ($newWidth >= $sourceWidth && $newHeight >= $sourceHeight) { + $newImage = $sourceImage; + } else { + $newImage = imagecreatetruecolor($newWidth, $newHeight); + + // Make a new transparent image and turn off alpha blending to keep the alpha channel + $background = imagecolorallocatealpha($newImage, 255, 255, 255, 127); + imagecolortransparent($newImage, $background); + imagealphablending($newImage, false); + imagesavealpha($newImage, true); + + imagecopyresampled($newImage, $sourceImage, 0, 0, 0, 0, $newWidth, $newHeight, $sourceWidth, $sourceHeight); + } + + // Output new image to buffer to save and clear it + ob_start(); + imagepng($newImage); + $newData = ob_get_contents(); + ob_end_clean(); + + @imagedestroy($sourceImage); + @imagedestroy($newImage); + + return $newData; + } + + /** + * Get allowed file extensions based on addontype + * @param int $addontype the addontype + * @return array $allowed allowed extensions + */ + function getAllowedExtensions($addontype) { + switch ($addontype) { + case ADDON_EXTENSION: $allowed = array('.xpi'); + break; + case ADDON_THEME: $allowed = array('.jar', '.xpi'); + break; + case ADDON_DICT: $allowed = array('.xpi'); + break; + case ADDON_SEARCH: $allowed = array('.xml'); + break; + case ADDON_LPAPP: $allowed = array('.xpi'); + break; + case ADDON_LPADDON: $allowed = array('.xpi'); + break; + default: $allowed = array(); + break; + } + return $allowed; + } + + /** + * Validate the install.rdf manifest data + * @param array $manifestData the manifest contents + * @return string if error + * @return boolean true if no error + */ + function validateManifestData($manifestData) { + // If the data is a string, it is an error message + if (is_string($manifestData)) { + return sprintf(_('devcp_error_manifest_parse'), $manifestData); + } + + // Check if install.rdf has an updateURL + if (isset($manifestData['updateURL'])) { + return _('devcp_error_updateurl'); + } + + // Check if install.rdf has an updateKey + if (isset($manifestData['updateKey'])) { + return _('devcp_error_updatekey'); + } + + // Check the GUID for existence + if (!isset($manifestData['id'])) { + return _('devcp_error_no_guid'); + } + + // Validate GUID + if (!preg_match('/^(\{[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\}|[a-z0-9-\._]*\@[a-z0-9-\._]+)$/i', $manifestData['id'])) { + return sprintf(_('devcp_error_invalid_guid'), $manifestData['id']); + } + + // Make sure GUID is not an application's GUID + if ($this->controller->Application->findByGuid($manifestData['id'])) { + return _('devcp_error_guid_application'); + } + + // Make sure version has no spaces + if (!isset($manifestData['version']) || preg_match('/.*\s.*/', $manifestData['version'])) { + return _('devcp_error_invalid_version_spaces'); + } + + // Validate version + if (!preg_match('/^\d+(\+|\w+)?(\.\d+(\+|\w+)?)*$/', $manifestData['version'])) { + return _('devcp_error_invalid_version'); + } + + return true; + } + + /** + * Validate the target applications + * @param array $targetApps the targetApps from install.rdf + * @return string if error + * @return array if no errors + */ + function validateTargetApplications($targetApps) { + $noMozApps = true; + $versionErrors = array(); + + if (count($targetApps) > 0) { + $i = 0; + + // Iterate through each target app and find it in the DB + foreach ($targetApps as $appKey => $appVal) { + if ($matchingApp = $this->controller->Application->find(array('guid' => $appKey), null, null, -1)) { + $return[$i]['application_id'] = $matchingApp['Application']['id']; + + // Mark as Moz-app if supported + if ($matchingApp['Application']['supported'] == 1) { + $noMozApps = false; + } + + // Check if the minVersion is valid + $matchingMinVers = $this->controller->Appversion->find("application_id={$matchingApp['Application']['id']} AND version='{$appVal['minVersion']}'", null, null, -1); + + if (empty($matchingMinVers)) { + $versionErrors[] = sprintf(_('devcp_error_invalid_appversion'), $appVal['minVersion'], $matchingApp['Translation']['name']['string']); + } + elseif (strpos($appVal['minVersion'], '*') !== false) { + $versionErrors[] = sprintf(_('devcp_error_invalid_minversion'), $appVal['minVersion'], $matchingApp['Translation']['name']['string']); + } + else { + $return[$i]['min'] = $matchingMinVers['Appversion']['id']; + } + + // Check if the maxVersion is valid + $matchingMaxVers = $this->controller->Appversion->find("application_id={$matchingApp['Application']['id']} AND version='{$appVal['maxVersion']}'", null, null, -1); + if (empty($matchingMaxVers)) { + $versionErrors[] = sprintf(_('devcp_error_invalid_appversion'), $appVal['maxVersion'], $matchingApp['Translation']['name']['string']); + } + else { + $return[$i]['max'] = $matchingMaxVers['Appversion']['id']; + } + $i++; + } + } + } + + $validAppReference = sprintf(_('devcp_error_appversion_reference_link'), '<a href="'.$this->controller->url('/pages/appversions').'">'._('devcp_error_appversion_reference_link_text').'</a>'); + + // Must have at least one Mozilla app + if ($noMozApps === true) { + return _('devcp_error_mozilla_application').'<br />'.$validAppReference; + } + + // Max/min version errors + if (count($versionErrors) > 0) { + $errorStr = implode($versionErrors, '<br />'); + return _('devcp_error_install_manifest').'<br />'.$errorStr.'<br />'.$validAppReference; + } + + return $return; + } + + /** + * Renames and moves a file out of temp repository + * @param array $data array of data in model format + */ + function moveFile($data) { + $fileUpdates = array(); + $applications = $this->controller->Application->getShortNames(); + $platforms = $this->controller->Platform->getShortNames(); + + // Construct new filename with name, version, supported apps, and OS + $filename = preg_replace(INVALID_FILENAME_CHARS, '_', $data['Addon']['name']); + + $filename .= '-'.$data['Version']['version']; + + if ($data['Addon']['addontype_id'] != ADDON_SEARCH) { + $filename .= '-'; + $appString = ''; + foreach ($data['appversions'] as $appversion) { + if ($appString != "") { + $appString .= '+'.$applications[$appversion['application_id']]; + } + else { + $appString = $applications[$appversion['application_id']]; + } + } + $filename .= $appString; + + if ($data['File']['db']['platform_id'] != PLATFORM_ALL) { + $filename .= '-'.$platforms[$data['File']['db']['platform_id']]; + } + } + + $filename .= $data['File']['details']['extension']; + $filename = strtolower($filename); + + // File paths + $currentPath = $data['File']['details']['path']; + $dirPath = REPO_PATH.'/'.$data['Addon']['id']; + $newPath = $dirPath.'/'.$filename; + + // Create directory if necessary + if (!file_exists($dirPath)) { + if (!mkdir($dirPath)) { + return sprintf(_('devcp_error_moving_file'), $data['File']['db']['filename']); + } + } + + // Move file + if (file_exists($currentPath)) { + // Bail if the file exists. See bug 470652 for a rough explanation + if (file_exists($newPath)) { + return sprintf(___('devcp_error_file_exists'), $filename); + } + // We must copy instead of rename now in case there are other platforms + if (!copy($currentPath, $newPath)) { + return sprintf(_('devcp_error_moving_file'), $data['File']['db']['filename']); + } + $fileUpdates['filename'] = $filename; + } + else { + return sprintf(_('devcp_error_moving_file'), $data['File']['db']['filename']); + } + + // Copy file to rsync area if public + if ($data['File']['db']['status'] == STATUS_PUBLIC) { + $this->controller->Amo->copyFileToPublic($data['Addon']['id'], $filename); + } + + return $fileUpdates; + } + + /** + * Create new file name and move files from temp to approval + * @param array $version version information + * @deprecated since 3.5 + */ + function moveFiles($version, $addontype_id) { + $errors =& $this->controller->Error; + $fileUpdates = array(); + $applications = $this->controller->Amo->getApplicationName(null, true); + $this->controller->Addon->id = $version['Version']['addon_id']; + $addon = $this->controller->Addon->read(); + + // Construct new filename with name, version, supported apps, and OS + $baseFilename = preg_replace(INVALID_FILENAME_CHARS, '_', $addon['Translation']['name']['string']); + + if ($addontype_id != ADDON_SEARCH) { + $baseFilename .= '-'.$version['Version']['version'].'-'; + $appString = ''; + foreach ($version['Application'] as $app) { + if ($appString != "") { + $appString .= '+'.$applications[$app['id']]['shortname']; + } + else { + $appString = $applications[$app['id']]['shortname']; + } + } + $baseFilename .= $appString; + + //Get platforms with shortnames + $platforms = $this->controller->Amo->getPlatformName('', true); + } + + foreach ($version['File'] as $file) { + $newFilename = $baseFilename; + $extension = substr($file['filename'], strrpos($file['filename'], '.')); + if ($file['platform_id'] != 1 && $addontype_id != ADDON_SEARCH) { + $newFilename .= '-'.$platforms[$file['platform_id']]['shortname']; + } + $newFilename .= $extension; + $newFilename = strtolower($newFilename); + + //File paths + $currentPath = REPO_PATH.'/temp/'.$file['filename']; + $dirPath = REPO_PATH.'/'.$addon['Addon']['id']; + $newPath = $dirPath.'/'.$newFilename; + + //Create directory if necessary + if (!file_exists($dirPath)) { + if (!mkdir($dirPath)) { + $errors->addError(sprintf(_('devcp_error_moving_file'), $file['filename'])); + } + } + + //Move file + if (file_exists($currentPath)) { + //Delete file if one already exists + if (file_exists($newPath)) { + unlink($newPath); + } + if (!rename($currentPath, $newPath)) { + $errors->addError(sprintf(_('devcp_error_moving_file'), $file['filename'])); + } + $fileUpdates[$file['id']]['filename'] = $newFilename; + } + else { + $errors->addError(sprintf(_('devcp_error_moving_file'), $file['filename'])); + } + } + + return $fileUpdates; + } + + /** + * Determine if all required fields are set in order to skip reviewing add-on info + */ + function noReviewRequired() { + + if ($this->controller->addVars['newAddon'] == true) { + return false; + } + else { + return true; + } + } + + /** + * To be run after a file is deleted to ensure nominated add-ons + * have files + */ + function postDelete($addon_id) { + $addon = $this->controller->Addon->findById($addon_id, array('status'), null, -1); + $file = $this->controller->File->query("SELECT File.id FROM files AS File INNER JOIN versions as Version ON Version.id = File.version_id AND Version.addon_id = {$addon_id}"); + + if ($addon['Addon']['status'] == STATUS_NOMINATED && empty($file)) { + $addonData = array('status' => STATUS_SANDBOX); + $this->controller->Addon->id = $addon_id; + $this->controller->Addon->save($addonData); + } + } + + /** + * Deletes a file from disk and database + * ENSURE USER HAS ALL NECESSARY PERMISSIONS BEFORE USING THIS METHOD + * @param int $file_id file id to delete + * @param int $addon_id add-on id the file belongs to + */ + function deleteFile($file_id, $addon_id) { + $file = $this->controller->File->findById($file_id); + + // Delete files from disk + $path = "/{$addon_id}/{$file['File']['filename']}"; + if (defined('REPO_PATH') && file_exists(REPO_PATH.$path)) { + unlink(REPO_PATH.$path); + } + if (defined('PUBLIC_STAGING_PATH') && file_exists(PUBLIC_STAGING_PATH.$path)) { + unlink(PUBLIC_STAGING_PATH.$path); + } + + // Delete approvals + $this->controller->File->execute("DELETE FROM approvals WHERE file_id='{$file_id}'"); + + // Delete file + $this->controller->File->execute("DELETE FROM files WHERE id='{$file_id}' LIMIT 1"); + } + + /** + * Deletes a version along with all dependent files, reviews, etc + * ENSURE USER HAS ALL NECESSARY PERMISSIONS BEFORE USING THIS METHOD + * @param int $version_id version id to delete + */ + function deleteVersion($version_id) { + // Pull version info without translations + $this->controller->Version->translationReplace = false; + $version = $this->controller->Version->findById($version_id); + $this->controller->Version->translationReplace = true; + + // Get translation ids of any translated fields of versions + $translation_ids = array(); + if (!empty($this->controller->Version->translated_fields)) { + foreach ($this->controller->Version->translated_fields as $translatedField) { + if (!empty($version['Version'][$translatedField])) { + $translation_ids[] = $version['Version'][$translatedField]; + } + } + } + + // Delete any files + if (!empty($version['File'])) { + foreach ($version['File'] as $file) { + $this->deleteFile($file['id'], $version['Version']['addon_id']); + } + } + + // Delete applications_versions rows + $this->controller->Version->execute("DELETE FROM applications_versions WHERE version_id={$version_id}"); + + // Delete reviews + $review_ids = array(); + if (!empty($this->controller->Review->translated_fields)) { + foreach ($this->controller->Review->translated_fields as $translatedField) { + if (!empty($version['Review'])) { + foreach ($version['Review'] as $review) { + $review_ids[] = $review['id']; + if (!empty($review[$translatedField])) { + $translation_ids[] = $review[$translatedField]; + } + } + } + } + } + + if (!empty($review_ids)) { + foreach ($review_ids as $review_id) { + $this->controller->Review->execute("DELETE FROM reviewratings WHERE review_id={$review_id}"); + $this->controller->Review->execute("DELETE FROM reviews WHERE id={$review_id}"); + } + } + + // Delete version + $this->controller->Version->execute("DELETE FROM versions WHERE id={$version_id}"); + + // Delete translations + if (!empty($translation_ids)) { + $this->controller->Version->execute("DELETE FROM translations WHERE id IN (".implode(',', $translation_ids).")"); + } + } + + /** + * Delete an addon, along with its versions, files, reviews, previews, + * favorites, features, tags, and translations + * @param int $id the add-on id + * @return boolean + */ + function deleteAddon($id) { + //Double-check permissions + if (!$this->controller->Amo->checkOwnership($id)) { + return false; + } + + $this->controller->Addon->id = $id; + $addon = $this->controller->Addon->read(); + + //Get translation ids of any translated fields + if (!empty($this->controller->Addon->translated_fields)) { + foreach ($this->controller->Addon->translated_fields as $translatedField) { + if (!empty($addon['Addon'][$translatedField])) { + $translationIds[] = $addon['Addon'][$translatedField]; + } + } + } + + //Loop through and delete versions + if (!empty($addon['Version'])) { + foreach ($addon['Version'] as $version) { + $this->deleteVersion($version['id']); + } + } + + //Delete addons_tags rows + $this->controller->Addon->execute("DELETE FROM addons_tags WHERE addon_id='{$id}'"); + + //Delete addons_users rows + $this->controller->Addon->execute("DELETE FROM addons_users WHERE addon_id='{$id}'"); + + //Delete favorites + $this->controller->Addon->execute("DELETE FROM favorites WHERE addon_id='{$id}'"); + + //Delete features + $this->controller->Addon->execute("DELETE FROM features WHERE addon_id='{$id}'"); + + //Delete previews + $this->controller->Addon->execute("DELETE FROM previews WHERE addon_id='{$id}'"); + + //Loop through reviews + if (!empty($addon['Review'])) { + foreach ($addon['Review'] as $review) { + //Delete review ratings + $this->controller->Addon->execute("DELETE FROM reviewratings WHERE review_id='{$review['id']}'"); + + //Delete review + $this->controller->Addon->execute("DELETE FROM reviews WHERE id='{$review['id']}'"); + } + } + + //Delete add-on + $this->controller->Addon->execute("DELETE FROM addons WHERE id='{$id}' LIMIT 1"); + + //Delete translations + if (!empty($translationIds)) { + $this->controller->Addon->execute("DELETE FROM translations WHERE id IN (".implode(',', $translationIds).")"); + } + + return true; + } + + /** + * Save localebox-formatted translations to their appropriate locales + * Post data is in the format: + * [Locales] => Array + * ( + * [0] => en-US + * [1] => de + * ) + * [Addon] => Array + * ( + * [name] => Array + * ( + * [0] => English Name + * [1] => German Name + * ) + * ) + * So we convert that into an array like: + * [en-US] => Array + * ( + * [Addon] => Array + * ( + * [name] => English Name + * ) + * ) + * and save it. + * @param array $data The post data to save + * @param array $models The models to process + */ + function saveTranslations($data, $models = array()) { + if (empty($models)) { + $models = array('Addon', 'Preview', 'Version'); + } + + $translations = array(); + $errors = 0; + + if (!empty($data['Locales'])) { + foreach ($data['Locales'] as $id => $locale) { + // Reformat each model's array + foreach ($models as $model) { + if (!empty($data[$model])) { + foreach ($data[$model] as $field => $values) { + if (!in_array($field, $this->controller->{$model}->translated_fields)) continue; + $translations[$locale][$model][$field] = $values[$id]; + } + } + } + } + + // Update translations + if (!empty($translations)) { + foreach ($translations as $locale => $translation) { + foreach ($models as $model) { + if (!empty($translation[$model])) { + $this->controller->{$model}->setLang($locale, $this->controller); + $theData = $this->controller->Amo->filterFields($translation[$model], array(), + array('id', 'guid', 'status')); + if (!empty($theData)) { + //Save without validation (validation causes problems!) + if (!$this->controller->{$model}->save($theData, false)) { + $errors++; + } + } + } + } + } + } + + // Reset langs + foreach ($models as $model) { + $this->controller->{$model}->setLang(LANG, $this->controller); + } + + if (!empty($errors)) { + return false; + } + else { + return true; + } + } + else { + return false; + } + } + + /** + * Strip localized fields from post data + * @param array $data The post data to strip + */ + function stripLocalized($data) { + + if (!empty($data['Addon'])) { + foreach ($this->controller->Addon->translated_fields as $field) { + unset($data['Addon'][$field]); + } + } + + if (!empty($data['Version'])) { + foreach ($this->controller->Version->translated_fields as $field) { + unset($data['Version'][$field]); + } + } + + unset($data['Locales']); + + return $data; + } + + /** + * Highlight another preview because the current is being removed/deleted + * @param array $preview The current preview data + * @param array $addon The current addon data + */ + function highlightNextPreview($preview, $addon) { + $oldId = $this->controller->Preview->id; + + foreach ($addon['Preview'] as $prev) { + if ($prev['id'] != $preview['Preview']['id']) { + $this->controller->Preview->id = $prev['id']; + $this->controller->Preview->save(array('highlight' => 1)); + break; + } + } + + $this->controller->Preview->id = $oldId; + } + + /** + * Remove highlight from highlighted previews + * @param int $addon_id Add-on's id + */ + function unhighlightOtherPreviews($addon_id) { + $this->controller->Preview->execute("UPDATE previews SET highlight=0 WHERE addon_id='{$addon_id}'"); + } + + function addPreview($addon_id, $data) { + $previewData = array('addon_id' => $addon_id, + 'filedata' => file_get_contents($data['File']['tmp_name']), + 'filetype' => $data['File']['type'], + 'highlight' => $data['highlight'], + 'thumbtype' => 'image/png' + ); + + //Check for allowed file extensions + $allowedImage = array('.png', '.jpg', '.gif'); + $extension = substr($data['File']['name'], strrpos($data['File']['name'], '.')); + if (!in_array($extension, $allowedImage)) { + $errors =& $this->controller->Error; + $errors->addError(sprintf(_('devcp_error_preview_extension'), $extension, implode(', ', $allowedImage)), 'main'); + return false; + } + + list($sourceWidth, $sourceHeight) = getimagesize($data['File']['tmp_name']); + + //Generate thumbnail (200 x 150) if necessary + if ($sourceHeight < 150 && $sourceWidth < 200) { + $previewData['thumbdata'] = $previewData['filedata']; + } + else { + $previewData['thumbdata'] = $this->resizeImage($previewData['filedata'], $sourceWidth, $sourceHeight, 200, 150); + } + + //Resize preview if too large (700 x 525) + if ($sourceWidth > 700 || $sourceHeight > 525) { + $previewData['filedata'] = $this->resizeImage($previewData['filedata'], $sourceWidth, $sourceHeight, 700, 525); + $previewData['filetype'] = 'image/png'; + } + + /* + //Debug preview adjustments + $full = fopen(REPO_PATH.'/full.png', 'wb'); + fwrite($full, $previewData['filedata']); + fclose($full); + + $new = fopen(REPO_PATH.'/new.png', 'wb'); + fwrite($new, $previewData['thumbdata']); + fclose($new); + + echo '<img src="../../../files/full.png">'; + echo '<img src="../../../files/new.png">'; + //pr($previewData); + */ + + return $previewData; + } + + /** + * Determine file status based on submission information + * @param array $addon Addon informaiton + * @return int $fileStatus the file status + */ + function determineFileStatus($addon) { + //If a trusted public add-on, go to sandbox unless specified public + if ($addon['trusted'] == 1 && $addon['status'] == STATUS_PUBLIC) { + if (!empty($this->controller->data['File']['status'])) { + if ($this->controller->data['File']['status'] == 'public') { + $fileStatus = STATUS_PUBLIC; + } + else { + $fileStatus = STATUS_SANDBOX; + } + } + elseif ($version = $this->controller->Version->read()) { + $fileStatus = $version['File'][0]['status']; + } + else { + $fileStatus = STATUS_SANDBOX; + } + } + //If an update to an untrusted public add-on, STATUS_PENDING + elseif ($addon['status'] == STATUS_PUBLIC) { + $fileStatus = STATUS_PENDING; + } + //In all other cases (new add-on and update to non-public add-on), STATUS_SANDBOX + else { + $fileStatus = STATUS_SANDBOX; + } + + return $fileStatus; + } + + function getAllowedAddonTypes($autoDetect, $isAdmin) { + $addontypes = array( + ADDON_EXTENSION => $this->controller->Addontype->getName(ADDON_EXTENSION), + ADDON_THEME => $this->controller->Addontype->getName(ADDON_THEME), + ADDON_DICT => $this->controller->Addontype->getName(ADDON_DICT), + ADDON_LPAPP => $this->controller->Addontype->getName(ADDON_LPAPP) + ); + + if ($autoDetect == true) { + $addontypes[0] = _('devcp_additem_addontype_autodetect'); + } + + if ($isAdmin == true) { + $addontypes[ADDON_SEARCH] = $this->controller->Addontype->getName(ADDON_SEARCH); + } + + ksort($addontypes); + + return $addontypes; + } + + function getLicenses($version_id=null) { + if ($version_id != null) { + $version = $this->controller->Version->findById($version_id); + $version = $version['Version']; + $license = $this->controller->License->findById($version['license_id']); + } + + // Add 'Please Choose...' only if no license has been selected. + if (!isset($version['license_id'])) { + $licenses['null'] = array( + 'name' => ___('devcp_uploader_please_choose'), + 'selected' => True); + } + + // Grab all the pre-approved licenses. + foreach ($this->controller->License->getNames() as $num => $builtin) { + $licenses['builtin_'.$num] = array( + 'name' => $builtin, + 'selected' => isset($license) && (string)$num === $license['License']['name']); + } + + // The trans array holds translations for all the custom licenses we'll + // be displaying. `other` starts off empty, for creating new licenses. + $trans['other']['text']['en-US'] = ''; + + if ($version_id != null) { + // Find all the custom licenses in use by this add-on. + $q = "SELECT Version.version, Version.license_id + FROM versions AS Version INNER JOIN licenses AS License + ON Version.license_id = License.id + WHERE Version.addon_id = {$version['addon_id']} + AND Version.license_id IS NOT NULL + AND License.name = -1 + GROUP BY License.id + ORDER BY Version.id DESC"; + foreach ($this->controller->Version->execute($q) as $existing) { + $existing = $existing['Version']; + $t = ___('devcp_license_existing'); + $val = 'existing_'.$existing['license_id']; + $licenses[$val] = array( + 'name' => sprintf($t, $version['addon_id'], $existing['version']), + 'selected' => $existing['license_id'] == $version['license_id']); + $trans[$val] = $this->controller->License->getAllTranslations($existing['license_id']); + } + } + + $licenses['other'] = array('name' => ___('devcp_uploader_option_other'), + 'selected' => False); + return array($licenses, $trans); + } + + function saveLicense($licenseData, $text, $params) { + $License = $this->controller->License; + if ($licenseData['name'] != 'null') { + $license = $licenseData['name']; + // If the license is pre-approved, we prefixed the id with builtin_. + if (preg_match('/^builtin_(\d+)$/', $license, $matches)) { + $license_id = $License->getBuiltin($matches[1]); + } else if ($license == 'other' || + preg_match('/^existing_(\d+)$/', $license, $matches)) { + // If it's 'other', we need to create a new license. + if ($license == 'other') { + $data['License']['name'] = -1; + $License->save($data); + $license_id = $License->getLastInsertId(); + } else { + $license_id = $matches[1]; + } + // Save any changed translation text. + $localized['text'] = $text; + $License->saveTranslations($license_id, $params, $localized); + } + return $license_id; + } + } +} +?> |