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