1<?php 2/** 3 * eGroupWare 4 * 5 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License 6 * @package importexport 7 * @link http://www.egroupware.org 8 * @author Cornelius Weiss <nelius@cwtech.de> 9 * @copyright Cornelius Weiss <nelius@cwtech.de> 10 * @version $Id$ 11 */ 12 13use EGroupware\Api; 14 15/** 16 * class importexport_helper_functions (only static methods) 17 * use importexport_helper_functions::method 18 */ 19class importexport_helper_functions { 20 21 /** 22 * Plugins are scanned and cached for all instances using this source path for given time (in seconds) 23 */ 24 const CACHE_EXPIRATION = 3600; 25 26 /** 27 * Make no changes (automatic category creation) 28 */ 29 public static $dry_run = false; 30 31 /** 32 * Relative date ranges for filtering 33 */ 34 public static $relative_dates = array( // Start: year,month,day,week, End: year,month,day,week 35 'Today' => array(0,0,0,0, 0,0,1,0), 36 'Yesterday' => array(0,0,-1,0, 0,0,0,0), 37 'This week' => array(0,0,0,0, 0,0,0,1), 38 'Last week' => array(0,0,0,-1, 0,0,0,0), 39 'This month' => array(0,0,0,0, 0,1,0,0), 40 'Last month' => array(0,-1,0,0, 0,0,0,0), 41 'Last 3 months' => array(0,-3,0,0, 0,0,0,0), 42 'This quarter'=> array(0,0,0,0, 0,0,0,0), // Just a marker, needs special handling 43 'Last quarter'=> array(0,-4,0,0, 0,-4,0,0), // Just a marker 44 'This year' => array(0,0,0,0, 1,0,0,0), 45 'Last year' => array(-1,0,0,0, 0,0,0,0), 46 '2 years ago' => array(-2,0,0,0, -1,0,0,0), 47 '3 years ago' => array(-3,0,0,0, -2,0,0,0), 48 ); 49 50 /** 51 * Files known to cause problems, and will be skipped in a plugin scan 52 * If you put appname => true, the whole app will be skipped. 53 */ 54 protected static $blacklist_files = array( 55 'api' => true, 56 'etemplate' => true, 57 'setup' => true, 58 'news_admin' => array( 59 'class.news_admin_import.inc.php', 60 ), 61 'importexport' => array( 62 'class.importexport_widget_filter.inc.php', 63 ) 64 ); 65 66 /** 67 * Class used to provide extra conversion functions 68 * 69 * Passed in as a param to conversion() 70 */ 71 protected static $cclass = null; 72 73 /** 74 * nothing to construct here, only static functions! 75 */ 76 77 /** 78 * Converts a custom time string to to unix timestamp 79 * The format of the time string is given by the argument $_format 80 * which takes the same parameters as the php date() function. 81 * 82 * @abstract supportet formatstrings: d,m,y,Y,H,h,i,s,O,a,A 83 * If timestring is empty, php strtotime is used. 84 * @param string $_string time string to convert 85 * @param string $_format format of time string e.g.: d.m.Y H:i 86 * @param int $_is_dst is day light saving time? 0 = no, 1 = yes, -1 = system default 87 */ 88 public static function custom_strtotime( $_string, $_format='', $_is_dst = -1) { 89 if ( empty( $_format ) ) return strtotime( $_string ); 90 $fparams = explode( ',', chunk_split( $_format, 1, ',' ) ); 91 $spos = 0; 92 foreach ( $fparams as $fparam ) { 93 94 switch ( $fparam ) { 95 case 'd': (int)$day = substr( $_string, $spos, 2 ); $spos += 2; break; 96 case 'm': (int)$mon = substr( $_string, $spos, 2 ); $spos += 2; break; 97 case 'y': (int)$year = substr( $_string, $spos, 2 ); $spos += 2; break; 98 case 'Y': (int)$year = substr( $_string, $spos, 4 ); $spos += 4; break; 99 case 'H': (int)$hour = substr( $_string, $spos, 2 ); $spos += 2; break; 100 case 'h': (int)$hour = substr( $_string, $spos, 2 ); $spos += 2; break; 101 case 'i': (int)$min = substr( $_string, $spos, 2 ); $spos += 2; break; 102 case 's': (int)$sec = substr( $_string, $spos, 2 ); $spos += 2; break; 103 case 'O': (int)$offset = $year = substr( $_string, $spos, 5 ); $spos += 5; break; 104 case 'a': (int)$hour = $fparam == 'am' ? $hour : $hour + 12; break; 105 case 'A': (int)$hour = $fparam == 'AM' ? $hour : $hour + 12; break; 106 default: $spos++; // seperator 107 } 108 } 109 110 print_debug("hour:$hour; min:$min; sec:$sec; mon:$mon; day:$day; year:$year;\n"); 111 $timestamp = mktime($hour, $min, $sec, $mon, $day, $year, $_is_dst); 112 113 // offset given? 114 if ( isset( $offset ) && strlen( $offset == 5 ) ) { 115 $operator = $offset{0}; 116 $ohour = 60 * 60 * (int)substr( $offset, 1, 2 ); 117 $omin = 60 * (int)substr( $offset, 3, 2 ); 118 if ( $operator == '+' ) $timestamp += $ohour + $omin; 119 else $timestamp -= $ohour + $omin; 120 } 121 return $timestamp; 122 } 123 /** 124 * converts accound_lid to account_id 125 * 126 * @param mixed $_account_lid comma seperated list or array with lids 127 * @return mixed comma seperated list or array with ids 128 */ 129 public static function account_name2id( &$_account_lids ) { 130 $account_lids = is_array( $_account_lids ) ? $_account_lids : explode( ',', $_account_lids ); 131 $skip = false; 132 foreach ( $account_lids as $key => $account_lid ) { 133 if($skip) { 134 unset($account_lids[$key]); 135 $skip = false; 136 continue; 137 } 138 $account_lid = trim($account_lid); 139 140 // Handle any IDs that slip in 141 if(is_numeric($account_lid) && $GLOBALS['egw']->accounts->id2name($account_lid)) { 142 unset($account_lids[$key]); 143 $account_ids[] = (int)$account_lid; 144 continue; 145 } 146 // Check for [username] 147 if(strpos($account_lid,'[') !== false) 148 { 149 if(preg_match('/\[(.+)\]/',$account_lid,$matches)) 150 { 151 $account_id = $GLOBALS['egw']->accounts->name2id($matches[1]); 152 unset($account_lids[$key]); 153 $account_ids[] = $account_id; 154 } 155 continue; 156 } 157 158 // Handle users listed as Lastname, Firstname instead of login ID 159 // Do this first, in case their first name matches a username 160 if ( $account_lids[$key+1][0] == ' ') 161 { 162 $query = array('type' => 'accounts', 'query_type' => 'exact'); 163 $given = $GLOBALS['egw']->accounts->search($query + array('query' => trim($account_lids[$key+1]))); 164 $family = $GLOBALS['egw']->accounts->search($query + array('query' => trim($account_lid))); 165 $ids = array_intersect_key($family, $given); 166 if($ids) 167 { 168 $account_ids[] = key($ids); 169 unset($account_lids[$key]); 170 $skip = true; // Skip the next one, it's the first name 171 continue ; 172 } 173 } 174 175 // Deal with groups listed as <name> Group, remove the Group 176 if(substr(trim($account_lid),-strlen(lang('Group'))) == lang('Group')) 177 { 178 $account_lid = trim(substr(trim($account_lid), 0, -strlen(lang('Group')))); 179 } 180 // Group <name> (no comma) 181 else if(strpos($account_lid, lang('Group')) === 0) 182 { 183 $account_lid = trim(substr(trim($account_lid), strlen(lang('Group')))); 184 } 185 186 if ( $account_id = $GLOBALS['egw']->accounts->name2id( $account_lid )) { 187 $account_ids[] = $account_id; 188 unset($account_lids[$key]); 189 continue; 190 } 191 if ( $account_id = $GLOBALS['egw']->accounts->name2id( trim($account_lid), 'account_fullname' )) { 192 $account_ids[] = $account_id; 193 unset($account_lids[$key]); 194 continue; 195 } 196 197 // Handle groups listed as Group, <name> 198 if ( $account_lids[$key][0] == ' ' && $account_id = $GLOBALS['egw']->accounts->name2id( $account_lid)) { 199 $account_ids[] = $account_id; 200 unset($account_lids[$key-1]); 201 unset($account_lids[$key]); 202 continue; 203 } 204 // Group, <name> - remove the Group part 205 if($account_lid == lang('Group')) 206 { 207 unset($account_lids[$key]); 208 continue; 209 } 210 } 211 $_account_lids = (is_array($_account_lids) ? $account_lids : implode(',',array_unique($account_lids))); 212 return is_array( $_account_lids ) ? array_unique($account_ids) : implode( ',', array_unique((array)$account_ids )); 213 214 } // end of member function account_lid2id 215 216 /** 217 * converts account_ids to account_lids 218 * 219 * @param mixed $_account_ids comma seperated list or array with ids 220 * @return mixed comma seperated list or array with lids 221 */ 222 public static function account_id2name( $_account_id ) { 223 $account_ids = is_array( $_account_id ) ? $_account_id : explode( ',', $_account_id ); 224 foreach ( $account_ids as $account_id ) { 225 if ( $account_lid = $GLOBALS['egw']->accounts->id2name( $account_id )) { 226 $account_lids[] = $account_lid; 227 } 228 } 229 return is_array( $_account_id ) ? $account_lids : implode( ',', (array)$account_lids ); 230 } // end of member function account_id2lid 231 232 /** 233 * converts cat_id to a cat_name 234 * 235 * @param mixed _cat_ids comma seperated list or array 236 * @return mixed comma seperated list or array with cat_names 237 */ 238 public static function cat_id2name( $_cat_ids ) { 239 $cat_ids = is_array( $_cat_ids ) ? $_cat_ids : explode( ',', $_cat_ids ); 240 foreach ( $cat_ids as $cat_id ) { 241 $cat_names[] = Api\Categories::id2name( (int)$cat_id ); 242 } 243 return is_array( $_cat_ids ) ? $cat_names : implode(',',(array)$cat_names); 244 } // end of member function category_id2name 245 246 /** 247 * converts cat_name to a cat_id. 248 * If a cat isn't found, it will be created. 249 * 250 * @param mixed $_cat_names comma seperated list or array. 251 * @param int $parent Optional parent ID to use for new categories 252 * @return mixed comma seperated list or array with cat_ids 253 */ 254 public static function cat_name2id( $_cat_names, $parent = 0 ) { 255 $cats = new Api\Categories(); // uses current user and app (egw_info[flags][currentapp]) 256 257 $cat_names = is_array( $_cat_names ) ? $_cat_names : explode( ',', $_cat_names ); 258 foreach ( $cat_names as $cat_name ) { 259 $cat_name = trim($cat_name); 260 if ( $cat_name == '' ) continue; 261 if ( ( $cat_id = $cats->name2id( $cat_name ) ) == 0 && !self::$dry_run) { 262 $cat_id = $cats->add( array( 263 'name' => $cat_name, 264 'parent' => $parent, 265 'access' => 'public', 266 'descr' => $cat_name. ' ('. lang('Automatically created by importexport'). ')' 267 )); 268 } 269 $cat_ids[] = $cat_id; 270 } 271 return is_array( $_cat_names ) ? $cat_ids : implode( ',', (array)$cat_ids ); 272 273 } // end of member function category_name2id 274 275 /** 276 * conversion 277 * 278 * Conversions enable you to change / adapt the content of each _record field for your needs. 279 * General syntax is: pattern1 |> replacement1 || ... || patternN |> replacementN 280 * If the pattern-part of a pair is ommited it will match everything ('^.*$'), which 281 * is only usefull for the last pair, as they are worked from left to right. 282 * Example: 1|>private||public 283 * This will translate a '1' in the _record field to 'privat' and everything else to 'public'. 284 * 285 * In addintion to the fields assign by the pattern of the reg.exp. 286 * you can use all other _record fields, with the syntax |[FIELDINDEX]. 287 * Example: 288 * Your record is: 289 * array( 0 => Company, 1 => NFamily, 2 => NGiven 290 * Your conversion string for field 0 (Company): 291 * .+|>|[0]: |[1], |[2]|||[1], |[2] 292 * This constructs something like 293 * Company: FamilyName, GivenName or FamilyName, GivenName if 'Company' is empty. 294 * 295 * Moreover the following helper functions can be used: 296 * cat(Cat1,...,CatN) returns a (','-separated) list with the cat_id's. If a 297 * category isn't found, it will be automaticaly added. 298 * 299 * account(name) returns an account ID, if found in the system 300 * list(sep, data, index) lets you explode a field on sep, then select just one part (index) 301 * 302 * Patterns as well as the replacement can be regular expressions (the replacement is done 303 * via str_replace). 304 * 305 * @param array _record reference with record to do the conversion with 306 * @param array _conversion array with conversion description 307 * @param object &$cclass calling class to process the '@ evals' 308 * @return bool 309 */ 310 public static function conversion( &$_record, $_conversion, &$_cclass = null ) { 311 if (empty( $_conversion ) ) return $_record; 312 313 self::$cclass =& $_cclass; 314 315 $PSep = '||'; // Pattern-Separator, separats the pattern-replacement-pairs in conversion 316 $ASep = '|>'; // Assignment-Separator, separats pattern and replacesment 317 $CPre = '|['; $CPos = ']'; // |[_record-idx] is expanded to the corespondig value 318 $TPre = '|T{'; $TPos = '}'; // |{_record-idx} is trimmed 319 $CntlPre = '|TC{'; // Filter all cntl-chars \x01-\x1f and trim 320 $CntlnCLPre = '|TCnCL{'; // Like |C{ but allowes CR and LF 321 $INE = '|INE{'; // Only insert if stuff in ^^ is not empty 322 323 foreach ( $_conversion as $idx => $conversion_string ) { 324 if ( empty( $conversion_string ) ) continue; 325 326 // fetch patterns ($rvalues) 327 $rvalues = array(); 328 $pat_reps = explode( $PSep, stripslashes( $conversion_string ) ); 329 foreach( $pat_reps as $k => $pat_rep ) { 330 list( $pattern, $replace ) = explode( $ASep, $pat_rep, 2 ); 331 if( $replace == '' ) { 332 $replace = $pattern; $pattern = '^.*$'; 333 } 334 $rvalues[$pattern] = $replace; // replace two with only one, added by the form 335 } 336 337 // conversion list may be longer than $_record aka (no_csv) 338 $val = array_key_exists( $idx, $_record ) ? $_record[$idx] : ''; 339 340 $c_functions = array('cat', 'account', 'strtotime', 'list'); 341 if($_cclass) { 342 // Add in additional methods 343 $reflection = new ReflectionClass(get_class($_cclass)); 344 $methods = $reflection->getMethods(ReflectionMethod::IS_STATIC); 345 foreach($methods as $method) { 346 $c_functions[] = $method->name; 347 } 348 } 349 $c_functions = implode('|', $c_functions); 350 foreach ( $rvalues as $pattern => $replace ) { 351 // Allow to include record indexes in pattern 352 $reg = '/\|\[([0-9]+)\]/'; 353 while( preg_match( $reg, $pattern, $vars ) ) { 354 // expand all _record fields 355 $pattern = str_replace( 356 $CPre . $vars[1] . $CPos, 357 $_record[array_search($vars[1], array_keys($_record))], 358 $pattern 359 ); 360 } 361 if( preg_match('/'. (string)$pattern.'/', $val) ) { 362 363 $val = preg_replace( '/'.(string)$pattern.'/', $replace, (string)$val ); 364 365 $reg = '/\|\[([a-zA-Z_0-9]+)\]/'; 366 while( preg_match( $reg, $val, $vars ) ) { 367 // expand all _record fields 368 $val = str_replace( 369 $CPre . $vars[1] . $CPos, 370 $_record[array_search($vars[1], array_keys($_record))], 371 $val 372 ); 373 } 374 $val = preg_replace_callback( "/($c_functions)\(([^)]*)\)/i", array( self, 'c2_dispatcher') , $val ); 375 break; 376 } 377 } 378 // clean each field 379 $val = preg_replace_callback("/(\|T\{|\|TC\{|\|TCnCL\{|\|INE\{)(.*)\}/", array( self, 'strclean'), $val ); 380 381 $_record[$idx] = $val; 382 } 383 return $_record; 384 } // end of member function conversion 385 386 /** 387 * callback for preg_replace_callback from self::conversion. 388 * This function gets called when 2nd level conversions are made, 389 * like the cat() and account() statements in the conversions. 390 * 391 * @param array $_matches 392 */ 393 private static function c2_dispatcher( $_matches ) { 394 $action = &$_matches[1]; // cat or account ... 395 $data = &$_matches[2]; // datas for action 396 397 switch ( $action ) { 398 case 'strtotime' : 399 list( $string, $format ) = explode( ',', $data ); 400 return self::custom_strtotime( trim( $string ), trim( $format ) ); 401 case 'list': 402 list( $split, $data, $index) = explode(',',$data); 403 $exploded = explode($split, $data); 404 // 1 based indexing for user ease 405 return $exploded[$index - 1]; 406 default : 407 if(self::$cclass && method_exists(self::$cclass, $action)) { 408 $class = get_class(self::$cclass); 409 return call_user_func("$class::$action", $data); 410 } 411 $method = (string)$action. ( is_int( $data ) ? '_id2name' : '_name2id' ); 412 if(self::$cclass && method_exists(self::$cclass, $method)) { 413 $class = get_class(self::$cclass); 414 return call_user_func("$class::$action", $data); 415 } else { 416 return self::$method( $data ); 417 } 418 } 419 } 420 421 private static function strclean( $_matches ) { 422 switch( $_matches[1] ) { 423 case '|T{' : return trim( $_matches[2] ); 424 case '|TC{' : return trim( preg_replace( '/[\x01-\x1F]+/', '', $_matches[2] ) ); 425 case '|TCnCL{' : return trim( preg_replace( '/[\x01-\x09\x11\x12\x14-\x1F]+/', '', $_matches[2] ) ); 426 case '|INE{' : return preg_match( '/\^.+\^/', $_matches[2] ) ? $_matches[2] : ''; 427 default: 428 throw new Exception('Error in conversion string! "'. substr( $_matches[1], 0, -1 ). '" is not valid!'); 429 } 430 } 431 432 /** 433 * returns a list of importexport plugins 434 * 435 * @param string $_tpye {import | export | all} 436 * @param string $_appname {<appname> | all} 437 * @return array(<appname> => array( <type> => array(<plugin> => <title>))) 438 */ 439 public static function get_plugins( $_appname = 'all', $_type = 'all' ) { 440 $plugins = Api\Cache::getTree( 441 __CLASS__, 442 'plugins', 443 array('importexport_helper_functions','_get_plugins'), 444 array(array_keys($GLOBALS['egw_info']['apps']), array('import', 'export')), 445 self::CACHE_EXPIRATION 446 ); 447 $appnames = $_appname == 'all' ? array_keys($GLOBALS['egw_info']['apps']) : (array)$_appname; 448 $types = $_type == 'all' ? array('import','export') : (array)$_type; 449 450 // Testing: comment out Api\Cache call, use this 451 //$plugins = self::_get_plugins($appnames, $types); 452 foreach($plugins as $appname => $_types) { 453 if(!in_array($appname, $appnames)) unset($plugins[$appname]); 454 } 455 foreach($plugins as $appname => $types) { 456 $plugins[$appname] = array_intersect_key($plugins[$appname], $types); 457 } 458 return $plugins; 459 } 460 461 public static function _get_plugins(Array $appnames, Array $types) { 462 $plugins = array(); 463 foreach ($appnames as $appname) { 464 if(array_key_exists($appname, self::$blacklist_files) && self::$blacklist_files[$appname] === true) continue; 465 466 $appdir = EGW_INCLUDE_ROOT. "/$appname/inc"; 467 if(!is_dir($appdir)) continue; 468 $d = dir($appdir); 469 470 // step through each file in appdir 471 while (false !== ($entry = $d->read())) { 472 // Blacklisted? 473 if(is_array(self::$blacklist_files[$appname]) && in_array($entry, self::$blacklist_files[$appname])) continue; 474 if (!preg_match('/^class\.([^.]+)\.inc\.php$/', $entry, $matches)) continue; 475 $classname = $matches[1]; 476 $file = $appdir. '/'. $entry; 477 478 foreach ($types as $type) { 479 if( !is_file($file) || strpos($entry, $type) === false || strpos($entry,'wizard') !== false) continue; 480 require_once($file); 481 $reflectionClass = new ReflectionClass($classname); 482 if($reflectionClass->IsInstantiable() && 483 $reflectionClass->implementsInterface('importexport_iface_'.$type.'_plugin')) { 484 try { 485 $plugin_object = new $classname; 486 } 487 catch (Exception $exception) { 488 continue; 489 } 490 $plugins[$appname][$type][$classname] = $plugin_object->get_name(); 491 unset ($plugin_object); 492 } 493 } 494 } 495 $d->close(); 496 497 $config = Api\Config::read('importexport'); 498 if($config['update'] == 'auto') { 499 self::load_defaults($appname); 500 } 501 } 502 //error_log(__CLASS__.__FUNCTION__.print_r($plugins,true)); 503 return $plugins; 504 } 505 506 /** 507 * returns list of apps which have plugins of given type. 508 * 509 * @param string $_type 510 * @return array $num => $appname 511 */ 512 public static function get_apps($_type, $ignore_acl = false) { 513 $apps = array_keys(self::get_plugins('all',$_type)); 514 if($ignore_acl) return $apps; 515 516 foreach($apps as $key => $app) { 517 if(!self::has_definitions($app, $_type)) unset($apps[$key]); 518 } 519 return $apps; 520 } 521 522 public static function load_defaults($appname) { 523 // Check for new definitions to import from $appname/setup/*.xml 524 $appdir = EGW_INCLUDE_ROOT. "/$appname/setup"; 525 if(!is_dir($appdir)) return; 526 $d = dir($appdir); 527 528 // step through each file in app's setup 529 while (false !== ($entry = $d->read())) { 530 $file = $appdir. '/'. $entry; 531 list( $filename, $extension) = explode('.',$entry); 532 if ( $extension != 'xml' ) continue; 533 try { 534 // import will skip invalid files 535 importexport_definitions_bo::import( $file ); 536 } catch (Exception $e) { 537 error_log(__CLASS__.__FUNCTION__. " import $appname definitions: " . $e->getMessage()); 538 } 539 } 540 $d->close(); 541 } 542 543 public static function guess_filetype( $_file ) { 544 545 } 546 547 /** 548 * returns if the given app has importexport definitions for the current user 549 * 550 * @param string $_appname {<appname> | all} 551 * @param string $_type {import | export | all} 552 * @return boolean 553 */ 554 public static function has_definitions( $_appname = 'all', $_type = 'all' ) { 555 $definitions = Api\Cache::getSession( 556 __CLASS__, 557 'has_definitions', 558 array('importexport_helper_functions','_has_definitions'), 559 array(array_keys($GLOBALS['egw_info']['apps']), array('import', 'export')), 560 self::CACHE_EXPIRATION 561 ); 562 $appnames = $_appname == 'all' ? array_keys($GLOBALS['egw_info']['apps']) : (array)$_appname; 563 $types = $_type == 'all' ? array('import','export') : (array)$_type; 564 565 // Testing: Comment out cache call above, use this 566 //$definitions = self::_has_definitions($appnames, $types); 567 568 foreach($definitions as $appname => $_types) { 569 if(!in_array($appname, $appnames)) unset($definitions[$appname]); 570 } 571 foreach($definitions as $appname => $_types) { 572 $definitions[$appname] = array_intersect_key($definitions[$appname], array_flip($types)); 573 } 574 return !empty($definitions[$appname]); 575 } 576 577 // Api\Cache needs this public 578 public static function _has_definitions(Array $appnames, Array $types) { 579 $def = new importexport_definitions_bo(array('application'=>$appnames, 'type' => $types)); 580 $list = array(); 581 foreach((array)$def->get_definitions() as $id) { 582 // Need to instanciate it to check, but if the user doesn't have permission, it throws an exception 583 try { 584 $definition = new importexport_definition($id); 585 if($def->is_permitted($definition->get_record_array())) { 586 $list[$definition->application][$definition->type][] = $id; 587 } 588 } catch (Exception $e) { 589 // That one doesn't work, keep going 590 } 591 $definition = null; 592 } 593 return $list; 594 } 595 596 /** 597 * Get a list of filterable fields, and options for those fields 598 * 599 * It tries to automatically pull filterable fields from the list of fields in the wizard, 600 * and sets widget properties. The plugin can edit / set the fields by implementing 601 * get_filter_fields(Array &$fields). 602 * 603 * Currently only supports select,select-cat,select-account,date,date-time 604 * 605 * @param $app_name String name of app 606 * @param $plugin_name Name of the plugin 607 * 608 * @return Array ([fieldname] => array(widget settings), ...) 609 */ 610 public static function get_filter_fields($app_name, $plugin_name, $wizard_plugin = null, $record_classname = null) 611 { 612 // We only filter on these field types. Others could be added, but they need the UI figured out 613 static $allowed_types = array('select','select-cat','select-account','date','date-time'); 614 615 $fields = array(); 616 try { 617 $plugin = is_object($plugin_name) ? $plugin_name : new $plugin_name(); 618 $plugin_name = get_class($plugin); 619 620 if($record_classname == null) $record_classname = $plugin::get_egw_record_class(); 621 if(!class_exists($record_classname)) throw new Exception('Bad class name ' . $record_classname); 622 623 if(!$wizard_plugin) 624 { 625 $wizard_name = $app_name . '_wizard_' . str_replace($app_name . '_', '', $plugin_name); 626 if(!class_exists($wizard_name)) throw new Exception('Bad wizard name ' . $wizard_name); 627 $wizard_plugin = new $wizard_name; 628 } 629 } 630 catch (Exception $e) 631 { 632 error_log($e->getMessage()); 633 return array(); 634 } 635 636 // Get field -> label map and initialize fields using wizard field order 637 $fields = $export_fields = array(); 638 if(method_exists($wizard_plugin, 'get_export_fields')) 639 { 640 $fields = $export_fields = $wizard_plugin->get_export_fields(); 641 } 642 643 foreach($record_classname::$types as $type => $type_fields) 644 { 645 // Only these for now, until filter methods for others are figured out 646 if(!in_array($type, $allowed_types)) continue; 647 foreach($type_fields as $field_name) 648 { 649 $fields[$field_name] = array( 650 'name' => $field_name, 651 'label' => $export_fields[$field_name] ? $export_fields[$field_name] : $field_name, 652 'type' => $type 653 ); 654 } 655 } 656 // Add custom fields 657 $custom = Api\Storage\Customfields::get($app_name); 658 foreach($custom as $field_name => $settings) 659 { 660 if(!in_array($settings['type'], $allowed_types)) continue; 661 $settings['name'] = '#'.$field_name; 662 $fields['#'.$field_name] = $settings; 663 } 664 665 foreach($fields as $field_name => &$settings) { 666 // Can't really filter on these (or at least no generic, sane way figured out yet) 667 if(!is_array($settings) || in_array($settings['type'], array('text','button', 'label','url','url-email','url-phone','htmlarea'))) 668 { 669 unset($fields[$field_name]); 670 continue; 671 } 672 if($settings['type'] == 'radio') $settings['type'] = 'select'; 673 switch($settings['type']) 674 { 675 case 'checkbox': 676 // This isn't quite right - there's only 2 options and you can select both 677 $settings['type'] = 'select-bool'; 678 $settings['rows'] = 1; 679 $settings['tags'] = true; 680 break; 681 case 'select-cat': 682 $settings['rows'] = "5,,,$app_name"; 683 $settings['tags'] = true; 684 break; 685 case 'select-account': 686 $settings['account_type'] = 'both'; 687 $settings['tags'] = true; 688 break; 689 case 'select': 690 $settings['rows'] = 5; 691 $settings['tags'] = true; 692 break; 693 } 694 } 695 696 if(method_exists($plugin, 'get_filter_fields')) 697 { 698 $plugin->get_filter_fields($fields); 699 } 700 return $fields; 701 } 702 703 /** 704 * Parse a relative date (Yesterday) into absolute (2012-12-31) date 705 * 706 * @param $value String description of date matching $relative_dates 707 * 708 * @return Array([from] => timestamp, [to]=> timestamp), inclusive 709 */ 710 public static function date_rel2abs($value) 711 { 712 if(is_array($value)) 713 { 714 $abs = array(); 715 foreach($value as $key => $val) 716 { 717 $abs[$key] = self::date_rel2abs($val); 718 } 719 return $abs; 720 } 721 if($date = self::$relative_dates[$value]) 722 { 723 $year = (int) date('Y'); 724 $month = (int) date('m'); 725 $day = (int) date('d'); 726 $today = mktime(0,0,0,date('m'),date('d'),date('Y')); 727 728 list($syear,$smonth,$sday,$sweek,$eyear,$emonth,$eday,$eweek) = $date; 729 730 if(stripos($value, 'quarter') !== false) 731 { 732 // Handle quarters 733 $start = mktime(0,0,0,((int)floor(($smonth+$month) / 3.1)) * 3 + 1, 1, $year); 734 $end = mktime(0,0,0,((int)floor(($emonth+$month) / 3.1)+1) * 3 + 1, 1, $year); 735 } 736 elseif ($syear || $eyear) 737 { 738 $start = mktime(0,0,0,1,1,$syear+$year); 739 $end = mktime(0,0,0,1,1,$eyear+$year); 740 } 741 elseif ($smonth || $emonth) 742 { 743 $start = mktime(0,0,0,$smonth+$month,1,$year); 744 $end = mktime(0,0,0,$emonth+$month,1,$year); 745 } 746 elseif ($sday || $eday) 747 { 748 $start = mktime(0,0,0,$month,$sday+$day,$year); 749 $end = mktime(0,0,0,$month,$eday+$day,$year); 750 } 751 elseif ($sweek || $eweek) 752 { 753 $wday = (int) date('w'); // 0=sun, ..., 6=sat 754 switch($GLOBALS['egw_info']['user']['preferences']['calendar']['weekdaystarts']) 755 { 756 case 'Sunday': 757 $weekstart = $today - $wday * 24*60*60; 758 break; 759 case 'Saturday': 760 $weekstart = $today - (6-$wday) * 24*60*60; 761 break; 762 case 'Moday': 763 default: 764 $weekstart = $today - ($wday ? $wday-1 : 6) * 24*60*60; 765 break; 766 } 767 $start = $weekstart + $sweek*7*24*60*60; 768 $end = $weekstart + $eweek*7*24*60*60; 769 } 770 $end_param = $end - 24*60*60; 771 772 // Take 1 second off end to provide an inclusive range.for filtering 773 $end -= 1; 774 775 //echo __METHOD__."($value,$start,$end) today=".date('l, Y-m-d H:i',$today)." ==> <br />".date('l, Y-m-d H:i:s',$start)." <= date <= ".date('l, Y-m-d H:i:s',$end)."</p>\n"; 776 return array('from' => $start, 'to' => $end); 777 } 778 return null; 779 } 780} // end of importexport_helper_functions 781