/
var
/
www
/
html
/
sugardemo
/
modules
/
ModuleBuilder
/
MB
/
Upload File
HOME
<?php /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ use Sugarcrm\Sugarcrm\Security\InputValidation\InputValidation; use Sugarcrm\Sugarcrm\Security\InputValidation\Request; use Sugarcrm\Sugarcrm\Util\Files\FileLoader; use Sugarcrm\Sugarcrm\Security\Validator\ConstraintBuilder; use Sugarcrm\Sugarcrm\Security\Validator\Validator; use Sugarcrm\Sugarcrm\Security\InputValidation\Exception\ViolationException; require_once 'modules/ModuleBuilder/parsers/constants.php'; class MBPackage { public $path; public $name; public $is_uninstallable = true; public $description = ''; public $modules = []; public $date_modified = ''; public $author = ''; public $key = ''; public $readme = ''; /** * @var Request */ protected $request; /** * Flavor compatibility map * * @var array */ protected static $compatibilityMap = [ 'PRO' => ['PRO', 'CORP', 'ENT', 'ULT'], 'CORP' => ['PRO', 'CORP', 'ENT', 'ULT'], 'ENT' => ['ENT', 'ULT'], 'ULT' => ['ENT', 'ULT'], ]; /** * Exportable application level customizations. Key is type for manifest, value is root directory and variable name. * * @var array */ protected static $appExtensions = [ 'language' => [ 'dir' => 'Extension/application/Ext/Language', 'varName' => 'app_list_strings', ], 'dropdown_filters' => [ 'dir' => 'Extension/application/Ext/DropdownFilters', 'varName' => 'role_dropdown_filters', ], ]; public function __construct($name) { $this->name = $name; $this->request = InputValidation::getService(); $this->load(); } public function loadModules($force = false) { if (!file_exists(MB_PACKAGE_PATH . '/' . $this->name . '/modules')) { return; } $d = dir(MB_PACKAGE_PATH . '/' . $this->name . '/modules'); while ($e = $d->read()) { if (substr($e, 0, 1) != '.' && is_dir(MB_PACKAGE_PATH . '/' . $this->name . '/modules/' . $e)) { $this->getModule($e, $force); } } } /** * Loads the translated module titles from the selected language into. * Will override currently loaded string to reflect undeployed label changes. * $app_list_strings * @param $languge String language identifyer * @return */ public function loadModuleTitles($languge = '') { if (empty($language)) { $language = $GLOBALS['current_language']; } global $app_list_strings; $packLangFilePath = $this->getPackageDir() . '/language/application/' . $language . '.lang.php'; if (file_exists($packLangFilePath)) { require FileLoader::validateFilePath($packLangFilePath); } } /** * @param $name * @param bool $force * @return MBModule */ public function getModule($name, $force = true) { if (!$force && !empty($this->modules[$name])) { return $this->modules[$name]; } $path = $this->getPackageDir(); $this->modules[$name] = new MBModule($name, $path, $this->name, $this->key); return $this->modules[$name]; } /** * Returns an MBModule by the given full name (package key + module name) * if it exists in this package * * @param string $name * @return MBModule */ public function getModuleByFullName($name) { foreach ($this->modules as $mname => $module) { if ($this->key . '_' . $mname == $name) { return $module; } } } public function deleteModule($name) { $this->modules[$name]->delete(); unset($this->modules[$name]); } public function getManifest($version_specific = false, $for_export = false) { global $sugar_flavor; //If we are exporting the package, we must ensure a different install key $pre = $for_export ? MB_EXPORTPREPEND : ''; $date = TimeDate::getInstance()->nowDb(); $time = time(); $this->description = to_html($this->description); $is_uninstallable = ($this->is_uninstallable ? true : false); if (isset($sugar_flavor)) { if (self::$compatibilityMap[$sugar_flavor]) { $flavors = self::$compatibilityMap[$sugar_flavor]; } else { $flavors = [$sugar_flavor]; } } else { $flavors = []; } $version = (!empty($version_specific)) ? $GLOBALS['sugar_version'] : ''; // Build an array and use var_export to build this file $manifest = [ 'built_in_version' => $GLOBALS['sugar_version'], 'acceptable_sugar_versions' => [$version], 'acceptable_sugar_flavors' => $flavors, 'readme' => $this->readme, 'key' => $this->key, 'author' => $this->author, 'description' => $this->description, 'icon' => '', 'is_uninstallable' => $is_uninstallable, 'name' => $pre . $this->name, 'published_date' => $date, 'type' => 'module', 'version' => $time, 'remove_tables' => 'prompt', ]; $header = file_get_contents('modules/ModuleBuilder/MB/header.php'); return $header . "\n// THIS CONTENT IS GENERATED BY MBPackage.php\n" . '$manifest = ' . var_export_helper($manifest) . ";\n\n"; /* return <<<EOQ $header \$manifest = array ( 'acceptable_sugar_versions' => array ( $version ), 'acceptable_sugar_flavors' => array( $flavor ), 'readme'=>'$this->readme', 'key'=>'$this->key', 'author' => '$this->author', 'description' => '$this->description', 'icon' => '', 'is_uninstallable' => $is_uninstallable, 'name' => '$pre$this->name', 'published_date' => '$date', 'type' => 'module', 'version' => '$time', 'remove_tables' => 'prompt', ); EOQ; */ } public function buildInstall($path) { $installdefs = ['id' => $this->name, 'beans' => [], 'layoutdefs' => [], 'relationships' => [], ]; foreach (array_keys($this->modules) as $module) { $this->modules[$module]->build($path); $this->modules[$module]->addInstallDefs($installdefs); } $this->path = $this->getPackageDir(); if (file_exists($this->path . '/language')) { $d = dir($this->path . '/language'); while ($e = $d->read()) { $lang_path = $this->path . '/language/' . $e; if (substr($e, 0, 1) != '.' && is_dir($lang_path)) { $f = dir($lang_path); while ($g = $f->read()) { if (substr($g, 0, 1) != '.' && is_file($lang_path . '/' . $g)) { $lang = substr($g, 0, strpos($g, '.')); $installdefs['language'][] = [ 'from' => '<basepath>/SugarModules/language/' . $e . '/' . $g, 'to_module' => $e, 'language' => $lang, ]; } } } } copy_recursive($this->path . '/language/', $path . '/language/'); } if (file_exists($this->path . '/icons/')) { $icon_path = $path . '/../icons/default/images/'; mkdir_recursive($icon_path); copy_recursive($this->path . '/icons/', $icon_path); $installdefs['image_dir'] = '<basepath>/icons'; } return "\n" . '$installdefs = ' . var_export_helper($installdefs) . ';'; } public function getPackageDir() { return MB_PACKAGE_PATH . '/' . basename($this->name); } public function getBuildDir() { return MB_PACKAGE_BUILD . DIRECTORY_SEPARATOR . basename($this->name); } public function getZipDir() { return $this->getPackageDir() . '/zips'; } public function load() { $path = $this->getPackageDir(); if (file_exists($path . '/manifest.php')) { require FileLoader::validateFilePath($path . '/manifest.php'); if (!empty($manifest)) { $this->date_modified = $manifest['published_date']; $this->is_uninstallable = $manifest['is_uninstallable']; $this->author = $manifest['author']; $this->key = $manifest['key']; $this->description = $manifest['description']; if (!empty($manifest['readme'])) { $this->readme = $manifest['readme']; } } } $this->loadModules(true); } public function save() { $path = $this->getPackageDir(); if (mkdir_recursive($path)) { //Save all the modules when we save a package $this->updateModulesMetaData(true); sugar_file_put_contents_atomic($path . '/manifest.php', $this->getManifest()); } } public function build($export = true, $clean = false) { $this->loadModules(); require_once 'include/utils/zip_utils.php'; $package_path = $this->getPackageDir(); $path = $this->getBuildDir() . '/SugarModules'; if ($clean && file_exists($path)) { rmdir_recursive($path); } if (mkdir_recursive($path)) { $manifest = $this->getManifest() . $this->buildInstall($path); $fp = sugar_fopen($this->getBuildDir() . '/manifest.php', 'w'); fwrite($fp, $manifest); fclose($fp); } if (file_exists('modules/ModuleBuilder/MB/LICENSE.txt')) { copy('modules/ModuleBuilder/MB/LICENSE.txt', $this->getBuildDir() . '/LICENSE.txt'); } elseif (file_exists('LICENSE')) { copy('LICENSE', $this->getBuildDir() . '/LICENSE'); } $date = date('Y_m_d_His'); $zipDir = $this->getZipDir(); if (!file_exists($zipDir)) { mkdir_recursive($zipDir); } zip_dir($this->getBuildDir(), $zipDir . '/' . $this->name . $date . '.zip'); if ($export) { header('Location:' . $zipDir . '/' . $this->name . $date . '.zip'); } return [ 'zip' => $zipDir . '/' . $this->name . $date . '.zip', 'manifest' => $this->getBuildDir() . '/manifest.php', 'name' => $this->name . $date, ]; } public function getNodes() { $this->loadModules(); $node = ['name' => $this->name, 'action' => 'module=ModuleBuilder&action=package&package=' . $this->name, 'children' => []]; foreach (array_keys($this->modules) as $module) { $node['children'][] = $this->modules[$module]->getNodes(); } return $node; } public function populateFromPost() { $this->description = trim((string)$this->request->getValidInputRequest('description')); $this->author = trim((string)$this->request->getValidInputRequest('author')); $this->key = trim((string)$this->request->getValidInputRequest('key')); $constraintBuilder = new ConstraintBuilder(); $constraints = $constraintBuilder->build('Assert\ComponentName'); $violations = Validator::getService()->validate($this->key, $constraints); if (safeCount($violations) > 0) { $sugarConfig = \SugarConfig::getInstance(); // Check softFail mode - enabled by default $softFail = $sugarConfig->get('validation.soft_fail', true); if (!$softFail) { $GLOBALS['log']->fatal('InputValidation: Violation for REQUEST -> key'); throw new ViolationException( 'Violation for REQUEST -> key', $violations ); } else { $GLOBALS['log']->warn('InputValidation: Violation for REQUEST -> key'); } } $this->readme = trim((string)$this->request->getValidInputRequest('readme')); } public function rename($new_name) { $old = $this->getPackageDir(); $this->name = $new_name; $new = $this->getPackageDir(); if (file_exists($new)) { return false; } if (rename($old, $new)) { return true; } return false; } public function updateModulesMetaData($save = false) { foreach (array_keys($this->modules) as $module) { $old_name = $this->modules[$module]->key_name; $this->modules[$module]->key_name = $this->key . '_' . $this->modules[$module]->name; $this->modules[$module]->renameRelationships( $this->modules[$module]->getModuleDir(), $old_name, $this->modules[$module]->key_name ); $this->modules[$module]->renameMetaData($this->modules[$module]->getModuleDir(), $old_name); $this->modules[$module]->renameLanguageFiles($this->modules[$module]->getModuleDir()); if ($save) { $this->modules[$module]->save(); } $this->modules[$module]->cleanupLayout(); } } public function copy($new_name) { $old = $this->getPackageDir(); $count = 0; $this->name = $new_name; $new = $this->getPackageDir(); while (file_exists($new)) { $count++; $this->name = $new_name . $count; $new = $this->getPackageDir(); } $new = $this->getPackageDir(); if (copy_recursive($old, $new)) { $this->updateModulesMetaData(); return true; } return false; } public function delete() { $modules = []; foreach ($this->modules as $module) { $modules[] = $module->key_name; } WorkFlow::deleteWorkFlowsByModule($modules); return rmdir_recursive($this->getPackageDir()); } //creation of the installdefs[] array for the manifest when exporting customizations public function customBuildInstall($modules, $path, $extensions = []) { $installdefs = ['id' => $this->name, 'relationships' => []]; foreach (self::$appExtensions as $type => $spec) { $include_path = $path . '/SugarModules/' . $spec['dir']; $it = $this->getDirectoryIterator($include_path); foreach ($it as $file) { $subPathName = $it->getSubPathname(); $def = [ 'from' => '<basepath>/SugarModules/' . $spec['dir'] . '/' . $subPathName, 'to_module' => 'application', ]; $baseName = $file->getBasename(); if ($type == 'language') { $def['language'] = substr($baseName, 0, strpos($baseName, '.')); } $installdefs[$type][] = $def; } } foreach ($modules as $value) { $custom_module = $this->getModuleCustomizations($value); foreach ($custom_module as $va => $_) { switch ($va) { case 'language': case 'Ext/Vardefs': case 'Ext/Language': // Old way if ($va === 'language') { $this->getLanguageManifestForModule($value, $installdefs); $this->getCustomFieldsManifestForModule($value, $installdefs); } else { // Build a full path to the Ext directory for the // package module $fullpath = "$path/Extension/modules/$value/Ext/"; $paths = [ 'Vardefs' => 'getCustomFieldsManifestForModule', 'Language' => 'getLanguageManifestForModule', ]; // Check to make sure that the directories in question // exist and are not empty (like when something might // have been disabled/deleted) foreach ($paths as $pathKey => $pathMethod) { $full = $fullpath . $pathKey; if ($this->isDirectoryExportable($full)) { $this->$pathMethod($value, $installdefs); } } } break; case 'metadata': $this->getCustomMetadataManifestForModule($value, $installdefs); break; case 'clients': $this->getCustomClientMetadata($value, $installdefs); break; } } $relationshipsMetaFiles = $this->getCustomRelationshipsMetaFilesByModuleName($value, true, true, $modules); if ($relationshipsMetaFiles) { foreach ($relationshipsMetaFiles as $file) { $installdefs['relationships'][] = [ 'meta_data' => str_replace( 'custom' . DIRECTORY_SEPARATOR, '<basepath>' . DIRECTORY_SEPARATOR, $file ), ]; } } }//foreach if (is_dir($path . DIRECTORY_SEPARATOR . 'Extension')) { $this->getExtensionsManifestForPackage($path, $installdefs); } $roles = $this->extractRoles($installdefs); $installdefs['roles'] = $this->getRoleNames($roles); return "\n" . '$installdefs = ' . var_export_helper($installdefs) . ';'; } /** * Extracts IDs of ACL roles from package manifest * * @param array $installdefs Package manifest * @return array */ protected function extractRoles(array $installdefs) { $roles = []; foreach ($installdefs as $section) { if (is_array($section)) { foreach ($section as $def) { if (isset($def['from']) && preg_match('/\/roles\/([^\/]+)/', $def['from'], $matches)) { $roles[$matches[1]] = true; } } } } return array_keys($roles); } /** * Retrieves names of the roles with the given IDs * * @param array $ids ACL role IDs * @return array */ protected function getRoleNames(array $ids) { $roles = []; foreach ($ids as $id) { $role = BeanFactory::retrieveBean('ACLRoles', $id); if ($role) { $roles[$id] = $role->name; } } return $roles; } private function getLanguageManifestForModule($module, &$installdefs) { $lang_path = 'custom' . DIRECTORY_SEPARATOR . 'modules' . DIRECTORY_SEPARATOR . $module . DIRECTORY_SEPARATOR . 'language'; if (!is_dir($lang_path)) { return; } foreach (scandir($lang_path) as $langFile) { if (substr($langFile, 0, 1) != '.' && is_file($lang_path . DIRECTORY_SEPARATOR . $langFile)) { $lang = substr($langFile, 0, strpos($langFile, '.')); $installdefs['language'][] = [ 'from' => '<basepath>/SugarModules/modules/' . $module . '/language/' . $langFile, 'to_module' => $module, 'language' => $lang, ]; } } } private function getCustomFieldsManifestForModule($module, &$installdefs) { $db = DBManagerFactory::getInstance(); $result = $db->query("SELECT * FROM fields_meta_data where custom_module='$module'"); while ($row = $db->fetchByAssoc($result)) { $name = $row['id']; foreach ($row as $col => $res) { switch ($col) { case 'custom_module': $installdefs['custom_fields'][$name]['module'] = $res; break; case 'required': $installdefs['custom_fields'][$name]['require_option'] = $res; break; case 'vname': $installdefs['custom_fields'][$name]['label'] = $res; break; case 'required': $installdefs['custom_fields'][$name]['require_option'] = $res; break; case 'massupdate': $installdefs['custom_fields'][$name]['mass_update'] = $res; break; case 'comments': $installdefs['custom_fields'][$name]['comments'] = $res; break; case 'help': $installdefs['custom_fields'][$name]['help'] = $res; break; case 'len': $installdefs['custom_fields'][$name]['max_size'] = $res; break; default: $installdefs['custom_fields'][$name][$col] = $res; }//switch }//foreach }//while } /** * Gets the custom metadata from inside the clients directory for a module * * @param string $module The module to scrape * @param array $installdefs The current install defs to append */ public function getCustomClientMetadata($module, &$installdefs) { $it = $this->getDirectoryIterator('custom/modules/' . $module . '/clients'); foreach ($it as $file) { $installdefs['copy'][] = [ 'from' => str_replace('custom/modules', '<basepath>/SugarModules/modules', $file), 'to' => $file->getPathname(), ]; } } private function getCustomMetadataManifestForModule($module, &$installdefs) { $meta_path = 'custom/modules/' . $module . '/metadata'; foreach (scandir($meta_path) as $meta_file) { if (substr($meta_file, 0, 1) != '.' && is_file($meta_path . '/' . $meta_file)) { if ($meta_file == 'listviewdefs.php') { $installdefs['copy'][] = [ 'from' => '<basepath>/SugarModules/modules/' . $module . '/metadata/' . $meta_file, 'to' => 'custom/modules/' . $module . '/metadata/' . $meta_file, ]; } else { $installdefs['copy'][] = [ 'from' => '<basepath>/SugarModules/modules/' . $module . '/metadata/' . $meta_file, 'to' => 'custom/modules/' . $module . '/metadata/' . $meta_file, ]; $installdefs['copy'][] = [ 'from' => '<basepath>/SugarModules/modules/' . $module . '/metadata/' . $meta_file, 'to' => 'custom/working/modules/' . $module . '/metadata/' . $meta_file, ]; } } } } /** * @param string $path * @param array $installdefs link * @todo private changed protected for testing purposes. * */ protected function getExtensionsManifestForPackage($path, &$installdefs) { if (empty($installdefs['copy'])) { $installdefs['copy'] = []; } $generalPath = DIRECTORY_SEPARATOR . 'Extension' . DIRECTORY_SEPARATOR . 'modules'; //do not process if path is not a valid directory, or recursiveIterator will break. if (!is_dir($path . $generalPath)) { return; } $recursiveIterator = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($path . $generalPath), RecursiveIteratorIterator::SELF_FIRST ); /* @var $fInfo SplFileInfo */ foreach (new RegexIterator($recursiveIterator, "/\.php$/i") as $fInfo) { $newPath = substr($fInfo->getPathname(), strrpos($fInfo->getPathname(), $generalPath)); $installdefs['copy'][] = [ 'from' => '<basepath>' . $newPath, 'to' => 'custom' . $newPath, ]; } } //return an array which contain the name of fields_meta_data table's columns public function getColumnsName() { $meta = BeanFactory::newBean('EditCustomFields'); $arr = []; foreach ($meta->getFieldDefinitions() as $key => $value) { $arr[] = $key; } return $arr; } //creation of the custom fields ZIP file (use getmanifest() and customBuildInstall() ) public function exportCustom($modules, $export = true, $clean = true) { $relationshipFiles = []; $path = $this->getBuildDir(); if ($clean && file_exists($path)) { rmdir_recursive($path); } //Copy the custom files to the build dir foreach ($modules as $module) { $pathmod = "$path/SugarModules/modules/$module"; // create module directory only if customizations that belong directly to the module exist if (file_exists("custom/modules/$module") && mkdir_recursive($pathmod)) { $this->export("custom/modules/$module", $pathmod); //Don't include cached extension files if (is_dir("$pathmod/Ext")) { rmdir_recursive("$pathmod/Ext"); } //Convert modstring files to extension compatible arrays $this->convertLangFilesToExtensions("$pathmod/language"); } $extensions = $this->getExtensionsList($module, $modules); $relMetaFiles = $this->getCustomRelationshipsMetaFilesByModuleName($module, true, false, $modules); $extensions = array_merge($extensions, $relMetaFiles); foreach ($extensions as $file) { $fileInfo = new SplFileInfo($file); $trimmedPath = ltrim($fileInfo->getPath(), 'custom'); sugar_mkdir($path . $trimmedPath, null, true); // append package name to the language file name in order to make the package have its own unique // language files and thus avoid collisions between package and instance customizations if (strpos($trimmedPath, '/Ext/Language') !== false) { $baseName = $fileInfo->getBasename('.lang.php'); $fileName = $baseName . '.' . $this->name . '.lang.php'; } else { $fileName = $fileInfo->getFilename(); } copy($file, $path . $trimmedPath . '/' . $fileName); } } $this->copyCustomIncludesForModules($modules, $path); if (!is_dir($path)) { sugar_die('Build directory has not been created'); } $manifest = $this->getManifest(true) . $this->customBuildInstall($modules, $path); sugar_file_put_contents($path . '/manifest.php', $manifest); if (file_exists('modules/ModuleBuilder/MB/LICENSE.txt')) { copy('modules/ModuleBuilder/MB/LICENSE.txt', $path . '/LICENSE.txt'); } elseif (file_exists('LICENSE')) { copy('LICENSE', $path . '/LICENSE'); } require_once 'include/utils/zip_utils.php'; $date = date('Y_m_d_His'); $zipDir = $this->getZipDir(); if (!file_exists($zipDir)) { mkdir_recursive($zipDir); } $cwd = getcwd(); chdir($this->getBuildDir()); zip_dir('.', $cwd . '/' . $zipDir . '/' . $this->name . $date . '.zip'); chdir($cwd); if ($clean && file_exists($this->getBuildDir())) { rmdir_recursive($this->getBuildDir()); } if ($export) { header('Location:' . $zipDir . '/' . $this->name . $date . '.zip'); } return $zipDir . '/' . $this->name . $date . '.zip'; } private function convertLangFilesToExtensions($langDir) { if (is_dir($langDir)) { foreach (scandir($langDir) as $langFile) { $mod_strings = []; if (strcasecmp(substr($langFile, -4), '.php') != 0) { continue; } include FileLoader::validateFilePath("$langDir/$langFile"); $out = "<?php \n // created: " . date('Y-m-d H:i:s') . "\n"; foreach ($mod_strings as $lbl_key => $lbl_val) { $out .= override_value_to_string('mod_strings', $lbl_key, $lbl_val) . "\n"; } $out .= "\n?>\n"; sugar_file_put_contents("$langDir/$langFile", $out); } } } private function copyCustomIncludesForModules($modules, $path) { foreach (self::$appExtensions as $spec) { $varName = $spec['varName']; $it = $this->getDirectoryIterator('custom/' . $spec['dir']); foreach ($it as $file) { ${$varName} = []; include $file; $values = $this->getCustomOptionsForModules($modules, ${$varName}); if (safeCount($values) > 0) { $contents = "<?php \n"; foreach ($values as $name => $arr) { $contents .= override_value_to_string($varName, $name, $arr); } $subPathName = $it->getSubPathname(); $subPathName = str_replace('.lang', '', $subPathName); $subPathName = substr($subPathName, 0, -4) . '.' . $this->name . substr($subPathName, -4); $destination = $path . '/SugarModules/' . $spec['dir'] . '/' . $subPathName; mkdir_recursive(dirname($destination)); sugar_file_put_contents($destination, $contents); } } } } /** * Returns iterator over exportable files in the given directory and subdirectories * * @param string $dir Directory path * @return RecursiveDirectoryIterator|SplFileInfo[] */ protected function getDirectoryIterator($dir) { if (file_exists($dir)) { $it = new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS); $it = new RecursiveIteratorIterator($it); $it = new RegexIterator($it, '/\.php$/'); return $it; } return new EmptyIterator(); } protected function getCustomOptionsForModules($modules, $list_strings) { $options = []; foreach ($modules as $module) { $bean = BeanFactory::newBean($module); if (!empty($bean)) { foreach ($bean->field_defs as $field => $def) { if (isset($def['options']) && is_scalar($def['options']) && isset($list_strings[$def['options']])) { $options[$def['options']] = $list_strings[$def['options']]; } } } } return $options; } /** * Returns associative array containing module names as keys and types of their customizations as values * * @return array */ public function getCustomModules() { global $mod_strings; global $modInvisList; $modulesWithCustomDropdowns = $this->getModulesWithApplicationExtensions(); $modules = array_merge($this->getSubdirectories('custom/modules/'), $modulesWithCustomDropdowns); $modules = array_unique($modules); //Use StudioBrowser to grab list of modules that are customizeable through studio. $sb = new StudioBrowser(); $sb->loadModules(); $studioModules = array_keys($sb->modules); //limit modules to process to the ones that can be edited in studio $modules = array_intersect($modules, $studioModules); $result = []; foreach ($modules as $module) { $result[$module] = $this->getModuleCustomizations($module); if (in_array($module, $modulesWithCustomDropdowns) && SugarAutoLoader::existingCustomOne("modules/{$module}/metadata/studio.php")) { $result[$module]['Dropdown'] = $mod_strings['LBL_EC_CUSTOMDROPDOWN']; } } return array_filter($result); } /** * Returns types of existing customizations for the given module * * @param string $module Module name * @return array */ protected function getModuleCustomizations($module) { global $mod_strings; $result = []; if (!SugarAutoLoader::existingCustomOne("modules/{$module}/metadata/studio.php")) { return $result; } $path = 'custom/modules/' . $module; $subdirectories = $this->getSubdirectories('custom/modules/' . $module); foreach ($subdirectories as $type) { switch ($type) { case 'language': $result[$type] = $mod_strings['LBL_EC_CUSTOMFIELD']; break; case 'metadata': case 'clients': // BWC modules keep metadata in the 'metadata' directory if (isModuleBWC($module)) { $result[$type] = $mod_strings['LBL_EC_CUSTOMLAYOUT']; } else { // New style pathing $fullpath = $path . '/' . $type; // Right now only views are customizable in studio $viewDirs = glob("$fullpath/*/views"); foreach ($viewDirs as $viewDir) { if ($this->isDirectoryExportable($viewDir)) { $result[$type] = $mod_strings['LBL_EC_CUSTOMLAYOUT']; break; } } } break; case 'Ext': // Simply checking the Ext directory isn't enough... // we need to check certain directories inside of it // to make sure there are things that are eligible // to export $fullpath = $path . '/' . $type; // Start first with custom fields if ($this->isDirectoryExportable("$fullpath/Vardefs")) { $result["$type/Vardefs"] = $mod_strings['LBL_EC_CUSTOMFIELD']; } // Now check custom labels if ($this->isDirectoryExportable("$fullpath/Language")) { $result["$type/Language"] = $mod_strings['LBL_EC_CUSTOMLABEL']; } break; default: $result[$type] = $mod_strings['LBL_UNDEFINED']; } } return $result; } /** * Returns array of subdirectories for the given directory * * @param string $path Directory path * @return array */ protected function getSubdirectories($path) { $path = rtrim($path, '/'); $subdirectories = []; if (is_dir($path)) { $files = scandir($path); foreach ($files as $value) { if (is_dir($path . '/' . $value) && $value != '.' && $value != '..') { $subdirectories[] = $value; } } } return $subdirectories; } /** * Returns array of module names that use application level extensions * * @return array */ protected function getModulesWithApplicationExtensions() { global $beanList; $app_list_strings = []; $role_dropdown_filters = []; foreach (self::$appExtensions as $spec) { $it = $this->getDirectoryIterator('custom/' . $spec['dir']); foreach ($it as $file) { include $file; } } $modules = []; if (safeCount($app_list_strings) > 0 || safeCount($role_dropdown_filters) > 0 ) { foreach ($beanList as $module => $_) { $bean = BeanFactory::newBean($module); if (!isset($bean->field_defs) || !is_array($bean->field_defs)) { continue; } foreach ($bean->field_defs as $field => $def) { if (isset($def['options']) && is_scalar($def['options'])) { foreach (self::$appExtensions as $spec) { $varName = $spec['varName']; if (isset(${$varName}[$def['options']])) { $modules[] = $module; continue 3; } } } } } } return $modules; } /** * Get _custom_ extensions for module. * Default path - custom/Extension/modules/$module/Ext. * * @param array $module Name. * @param mixed $includeRelationships ARRAY - relationships files between $module and names in array; * TRUE - with all relationships files; * @return array Paths. */ protected function getExtensionsList($module, $includeRelationships = true) { if (BeanFactory::getBeanClass($module) === false) { return []; } $result = []; $includeMask = false; $extPath = sprintf('custom%1$sExtension%1$smodules%1$s' . $module . '%1$sExt', DIRECTORY_SEPARATOR); //do not process if path is not a valid directory, or recursiveIterator will break. if (!is_dir($extPath)) { return $result; } if (is_array($includeRelationships)) { $includeMask = []; $customRels = $this->getCustomRelationshipsByModuleName($module); $includeRelationships[] = $module; foreach ($customRels as $k => $v) { if (in_array($v->getLhsModule(), $includeRelationships) && in_array($v->getRhsModule(), $includeRelationships) ) { $includeMask[] = $k; } } } $recursiveIterator = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($extPath), RecursiveIteratorIterator::SELF_FIRST ); /* @var $fileInfo SplFileInfo */ foreach ($recursiveIterator as $fileInfo) { if ($fileInfo->isFile() && !in_array($fileInfo->getPathname(), $result)) { //get the filename in lowercase for easier comparison $fn = $fileInfo->getFilename(); if (!empty($fn)) { $fn = strtolower($fn); } if ($this->filterExportedRelationshipFile($fn, $module, $includeRelationships)) { $result[] = $fileInfo->getPathname(); } } } return $result; } /** * Processes the name of the file and compares against the passed in module names to * evaluate whether the file should be included for export. Returns true or false * * @param $fn (file name that is being evaluated) * @param $module (name of current module being evaluated) * @param $includeRelationship (list of related modules that are also being exported, to be used as filters) * @return boolean true or false */ public function filterExportedRelationshipFile($fn, $module, $includeRelationships) { $shouldExport = false; if (empty($fn) || !is_array($includeRelationships) || empty($includeRelationships)) { return $shouldExport; } //if file name does not contain the current module name then it is not a relationship file, //or if the module has the current module name twice seperated with an underscore, then this is a relationship within itself //in both cases set the shouldExport flag to true $lc_mod = strtolower($module); $fn = strtolower($fn); if ((strpos($fn, $lc_mod) === false) || (strpos($fn, $lc_mod . '_' . $lc_mod) !== false)) { $shouldExport = true; } else { //grab only rels that have both modules in the export list foreach ($includeRelationships as $relatedModule) { //skip if the related module is empty if (empty($relatedModule)) { continue; } //if the filename also has the related module name, then add the relationship file //strip the current module,as we have already checked for existance of module name and dont want any false positives $fn = str_replace($lc_mod, '', $fn); if (strpos($fn, strtolower($relatedModule)) !== false) { //both modules exist in the filename lets include in the results array $shouldExport = true; break; } } } return $shouldExport; } /** * Returns a set of field defs for fields that will exist when this package is deployed * based on the relationships in all of its modules. * * @param $moduleName (module must be from whithin this package) * @return array Field defs */ public function getRelationshipsForModule($moduleName) { $ret = []; if (isset($this->modules[$moduleName])) { $keyName = $this->modules[$moduleName]->key_name; foreach ($this->modules as $mName => $module) { $rels = $module->getRelationships(); $relList = $rels->getRelationshipList(); foreach ($relList as $rName) { $rel = $rels->get($rName); if ($rel->lhs_module == $keyName || $rel->rhs_module == $keyName) { $ret[$rName] = $rel; } } } } return $ret; } private function exportProjectInstall() { $installdefs = ['id' => MB_EXPORTPREPEND . $this->name]; $installdefs['copy'][] = [ 'from' => '<basepath>/' . $this->name, 'to' => 'custom/modulebuilder/packages/' . $this->name, ]; return "\n" . '$installdefs = ' . var_export_helper($installdefs) . ';'; } public function exportProject() { $tmppath = 'custom/modulebuilder/projectTMP/'; if (file_exists($this->getPackageDir())) { if (mkdir_recursive($tmppath)) { $packageName = basename($this->name); copy_recursive($this->getPackageDir(), $tmppath . DIRECTORY_SEPARATOR . $packageName); $zipsDir = $tmppath . DIRECTORY_SEPARATOR . $packageName . DIRECTORY_SEPARATOR . 'zips'; if (file_exists($zipsDir)) { rmdir_recursive($zipsDir); } $manifest = $this->getManifest(true, true) . $this->exportProjectInstall(); $fp = sugar_fopen($tmppath . '/manifest.php', 'w'); fwrite($fp, $manifest); fclose($fp); if (file_exists('modules/ModuleBuilder/MB/LICENSE.txt')) { copy('modules/ModuleBuilder/MB/LICENSE.txt', $tmppath . '/LICENSE.txt'); } elseif (file_exists('LICENSE')) { copy('LICENSE', $tmppath . '/LICENSE'); } $readme_contents = $this->readme; $readmefp = sugar_fopen($tmppath . '/README.txt', 'w'); fwrite($readmefp, $readme_contents); fclose($readmefp); } } require_once 'include/utils/zip_utils.php'; $date = date('Y_m_d_His'); $zipDir = 'custom/modulebuilder/packages/ExportProjectZips'; if (!file_exists($zipDir)) { mkdir_recursive($zipDir); } $cwd = getcwd(); chdir($tmppath); zip_dir('.', $cwd . '/' . $zipDir . '/project_' . $this->name . $date . '.zip'); chdir($cwd); if (file_exists($tmppath)) { rmdir_recursive($tmppath); } header('Location:' . $zipDir . '/project_' . $this->name . $date . '.zip'); return $zipDir . '/project_' . $this->name . $date . '.zip'; } /** * This returns an UNFILTERED list of custom relationships by module name. You will have to filter the relationships * by the modules being exported after calling this method * @param string $moduleName * @param bool $lhs Return relationships where $moduleName - left module in join. * @return mixed Array or false when module name is wrong. */ protected function getCustomRelationshipsByModuleName($moduleName, $lhs = false) { if (BeanFactory::getBeanClass($moduleName) === false) { return false; } $result = []; $relation = null; $module = StudioModuleFactory::getStudioModule($moduleName); /* @var $rel DeployedRelationships */ $rel = $module->getRelationships(); $relList = $rel->getRelationshipList(); foreach ($relList as $relationshipName) { $relation = $rel->get($relationshipName); if ($relation->getFromStudio()) { if ($lhs && $relation->getLhsModule() != $moduleName) { continue; } $result[$relationshipName] = $relation; } } return $result; } /** * @param string $moduleName * @param bool $lhs Return relationships where $moduleName - left module in join. * @param bool $metadataOnly Return only relationships metadata file. * @param $includeRelationship (list of related modules that are also being exported) * @return array (array of relationships filtered to only include relationships to modules being exported) */ protected function getCustomRelationshipsMetaFilesByModuleName($moduleName, $lhs = false, $metadataOnly = false, $exportedModulesFilter = []) { $path = $metadataOnly ? 'custom' . DIRECTORY_SEPARATOR . 'metadata' . DIRECTORY_SEPARATOR : 'custom' . DIRECTORY_SEPARATOR; $result = []; //do not process if path is not a valid directory, or recursiveIterator will break. if (!is_dir($path)) { return $result; } $relationships = $this->getCustomRelationshipsByModuleName($moduleName, $lhs); if (!$relationships) { return []; } $recursiveIterator = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($path), RecursiveIteratorIterator::SELF_FIRST ); /** * @var $fileInfo SplFileInfo */ foreach ($recursiveIterator as $fileInfo) { if ($fileInfo->isFile() && !in_array($fileInfo->getPathname(), $result)) { foreach ($relationships as $k => $v) { if (strpos($fileInfo->getFilename(), (string) $k) !== false) { //filter by modules being exported if ($this->filterExportedRelationshipFile($fileInfo->getFilename(), $moduleName, $exportedModulesFilter)) { $result[] = $fileInfo->getPathname(); break; } } } } } return $result; } public function deleteBuild() { return rmdir_recursive($this->getBuildDir()); } /** * Checks a directory to make sure there is something in it for export * * @param string $path Directory path to check for files * @return boolean */ public function isDirectoryExportable($path) { if (file_exists($path)) { $it = $this->getDirectoryIterator($path); $it->rewind(); return $it->valid(); } return false; } /** * Exports source directory to the destination directory * * @param string $src Source directory * @param string $dst Destination directory */ protected function export($src, $dst) { $it = $this->getDirectoryIterator($src); foreach ($it as $file) { $subPathName = $it->getSubPathname(); $dstPath = $dst . '/' . $subPathName; $dirName = dirname($dstPath); if (!is_dir($dirName)) { mkdir_recursive($dirName); } copy($file, $dstPath); } } }