Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Fetching contributors…

Cannot retrieve contributors at this time

5607 lines (5078 sloc) 218.266 kb
<?php
if(!defined('sugarEntry') || !sugarEntry) die('Not A Valid Entry Point');
/*********************************************************************************
* SugarCRM Community Edition is a customer relationship management program developed by
* SugarCRM, Inc. Copyright (C) 2004-2012 SugarCRM Inc.
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License version 3 as published by the
* Free Software Foundation with the addition of the following permission added
* to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK
* IN WHICH THE COPYRIGHT IS OWNED BY SUGARCRM, SUGARCRM DISCLAIMS THE WARRANTY
* OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along with
* this program; if not, see http://www.gnu.org/licenses or write to the Free
* Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301 USA.
*
* You can contact SugarCRM, Inc. headquarters at 10050 North Wolfe Road,
* SW2-130, Cupertino, CA 95014, USA. or at email address contact@sugarcrm.com.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "Powered by
* SugarCRM" logo. If the display of the logo is not reasonably feasible for
* technical reasons, the Appropriate Legal Notices must display the words
* "Powered by SugarCRM".
********************************************************************************/
/*********************************************************************************
* Description: Defines the base class for all data entities used throughout the
* application. The base class including its methods and variables is designed to
* be overloaded with module-specific methods and variables particular to the
* module's base entity class.
* Portions created by SugarCRM are Copyright (C) SugarCRM, Inc.
* All Rights Reserved.
*******************************************************************************/
require_once('modules/DynamicFields/DynamicField.php');
require_once("data/Relationships/RelationshipFactory.php");
/**
* SugarBean is the base class for all business objects in Sugar. It implements
* the primary functionality needed for manipulating business objects: create,
* retrieve, update, delete. It allows for searching and retrieving list of records.
* It allows for retrieving related objects (e.g. contacts related to a specific account).
*
* In the current implementation, there can only be one bean per folder.
* Naming convention has the bean name be the same as the module and folder name.
* All bean names should be singular (e.g. Contact). The primary table name for
* a bean should be plural (e.g. contacts).
* @api
*/
class SugarBean
{
/**
* A pointer to the database object
*
* @var DBManager
*/
var $db;
/**
* Unique object identifier
*
* @var string
*/
public $id;
/**
* When createing a bean, you can specify a value in the id column as
* long as that value is unique. During save, if the system finds an
* id, it assumes it is an update. Setting new_with_id to true will
* make sure the system performs an insert instead of an update.
*
* @var BOOL -- default false
*/
var $new_with_id = false;
/**
* How deep logic hooks can go
* @var int
*/
protected $max_logic_depth = 10;
/**
* Disble vardefs. This should be set to true only for beans that do not have varders. Tracker is an example
*
* @var BOOL -- default false
*/
var $disable_vardefs = false;
/**
* holds the full name of the user that an item is assigned to. Only used if notifications
* are turned on and going to be sent out.
*
* @var String
*/
var $new_assigned_user_name;
/**
* An array of booleans. This array is cleared out when data is loaded.
* As date/times are converted, a "1" is placed under the key, the field is converted.
*
* @var Array of booleans
*/
var $processed_dates_times = array();
/**
* Whether to process date/time fields for storage in the database in GMT
*
* @var BOOL
*/
var $process_save_dates =true;
/**
* This signals to the bean that it is being saved in a mass mode.
* Examples of this kind of save are import and mass update.
* We turn off notificaitons of this is the case to make things more efficient.
*
* @var BOOL
*/
var $save_from_post = true;
/**
* When running a query on related items using the method: retrieve_by_string_fields
* this value will be set to true if more than one item matches the search criteria.
*
* @var BOOL
*/
var $duplicates_found = false;
/**
* true if this bean has been deleted, false otherwise.
*
* @var BOOL
*/
var $deleted = 0;
/**
* Should the date modified column of the bean be updated during save?
* This is used for admin level functionality that should not be updating
* the date modified. This is only used by sync to allow for updates to be
* replicated in a way that will not cause them to be replicated back.
*
* @var BOOL
*/
var $update_date_modified = true;
/**
* Should the modified by column of the bean be updated during save?
* This is used for admin level functionality that should not be updating
* the modified by column. This is only used by sync to allow for updates to be
* replicated in a way that will not cause them to be replicated back.
*
* @var BOOL
*/
var $update_modified_by = true;
/**
* Setting this to true allows for updates to overwrite the date_entered
*
* @var BOOL
*/
var $update_date_entered = false;
/**
* This allows for seed data to be created without using the current uesr to set the id.
* This should be replaced by altering the current user before the call to save.
*
* @var unknown_type
*/
//TODO This should be replaced by altering the current user before the call to save.
var $set_created_by = true;
var $team_set_id;
/**
* The database table where records of this Bean are stored.
*
* @var String
*/
var $table_name = '';
/**
* This is the singular name of the bean. (i.e. Contact).
*
* @var String
*/
var $object_name = '';
/** Set this to true if you query contains a sub-select and bean is converting both select statements
* into count queries.
*/
var $ungreedy_count=false;
/**
* The name of the module folder for this type of bean.
*
* @var String
*/
var $module_dir = '';
var $module_name = '';
var $field_name_map;
var $field_defs;
var $custom_fields;
var $column_fields = array();
var $list_fields = array();
var $additional_column_fields = array();
var $relationship_fields = array();
var $current_notify_user;
var $fetched_row=false;
var $fetched_rel_row = array();
var $layout_def;
var $force_load_details = false;
var $optimistic_lock = false;
var $disable_custom_fields = false;
var $number_formatting_done = false;
var $process_field_encrypted=false;
/*
* The default ACL type
*/
var $acltype = 'module';
var $additional_meta_fields = array();
/**
* Set to true in the child beans if the module supports importing
*/
var $importable = false;
/**
* Set to true in the child beans if the module use the special notification template
*/
var $special_notification = false;
/**
* Set to true if the bean is being dealt with in a workflow
*/
var $in_workflow = false;
/**
*
* By default it will be true but if any module is to be kept non visible
* to tracker, then its value needs to be overriden in that particular module to false.
*
*/
var $tracker_visibility = true;
/**
* Used to pass inner join string to ListView Data.
*/
var $listview_inner_join = array();
/**
* Set to true in <modules>/Import/views/view.step4.php if a module is being imported
*/
var $in_import = false;
/**
* A way to keep track of the loaded relationships so when we clone the object we can unset them.
*
* @var array
*/
protected $loaded_relationships = array();
/**
* set to true if dependent fields updated
*/
protected $is_updated_dependent_fields = false;
/**
* Blowfish encryption key
* @var string
*/
static protected $field_key;
/**
* Constructor for the bean, it performs following tasks:
*
* 1. Initalized a database connections
* 2. Load the vardefs for the module implemeting the class. cache the entries
* if needed
* 3. Setup row-level security preference
* All implementing classes must call this constructor using the parent::SugarBean() class.
*
*/
function SugarBean()
{
global $dictionary, $current_user;
static $loaded_defs = array();
$this->db = DBManagerFactory::getInstance();
if (empty($this->module_name))
$this->module_name = $this->module_dir;
if((false == $this->disable_vardefs && empty($loaded_defs[$this->object_name])) || !empty($GLOBALS['reload_vardefs']))
{
VardefManager::loadVardef($this->module_dir, $this->object_name);
// build $this->column_fields from the field_defs if they exist
if (!empty($dictionary[$this->object_name]['fields'])) {
foreach ($dictionary[$this->object_name]['fields'] as $key=>$value_array) {
$column_fields[] = $key;
if(!empty($value_array['required']) && !empty($value_array['name'])) {
$this->required_fields[$value_array['name']] = 1;
}
}
$this->column_fields = $column_fields;
}
//setup custom fields
if(!isset($this->custom_fields) &&
empty($this->disable_custom_fields))
{
$this->setupCustomFields($this->module_dir);
}
//load up field_arrays from CacheHandler;
if(empty($this->list_fields))
$this->list_fields = $this->_loadCachedArray($this->module_dir, $this->object_name, 'list_fields');
if(empty($this->column_fields))
$this->column_fields = $this->_loadCachedArray($this->module_dir, $this->object_name, 'column_fields');
if(empty($this->required_fields))
$this->required_fields = $this->_loadCachedArray($this->module_dir, $this->object_name, 'required_fields');
if(isset($GLOBALS['dictionary'][$this->object_name]) && !$this->disable_vardefs)
{
$this->field_name_map = $dictionary[$this->object_name]['fields'];
$this->field_defs = $dictionary[$this->object_name]['fields'];
if(!empty($dictionary[$this->object_name]['optimistic_locking']))
{
$this->optimistic_lock=true;
}
}
$loaded_defs[$this->object_name]['column_fields'] =& $this->column_fields;
$loaded_defs[$this->object_name]['list_fields'] =& $this->list_fields;
$loaded_defs[$this->object_name]['required_fields'] =& $this->required_fields;
$loaded_defs[$this->object_name]['field_name_map'] =& $this->field_name_map;
$loaded_defs[$this->object_name]['field_defs'] =& $this->field_defs;
}
else
{
$this->column_fields =& $loaded_defs[$this->object_name]['column_fields'] ;
$this->list_fields =& $loaded_defs[$this->object_name]['list_fields'];
$this->required_fields =& $loaded_defs[$this->object_name]['required_fields'];
$this->field_name_map =& $loaded_defs[$this->object_name]['field_name_map'];
$this->field_defs =& $loaded_defs[$this->object_name]['field_defs'];
$this->added_custom_field_defs = true;
if(!isset($this->custom_fields) &&
empty($this->disable_custom_fields))
{
$this->setupCustomFields($this->module_dir, false);
}
if(!empty($dictionary[$this->object_name]['optimistic_locking']))
{
$this->optimistic_lock=true;
}
}
if($this->bean_implements('ACL') && !empty($GLOBALS['current_user'])){
$this->acl_fields = (isset($dictionary[$this->object_name]['acl_fields']) && $dictionary[$this->object_name]['acl_fields'] === false)?false:true;
}
$this->populateDefaultValues();
}
/**
* Returns the object name. If object_name is not set, table_name is returned.
*
* All implementing classes must set a value for the object_name variable.
*
* @param array $arr row of data fetched from the database.
* @return nothing
*
*/
function getObjectName()
{
if ($this->object_name)
return $this->object_name;
// This is a quick way out. The generated metadata files have the table name
// as the key. The correct way to do this is to override this function
// in bean and return the object name. That requires changing all the beans
// as well as put the object name in the generator.
return $this->table_name;
}
/**
* Returns a list of fields with their definitions that have the audited property set to true.
* Before calling this function, check whether audit has been enabled for the table/module or not.
* You would set the audit flag in the implemting module's vardef file.
*
* @return an array of
* @see is_AuditEnabled
*
* Internal function, do not override.
*/
function getAuditEnabledFieldDefinitions()
{
$aclcheck = $this->bean_implements('ACL');
$is_owner = $this->isOwner($GLOBALS['current_user']->id);
if (!isset($this->audit_enabled_fields))
{
$this->audit_enabled_fields=array();
foreach ($this->field_defs as $field => $properties)
{
if (
(
!empty($properties['Audited']) || !empty($properties['audited']))
)
{
$this->audit_enabled_fields[$field]=$properties;
}
}
}
return $this->audit_enabled_fields;
}
/**
* Return true if auditing is enabled for this object
* You would set the audit flag in the implemting module's vardef file.
*
* @return boolean
*
* Internal function, do not override.
*/
function is_AuditEnabled()
{
global $dictionary;
if (isset($dictionary[$this->getObjectName()]['audited']))
{
return $dictionary[$this->getObjectName()]['audited'];
}
else
{
return false;
}
}
/**
* Returns the name of the audit table.
* Audit table's name is based on implementing class' table name.
*
* @return String Audit table name.
*
* Internal function, do not override.
*/
function get_audit_table_name()
{
return $this->getTableName().'_audit';
}
/**
* Returns the name of the custom table.
* Custom table's name is based on implementing class' table name.
*
* @return String Custom table name.
*
* Internal function, do not override.
*/
public function get_custom_table_name()
{
return $this->getTableName().'_cstm';
}
/**
* If auditing is enabled, create the audit table.
*
* Function is used by the install scripts and a repair utility in the admin panel.
*
* Internal function, do not override.
*/
function create_audit_table()
{
global $dictionary;
$table_name=$this->get_audit_table_name();
require('metadata/audit_templateMetaData.php');
// Bug: 52583 Need ability to customize template for audit tables
$custom = 'custom/metadata/audit_templateMetaData_' . $this->getTableName() . '.php';
if (file_exists($custom))
{
require($custom);
}
$fieldDefs = $dictionary['audit']['fields'];
$indices = $dictionary['audit']['indices'];
// Renaming template indexes to fit the particular audit table (removed the brittle hard coding)
foreach($indices as $nr => $properties){
$indices[$nr]['name'] = 'idx_' . strtolower($this->getTableName()) . '_' . $properties['name'];
}
$engine = null;
if(isset($dictionary['audit']['engine'])) {
$engine = $dictionary['audit']['engine'];
} else if(isset($dictionary[$this->getObjectName()]['engine'])) {
$engine = $dictionary[$this->getObjectName()]['engine'];
}
$this->db->createTableParams($table_name, $fieldDefs, $indices, $engine);
}
/**
* Returns the implementing class' table name.
*
* All implementing classes set a value for the table_name variable. This value is returned as the
* table name. If not set, table name is extracted from the implementing module's vardef.
*
* @return String Table name.
*
* Internal function, do not override.
*/
public function getTableName()
{
if(isset($this->table_name))
{
return $this->table_name;
}
global $dictionary;
return $dictionary[$this->getObjectName()]['table'];
}
/**
* Returns field definitions for the implementing module.
*
* The definitions were loaded in the constructor.
*
* @return Array Field definitions.
*
* Internal function, do not override.
*/
function getFieldDefinitions()
{
return $this->field_defs;
}
/**
* Returns index definitions for the implementing module.
*
* The definitions were loaded in the constructor.
*
* @return Array Index definitions.
*
* Internal function, do not override.
*/
function getIndices()
{
global $dictionary;
if(isset($dictionary[$this->getObjectName()]['indices']))
{
return $dictionary[$this->getObjectName()]['indices'];
}
return array();
}
/**
* Returns field definition for the requested field name.
*
* The definitions were loaded in the constructor.
*
* @param string field name,
* @return Array Field properties or boolean false if the field doesn't exist
*
* Internal function, do not override.
*/
function getFieldDefinition($name)
{
if ( !isset($this->field_defs[$name]) )
return false;
return $this->field_defs[$name];
}
/**
* Returnss definition for the id field name.
*
* The definitions were loaded in the constructor.
*
* @return Array Field properties.
*
* Internal function, do not override.
*/
function getPrimaryFieldDefinition()
{
$def = $this->getFieldDefinition("id");
if(empty($def)) {
$def = $this->getFieldDefinition(0);
}
if (empty($def)) {
$defs = $this->field_defs;
reset($defs);
$def = current($defs);
}
return $def;
}
/**
* Returns the value for the requested field.
*
* When a row of data is fetched using the bean, all fields are created as variables in the context
* of the bean and then fetched values are set in these variables.
*
* @param string field name,
* @return varies Field value.
*
* Internal function, do not override.
*/
function getFieldValue($name)
{
if (!isset($this->$name)){
return FALSE;
}
if($this->$name === TRUE){
return 1;
}
if($this->$name === FALSE){
return 0;
}
return $this->$name;
}
/**
* Basically undoes the effects of SugarBean::populateDefaultValues(); this method is best called right after object
* initialization.
*/
public function unPopulateDefaultValues()
{
if ( !is_array($this->field_defs) )
return;
foreach ($this->field_defs as $field => $value) {
if( !empty($this->$field)
&& ((isset($value['default']) && $this->$field == $value['default']) || (!empty($value['display_default']) && $this->$field == $value['display_default']))
) {
$this->$field = null;
continue;
}
if(!empty($this->$field) && !empty($value['display_default']) && in_array($value['type'], array('date', 'datetime', 'datetimecombo')) &&
$this->$field == $this->parseDateDefault($value['display_default'], ($value['type'] != 'date'))) {
$this->$field = null;
}
}
}
/**
* Create date string from default value
* like '+1 month'
* @param string $value
* @param bool $time Should be expect time set too?
* @return string
*/
protected function parseDateDefault($value, $time = false)
{
global $timedate;
if($time) {
$dtAry = explode('&', $value, 2);
$dateValue = $timedate->getNow(true)->modify($dtAry[0]);
if(!empty($dtAry[1])) {
$timeValue = $timedate->fromString($dtAry[1]);
$dateValue->setTime($timeValue->hour, $timeValue->min, $timeValue->sec);
}
return $timedate->asUser($dateValue);
} else {
return $timedate->asUserDate($timedate->getNow(true)->modify($value));
}
}
function populateDefaultValues($force=false){
if ( !is_array($this->field_defs) )
return;
foreach($this->field_defs as $field=>$value){
if((isset($value['default']) || !empty($value['display_default'])) && ($force || empty($this->$field))){
$type = $value['type'];
switch($type){
case 'date':
if(!empty($value['display_default'])){
$this->$field = $this->parseDateDefault($value['display_default']);
}
break;
case 'datetime':
case 'datetimecombo':
if(!empty($value['display_default'])){
$this->$field = $this->parseDateDefault($value['display_default'], true);
}
break;
case 'multienum':
if(empty($value['default']) && !empty($value['display_default']))
$this->$field = $value['display_default'];
else
$this->$field = $value['default'];
break;
case 'bool':
if(isset($this->$field)){
break;
}
default:
if ( isset($value['default']) && $value['default'] !== '' ) {
$this->$field = htmlentities($value['default'], ENT_QUOTES, 'UTF-8');
} else {
$this->$field = '';
}
} //switch
}
} //foreach
}
/**
* Removes relationship metadata cache.
*
* Every module that has relationships defined with other modules, has this meta data cached. The cache is
* stores in 2 locations: relationships table and file system. This method clears the cache from both locations.
*
* @param string $key module whose meta cache is to be cleared.
* @param string $db database handle.
* @param string $tablename table name
* @param string $dictionary vardef for the module
* @param string $module_dir name of subdirectory where module is installed.
*
* @return Nothing
* @static
*
* Internal function, do not override.
*/
function removeRelationshipMeta($key,$db,$tablename,$dictionary,$module_dir)
{
//load the module dictionary if not supplied.
if ((!isset($dictionary) or empty($dictionary)) && !empty($module_dir))
{
$filename='modules/'. $module_dir . '/vardefs.php';
if(file_exists($filename))
{
include($filename);
}
}
if (!is_array($dictionary) or !array_key_exists($key, $dictionary))
{
$GLOBALS['log']->fatal("removeRelationshipMeta: Metadata for table ".$tablename. " does not exist");
display_notice("meta data absent for table ".$tablename." keyed to $key ");
}
else
{
if (isset($dictionary[$key]['relationships']))
{
$RelationshipDefs = $dictionary[$key]['relationships'];
foreach ($RelationshipDefs as $rel_name)
{
Relationship::delete($rel_name,$db);
}
}
}
}
/**
* This method has been deprecated.
*
* @see removeRelationshipMeta()
* @deprecated 4.5.1 - Nov 14, 2006
* @static
*/
function remove_relationship_meta($key,$db,$log,$tablename,$dictionary,$module_dir)
{
SugarBean::removeRelationshipMeta($key,$db,$tablename,$dictionary,$module_dir);
}
/**
* Populates the relationship meta for a module.
*
* It is called during setup/install. It is used statically to create relationship meta data for many-to-many tables.
*
* @param string $key name of the object.
* @param object $db database handle.
* @param string $tablename table, meta data is being populated for.
* @param array dictionary vardef dictionary for the object. *
* @param string module_dir name of subdirectory where module is installed.
* @param boolean $iscustom Optional,set to true if module is installed in a custom directory. Default value is false.
* @static
*
* Internal function, do not override.
*/
function createRelationshipMeta($key,$db,$tablename,$dictionary,$module_dir,$iscustom=false)
{
//load the module dictionary if not supplied.
if (empty($dictionary) && !empty($module_dir))
{
if($iscustom)
{
$filename='custom/modules/' . $module_dir . '/Ext/Vardefs/vardefs.ext.php';
}
else
{
if ($key == 'User')
{
// a very special case for the Employees module
// this must be done because the Employees/vardefs.php does an include_once on
// Users/vardefs.php
$filename='modules/Users/vardefs.php';
}
else
{
$filename='modules/'. $module_dir . '/vardefs.php';
}
}
if(file_exists($filename))
{
include($filename);
// cn: bug 7679 - dictionary entries defined as $GLOBALS['name'] not found
if(empty($dictionary) || !empty($GLOBALS['dictionary'][$key]))
{
$dictionary = $GLOBALS['dictionary'];
}
}
else
{
$GLOBALS['log']->debug("createRelationshipMeta: no metadata file found" . $filename);
return;
}
}
if (!is_array($dictionary) or !array_key_exists($key, $dictionary))
{
$GLOBALS['log']->fatal("createRelationshipMeta: Metadata for table ".$tablename. " does not exist");
display_notice("meta data absent for table ".$tablename." keyed to $key ");
}
else
{
if (isset($dictionary[$key]['relationships']))
{
$RelationshipDefs = $dictionary[$key]['relationships'];
$delimiter=',';
global $beanList;
$beanList_ucase=array_change_key_case ( $beanList ,CASE_UPPER);
foreach ($RelationshipDefs as $rel_name=>$rel_def)
{
if (isset($rel_def['lhs_module']) and !isset($beanList_ucase[strtoupper($rel_def['lhs_module'])])) {
$GLOBALS['log']->debug('skipping orphaned relationship record ' . $rel_name . ' lhs module is missing ' . $rel_def['lhs_module']);
continue;
}
if (isset($rel_def['rhs_module']) and !isset($beanList_ucase[strtoupper($rel_def['rhs_module'])])) {
$GLOBALS['log']->debug('skipping orphaned relationship record ' . $rel_name . ' rhs module is missing ' . $rel_def['rhs_module']);
continue;
}
//check whether relationship exists or not first.
if (Relationship::exists($rel_name,$db))
{
$GLOBALS['log']->debug('Skipping, reltionship already exists '.$rel_name);
}
else
{
$seed = BeanFactory::getBean("Relationships");
$keys = array_keys($seed->field_defs);
$toInsert = array();
foreach($keys as $key)
{
if ($key == "id")
{
$toInsert[$key] = create_guid();
}
else if ($key == "relationship_name")
{
$toInsert[$key] = $rel_name;
}
else if (isset($rel_def[$key]))
{
$toInsert[$key] = $rel_def[$key];
}
//todo specify defaults if meta not defined.
}
$column_list = implode(",", array_keys($toInsert));
$value_list = "'" . implode("','", array_values($toInsert)) . "'";
//create the record. todo add error check.
$insert_string = "INSERT into relationships (" .$column_list. ") values (".$value_list.")";
$db->query($insert_string, true);
}
}
}
else
{
//todo
//log informational message stating no relationships meta was set for this bean.
}
}
}
/**
* This method has been deprecated.
* @see createRelationshipMeta()
* @deprecated 4.5.1 - Nov 14, 2006
* @static
*/
function create_relationship_meta($key,&$db,&$log,$tablename,$dictionary,$module_dir)
{
SugarBean::createRelationshipMeta($key,$db,$tablename,$dictionary,$module_dir);
}
/**
* Handle the following when a SugarBean object is cloned
*
* Currently all this does it unset any relationships that were created prior to cloning the object
*
* @api
*/
public function __clone()
{
if(!empty($this->loaded_relationships)) {
foreach($this->loaded_relationships as $rel) {
unset($this->$rel);
}
}
}
/**
* Loads the request relationship. This method should be called before performing any operations on the related data.
*
* This method searches the vardef array for the requested attribute's definition. If the attribute is of the type
* link then it creates a similary named variable and loads the relationship definition.
*
* @param string $rel_name relationship/attribute name.
* @return nothing.
*/
function load_relationship($rel_name)
{
$GLOBALS['log']->debug("SugarBean[{$this->object_name}].load_relationships, Loading relationship (".$rel_name.").");
if (empty($rel_name))
{
$GLOBALS['log']->error("SugarBean.load_relationships, Null relationship name passed.");
return false;
}
$fieldDefs = $this->getFieldDefinitions();
//find all definitions of type link.
if (!empty($fieldDefs[$rel_name]))
{
//initialize a variable of type Link
require_once('data/Link2.php');
$class = load_link_class($fieldDefs[$rel_name]);
if (isset($this->$rel_name) && $this->$rel_name instanceof $class) {
return true;
}
//if rel_name is provided, search the fieldef array keys by name.
if (isset($fieldDefs[$rel_name]['type']) && $fieldDefs[$rel_name]['type'] == 'link')
{
if ($class == "Link2")
$this->$rel_name = new $class($rel_name, $this);
else
$this->$rel_name = new $class($fieldDefs[$rel_name]['relationship'], $this, $fieldDefs[$rel_name]);
if (empty($this->$rel_name) ||
(method_exists($this->$rel_name, "loadedSuccesfully") && !$this->$rel_name->loadedSuccesfully()))
{
unset($this->$rel_name);
return false;
}
// keep track of the loaded relationships
$this->loaded_relationships[] = $rel_name;
return true;
}
}
$GLOBALS['log']->debug("SugarBean.load_relationships, Error Loading relationship (".$rel_name.")");
return false;
}
/**
* Loads all attributes of type link.
*
* DO NOT CALL THIS FUNCTION IF YOU CAN AVOID IT. Please use load_relationship directly instead.
*
* Method searches the implmenting module's vardef file for attributes of type link, and for each attribute
* create a similary named variable and load the relationship definition.
*
* @return Nothing
*
* Internal function, do not override.
*/
function load_relationships()
{
$GLOBALS['log']->debug("SugarBean.load_relationships, Loading all relationships of type link.");
$linked_fields=$this->get_linked_fields();
foreach($linked_fields as $name=>$properties)
{
$this->load_relationship($name);
}
}
/**
* Returns an array of beans of related data.
*
* For instance, if an account is related to 10 contacts , this function will return an array of contacts beans (10)
* with each bean representing a contact record.
* Method will load the relationship if not done so already.
*
* @param string $field_name relationship to be loaded.
* @param string $bean name class name of the related bean.
* @param array $sort_array optional, unused
* @param int $begin_index Optional, default 0, unused.
* @param int $end_index Optional, default -1
* @param int $deleted Optional, Default 0, 0 adds deleted=0 filter, 1 adds deleted=1 filter.
* @param string $optional_where, Optional, default empty.
*
* Internal function, do not override.
*/
function get_linked_beans($field_name,$bean_name, $sort_array = array(), $begin_index = 0, $end_index = -1,
$deleted=0, $optional_where="")
{
//if bean_name is Case then use aCase
if($bean_name=="Case")
$bean_name = "aCase";
if($this->load_relationship($field_name)) {
if ($this->$field_name instanceof Link) {
// some classes are still based on Link, e.g. TeamSetLink
return array_values($this->$field_name->getBeans(new $bean_name(), $sort_array, $begin_index, $end_index, $deleted, $optional_where));
} else {
// Link2 style
if ($end_index != -1 || !empty($deleted) || !empty($optional_where))
return array_values($this->$field_name->getBeans(array(
'where' => $optional_where,
'deleted' => $deleted,
'limit' => ($end_index - $begin_index)
)));
else
return array_values($this->$field_name->getBeans());
}
}
else
return array();
}
/**
* Returns an array of fields that are of type link.
*
* @return array List of fields.
*
* Internal function, do not override.
*/
function get_linked_fields()
{
$linked_fields=array();
// require_once('data/Link.php');
$fieldDefs = $this->getFieldDefinitions();
//find all definitions of type link.
if (!empty($fieldDefs))
{
foreach ($fieldDefs as $name=>$properties)
{
if (array_search('link',$properties) === 'type')
{
$linked_fields[$name]=$properties;
}
}
}
return $linked_fields;
}
/**
* Returns an array of fields that are able to be Imported into
* i.e. 'importable' not set to 'false'
*
* @return array List of fields.
*
* Internal function, do not override.
*/
function get_importable_fields()
{
$importableFields = array();
$fieldDefs= $this->getFieldDefinitions();
if (!empty($fieldDefs)) {
foreach ($fieldDefs as $key=>$value_array) {
if ( (isset($value_array['importable'])
&& (is_string($value_array['importable']) && $value_array['importable'] == 'false'
|| is_bool($value_array['importable']) && $value_array['importable'] == false))
|| (isset($value_array['type']) && $value_array['type'] == 'link')
|| (isset($value_array['auto_increment'])
&& ($value_array['type'] == true || $value_array['type'] == 'true')) ) {
// only allow import if we force it
if (isset($value_array['importable'])
&& (is_string($value_array['importable']) && $value_array['importable'] == 'true'
|| is_bool($value_array['importable']) && $value_array['importable'] == true)) {
$importableFields[$key]=$value_array;
}
}
else {
$importableFields[$key]=$value_array;
}
}
}
return $importableFields;
}
/**
* Returns an array of fields that are of type relate.
*
* @return array List of fields.
*
* Internal function, do not override.
*/
function get_related_fields()
{
$related_fields=array();
// require_once('data/Link.php');
$fieldDefs = $this->getFieldDefinitions();
//find all definitions of type link.
if (!empty($fieldDefs))
{
foreach ($fieldDefs as $name=>$properties)
{
if (array_search('relate',$properties) === 'type')
{
$related_fields[$name]=$properties;
}
}
}
return $related_fields;
}
/**
* Returns an array of fields that are required for import
*
* @return array
*/
function get_import_required_fields()
{
$importable_fields = $this->get_importable_fields();
$required_fields = array();
foreach ( $importable_fields as $name => $properties ) {
if ( isset($properties['importable']) && is_string($properties['importable']) && $properties['importable'] == 'required' ) {
$required_fields[$name] = $properties;
}
}
return $required_fields;
}
/**
* Iterates through all the relationships and deletes all records for reach relationship.
*
* @param string $id Primary key value of the parent reocrd
*/
function delete_linked($id)
{
$linked_fields=$this->get_linked_fields();
foreach ($linked_fields as $name => $value)
{
if ($this->load_relationship($name))
{
$this->$name->delete($id);
}
else
{
$GLOBALS['log']->fatal("error loading relationship $name");
}
}
}
/**
* Creates tables for the module implementing the class.
* If you override this function make sure that your code can handles table creation.
*
*/
function create_tables()
{
global $dictionary;
$key = $this->getObjectName();
if (!array_key_exists($key, $dictionary))
{
$GLOBALS['log']->fatal("create_tables: Metadata for table ".$this->table_name. " does not exist");
display_notice("meta data absent for table ".$this->table_name." keyed to $key ");
}
else
{
if(!$this->db->tableExists($this->table_name))
{
$this->db->createTable($this);
if($this->bean_implements('ACL')){
if(!empty($this->acltype)){
ACLAction::addActions($this->getACLCategory(), $this->acltype);
}else{
ACLAction::addActions($this->getACLCategory());
}
}
}
else
{
echo "Table already exists : $this->table_name<br>";
}
if($this->is_AuditEnabled()){
if (!$this->db->tableExists($this->get_audit_table_name())) {
$this->create_audit_table();
}
}
}
}
/**
* Delete the primary table for the module implementing the class.
* If custom fields were added to this table/module, the custom table will be removed too, along with the cache
* entries that define the custom fields.
*
*/
function drop_tables()
{
global $dictionary;
$key = $this->getObjectName();
if (!array_key_exists($key, $dictionary))
{
$GLOBALS['log']->fatal("drop_tables: Metadata for table ".$this->table_name. " does not exist");
echo "meta data absent for table ".$this->table_name."<br>\n";
} else {
if(empty($this->table_name))return;
if ($this->db->tableExists($this->table_name))
$this->db->dropTable($this);
if ($this->db->tableExists($this->table_name. '_cstm'))
{
$this->db->dropTableName($this->table_name. '_cstm');
DynamicField::deleteCache();
}
if ($this->db->tableExists($this->get_audit_table_name())) {
$this->db->dropTableName($this->get_audit_table_name());
}
}
}
/**
* Loads the definition of custom fields defined for the module.
* Local file system cache is created as needed.
*
* @param string $module_name setting up custom fields for this module.
* @param boolean $clean_load Optional, default true, rebuilds the cache if set to true.
*/
function setupCustomFields($module_name, $clean_load=true)
{
$this->custom_fields = new DynamicField($module_name);
$this->custom_fields->setup($this);
}
/**
* Cleans char, varchar, text, etc. fields of XSS type materials
*/
function cleanBean() {
foreach($this->field_defs as $key => $def) {
if (isset($def['type'])) {
$type=$def['type'];
}
if(isset($def['dbType']))
$type .= $def['dbType'];
if($def['type'] == 'html') {
$this->$key = SugarCleaner::cleanHtml($this->$key, true);
} elseif((strpos($type, 'char') !== false ||
strpos($type, 'text') !== false ||
$type == 'enum') &&
!empty($this->$key)
) {
$this->$key = SugarCleaner::cleanHtml($this->$key);
}
}
}
/**
* Implements a generic insert and update logic for any SugarBean
* This method only works for subclasses that implement the same variable names.
* This method uses the presence of an id field that is not null to signify and update.
* The id field should not be set otherwise.
*
* @param boolean $check_notify Optional, default false, if set to true assignee of the record is notified via email.
* @todo Add support for field type validation and encoding of parameters.
*/
function save($check_notify = FALSE)
{
$this->in_save = true;
// cn: SECURITY - strip XSS potential vectors
$this->cleanBean();
// This is used so custom/3rd-party code can be upgraded with fewer issues, this will be removed in a future release
$this->fixUpFormatting();
global $timedate;
global $current_user, $action;
$isUpdate = true;
if(empty($this->id))
{
$isUpdate = false;
}
if ( $this->new_with_id == true )
{
$isUpdate = false;
}
if(empty($this->date_modified) || $this->update_date_modified)
{
$this->date_modified = $GLOBALS['timedate']->nowDb();
}
$this->_checkOptimisticLocking($action, $isUpdate);
if(!empty($this->modified_by_name)) $this->old_modified_by_name = $this->modified_by_name;
if($this->update_modified_by)
{
$this->modified_user_id = 1;
if (!empty($current_user))
{
$this->modified_user_id = $current_user->id;
$this->modified_by_name = $current_user->user_name;
}
}
if ($this->deleted != 1)
$this->deleted = 0;
if(!$isUpdate)
{
if (empty($this->date_entered))
{
$this->date_entered = $this->date_modified;
}
if($this->set_created_by == true)
{
// created by should always be this user
$this->created_by = (isset($current_user)) ? $current_user->id : "";
}
if( $this->new_with_id == false)
{
$this->id = create_guid();
}
}
require_once("data/BeanFactory.php");
BeanFactory::registerBean($this->module_name, $this);
if (empty($GLOBALS['updating_relationships']) && empty($GLOBALS['saving_relationships']) && empty ($GLOBALS['resavingRelatedBeans']))
{
$GLOBALS['saving_relationships'] = true;
// let subclasses save related field changes
$this->save_relationship_changes($isUpdate);
$GLOBALS['saving_relationships'] = false;
}
if($isUpdate && !$this->update_date_entered)
{
unset($this->date_entered);
}
// call the custom business logic
$custom_logic_arguments['check_notify'] = $check_notify;
$this->call_custom_logic("before_save", $custom_logic_arguments);
unset($custom_logic_arguments);
if(isset($this->custom_fields))
{
$this->custom_fields->bean = $this;
$this->custom_fields->save($isUpdate);
}
// use the db independent query generator
$this->preprocess_fields_on_save();
//construct the SQL to create the audit record if auditing is enabled.
$dataChanges=array();
if ($this->is_AuditEnabled()) {
if ($isUpdate && !isset($this->fetched_row)) {
$GLOBALS['log']->debug('Auditing: Retrieve was not called, audit record will not be created.');
} else {
$dataChanges=$this->db->getDataChanges($this);
}
}
$this->_sendNotifications($check_notify);
if ($isUpdate) {
$this->db->update($this);
} else {
$this->db->insert($this);
}
if (!empty($dataChanges) && is_array($dataChanges))
{
foreach ($dataChanges as $change)
{
$this->db->save_audit_records($this,$change);
}
}
if (empty($GLOBALS['resavingRelatedBeans'])){
SugarRelationship::resaveRelatedBeans();
}
//If we aren't in setup mode and we have a current user and module, then we track
if(isset($GLOBALS['current_user']) && isset($this->module_dir))
{
$this->track_view($current_user->id, $this->module_dir, 'save');
}
$this->call_custom_logic('after_save', '');
//Now that the record has been saved, we don't want to insert again on further saves
$this->new_with_id = false;
$this->in_save = false;
return $this->id;
}
/**
* Performs a check if the record has been modified since the specified date
*
* @param date $date Datetime for verification
* @param string $modified_user_id User modified by
*/
function has_been_modified_since($date, $modified_user_id)
{
global $current_user;
$date = $this->db->convert($this->db->quoted($date), 'datetime');
if (isset($current_user))
{
$query = "SELECT date_modified FROM $this->table_name WHERE id='$this->id' AND modified_user_id != '$current_user->id'
AND (modified_user_id != '$modified_user_id' OR date_modified > $date)";
$result = $this->db->query($query);
if($this->db->fetchByAssoc($result))
{
return true;
}
}
return false;
}
/**
* Determines which users receive a notification
*/
function get_notification_recipients() {
$notify_user = new User();
$notify_user->retrieve($this->assigned_user_id);
$this->new_assigned_user_name = $notify_user->full_name;
$GLOBALS['log']->info("Notifications: recipient is $this->new_assigned_user_name");
$user_list = array($notify_user);
return $user_list;
/*
//send notifications to followers, but ensure to not query for the assigned_user.
return SugarFollowing::getFollowers($this, $notify_user);
*/
}
/**
* Handles sending out email notifications when items are first assigned to users
*
* @param string $notify_user user to notify
* @param string $admin the admin user that sends out the notification
*/
function send_assignment_notifications($notify_user, $admin)
{
global $current_user;
if(($this->object_name == 'Meeting' || $this->object_name == 'Call') || $notify_user->receive_notifications)
{
$sendToEmail = $notify_user->emailAddress->getPrimaryAddress($notify_user);
$sendEmail = TRUE;
if(empty($sendToEmail)) {
$GLOBALS['log']->warn("Notifications: no e-mail address set for user {$notify_user->user_name}, cancelling send");
$sendEmail = FALSE;
}
$notify_mail = $this->create_notification_email($notify_user);
$notify_mail->setMailerForSystem();
if(empty($admin->settings['notify_send_from_assigning_user'])) {
$notify_mail->From = $admin->settings['notify_fromaddress'];
$notify_mail->FromName = (empty($admin->settings['notify_fromname'])) ? "" : $admin->settings['notify_fromname'];
} else {
// Send notifications from the current user's e-mail (if set)
$fromAddress = $current_user->emailAddress->getReplyToAddress($current_user);
$fromAddress = !empty($fromAddress) ? $fromAddress : $admin->settings['notify_fromaddress'];
$notify_mail->From = $fromAddress;
//Use the users full name is available otherwise default to system name
$from_name = !empty($admin->settings['notify_fromname']) ? $admin->settings['notify_fromname'] : "";
$from_name = !empty($current_user->full_name) ? $current_user->full_name : $from_name;
$notify_mail->FromName = $from_name;
}
$oe = new OutboundEmail();
$oe = $oe->getUserMailerSettings($current_user);
//only send if smtp server is defined
if($sendEmail){
$smtpVerified = false;
//first check the user settings
if(!empty($oe->mail_smtpserver)){
$smtpVerified = true;
}
//if still not verified, check against the system settings
if (!$smtpVerified){
$oe = $oe->getSystemMailerSettings();
if(!empty($oe->mail_smtpserver)){
$smtpVerified = true;
}
}
//if smtp was not verified against user or system, then do not send out email
if (!$smtpVerified){
$GLOBALS['log']->fatal("Notifications: error sending e-mail, smtp server was not found ");
//break out
return;
}
if(!$notify_mail->Send()) {
$GLOBALS['log']->fatal("Notifications: error sending e-mail (method: {$notify_mail->Mailer}), (error: {$notify_mail->ErrorInfo})");
}else{
$GLOBALS['log']->info("Notifications: e-mail successfully sent");
}
}
}
}
/**
* This function handles create the email notifications email.
* @param string $notify_user the user to send the notification email to
*/
function create_notification_email($notify_user) {
global $sugar_version;
global $sugar_config;
global $app_list_strings;
global $current_user;
global $locale;
global $beanList;
$OBCharset = $locale->getPrecedentPreference('default_email_charset');
require_once("include/SugarPHPMailer.php");
$notify_address = $notify_user->emailAddress->getPrimaryAddress($notify_user);
$notify_name = $notify_user->full_name;
$GLOBALS['log']->debug("Notifications: user has e-mail defined");
$notify_mail = new SugarPHPMailer();
$notify_mail->AddAddress($notify_address,$locale->translateCharsetMIME(trim($notify_name), 'UTF-8', $OBCharset));
if(empty($_SESSION['authenticated_user_language'])) {
$current_language = $sugar_config['default_language'];
} else {
$current_language = $_SESSION['authenticated_user_language'];
}
$xtpl = new XTemplate(get_notify_template_file($current_language));
if($this->module_dir == "Cases") {
$template_name = "Case"; //we should use Case, you can refer to the en_us.notify_template.html.
}
else {
$template_name = $beanList[$this->module_dir]; //bug 20637, in workflow this->object_name = strange chars.
}
$this->current_notify_user = $notify_user;
if(in_array('set_notification_body', get_class_methods($this))) {
$xtpl = $this->set_notification_body($xtpl, $this);
} else {
$xtpl->assign("OBJECT", translate('LBL_MODULE_NAME'));
$template_name = "Default";
}
if(!empty($_SESSION["special_notification"]) && $_SESSION["special_notification"]) {
$template_name = $beanList[$this->module_dir].'Special';
}
if($this->special_notification) {
$template_name = $beanList[$this->module_dir].'Special';
}
$xtpl->assign("ASSIGNED_USER", $this->new_assigned_user_name);
$xtpl->assign("ASSIGNER", $current_user->name);
$port = '';
if(isset($_SERVER['SERVER_PORT']) && $_SERVER['SERVER_PORT'] != 80 && $_SERVER['SERVER_PORT'] != 443) {
$port = $_SERVER['SERVER_PORT'];
}
if (!isset($_SERVER['HTTP_HOST'])) {
$_SERVER['HTTP_HOST'] = '';
}
$httpHost = $_SERVER['HTTP_HOST'];
if($colon = strpos($httpHost, ':')) {
$httpHost = substr($httpHost, 0, $colon);
}
$parsedSiteUrl = parse_url($sugar_config['site_url']);
$host = $parsedSiteUrl['host'];
if(!isset($parsedSiteUrl['port'])) {
$parsedSiteUrl['port'] = 80;
}
$port = ($parsedSiteUrl['port'] != 80) ? ":".$parsedSiteUrl['port'] : '';
$path = !empty($parsedSiteUrl['path']) ? $parsedSiteUrl['path'] : "";
$cleanUrl = "{$parsedSiteUrl['scheme']}://{$host}{$port}{$path}";
$xtpl->assign("URL", $cleanUrl."/index.php?module={$this->module_dir}&action=DetailView&record={$this->id}");
$xtpl->assign("SUGAR", "Sugar v{$sugar_version}");
$xtpl->parse($template_name);
$xtpl->parse($template_name . "_Subject");
$notify_mail->Body = from_html(trim($xtpl->text($template_name)));
$notify_mail->Subject = from_html($xtpl->text($template_name . "_Subject"));
// cn: bug 8568 encode notify email in User's outbound email encoding
$notify_mail->prepForOutbound();
return $notify_mail;
}
/**
* This function is a good location to save changes that have been made to a relationship.
* This should be overridden in subclasses that have something to save.
*
* @param boolean $is_update true if this save is an update.
* @param array $exclude a way to exclude relationships
*/
public function save_relationship_changes($is_update, $exclude = array())
{
list($new_rel_id, $new_rel_link) = $this->set_relationship_info($exclude);
$new_rel_id = $this->handle_preset_relationships($new_rel_id, $new_rel_link, $exclude);
$this->handle_remaining_relate_fields($exclude);
$this->handle_request_relate($new_rel_id, $new_rel_link);
}
/**
* Look in the bean for the new relationship_id and relationship_name if $this->not_use_rel_in_req is set to true,
* otherwise check the $_REQUEST param for a relate_id and relate_to field. Once we have that make sure that it's
* not excluded from the passed in array of relationships to exclude
*
* @param array $exclude any relationship's to exclude
* @return array The relationship_id and relationship_name in an array
*/
protected function set_relationship_info($exclude = array())
{
$new_rel_id = false;
$new_rel_link = false;
// check incoming data
if (isset($this->not_use_rel_in_req) && $this->not_use_rel_in_req == true) {
// if we should use relation data from properties (for REQUEST-independent calls)
$rel_id = isset($this->new_rel_id) ? $this->new_rel_id : '';
$rel_link = isset($this->new_rel_relname) ? $this->new_rel_relname : '';
}
else
{
// if we should use relation data from REQUEST
$rel_id = isset($_REQUEST['relate_id']) ? $_REQUEST['relate_id'] : '';
$rel_link = isset($_REQUEST['relate_to']) ? $_REQUEST['relate_to'] : '';
}
// filter relation data
if ($rel_id && $rel_link && !in_array($rel_link, $exclude) && $rel_id != $this->id) {
$new_rel_id = $rel_id;
$new_rel_link = $rel_link;
// Bug #53223 : wrong relationship from subpanel create button
// if LHSModule and RHSModule are same module use left link to add new item b/s of:
// $rel_id and $rel_link are not emty - request is from subpanel
// $rel_link contains relationship name - checked by call load_relationship
$isRelationshipLoaded = $this->load_relationship($rel_link);
if ($isRelationshipLoaded && !empty($this->$rel_link) && $this->$rel_link->getRelationshipObject() && $this->$rel_link->getRelationshipObject()->getLHSModule() == $this->$rel_link->getRelationshipObject()->getRHSModule() )
{
$new_rel_link = $this->$rel_link->getRelationshipObject()->getLHSLink();
}
else
{
//Try to find the link in this bean based on the relationship
foreach ($this->field_defs as $key => $def)
{
if (isset($def['type']) && $def['type'] == 'link' && isset($def['relationship']) && $def['relationship'] == $rel_link)
{
$new_rel_link = $key;
}
}
}
}
return array($new_rel_id, $new_rel_link);
}
/**
* Handle the preset fields listed in the fixed relationship_fields array hardcoded into the OOB beans
*
* TODO: remove this mechanism and replace with mechanism exclusively based on the vardefs
*
* @api
* @see save_relationship_changes
* @param string|boolean $new_rel_id String of the ID to add
* @param string Relationship Name
* @param array $exclude any relationship's to exclude
* @return string|boolean Return the new_rel_id if it was not used. False if it was used.
*/
protected function handle_preset_relationships($new_rel_id, $new_rel_link, $exclude = array())
{
if (isset($this->relationship_fields) && is_array($this->relationship_fields)) {
foreach ($this->relationship_fields as $id => $rel_name)
{
if (in_array($id, $exclude)) continue;
if(!empty($this->$id))
{
// Bug #44930 We do not need to update main related field if it is changed from sub-panel.
if ($rel_name == $new_rel_link && $this->$id != $new_rel_id)
{
$new_rel_id = '';
}
$GLOBALS['log']->debug('save_relationship_changes(): From relationship_field array - adding a relationship record: '.$rel_name . ' = ' . $this->$id);
//already related the new relationship id so let's set it to false so we don't add it again using the _REQUEST['relate_i'] mechanism in a later block
$this->load_relationship($rel_name);
$rel_add = $this->$rel_name->add($this->$id);
// move this around to only take out the id if it was save successfully
if ($this->$id == $new_rel_id && $rel_add == true) {
$new_rel_id = false;
}
} else {
//if before value is not empty then attempt to delete relationship
if (!empty($this->rel_fields_before_value[$id])) {
$GLOBALS['log']->debug('save_relationship_changes(): From relationship_field array - attempting to remove the relationship record, using relationship attribute' . $rel_name);
$this->load_relationship($rel_name);
$this->$rel_name->delete($this->id, $this->rel_fields_before_value[$id]);
}
}
}
}
return $new_rel_id;
}
/**
* Next, we'll attempt to update all of the remaining relate fields in the vardefs that have 'save' set in their field_def
* Only the 'save' fields should be saved as some vardef entries today are not for display only purposes and break the application if saved
* If the vardef has entries for field <a> of type relate, where a->id_name = <b> and field <b> of type link
* then we receive a value for b from the MVC in the _REQUEST, and it should be set in the bean as $this->$b
*
* @api
* @see save_relationship_changes
* @param array $exclude any relationship's to exclude
* @return array the list of relationships that were added or removed successfully or if they were a failure
*/
protected function handle_remaining_relate_fields($exclude = array())
{
$modified_relationships = array(
'add' => array('success' => array(), 'failure' => array()),
'remove' => array('success' => array(), 'failure' => array()),
);
foreach ($this->field_defs as $def)
{
if ($def ['type'] == 'relate' && isset ($def ['id_name']) && isset ($def ['link']) && isset ($def['save'])) {
if (in_array($def['id_name'], $exclude) || in_array($def['id_name'], $this->relationship_fields))
continue; // continue to honor the exclude array and exclude any relationships that will be handled by the relationship_fields mechanism
$linkField = $def ['link'];
if (isset($this->field_defs[$linkField])) {
if ($this->load_relationship($linkField)) {
$idName = $def['id_name'];
if (!empty($this->rel_fields_before_value[$idName]) && empty($this->$idName)) {
//if before value is not empty then attempt to delete relationship
$GLOBALS['log']->debug("save_relationship_changes(): From field_defs - attempting to remove the relationship record: {$def [ 'link' ]} = {$this->rel_fields_before_value[$def [ 'id_name' ]]}");
$success = $this->$def ['link']->delete($this->id, $this->rel_fields_before_value[$def ['id_name']]);
// just need to make sure it's true and not an array as it's possible to return an array
if($success == true) {
$modified_relationships['remove']['success'][] = $def['link'];
} else {
$modified_relationships['remove']['failure'][] = $def['link'];
}
$GLOBALS['log']->debug("save_relationship_changes(): From field_defs - attempting to remove the relationship record returned " . var_export($success, true));
}
if (!empty($this->$idName) && is_string($this->$idName)) {
$GLOBALS['log']->debug("save_relationship_changes(): From field_defs - attempting to add a relationship record - {$def [ 'link' ]} = {$this->$def [ 'id_name' ]}");
$success = $this->$linkField->add($this->$idName);
// just need to make sure it's true and not an array as it's possible to return an array
if($success == true) {
$modified_relationships['add']['success'][] = $linkField;
} else {
$modified_relationships['add']['failure'][] = $linkField;
}
$GLOBALS['log']->debug("save_relationship_changes(): From field_defs - add a relationship record returned " . var_export($success, true));
}
} else {
$GLOBALS['log']->fatal("Failed to load relationship {$linkField} while saving {$this->module_dir}");
}
}
}
}
return $modified_relationships;
}
/**
* Finally, we update a field listed in the _REQUEST['%/relate_id']/_REQUEST['relate_to'] mechanism (if it has not already been updated)
*
* @api
* @see save_relationship_changes
* @param string|boolean $new_rel_id
* @param string $new_rel_link
* @return boolean
*/
protected function handle_request_relate($new_rel_id, $new_rel_link)
{
if (!empty($new_rel_id)) {
if ($this->load_relationship($new_rel_link)) {
return $this->$new_rel_link->add($new_rel_id);
} else {
$lower_link = strtolower($new_rel_link);
if ($this->load_relationship($lower_link)) {
return $this->$lower_link->add($new_rel_id);
} else {
require_once('data/Link2.php');
$rel = Relationship::retrieve_by_modules($new_rel_link, $this->module_dir, $this->db, 'many-to-many');
if (!empty($rel)) {
foreach ($this->field_defs as $field => $def) {
if ($def['type'] == 'link' && !empty($def['relationship']) && $def['relationship'] == $rel) {
$this->load_relationship($field);
return $this->$field->add($new_rel_id);
}
}
//ok so we didn't find it in the field defs let's save it anyway if we have the relationshp
$this->$rel = new Link2($rel, $this, array());
return $this->$rel->add($new_rel_id);
}
}
}
}
// nothing was saved so just return false;
return false;
}
/**
* This function retrieves a record of the appropriate type from the DB.
* It fills in all of the fields from the DB into the object it was called on.
*
* @param $id - If ID is specified, it overrides the current value of $this->id. If not specified the current value of $this->id will be used.
* @return this - The object that it was called apon or null if exactly 1 record was not found.
*
*/
function check_date_relationships_load()
{
global $disable_date_format;
global $timedate;
if (empty($timedate))
$timedate=TimeDate::getInstance();
if(empty($this->field_defs))
{
return;
}
foreach($this->field_defs as $fieldDef)
{
$field = $fieldDef['name'];
if(!isset($this->processed_dates_times[$field]))
{
$this->processed_dates_times[$field] = '1';
if(empty($this->$field)) continue;
if($field == 'date_modified' || $field == 'date_entered')
{
$this->$field = $this->db->fromConvert($this->$field, 'datetime');
if(empty($disable_date_format)) {
$this->$field = $timedate->to_display_date_time($this->$field);
}
}
elseif(isset($this->field_name_map[$field]['type']))
{
$type = $this->field_name_map[$field]['type'];
if($type == 'relate' && isset($this->field_name_map[$field]['custom_module']))
{
$type = $this->field_name_map[$field]['type'];
}
if($type == 'date')
{
if($this->$field == '0000-00-00')
{
$this->$field = '';
} elseif(!empty($this->field_name_map[$field]['rel_field']))
{
$rel_field = $this->field_name_map[$field]['rel_field'];
if(!empty($this->$rel_field))
{
if(empty($disable_date_format)) {
$mergetime = $timedate->merge_date_time($this->$field,$this->$rel_field);
$this->$field = $timedate->to_display_date($mergetime);
$this->$rel_field = $timedate->to_display_time($mergetime);
}
}
}
else
{
if(empty($disable_date_format)) {
$this->$field = $timedate->to_display_date($this->$field, false);
}
}
} elseif($type == 'datetime' || $type == 'datetimecombo')
{
if($this->$field == '0000-00-00 00:00:00')
{
$this->$field = '';
}
else
{
if(empty($disable_date_format)) {
$this->$field = $timedate->to_display_date_time($this->$field, true, true);
}
}
} elseif($type == 'time')
{
if($this->$field == '00:00:00')
{
$this->$field = '';
} else
{
//$this->$field = from_db_convert($this->$field, 'time');
if(empty($this->field_name_map[$field]['rel_field']) && empty($disable_date_format))
{
$this->$field = $timedate->to_display_time($this->$field,true, false);
}
}
} elseif($type == 'encrypt' && empty($disable_date_format)){
$this->$field = $this->decrypt_after_retrieve($this->$field);
}
}
}
}
}
/**
* This function processes the fields before save.
* Interal function, do not override.
*/
function preprocess_fields_on_save()
{
$GLOBALS['log']->deprecated('SugarBean.php: preprocess_fields_on_save() is deprecated');
}
/**
* Removes formatting from values posted from the user interface.
* It only unformats numbers. Function relies on user/system prefernce for format strings.
*
* Internal Function, do not override.
*/
function unformat_all_fields()
{
$GLOBALS['log']->deprecated('SugarBean.php: unformat_all_fields() is deprecated');
}
/**
* This functions adds formatting to all number fields before presenting them to user interface.
*
* Internal function, do not override.
*/
function format_all_fields()
{
$GLOBALS['log']->deprecated('SugarBean.php: format_all_fields() is deprecated');
}
function format_field($fieldDef)
{
$GLOBALS['log']->deprecated('SugarBean.php: format_field() is deprecated');
}
/**
* Function corrects any bad formatting done by 3rd party/custom code
*
* This function will be removed in a future release, it is only here to assist upgrading existing code that expects formatted data in the bean
*/
function fixUpFormatting()
{
global $timedate;
static $boolean_false_values = array('off', 'false', '0', 'no');
foreach($this->field_defs as $field=>$def)
{
if ( !isset($this->$field) ) {
continue;
}
if ( (isset($def['source'])&&$def['source']=='non-db') || $field == 'deleted' ) {
continue;
}
if ( isset($this->fetched_row[$field]) && $this->$field == $this->fetched_row[$field] ) {
// Don't hand out warnings because the field was untouched between retrieval and saving, most database drivers hand pretty much everything back as strings.
continue;
}
$reformatted = false;
switch($def['type']) {
case 'datetime':
case 'datetimecombo':
if(empty($this->$field)) break;
if ($this->$field == 'NULL') {
$this->$field = '';
break;
}
if ( ! preg_match('/^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$/',$this->$field) ) {
// This appears to be formatted in user date/time
$this->$field = $timedate->to_db($this->$field);
$reformatted = true;
}
break;
case 'date':
if(empty($this->$field)) break;
if ($this->$field == 'NULL') {
$this->$field = '';
break;
}
if ( ! preg_match('/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/',$this->$field) ) {
// This date appears to be formatted in the user's format
$this->$field = $timedate->to_db_date($this->$field, false);
$reformatted = true;
}
break;
case 'time':
if(empty($this->$field)) break;
if ($this->$field == 'NULL') {
$this->$field = '';
break;
}
if ( preg_match('/(am|pm)/i',$this->$field) ) {
// This time appears to be formatted in the user's format
$this->$field = $timedate->fromUserTime($this->$field)->format(TimeDate::DB_TIME_FORMAT);
$reformatted = true;
}
break;
case 'double':
case 'decimal':
case 'currency':
case 'float':
if ( $this->$field === '' || $this->$field == NULL || $this->$field == 'NULL') {
continue;
}
if ( is_string($this->$field) ) {
$this->$field = (float)unformat_number($this->$field);
$reformatted = true;
}
break;
case 'uint':
case 'ulong':
case 'long':
case 'short':
case 'tinyint':
case 'int':
if ( $this->$field === '' || $this->$field == NULL || $this->$field == 'NULL') {
continue;
}
if ( is_string($this->$field) ) {
$this->$field = (int)unformat_number($this->$field);
$reformatted = true;
}
break;
case 'bool':
if (empty($this->$field)) {
$this->$field = false;
} else if(true === $this->$field || 1 == $this->$field) {
$this->$field = true;
} else if(in_array(strval($this->$field), $boolean_false_values)) {
$this->$field = false;
$reformatted = true;
} else {
$this->$field = true;
$reformatted = true;
}
break;
case 'encrypt':
$this->$field = $this->encrpyt_before_save($this->$field);
break;
}
if ( $reformatted ) {
$GLOBALS['log']->deprecated('Formatting correction: '.$this->module_dir.'->'.$field.' had formatting automatically corrected. This will be removed in the future, please upgrade your external code');
}
}
}
/**
* Function fetches a single row of data given the primary key value.
*
* The fetched data is then set into the bean. The function also processes the fetched data by formattig
* date/time and numeric values.
*
* @param string $id Optional, default -1, is set to -1 id value from the bean is used, else, passed value is used
* @param boolean $encode Optional, default true, encodes the values fetched from the database.
* @param boolean $deleted Optional, default true, if set to false deleted filter will not be added.
*
* Internal function, do not override.
*/
function retrieve($id = -1, $encode=true,$deleted=true)
{
$custom_logic_arguments['id'] = $id;
$this->call_custom_logic('before_retrieve', $custom_logic_arguments);
if ($id == -1)
{
$id = $this->id;
}
if(isset($this->custom_fields))
{
$custom_join = $this->custom_fields->getJOIN();
}
else
$custom_join = false;
if($custom_join)
{
$query = "SELECT $this->table_name.*". $custom_join['select']. " FROM $this->table_name ";
}
else
{
$query = "SELECT $this->table_name.* FROM $this->table_name ";
}
if($custom_join)
{
$query .= ' ' . $custom_join['join'];
}
$query .= " WHERE $this->table_name.id = ".$this->db->quoted($id);
if ($deleted) $query .= " AND $this->table_name.deleted=0";
$GLOBALS['log']->debug("Retrieve $this->object_name : ".$query);
$result = $this->db->limitQuery($query,0,1,true, "Retrieving record by id $this->table_name:$id found ");
if(empty($result))
{
return null;
}
$row = $this->db->fetchByAssoc($result, $encode);
if(empty($row))
{
return null;
}
//make copy of the fetched row for construction of audit record and for business logic/workflow
$row = $this->convertRow($row);
$this->fetched_row=$row;
$this->populateFromRow($row);
global $module, $action;
//Just to get optimistic locking working for this release
if($this->optimistic_lock && $module == $this->module_dir && $action =='EditView' )
{
$_SESSION['o_lock_id']= $id;
$_SESSION['o_lock_dm']= $this->date_modified;
$_SESSION['o_lock_on'] = $this->object_name;
}
$this->processed_dates_times = array();
$this->check_date_relationships_load();
if($custom_join)
{
$this->custom_fields->fill_relationships();
}
$this->is_updated_dependent_fields = false;
$this->fill_in_additional_detail_fields();
$this->fill_in_relationship_fields();
// save related fields values for audit
foreach ($this->get_related_fields() as $rel_field_name)
{
if (! empty($this->$rel_field_name['name']))
{
$this->fetched_rel_row[$rel_field_name['name']] = $this->$rel_field_name['name'];
}
}
//make a copy of fields in the relationship_fields array. These field values will be used to
//clear relationship.
foreach ( $this->field_defs as $key => $def )
{
if ($def [ 'type' ] == 'relate' && isset ( $def [ 'id_name'] ) && isset ( $def [ 'link'] ) && isset ( $def[ 'save' ])) {
if (isset($this->$key)) {
$this->rel_fields_before_value[$key]=$this->$key;
if (isset($this->$def [ 'id_name']))
$this->rel_fields_before_value[$def [ 'id_name']]=$this->$def [ 'id_name'];
}
else
$this->rel_fields_before_value[$key]=null;
}
}
if (isset($this->relationship_fields) && is_array($this->relationship_fields))
{
foreach ($this->relationship_fields as $rel_id=>$rel_name)
{
if (isset($this->$rel_id))
$this->rel_fields_before_value[$rel_id]=$this->$rel_id;
else
$this->rel_fields_before_value[$rel_id]=null;
}
}
// call the custom business logic
$custom_logic_arguments['id'] = $id;
$custom_logic_arguments['encode'] = $encode;
$this->call_custom_logic("after_retrieve", $custom_logic_arguments);
unset($custom_logic_arguments);
return $this;
}
/**
* Sets value from fetched row into the bean.
*
* @param array $row Fetched row
* @todo loop through vardefs instead
* @internal runs into an issue when populating from field_defs for users - corrupts user prefs
*
* Internal function, do not override.
*/
function populateFromRow($row)
{
$nullvalue='';
foreach($this->field_defs as $field=>$field_value)
{
if($field == 'user_preferences' && $this->module_dir == 'Users')
continue;
if(isset($row[$field]))
{
$this->$field = $row[$field];
$owner = $field . '_owner';
if(!empty($row[$owner])){
$this->$owner = $row[$owner];
}
}
else
{
$this->$field = $nullvalue;
}
}
}
/**
* Add any required joins to the list count query. The joins are required if there
* is a field in the $where clause that needs to be joined.
*
* @param string $query
* @param string $where
*
* Internal Function, do Not override.
*/
function add_list_count_joins(&$query, $where)
{
$custom_join = $this->custom_fields->getJOIN();
if($custom_join)
{
$query .= $custom_join['join'];
}
}
/**
* Changes the select expression of the given query to be 'count(*)' so you
* can get the number of items the query will return. This is used to
* populate the upper limit on ListViews.
*
* @param string $query Select query string
* @return string count query
*
* Internal function, do not override.
*/
function create_list_count_query($query)
{
// remove the 'order by' clause which is expected to be at the end of the query
$pattern = '/\sORDER BY.*/is'; // ignores the case
$replacement = '';
$query = preg_replace($pattern, $replacement, $query);
//handle distinct clause
$star = '*';
if(substr_count(strtolower($query), 'distinct')){
if (!empty($this->seed) && !empty($this->seed->table_name ))
$star = 'DISTINCT ' . $this->seed->table_name . '.id';
else
$star = 'DISTINCT ' . $this->table_name . '.id';
}
// change the select expression to 'count(*)'
$pattern = '/SELECT(.*?)(\s){1}FROM(\s){1}/is'; // ignores the case
$replacement = 'SELECT count(' . $star . ') c FROM ';
//if the passed query has union clause then replace all instances of the pattern.
//this is very rare. I have seen this happening only from projects module.
//in addition to this added a condition that has union clause and uses
//sub-selects.
if (strstr($query," UNION ALL ") !== false) {
//separate out all the queries.
$union_qs=explode(" UNION ALL ", $query);
foreach ($union_qs as $key=>$union_query) {
$star = '*';
preg_match($pattern, $union_query, $matches);
if (!empty($matches)) {
if (stristr($matches[0], "distinct")) {
if (!empty($this->seed) && !empty($this->seed->table_name ))
$star = 'DISTINCT ' . $this->seed->table_name . '.id';
else
$star = 'DISTINCT ' . $this->table_name . '.id';
}
} // if
$replacement = 'SELECT count(' . $star . ') c FROM ';
$union_qs[$key] = preg_replace($pattern, $replacement, $union_query,1);
}
$modified_select_query=implode(" UNION ALL ",$union_qs);
} else {
$modified_select_query = preg_replace($pattern, $replacement, $query,1);
}
return $modified_select_query;
}
/**
* This function returns a paged list of the current object type. It is intended to allow for
* hopping back and forth through pages of data. It only retrieves what is on the current page.
*
* @internal This method must be called on a new instance. It trashes the values of all the fields in the current one.
* @param string $order_by
* @param string $where Additional where clause
* @param int $row_offset Optaional,default 0, starting row number
* @param init $limit Optional, default -1
* @param int $max Optional, default -1
* @param boolean $show_deleted Optional, default 0, if set to 1 system will show deleted records.
* @return array Fetched data.
*
* Internal function, do not override.
*
*/
function get_list($order_by = "", $where = "", $row_offset = 0, $limit=-1, $max=-1, $show_deleted = 0, $singleSelect=false, $select_fields = array())
{
$GLOBALS['log']->debug("get_list: order_by = '$order_by' and where = '$where' and limit = '$limit'");
if(isset($_SESSION['show_deleted']))
{
$show_deleted = 1;
}
$order_by=$this->process_order_by($order_by);
if($this->bean_implements('ACL') && ACLController::requireOwner($this->module_dir, 'list') )
{
global $current_user;
$owner_where = $this->getOwnerWhere($current_user->id);
//rrs - because $this->getOwnerWhere() can return '' we need to be sure to check for it and
//handle it properly else you could get into a situation where you are create a where stmt like
//WHERE .. AND ''
if(!empty($owner_where)){
if(empty($where)){
$where = $owner_where;
}else{
$where .= ' AND '. $owner_where;
}
}
}
$query = $this->create_new_list_query($order_by, $where,$select_fields,array(), $show_deleted,'',false,null,$singleSelect);
return $this->process_list_query($query, $row_offset, $limit, $max, $where);
}
/**
* Prefixes column names with this bean's table name.
*
* @param string $order_by Order by clause to be processed
* @param SugarBean $submodule name of the module this order by clause is for
* @return string Processed order by clause
*
* Internal function, do not override.
*/
function process_order_by ($order_by, $submodule = null)
{
if (empty($order_by))
return $order_by;
//submodule is empty,this is for list object in focus
if (empty($submodule))
{
$bean_queried = $this;
}
else
{
//submodule is set, so this is for subpanel, use submodule
$bean_queried = $submodule;
}
$elements = explode(',',$order_by);
foreach ($elements as $key=>$value)
{
if (strchr($value,'.') === false)
{
//value might have ascending and descending decorations
$list_column = explode(' ',trim($value));
if (isset($list_column[0]))
{
$list_column_name=trim($list_column[0]);
if (isset($bean_queried->field_defs[$list_column_name]))
{
$source=isset($bean_queried->field_defs[$list_column_name]['source']) ? $bean_queried->field_defs[$list_column_name]['source']:'db';
if (empty($bean_queried->field_defs[$list_column_name]['table']) && $source=='db')
{
$list_column[0] = $bean_queried->table_name .".".$list_column[0] ;
}
if (empty($bean_queried->field_defs[$list_column_name]['table']) && $source=='custom_fields')
{
$list_column[0] = $bean_queried->table_name ."_cstm.".$list_column[0] ;
}
// Bug 38803 - Use CONVERT() function when doing an order by on ntext, text, and image fields
if ($source != 'non-db' && $this->db->isTextType($this->db->getFieldType($bean_queried->field_defs[$list_column_name]))) {
$list_column[0] = $this->db->convert($list_column[0], "text2char");
}
$value = implode(' ',$list_column);
} else {
$GLOBALS['log']->debug("process_order_by: ($list_column[0]) does not have a vardef entry.");
}
}
}
$elements[$key]=$value;
}
return implode(',', $elements);
}
/**
* Returns a detail object like retrieving of the current object type.
*
* It is intended for use in navigation buttons on the DetailView. It will pass an offset and limit argument to the sql query.
* @internal This method must be called on a new instance. It overrides the values of all the fields in the current one.
*
* @param string $order_by
* @param string $where Additional where clause
* @param int $row_offset Optaional,default 0, starting row number
* @param init $limit Optional, default -1
* @param int $max Optional, default -1
* @param boolean $show_deleted Optioanl, default 0, if set to 1 system will show deleted records.
* @return array Fetched data.
*
* Internal function, do not override.
*/
function get_detail($order_by = "", $where = "", $offset = 0, $row_offset = 0, $limit=-1, $max=-1, $show_deleted = 0)
{
$GLOBALS['log']->debug("get_detail: order_by = '$order_by' and where = '$where' and limit = '$limit' and offset = '$offset'");
if(isset($_SESSION['show_deleted']))
{
$show_deleted = 1;
}
if($this->bean_implements('ACL') && ACLController::requireOwner($this->module_dir, 'list') )
{
global $current_user;
$owner_where = $this->getOwnerWhere($current_user->id);
if(empty($where))
{
$where = $owner_where;
}
else
{
$where .= ' AND '. $owner_where;
}
}
$query = $this->create_new_list_query($order_by, $where,array(),array(), $show_deleted, $offset);
//Add Limit and Offset to query
//$query .= " LIMIT 1 OFFSET $offset";
return $this->process_detail_query($query, $row_offset, $limit, $max, $where, $offset);
}
/**
* Fetches data from all related tables.
*
* @param object $child_seed
* @param string $related_field_name relation to fetch data for
* @param string $order_by Optional, default empty
* @param string $where Optional, additional where clause
* @return array Fetched data.
*
* Internal function, do not override.
*/
function get_related_list($child_seed,$related_field_name, $order_by = "", $where = "",
$row_offset = 0, $limit=-1, $max=-1, $show_deleted = 0)
{
global $layout_edit_mode;
$query_array = array();
if(isset($layout_edit_mode) && $layout_edit_mode)
{
$response = array();
$child_seed->assign_display_fields($child_seed->module_dir);
$response['list'] = array($child_seed);
$response['row_count'] = 1;
$response['next_offset'] = 0;
$response['previous_offset'] = 0;
return $response;
}
$GLOBALS['log']->debug("get_related_list: order_by = '$order_by' and where = '$where' and limit = '$limit'");
if(isset($_SESSION['show_deleted']))
{
$show_deleted = 1;
}
$this->load_relationship($related_field_name);
if ($this->$related_field_name instanceof Link) {
$query_array = $this->$related_field_name->getQuery(true);
} else {
$query_array = $this->$related_field_name->getQuery(array(
"return_as_array" => true,
'where' => '1=1' // hook for 'where' clause in M2MRelationship file
));
}
$entire_where = $query_array['where'];
if(!empty($where))
{
if(empty($entire_where))
{
$entire_where = ' WHERE ' . $where;
}
else
{
$entire_where .= ' AND ' . $where;
}
}
$query = 'SELECT '.$child_seed->table_name.'.* ' . $query_array['from'] . ' ' . $entire_where;
if(!empty($order_by))
{
$query .= " ORDER BY " . $order_by;
}
return $child_seed->process_list_query($query, $row_offset, $limit, $max, $where);
}
protected static function build_sub_queries_for_union($subpanel_list, $subpanel_def, $parentbean, $order_by)
{
global $layout_edit_mode, $beanFiles, $beanList;
$subqueries = array();
foreach($subpanel_list as $this_subpanel)
{
if(!$this_subpanel->isDatasourceFunction() || ($this_subpanel->isDatasourceFunction()
&& isset($this_subpanel->_instance_properties['generate_select'])
&& $this_subpanel->_instance_properties['generate_select']==true))
{
//the custom query function must return an array with
if ($this_subpanel->isDatasourceFunction()) {
$shortcut_function_name = $this_subpanel->get_data_source_name();
$parameters=$this_subpanel->get_function_parameters();
if (!empty($parameters))
{
//if the import file function is set, then import the file to call the custom function from
if (is_array($parameters) && isset($parameters['import_function_file'])){
//this call may happen multiple times, so only require if function does not exist
if(!function_exists($shortcut_function_name)){
require_once($parameters['import_function_file']);
}
//call function from required file
$query_array = $shortcut_function_name($parameters);
}else{
//call function from parent bean
$query_array = $parentbean->$shortcut_function_name($parameters);
}
}
else
{
$query_array = $parentbean->$shortcut_function_name();
}
} else {
$related_field_name = $this_subpanel->get_data_source_name();
if (!$parentbean->load_relationship($related_field_name)){
unset ($parentbean->$related_field_name);
continue;
}
$query_array = $parentbean->$related_field_name->getSubpanelQuery(array(), true);
}
$table_where = preg_replace('/^\s*WHERE/i', '', $this_subpanel->get_where());
$where_definition = preg_replace('/^\s*WHERE/i', '', $query_array['where']);
if(!empty($table_where))
{
if(empty($where_definition))
{
$where_definition = $table_where;
}
else
{
$where_definition .= ' AND ' . $table_where;
}
}
$submodulename = $this_subpanel->_instance_properties['module'];
$submoduleclass = $beanList[$submodulename];
//require_once($beanFiles[$submoduleclass]);
/** @var SugarBean $submodule */
$submodule = new $submoduleclass();
$subwhere = $where_definition;
$list_fields = $this_subpanel->get_list_fields();
foreach($list_fields as $list_key=>$list_field)
{
if(isset($list_field['usage']) && $list_field['usage'] == 'display_only')
{
unset($list_fields[$list_key]);
}
}
if(!$subpanel_def->isCollection() && isset($list_fields[$order_by]) && isset($submodule->field_defs[$order_by])&& (!isset($submodule->field_defs[$order_by]['source']) || $submodule->field_defs[$order_by]['source'] == 'db'))
{
$order_by = $submodule->table_name .'.'. $order_by;
}
$table_name = $this_subpanel->table_name;
$panel_name=$this_subpanel->name;
$params = array();
$params['distinct'] = $this_subpanel->distinct_query();
$params['joined_tables'] = $query_array['join_tables'];
$params['include_custom_fields'] = !$subpanel_def->isCollection();
$params['collection_list'] = $subpanel_def->get_inst_prop_value('collection_list');
// use single select in case when sorting by relate field
$singleSelect = $submodule->is_relate_field($order_by);
$subquery = $submodule->create_new_list_query('',$subwhere ,$list_fields,$params, 0,'', true,$parentbean, $singleSelect);
$subquery['select'] = $subquery['select']." , '$panel_name' panel_name ";
$subquery['from'] = $subquery['from'].$query_array['join'];
$subquery['query_array'] = $query_array;
$subquery['params'] = $params;
$subqueries[] = $subquery;
}
}
return $subqueries;
}
/**
* Constructs a query to fetch data for supanels and list views
*
* It constructs union queries for activities subpanel.
*
* @param SugarBean $parentbean constructing queries for link attributes in this bean
* @param string $order_by Optional, order by clause
* @param string $sort_order Optional, sort order
* @param string $where Optional, additional where clause
*
* Internal Function, do not overide.
*/
function get_union_related_list($parentbean, $order_by = "", $sort_order='', $where = "",
$row_offset = 0, $limit=-1, $max=-1, $show_deleted = 0, $subpanel_def)
{
$secondary_queries = array();
global $layout_edit_mode, $beanFiles, $beanList;
if(isset($_SESSION['show_deleted']))
{
$show_deleted = 1;
}
$final_query = '';
$final_query_rows = '';
$subpanel_list=array();
if ($subpanel_def->isCollection())
{
$subpanel_def->load_sub_subpanels();
$subpanel_list=$subpanel_def->sub_subpanels;
}
else
{
$subpanel_list[]=$subpanel_def;
}
$first = true;
//Breaking the building process into two loops. The first loop gets a list of all the sub-queries.
//The second loop merges the queries and forces them to select the same number of columns
//All columns in a sub-subpanel group must have the same aliases
//If the subpanel is a datasource function, it can't be a collection so we just poll that function for the and return that
foreach($subpanel_list as $this_subpanel)
{
if($this_subpanel->isDatasourceFunction() && empty($this_subpanel->_instance_properties['generate_select']))
{
$shortcut_function_name = $this_subpanel->get_data_source_name();
$parameters=$this_subpanel->get_function_parameters();
if (!empty($parameters))
{
//if the import file function is set, then import the file to call the custom function from
if (is_array($parameters) && isset($parameters['import_function_file'])){
//this call may happen multiple times, so only require if function does not exist
if(!function_exists($shortcut_function_name)){
require_once($parameters['import_function_file']);
}
//call function from required file
$tmp_final_query = $shortcut_function_name($parameters);
}else{
//call function from parent bean
$tmp_final_query = $parentbean->$shortcut_function_name($parameters);
}
} else {
$tmp_final_query = $parentbean->$shortcut_function_name();
}
if(!$first)
{
$final_query_rows .= ' UNION ALL ( '.$parentbean->create_list_count_query($tmp_final_query, $parameters) . ' )';
$final_query .= ' UNION ALL ( '.$tmp_final_query . ' )';
} else {
$final_query_rows = '(' . $parentbean->create_list_count_query($tmp_final_query, $parameters) . ')';
$final_query = '(' . $tmp_final_query . ')';
$first = false;
}
}
}
//If final_query is still empty, its time to build the sub-queries
if (empty($final_query))
{
$subqueries = SugarBean::build_sub_queries_for_union($subpanel_list, $subpanel_def, $parentbean, $order_by);
$all_fields = array();
foreach($subqueries as $i => $subquery)
{
$query_fields = $GLOBALS['db']->getSelectFieldsFromQuery($subquery['select']);
foreach($query_fields as $field => $select)
{
if (!in_array($field, $all_fields))
$all_fields[] = $field;
}
$subqueries[$i]['query_fields'] = $query_fields;
}
$first = true;
//Now ensure the queries have the same set of fields in the same order.
foreach($subqueries as $subquery)
{
$subquery['select'] = "SELECT";
foreach($all_fields as $field)
{
if (!isset($subquery['query_fields'][$field]))
{
$subquery['select'] .= " ' ' $field,";
}
else
{
$subquery['select'] .= " {$subquery['query_fields'][$field]},";
}
}
$subquery['select'] = substr($subquery['select'], 0 , strlen($subquery['select']) - 1);
//Put the query into the final_query
$query = $subquery['select'] . " " . $subquery['from'] . " " . $subquery['where'];
if(!$first)
{
$query = ' UNION ALL ( '.$query . ' )';
$final_query_rows .= " UNION ALL ";
} else {
$query = '(' . $query . ')';
$first = false;
}
$query_array = $subquery['query_array'];
$select_position=strpos($query_array['select'],"SELECT");
$distinct_position=strpos($query_array['select'],"DISTINCT");
if (!empty($subquery['params']['distinct']) && !empty($subpanel_def->table_name))
{
$query_rows = "( SELECT count(DISTINCT ". $subpanel_def->table_name . ".id)". $subquery['from_min'].$query_array['join']. $subquery['where'].' )';
}
elseif ($select_position !== false && $distinct_position!= false)
{
$query_rows = "( ".substr_replace($query_array['select'],"SELECT count(",$select_position,6). ")" . $subquery['from_min'].$query_array['join']. $subquery['where'].' )';
}
else
{
//resort to default behavior.
$query_rows = "( SELECT count(*)". $subquery['from_min'].$query_array['join']. $subquery['where'].' )';
}
if(!empty($subquery['secondary_select']))
{
$subquerystring= $subquery['secondary_select'] . $subquery['secondary_from'].$query_array['join']. $subquery['where'];
if (!empty($subquery['secondary_where']))
{
if (empty($subquery['where']))
{
$subquerystring.=" WHERE " .$subquery['secondary_where'];
}
else
{
$subquerystring.=" AND " .$subquery['secondary_where'];
}
}
$secondary_queries[]=$subquerystring;
}
$final_query .= $query;
$final_query_rows .= $query_rows;
}
}
if(!empty($order_by))
{
$submodule = false;
if(!$subpanel_def->isCollection())
{
$submodulename = $subpanel_def->_instance_properties['module'];
$submoduleclass = $beanList[$submodulename];
$submodule = new $submoduleclass();
}
if(!empty($submodule) && !empty($submodule->table_name))
{
$final_query .= " ORDER BY " .$parentbean->process_order_by($order_by, $submodule);
}
else
{
$final_query .= " ORDER BY ". $order_by . ' ';
}
if(!empty($sort_order))
{
$final_query .= ' ' .$sort_order;
}
}
if(isset($layout_edit_mode) && $layout_edit_mode)
{
$response = array();
if(!empty($submodule))
{
$submodule->assign_display_fields($submodule->module_dir);
$response['list'] = array($submodule);
}
else
{
$response['list'] = array();
}
$response['parent_data'] = array();
$response['row_count'] = 1;
$response['next_offset'] = 0;
$response['previous_offset'] = 0;
return $response;
}
return $parentbean->process_union_list_query($parentbean, $final_query, $row_offset, $limit, $max, '',$subpanel_def, $final_query_rows, $secondary_queries);
}
/**
* Returns a full (ie non-paged) list of the current object type.
*
* @param string $order_by the order by SQL parameter. defaults to ""
* @param string $where where clause. defaults to ""
* @param boolean $check_dates. defaults to false
* @param int $show_deleted show deleted records. defaults to 0
*/
function get_full_list($order_by = "", $where = "", $check_dates=false, $show_deleted = 0)
{
$GLOBALS['log']->debug("get_full_list: order_by = '$order_by' and where = '$where'");
if(isset($_SESSION['show_deleted']))
{
$show_deleted = 1;
}
$query = $this->create_new_list_query($order_by, $where,array(),array(), $show_deleted);
return $this->process_full_list_query($query, $check_dates);
}
/**
* Return the list query used by the list views and export button. Next generation of create_new_list_query function.
*
* Override this function to return a custom query.
*
* @param string $order_by custom order by clause
* @param string $where custom where clause
* @param array $filter Optioanal
* @param array $params Optional *
* @param int $show_deleted Optional, default 0, show deleted records is set to 1.
* @param string $join_type
* @param boolean $return_array Optional, default false, response as array
* @param object $parentbean creating a subquery for this bean.
* @param boolean $singleSelect Optional, default false.
* @return String select query string, optionally an array value will be returned if $return_array= true.
*/
function create_new_list_query($order_by, $where,$filter=array(),$params=array(), $show_deleted = 0,$join_type='', $return_array = false,$parentbean=null, $singleSelect = false, $ifListForExport = false)
{
global $beanFiles, $beanList;
$selectedFields = array();
$secondarySelectedFields = array();
$ret_array = array();
$distinct = '';
if($this->bean_implements('ACL') && ACLController::requireOwner($this->module_dir, 'list') )
{
global $current_user;
$owner_where = $this->getOwnerWhere($current_user->id);
if(empty($where))
{
$where = $owner_where;
}
else
{
$where .= ' AND '. $owner_where;
}
}
if(!empty($params['distinct']))
{
$distinct = ' DISTINCT ';
}
if(empty($filter))
{
$ret_array['select'] = " SELECT $distinct $this->table_name.* ";
}
else
{
$ret_array['select'] = " SELECT $distinct $this->table_name.id ";
}
$ret_array['from'] = " FROM $this->table_name ";
$ret_array['from_min'] = $ret_array['from'];
$ret_array['secondary_from'] = $ret_array['from'] ;
$ret_array['where'] = '';
$ret_array['order_by'] = '';
//secondary selects are selects that need to be run after the primary query to retrieve additional info on main
if($singleSelect)
{
$ret_array['secondary_select']=& $ret_array['select'];
$ret_array['secondary_from'] = & $ret_array['from'];
}
else
{
$ret_array['secondary_select'] = '';
}
$custom_join = false;
if((!isset($params['include_custom_fields']) || $params['include_custom_fields']) && isset($this->custom_fields))