2009-08-26 19:12:43 +02:00
< ? php
/**
* EGroupware - Document merge print
*
* @ link http :// www . egroupware . org
* @ author Ralf Becker < RalfBecker - AT - outdoor - training . de >
* @ package addressbook
2011-06-14 12:48:15 +02:00
* @ copyright ( c ) 2007 - 11 by Ralf Becker < RalfBecker - AT - outdoor - training . de >
2009-08-26 19:12:43 +02:00
* @ license http :// opensource . org / licenses / gpl - license . php GPL - GNU General Public License
* @ version $Id $
*/
/**
* Document merge print
*/
abstract class bo_merge
{
/**
* Instance of the addressbook_bo class
*
* @ var addressbook_bo
*/
var $contacts ;
/**
* Datetime format according to user preferences
*
* @ var string
*/
var $datetime_format = 'Y-m-d H:i' ;
2012-02-13 19:59:00 +01:00
/**
* Fields that are to be treated as datetimes , when merged into spreadsheets
*/
var $date_fields = array ();
2009-08-26 19:12:43 +02:00
/**
* Mimetype of document processed by merge
*
* @ var string
*/
var $mimetype ;
2009-10-14 10:12:11 +02:00
/**
* Plugins registered by extending class to create a table with multiple rows
*
* $$table / $plugin $ $ ... $$endtable $ $
*
* Callback returns replacements for row $n ( stringing with 0 ) or null if no further rows
*
* @ var array $plugin => array callback ( $plugin , $id , $n )
*/
var $table_plugins = array ();
2011-06-16 13:36:15 +02:00
/**
* Export limit in number of entries or some non - numerical value , if no export allowed at all , empty means no limit
*
* Set by constructor to $GLOBALS [ egw_info ][ server ][ export_limit ]
*
* @ var int | string
*/
public $export_limit ;
2011-09-05 22:24:10 +02:00
/**
* Configuration for HTML Tidy to clean up any HTML content that is kept
*/
public static $tidy_config = array (
2011-09-06 19:22:48 +02:00
'output-xml' => true , // Entity encoding
2011-09-05 22:24:10 +02:00
'show-body-only' => true ,
2011-09-06 20:21:39 +02:00
'output-encoding' => 'utf-8' ,
'input-encoding' => 'utf-8' ,
2011-09-06 19:22:48 +02:00
'quote-ampersand' => false , // Prevent double encoding
'quote-nbsp' => true , // XSLT can handle spaces easier
'preserve-entities' => true ,
2011-09-07 17:15:08 +02:00
'wrap' => 0 , // Wrapping can break output
2011-09-05 22:24:10 +02:00
);
2012-02-01 01:03:31 +01:00
/**
* Parse HTML styles into target document style , if possible
*/
protected $parse_html_styles = true ;
2009-08-26 19:12:43 +02:00
/**
* Constructor
*
* @ return bo_merge
*/
function __construct ()
{
2011-07-06 00:21:49 +02:00
// Common messages are in preferences
translation :: add_app ( 'preferences' );
// All contact fields are in addressbook
2011-07-05 23:44:21 +02:00
translation :: add_app ( 'addressbook' );
2011-07-06 00:21:49 +02:00
2009-08-26 19:12:43 +02:00
$this -> contacts = new addressbook_bo ();
$this -> datetime_format = $GLOBALS [ 'egw_info' ][ 'user' ][ 'preferences' ][ 'common' ][ 'dateformat' ] . ' ' .
( $GLOBALS [ 'egw_info' ][ 'user' ][ 'preferences' ][ 'common' ][ 'timeformat' ] == 12 ? 'h:i a' : 'H:i' );
2011-06-16 13:36:15 +02:00
2011-09-13 11:05:33 +02:00
$this -> export_limit = self :: getExportLimit ();
2009-08-26 19:12:43 +02:00
}
2011-08-17 11:36:51 +02:00
/**
* Hook returning options for export_limit_excepted groups
*
* @ param array $config
*/
public static function hook_export_limit_excepted ( $config )
{
$accountsel = new uiaccountsel ();
2011-08-30 10:16:51 +02:00
return '<input type="hidden" value="" name="newsettings[export_limit_excepted]" />' .
$accountsel -> selection ( 'newsettings[export_limit_excepted]' , 'export_limit_excepted' , $config [ 'export_limit_excepted' ], 'both' , 4 );
2011-08-17 11:36:51 +02:00
}
2009-08-26 19:12:43 +02:00
/**
* Get all replacements , must be implemented in extending class
*
* Can use eg . the following high level methods :
* - contact_replacements ( $contact_id , $prefix = '' )
* - format_datetime ( $time , $format = null )
*
* @ param int $id id of entry
* @ param string & $content = null content to create some replacements only if they are use
* @ return array | boolean array with replacements or false if entry not found
*/
2011-01-27 18:08:40 +01:00
abstract protected function get_replacements ( $id , & $content = null );
2009-08-26 19:12:43 +02:00
/**
* Return if merge - print is implemented for given mime - type ( and / or extension )
*
* @ param string $mimetype eg . text / plain
* @ param string $extension only checked for applications / msword and . rtf
*/
static public function is_implemented ( $mimetype , $extension = null )
{
static $zip_available ;
if ( is_null ( $zip_available ))
{
$zip_available = check_load_extension ( 'zip' ) &&
class_exists ( 'ZipArchive' ); // some PHP has zip extension, but no ZipArchive (eg. RHEL5!)
}
switch ( $mimetype )
{
case 'application/msword' :
if ( strtolower ( $extension ) != '.rtf' ) break ;
case 'application/rtf' :
case 'text/rtf' :
return true ; // rtf files
2009-09-01 12:25:36 +02:00
case 'application/vnd.oasis.opendocument.text' : // oo text
case 'application/vnd.oasis.opendocument.spreadsheet' : // oo spreadsheet
2009-08-26 19:12:43 +02:00
if ( ! $zip_available ) break ;
return true ; // open office write xml files
2009-09-01 16:41:48 +02:00
case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' : // ms word 2007 xml format
2009-08-26 19:12:43 +02:00
case 'application/vnd.openxmlformats-officedocument.wordprocessingml.d' : // mimetypes in vfs are limited to 64 chars
2009-09-01 16:41:48 +02:00
case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' : // ms excel 2007 xml format
case 'application/vnd.openxmlformats-officedocument.spreadsheetml.shee' :
2009-08-26 19:12:43 +02:00
if ( ! $zip_available ) break ;
return true ; // ms word xml format
2009-11-03 12:05:09 +01:00
case 'application/xml' :
return true ; // alias for text/xml, eg. ms office 2003 word format
2011-06-30 11:47:29 +02:00
case 'message/rfc822' :
2011-06-30 17:28:29 +02:00
return true ; // ToDo: check if you are theoretical able to send mail
2009-08-26 19:12:43 +02:00
default :
if ( substr ( $mimetype , 0 , 5 ) == 'text/' )
{
return true ; // text files
}
break ;
}
return false ;
// As browsers not always return correct mime types, one could use a negative list instead
//return !($mimetype == egw_vfs::DIR_MIME_TYPE || substr($mimetype,0,6) == 'image/');
}
/**
* Return replacements for a contact
*
* @ param int | string | array $contact contact - array or id
2011-01-27 18:08:40 +01:00
* @ param string $prefix = '' prefix like eg . 'user'
2009-08-26 19:12:43 +02:00
* @ return array
*/
2009-08-26 22:50:14 +02:00
public function contact_replacements ( $contact , $prefix = '' )
2009-08-26 19:12:43 +02:00
{
if ( ! is_array ( $contact ))
{
$contact = $this -> contacts -> read ( $contact );
}
if ( ! is_array ( $contact )) return array ();
$replacements = array ();
foreach ( array_keys ( $this -> contacts -> contact_fields ) as $name )
{
$value = $contact [ $name ];
switch ( $name )
{
case 'created' : case 'modified' :
2012-04-24 16:36:31 +02:00
if ( $value ) $value = egw_time :: to ( $value );
2009-08-26 19:12:43 +02:00
break ;
case 'bday' :
if ( $value )
{
list ( $y , $m , $d ) = explode ( '-' , $value );
$value = common :: dateformatorder ( $y , $m , $d , true );
}
break ;
case 'owner' : case 'creator' : case 'modifier' :
$value = common :: grab_owner_name ( $value );
break ;
case 'cat_id' :
if ( $value )
{
// if cat-tree is displayed, we return a full category path not just the name of the cat
$use = $GLOBALS [ 'egw_info' ][ 'server' ][ 'cat_tab' ] == 'Tree' ? 'path' : 'name' ;
$cats = array ();
foreach ( is_array ( $value ) ? $value : explode ( ',' , $value ) as $cat_id )
{
$cats [] = $GLOBALS [ 'egw' ] -> categories -> id2name ( $cat_id , $use );
}
$value = implode ( ', ' , $cats );
}
break ;
case 'jpegphoto' : // returning a link might make more sense then the binary photo
if ( $contact [ 'photo' ])
{
$value = ( $GLOBALS [ 'egw_info' ][ 'server' ][ 'webserver_url' ][ 0 ] == '/' ?
( $_SERVER [ 'HTTPS' ] ? 'https://' : 'http://' ) . $_SERVER [ 'HTTP_HOST' ] : '' ) .
$GLOBALS [ 'egw' ] -> link ( '/index.php' , $contact [ 'photo' ]);
}
break ;
case 'tel_prefer' :
if ( $value && $contact [ $value ])
{
$value = $contact [ $value ];
}
break ;
case 'account_id' :
if ( $value )
{
$replacements [ '$$' . ( $prefix ? $prefix . '/' : '' ) . 'account_lid$$' ] = $GLOBALS [ 'egw' ] -> accounts -> id2name ( $value );
}
break ;
}
if ( $name != 'photo' ) $replacements [ '$$' . ( $prefix ? $prefix . '/' : '' ) . $name . '$$' ] = $value ;
}
// set custom fields
foreach ( $this -> contacts -> customfields as $name => $field )
{
$name = '#' . $name ;
$value = ( string ) $contact [ $name ];
switch ( $field [ 'type' ])
{
case 'select-account' :
if ( $value ) $value = common :: grab_owner_name ( $value );
break ;
case 'select' :
if ( count ( $field [ 'values' ]) == 1 && isset ( $field [ 'values' ][ '@' ]))
{
$field [ 'values' ] = customfields_widget :: _get_options_from_file ( $field [ 'values' ][ '@' ]);
}
$values = array ();
foreach ( $field [ 'rows' ] > 1 ? explode ( ',' , $value ) : ( array ) $value as $value )
{
$values [] = $field [ 'values' ][ $value ];
}
$value = implode ( ', ' , $values );
break ;
case 'date' :
case 'date-time' :
if ( $value )
{
$format = $field [ 'len' ] ? $field [ 'len' ] : ( $field [ 'type' ] == 'date' ? 'Y-m-d' : 'Y-m-d H:i:s' );
$date = array_combine ( preg_split ( '/[\\/. :-]/' , $format ), preg_split ( '/[\\/. :-]/' , $value ));
$value = common :: dateformatorder ( $date [ 'Y' ], $date [ 'm' ], $date [ 'd' ], true );
if ( isset ( $date [ 'H' ])) $value .= ' ' . common :: formattime ( $date [ 'H' ], $date [ 'i' ]);
}
break ;
}
$replacements [ '$$' . ( $prefix ? $prefix . '/' : '' ) . $name . '$$' ] = $value ;
}
2011-02-16 18:47:31 +01:00
// Add in extra cat field
$cats = array ();
foreach ( is_array ( $contact [ 'cat_id' ]) ? $contact [ 'cat_id' ] : explode ( ',' , $contact [ 'cat_id' ]) as $cat_id )
{
if ( ! $cat_id ) continue ;
2011-02-16 18:56:00 +01:00
if ( $GLOBALS [ 'egw' ] -> categories -> id2name ( $cat_id , 'main' ) != $cat_id )
{
2011-02-17 17:40:57 +01:00
$path = $GLOBALS [ 'egw' ] -> categories -> id2name ( $cat_id , 'path' );
$path = explode ( ' / ' , $path );
unset ( $path [ 0 ]); // Drop main
$cats [ $GLOBALS [ 'egw' ] -> categories -> id2name ( $cat_id , 'main' )][] = implode ( ' / ' , $path );
2011-02-22 21:14:35 +01:00
} elseif ( $cat_id ) {
$cats [ $cat_id ] = array ();
2011-02-16 18:56:00 +01:00
}
}
foreach ( $cats as $main => $cat ) {
$replacements [ '$$' . ( $prefix ? $prefix . '/' : '' ) . 'categories$$' ] .= $GLOBALS [ 'egw' ] -> categories -> id2name ( $main , 'name' )
. ( count ( $cat ) > 0 ? ': ' : '' ) . implode ( ', ' , $cats [ $main ]) . " \n " ;
2011-02-16 18:47:31 +01:00
}
2009-08-26 19:12:43 +02:00
return $replacements ;
}
2011-10-26 00:30:51 +02:00
/**
* Get links for the given record
*
* Uses egw_link system to get link titles
2012-02-28 01:23:41 +01:00
*
* @ param app Name of current app
* @ param id ID of current entry
* @ param only_app Restrict links to only given application
* @ param exclude Exclude links to these applications
* @ param style String One of :
* 'title' - plain text , just the title of the link
* 'link' - URL to the entry
* 'href' - HREF tag wrapped around the title
2011-10-26 00:30:51 +02:00
*/
2012-02-28 01:23:41 +01:00
protected function get_links ( $app , $id , $only_app = '' , $exclude = array (), $style = 'title' )
2011-10-26 00:30:51 +02:00
{
2011-10-26 00:50:05 +02:00
$links = egw_link :: get_links ( $app , $id , $only_app );
2011-10-26 00:30:51 +02:00
$link_titles = array ();
foreach ( $links as $link_id => $link_info )
{
2012-01-05 18:43:44 +01:00
// Using only_app only returns the ID
if ( ! is_array ( $link_info ) && $only_app && $only_app [ 0 ] !== '!' )
{
$link_info = array (
'app' => $only_app ,
'id' => $link_info
);
}
2012-01-06 18:28:40 +01:00
if ( $exclude && in_array ( $link_info [ 'id' ], $exclude )) continue ;
2011-10-27 18:08:59 +02:00
$title = egw_link :: title ( $link_info [ 'app' ], $link_info [ 'id' ]);
if ( class_exists ( 'stylite_links_stream_wrapper' ) && $link_info [ 'app' ] != egw_link :: VFS_APPNAME )
{
2011-10-31 16:47:24 +01:00
$title = stylite_links_stream_wrapper :: entry2name ( $link_info [ 'app' ], $link_info [ 'id' ], $title );
2011-10-27 18:08:59 +02:00
}
2012-02-28 01:23:41 +01:00
if ( $style == 'href' || $style == 'link' )
{
$link = egw_link :: view ( $link_info [ 'app' ], $link_info [ 'id' ], $link_info );
if ( $link_info [ 'app' ] != egw_link :: VFS_APPNAME )
{
2012-04-03 00:14:15 +02:00
// Set app to false so we always get an external link
$link = str_replace ( ',' , '%2C' , egw :: link ( '/index.php' , $link , false ));
2012-02-28 01:23:41 +01:00
}
else
{
2012-04-03 00:14:15 +02:00
$link = egw :: link ( $link , array ());
2012-02-28 01:23:41 +01:00
}
// Prepend site
if ( $link { 0 } == '/' )
{
$link = ( $_SERVER [ 'HTTPS' ] || $GLOBALS [ 'egw_info' ][ 'server' ][ 'enforce_ssl' ] ? 'https://' : 'http://' ) .
( $GLOBALS [ 'egw_info' ][ 'server' ][ 'hostname' ] ? $GLOBALS [ 'egw_info' ][ 'server' ][ 'hostname' ] : $_SERVER [ 'HTTP_HOST' ]) . $link ;
}
$title = $style == 'href' ? html :: a_href ( html :: htmlspecialchars ( $title ), $link ) : $link ;
}
2011-10-27 18:08:59 +02:00
$link_titles [] = $title ;
2011-10-26 00:30:51 +02:00
}
2011-10-27 18:08:59 +02:00
return implode ( " \n " , $link_titles );
2011-10-26 00:30:51 +02:00
}
2009-08-26 19:12:43 +02:00
/**
* Format a datetime
*
2009-12-24 03:00:51 +01:00
* @ param int | string | DateTime $time unix timestamp or Y - m - d H : i : s string ( in user time ! )
2009-08-26 19:12:43 +02:00
* @ param string $format = null format string , default $this -> datetime_format
2009-12-24 03:00:51 +01:00
* @ deprecated use egw_time :: to ( $time = 'now' , $format = '' )
2009-08-26 19:12:43 +02:00
* @ return string
*/
protected function format_datetime ( $time , $format = null )
{
2012-04-24 16:36:31 +02:00
trigger_error ( __METHOD__ . ' is deprecated, use egw_time::to($time, $format)' , E_USER_DEPRECATED );
2009-08-26 19:12:43 +02:00
if ( is_null ( $format )) $format = $this -> datetime_format ;
2009-12-24 03:00:51 +01:00
return egw_time :: to ( $time , $format );
2009-08-26 19:12:43 +02:00
}
2011-08-26 15:50:20 +02:00
/**
* Checks if current user is excepted from the export - limit :
* a ) access to admin application
* b ) he or one of his memberships is named in export_limit_excepted config var
*
* @ return boolean
*/
public static function is_export_limit_excepted ()
{
static $is_excepted ;
if ( is_null ( $is_excepted ))
{
$is_excepted = isset ( $GLOBALS [ 'egw_info' ][ 'user' ][ 'apps' ][ 'admin' ]);
// check export-limit and fail if user tries to export more entries then allowed
if ( ! $is_excepted && ( is_array ( $export_limit_excepted = $GLOBALS [ 'egw_info' ][ 'server' ][ 'export_limit_excepted' ]) ||
is_array ( $export_limit_excepted = unserialize ( $export_limit_excepted ))))
{
$id_and_memberships = $GLOBALS [ 'egw' ] -> accounts -> memberships ( $GLOBALS [ 'egw_info' ][ 'user' ][ 'account_id' ], true );
$id_and_memberships [] = $GLOBALS [ 'egw_info' ][ 'user' ][ 'account_id' ];
$is_excepted = ( bool ) array_intersect ( $id_and_memberships , $export_limit_excepted );
}
}
return $is_excepted ;
}
2011-09-13 11:05:33 +02:00
/**
* getExportLimit
2011-11-16 10:26:23 +01:00
* checks if there is an exportlimit set , and returns
2011-09-13 11:05:33 +02:00
* @ param mixed $app_limit checks and validates app_limit , if not set returns the global limit
*
* @ return mixed - no if no export is allowed , false if there is no restriction and int as there is a valid restriction
* you may have to cast the returned value to int , if you want to use it as number
*/
2011-09-16 15:03:46 +02:00
public static function getExportLimit ( $app = 'common' )
2011-09-13 11:05:33 +02:00
{
2011-09-16 15:03:46 +02:00
static $exportLimitStore ;
if ( is_null ( $exportLimitStore )) $exportLimitStore = array ();
if ( empty ( $app )) $app = 'common' ;
//error_log(__METHOD__.__LINE__.' called with app:'.$app);
if ( ! array_key_exists ( $app , $exportLimitStore ))
2011-09-13 11:05:33 +02:00
{
2011-09-16 15:03:46 +02:00
//error_log(__METHOD__.__LINE__.' -> '.$app_limit.' '.function_backtrace());
$exportLimitStore [ $app ] = $GLOBALS [ 'egw_info' ][ 'server' ][ 'export_limit' ];
2011-11-16 10:26:23 +01:00
if ( $app != 'common' )
2011-09-16 15:03:46 +02:00
{
$app_limit = $GLOBALS [ 'egw' ] -> hooks -> single ( 'export_limit' , $app );
if ( $app_limit ) $exportLimitStore [ $app ] = $app_limit ;
}
//error_log(__METHOD__.__LINE__.' building cache for app:'.$app.' -> '.$exportLimitStore[$app]);
if ( empty ( $exportLimitStore [ $app ]))
{
$exportLimitStore [ $app ] = false ;
return false ;
}
2011-11-16 10:26:23 +01:00
2011-09-16 15:03:46 +02:00
if ( is_numeric ( $exportLimitStore [ $app ]))
{
$exportLimitStore [ $app ] = ( int ) $exportLimitStore [ $app ];
}
else
{
$exportLimitStore [ $app ] = 'no' ;
}
//error_log(__METHOD__.__LINE__.' -> '.$exportLimit);
2011-09-13 11:05:33 +02:00
}
2011-09-16 15:03:46 +02:00
//error_log(__METHOD__.__LINE__.' app:'.$app.' -> '.$exportLimitStore[$app]);
return $exportLimitStore [ $app ];
2011-09-13 11:05:33 +02:00
}
/**
* hasExportLimit
* checks wether there is an exportlimit set , and returns true or false
* @ param mixed $app_limit app_limit , if not set checks the global limit
* @ param string $checkas [ AND | ISALLOWED ], AND default ; if set to ISALLOWED it is checked if Export is allowed
*
* @ return bool - true if no export is allowed or a limit is set , false if there is no restriction
*/
2011-09-16 15:03:46 +02:00
public static function hasExportLimit ( $app_limit , $checkas = 'AND' )
2011-09-13 11:05:33 +02:00
{
if ( strtoupper ( $checkas ) == 'ISALLOWED' ) return ( empty ( $app_limit ) || ( $app_limit != 'no' && $app_limit > 0 ) );
if ( empty ( $app_limit )) return false ;
if ( $app_limit == 'no' ) return true ;
if ( $app_limit > 0 ) return true ;
}
2009-08-26 19:12:43 +02:00
/**
* Merges a given document with contact data
*
* @ param string $document path / url of document
* @ param array $ids array with contact id ( s )
* @ param string & $err error - message on error
* @ param string $mimetype mimetype of complete document , eg . text /* , application / vnd . oasis . opendocument . text , application / rtf
* @ param array $fix = null regular expression => replacement pairs eg . to fix garbled placeholders
* @ return string | boolean merged document or false on error
*/
2011-01-27 18:08:40 +01:00
public function & merge ( $document , $ids , & $err , $mimetype , array $fix = null )
2009-08-26 19:12:43 +02:00
{
if ( ! ( $content = file_get_contents ( $document )))
{
$err = lang ( " Document '%1' does not exist or is not readable for you! " , $document );
return false ;
}
2011-06-16 13:36:15 +02:00
2011-09-16 15:03:46 +02:00
if ( self :: hasExportLimit ( $this -> export_limit ) && ! self :: is_export_limit_excepted () && count ( $ids ) > ( int ) $this -> export_limit )
2011-06-16 13:36:15 +02:00
{
2011-09-14 12:27:53 +02:00
$err = lang ( 'No rights to export more than %1 entries!' ,( int ) $this -> export_limit );
2011-06-16 13:36:15 +02:00
return false ;
}
2010-10-27 11:34:42 +02:00
// fix application/msword mimetype for rtf files
if ( $mimetype == 'application/msword' && strtolower ( substr ( $document , - 4 )) == '.rtf' )
{
$mimetype = 'application/rtf' ;
}
2011-07-20 01:32:01 +02:00
2011-06-21 23:14:16 +02:00
try {
2011-07-20 01:32:01 +02:00
$content = $this -> merge_string ( $content , $ids , $err , $mimetype , $fix );
2011-08-02 20:41:18 +02:00
} catch ( Exception $e ) {
$err = $e -> getMessage ();
return false ;
}
return $content ;
}
2011-08-17 11:36:51 +02:00
2011-08-23 00:30:14 +02:00
protected function apply_styles ( & $content , $mimetype )
{
if ( $mimetype == 'application/xml' &&
preg_match ( '/' . preg_quote ( '<?mso-application progid="' ) . '([^"]+)' . preg_quote ( '"?>' ) . '/' , substr ( $content , 0 , 200 ), $matches ))
{
$mso_application_progid = $matches [ 1 ];
}
else
{
$mso_application_progid = '' ;
}
2011-08-02 20:41:18 +02:00
// Tags we can replace with the target document's version
$replace_tags = array ();
2011-09-12 12:51:21 +02:00
switch ( $mimetype . $mso_application_progid )
2011-08-02 20:41:18 +02:00
{
2011-09-12 12:51:21 +02:00
case 'application/vnd.oasis.opendocument.text' : // open office
case 'application/vnd.oasis.opendocument.spreadsheet' :
// It seems easier to split the parent tags here
$replace_tags = array (
'/<(ol|ul|table)( [^>]*)?>/' => '</text:p><$1$2>' ,
'/<\/(ol|ul|table)>/' => '</$1><text:p>' ,
//'/<(li)(.*?)>(.*?)<\/\1>/' => '<$1 $2>$3</$1>',
);
$content = preg_replace ( array_keys ( $replace_tags ), array_values ( $replace_tags ), $content );
2011-09-12 12:21:18 +02:00
2011-09-12 12:51:21 +02:00
$doc = new DOMDocument ();
$xslt = new XSLTProcessor ();
$doc -> load ( EGW_INCLUDE_ROOT . '/etemplate/templates/default/openoffice.xslt' );
$xslt -> importStyleSheet ( $doc );
//echo $content;die();
break ;
case 'application/xmlWord.Document' : // Word 2003*/
case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' : // ms office 2007
case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' :
$replace_tags = array (
'b' , 'strong' , 'i' , 'em' , 'u' , 'span'
);
// It seems easier to split the parent tags here
$replace_tags = array (
// Tables, lists don't go inside <w:p>
'/<(ol|ul|table)( [^>]*)?>/' => '</w:t></w:r></w:p><$1$2>' ,
'/<\/(ol|ul|table)>/' => '</$1><w:p><w:r><w:t>' ,
// Fix for things other than text (newlines) inside table row
'/<(td)( [^>]*)?>((?!<w:t>))(.*?)<\/td>[\s]*?/' => '<$1$2><w:t>$4</w:t></td>' ,
// Remove extra whitespace
'/<li([^>]*?)>[^:print:]*?(.*?)<\/li>/' => '<li$1>$2</li>' , // This doesn't get it all
'/<w:t>[\s]+(.*?)<\/w:t>/' => '<w:t>$1</w:t>' ,
// Remove spans with no attributes, linebreaks inside them cause problems
'/<span>(.*?)<\/span>/' => '$1'
);
2012-02-01 01:03:31 +01:00
$content = preg_replace ( array_keys ( $replace_tags ), array_values ( $replace_tags ), $content , - 1 , $count );
2012-03-04 14:33:10 +01:00
/*
In the case where you have something like < span >< span ></ w : t >< w : br />< w : t ></ span ></ span > ( invalid - mismatched tags ),
it takes multiple runs to get rid of both spans . So , loop .
2012-02-01 01:03:31 +01:00
OO . o files have not yet been shown to have this problem .
*/
$count = $i = 0 ;
do
{
$content = preg_replace ( '/<span>(.*?)<\/span>/' , '$1' , $content , - 1 , $count );
$i ++ ;
} while ( $count > 0 && $i < 10 );
2011-09-12 12:51:21 +02:00
//echo $content;die();
$doc = new DOMDocument ();
$xslt = new XSLTProcessor ();
$xslt_file = $mimetype == 'application/xml' ? 'wordml.xslt' : 'msoffice.xslt' ;
$doc -> load ( EGW_INCLUDE_ROOT . '/etemplate/templates/default/' . $xslt_file );
$xslt -> importStyleSheet ( $doc );
break ;
2011-08-02 20:41:18 +02:00
}
2011-09-06 19:22:48 +02:00
2011-08-02 20:41:18 +02:00
// XSLT transform known tags
if ( $xslt )
{
2012-02-01 01:03:31 +01:00
// does NOT work with php 5.2.6: Catchable fatal error: argument 1 to transformToXml() must be of type DOMDocument
//$element = new SimpleXMLelement($content);
$element = new DOMDocument ( '1.0' , 'utf-8' );
$result = $element -> loadXML ( $content );
if ( ! $result )
2011-07-20 01:32:01 +02:00
{
2012-02-01 01:03:31 +01:00
throw new Exception ( 'Unable to parse merged document for styles. Check warnings in log for details.' );
}
$content = $xslt -> transformToXml ( $element );
2011-09-12 12:51:21 +02:00
//echo $content;die();
2012-02-01 01:03:31 +01:00
// Word 2003 needs two declarations, add extra declaration back in
if ( $mimetype == 'application/xml' && $mso_application_progid == 'Word.Document' && strpos ( $content , '<?xml' ) !== 0 ) {
$content = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' . $content ;
2011-08-02 20:41:18 +02:00
}
2012-02-01 01:03:31 +01:00
// Validate
/*
$doc = new DOMDocument ();
$doc -> loadXML ( $content );
$doc -> schemaValidate ( * Schema ( xsd ) file * );
*/
2011-06-21 23:14:16 +02:00
}
2010-10-27 11:34:42 +02:00
}
/**
* Merges a given document with contact data
*
* @ param string $content
* @ param array $ids array with contact id ( s )
* @ param string & $err error - message on error
* @ param string $mimetype mimetype of complete document , eg . text /* , application / vnd . oasis . opendocument . text , application / rtf
* @ param array $fix = null regular expression => replacement pairs eg . to fix garbled placeholders
* @ return string | boolean merged document or false on error
*/
2011-01-27 18:08:40 +01:00
public function & merge_string ( $content , $ids , & $err , $mimetype , array $fix = null )
2010-10-27 11:34:42 +02:00
{
2010-03-08 22:56:05 +01:00
if ( $mimetype == 'application/xml' &&
preg_match ( '/' . preg_quote ( '<?mso-application progid="' ) . '([^"]+)' . preg_quote ( '"?>' ) . '/' , substr ( $content , 0 , 200 ), $matches ))
{
$mso_application_progid = $matches [ 1 ];
}
else
{
$mso_application_progid = '' ;
}
2009-11-04 15:18:11 +01:00
// alternative syntax using double curly brackets (eg. {{cat_id}} instead $$cat_id$$),
// agressivly removing all xml-tags eg. Word adds within placeholders
$content = preg_replace_callback ( '/{{[^}]+}}/i' , create_function ( '$p' , 'return \'$$\'.strip_tags(substr($p[0],2,-2)).\'$$\';' ), $content );
2009-08-26 19:12:43 +02:00
// make currently processed mimetype available to class methods;
$this -> mimetype = $mimetype ;
// fix garbled placeholders
if ( $fix && is_array ( $fix ))
{
$content = preg_replace ( array_keys ( $fix ), array_values ( $fix ), $content );
//die("<pre>".htmlspecialchars($content)."</pre>\n");
}
list ( $contentstart , $contentrepeat , $contentend ) = preg_split ( '/\$\$pagerepeat\$\$/' , $content , - 1 , PREG_SPLIT_NO_EMPTY ); //get differt parts of document, seperatet by Pagerepeat
2010-12-15 11:50:29 +01:00
if ( $mimetype == 'text/plain' && count ( $ids ) > 1 )
{
// textdocuments are simple, they do not hold start and end, but they may have content before and after the $$pagerepeat$$ tag
// header and footer should not hold any $$ tags; if we find $$ tags with the header, we assume it is the pagerepeatcontent
2010-12-15 13:06:53 +01:00
$nohead = false ;
2010-12-15 11:50:29 +01:00
if ( stripos ( $contentstart , '$$' ) !== false ) $nohead = true ;
if ( $nohead )
{
$contentend = $contentrepeat ;
$contentrepeat = $contentstart ;
$contentstart = '' ;
}
2011-03-18 16:14:55 +01:00
2010-12-15 11:50:29 +01:00
}
2009-08-26 19:12:43 +02:00
if ( $mimetype == 'application/vnd.oasis.opendocument.text' && count ( $ids ) > 1 )
{
2012-05-07 23:09:31 +02:00
if ( strpos ( $content , '$$pagerepeat' ) === false )
{
//for odt files we have to split the content and add a style for page break to the style area
list ( $contentstart , $contentrepeat , $contentend ) = preg_split ( '/office:body>/' , $content , - 1 , PREG_SPLIT_NO_EMPTY ); //get differt parts of document, seperatet by Pagerepeat
$contentstart = substr ( $contentstart , 0 , strlen ( $contentstart ) - 1 ); //remove "<"
$contentrepeat = substr ( $contentrepeat , 0 , strlen ( $contentrepeat ) - 2 ); //remove "</";
// need to add page-break style to the style list
list ( $stylestart , $stylerepeat , $styleend ) = preg_split ( '/<\/office:automatic-styles>/' , $content , - 1 , PREG_SPLIT_NO_EMPTY ); //get differt parts of document style sheets
$contentstart = $stylestart . '<style:style style:name="P200" style:family="paragraph" style:parent-style-name="Standard"><style:paragraph-properties fo:break-before="page"/></style:style></office:automatic-styles>' ;
$contentstart .= '<office:body>' ;
$contentend = '</office:body></office:document-content>' ;
}
else
{
// Template specifies where to repeat
list ( $contentstart , $contentrepeat , $contentend ) = preg_split ( '/\$\$pagerepeat\$\$/' , $content , - 1 , PREG_SPLIT_NO_EMPTY ); //get different parts of document, seperated by pagerepeat
}
2009-08-26 19:12:43 +02:00
}
2009-09-13 14:06:40 +02:00
if ( $mimetype == 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' && count ( $ids ) > 1 )
{
2009-12-03 17:15:23 +01:00
//for Word 2007 XML files we have to split the content and add a style for page break to the style area
2009-09-13 14:06:40 +02:00
list ( $contentstart , $contentrepeat , $contentend ) = preg_split ( '/w:body>/' , $content , - 1 , PREG_SPLIT_NO_EMPTY ); //get differt parts of document, seperatet by Pagerepeat
$contentstart = substr ( $contentstart , 0 , strlen ( $contentstart ) - 1 ); //remove "</"
$contentrepeat = substr ( $contentrepeat , 0 , strlen ( $contentrepeat ) - 2 ); //remove "</";
$contentstart .= '<w:body>' ;
$contentend = '</w:body></w:document>' ;
}
2009-08-26 19:12:43 +02:00
list ( $Labelstart , $Labelrepeat , $Labeltend ) = preg_split ( '/\$\$label\$\$/' , $contentrepeat , - 1 , PREG_SPLIT_NO_EMPTY ); //get the Lable content
preg_match_all ( '/\$\$labelplacement\$\$/' , $contentrepeat , $countlables , PREG_SPLIT_NO_EMPTY );
2009-12-30 14:59:08 +01:00
$countlables = count ( $countlables [ 0 ]);
2009-08-26 19:12:43 +02:00
preg_replace ( '/\$\$labelplacement\$\$/' , '' , $Labelrepeat , 1 );
if ( $countlables > 1 ) $lableprint = true ;
if ( count ( $ids ) > 1 && ! $contentrepeat )
{
2011-09-14 12:27:53 +02:00
$err = lang ( 'for more than one contact in a document use the tag pagerepeat!' );
2009-08-26 19:12:43 +02:00
return false ;
}
foreach (( array ) $ids as $id )
{
if ( $contentrepeat ) $content = $contentrepeat ; //content to repeat
if ( $lableprint ) $content = $Labelrepeat ;
2011-09-13 17:41:31 +02:00
// generate replacements; if exeption is thrown, catch it set error message and return false
try
{
if ( ! ( $replacements = $this -> get_replacements ( $id , $content )))
{
$err = lang ( 'Entry not found!' );
return false ;
}
}
catch ( egw_exception_wrong_userinput $e )
2009-08-26 19:12:43 +02:00
{
2011-09-13 17:41:31 +02:00
// if this returns with an exeption, something failed big time
$err = $e -> getMessage ();
2009-08-26 19:12:43 +02:00
return false ;
2011-03-18 16:14:55 +01:00
}
2011-09-13 17:41:31 +02:00
2009-08-26 19:12:43 +02:00
// some general replacements: current user, date and time
if ( strpos ( $content , '$$user/' ) !== null && ( $user = $GLOBALS [ 'egw' ] -> accounts -> id2name ( $GLOBALS [ 'egw_info' ][ 'user' ][ 'account_id' ], 'person_id' )))
{
$replacements += $this -> contact_replacements ( $user , 'user' );
}
2009-12-24 03:00:51 +01:00
$replacements [ '$$date$$' ] = egw_time :: to ( 'now' , true );
$replacements [ '$$datetime$$' ] = egw_time :: to ( 'now' );
$replacements [ '$$time$$' ] = egw_time :: to ( 'now' , false );
2009-08-26 19:12:43 +02:00
2009-10-14 10:12:11 +02:00
// does our extending class registered table-plugins AND document contains table tags
if ( $this -> table_plugins && preg_match_all ( '/\\$\\$table\\/([A-Za-z0-9_]+)\\$\\$(.*?)\\$\\$endtable\\$\\$/s' , $content , $matches , PREG_SET_ORDER ))
2009-10-09 13:22:23 +02:00
{
2009-10-14 10:12:11 +02:00
// process each table
foreach ( $matches as $match )
{
$plugin = $match [ 1 ]; // plugin name
$callback = $this -> table_plugins [ $plugin ];
$repeat = $match [ 2 ]; // line to repeat
$repeats = '' ;
if ( isset ( $callback ))
{
2011-02-07 12:08:08 +01:00
for ( $n = 0 ; ( $row_replacements = $this -> $callback ( $plugin , $id , $n , $repeat )); ++ $n )
2009-10-14 10:12:11 +02:00
{
2011-02-28 20:16:58 +01:00
$_repeat = $this -> process_commands ( $repeat , $row_replacements );
$repeats .= $this -> replace ( $_repeat , $row_replacements , $mimetype , $mso_application_progid );
2009-10-14 10:12:11 +02:00
}
}
$content = str_replace ( $match [ 0 ], $repeats , $content );
}
2009-10-07 19:46:09 +02:00
}
2010-03-08 22:56:05 +01:00
$content = $this -> replace ( $content , $replacements , $mimetype , $mso_application_progid );
2009-10-14 10:12:11 +02:00
2011-02-23 22:01:37 +01:00
$content = $this -> process_commands ( $content , $replacements );
2009-08-26 19:12:43 +02:00
// remove not existing replacements (eg. from calendar array)
if ( strpos ( $content , '$$' ) !== null )
{
2009-09-01 16:41:48 +02:00
$content = preg_replace ( '/\$\$[a-z0-9_\/]+\$\$/i' , '' , $content );
2009-08-26 19:12:43 +02:00
}
2011-02-16 18:47:31 +01:00
if ( $contentrepeat ) $contentrep [ is_array ( $id ) ? implode ( ':' , $id ) : $id ] = $content ;
2009-08-26 19:12:43 +02:00
}
if ( $Labelrepeat )
{
$countpage = 0 ;
$count = 0 ;
$contentrepeatpages [ $countpage ] = $Labelstart . $Labeltend ;
foreach ( $contentrep as $Label )
{
2009-12-30 14:59:08 +01:00
$contentrepeatpages [ $countpage ] = preg_replace ( '/\$\$labelplacement\$\$/' , $Label , $contentrepeatpages [ $countpage ], 1 );
2009-08-26 19:12:43 +02:00
$count = $count + 1 ;
2009-12-30 14:59:08 +01:00
if (( $count % $countlables ) == 0 && count ( $contentrep ) > $count ) //new page
2009-08-26 19:12:43 +02:00
{
2009-12-30 14:59:08 +01:00
$countpage = $countpage + 1 ;
2009-08-26 19:12:43 +02:00
$contentrepeatpages [ $countpage ] = $Labelstart . $Labeltend ;
}
}
$contentrepeatpages [ $countpage ] = preg_replace ( '/\$\$labelplacement\$\$/' , '' , $contentrepeatpages [ $countpage ], - 1 ); //clean empty fields
switch ( $mimetype )
{
case 'application/rtf' :
case 'text/rtf' :
return $contentstart . implode ( '\\par \\page\\pard\\plain' , $contentrepeatpages ) . $contentend ;
case 'application/vnd.oasis.opendocument.text' :
2009-09-01 12:25:36 +02:00
case 'application/vnd.oasis.opendocument.spreadsheet' :
2011-08-17 01:07:44 +02:00
return $contentstart . implode ( '<text:line-break />' , $contentrepeatpages ) . $contentend ;
2009-08-26 19:12:43 +02:00
case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' :
2009-09-01 16:41:48 +02:00
case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' :
2011-08-22 16:20:44 +02:00
return $contentstart . implode ( '<w:br w:type="page" />' , $contentrepeatpages ) . $contentend ;
2010-12-15 11:50:29 +01:00
case 'text/plain' :
return $contentstart . implode ( " \r \n " , $contentrep ) . $contentend ;
2009-08-26 19:12:43 +02:00
}
$err = lang ( '%1 not implemented for %2!' , '$$labelplacement$$' , $mimetype );
return false ;
}
if ( $contentrepeat )
{
switch ( $mimetype )
{
case 'application/rtf' :
case 'text/rtf' :
return $contentstart . implode ( '\\par \\page\\pard\\plain' , $contentrep ) . $contentend ;
case 'application/vnd.oasis.opendocument.text' :
2009-09-01 12:25:36 +02:00
case 'application/vnd.oasis.opendocument.spreadsheet' :
2009-12-03 17:15:23 +01:00
case 'application/xml' :
2012-05-16 19:36:07 +02:00
case 'text/html' :
2009-08-26 19:12:43 +02:00
return $contentstart . implode ( '' , $contentrep ) . $contentend ;
case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' :
2009-09-01 16:41:48 +02:00
case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' :
2009-09-13 14:06:40 +02:00
return $contentstart . implode ( '<w:br w:type="page" />' , $contentrep ) . $contentend ;
2010-12-15 11:50:29 +01:00
case 'text/plain' :
return $contentstart . implode ( " \r \n " , $contentrep ) . $contentend ;
2009-08-26 19:12:43 +02:00
}
$err = lang ( '%1 not implemented for %2!' , '$$pagerepeat$$' , $mimetype );
return false ;
}
2011-04-06 22:45:14 +02:00
2009-08-26 19:12:43 +02:00
return $content ;
}
2009-10-14 10:12:11 +02:00
/**
* Replace placeholders in $content of $mimetype with $replacements
*
* @ param string $content
* @ param array $replacements name => replacement pairs
* @ param string $mimetype mimetype of content
2010-03-08 22:56:05 +01:00
* @ param string $mso_application_progid = '' MS Office 2003 : 'Excel.Sheet' or 'Word.Document'
2009-10-14 10:12:11 +02:00
* @ return string
*/
2010-03-08 22:56:05 +01:00
protected function replace ( $content , array $replacements , $mimetype , $mso_application_progid = '' )
2009-10-14 10:12:11 +02:00
{
switch ( $mimetype )
{
case 'application/vnd.oasis.opendocument.text' : // open office
case 'application/vnd.oasis.opendocument.spreadsheet' :
case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' : // ms office 2007
case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' :
2009-11-03 14:10:49 +01:00
case 'application/xml' :
2009-10-14 10:12:11 +02:00
case 'text/xml' :
2010-08-23 14:03:03 +02:00
case 'text/html' :
2009-10-14 10:12:11 +02:00
$is_xml = true ;
$charset = 'utf-8' ; // xml files --> always use utf-8
break ;
default : // div. text files --> use our export-charset, defined in addressbook prefs
$charset = $this -> contacts -> prefs [ 'csv_charset' ];
break ;
}
2010-08-23 14:03:03 +02:00
//error_log(__METHOD__."('$document', ... ,$mimetype) --> $charset (egw=".translation::charset().', export='.$this->contacts->prefs['csv_charset'].')');
2011-07-20 01:32:01 +02:00
2009-10-14 10:12:11 +02:00
// do we need to convert charset
2010-08-23 14:03:03 +02:00
if ( $charset && $charset != translation :: charset ())
2009-10-14 10:12:11 +02:00
{
2010-08-23 14:03:03 +02:00
$replacements = translation :: convert ( $replacements , translation :: charset (), $charset );
2009-10-14 10:12:11 +02:00
}
2012-07-02 17:24:15 +02:00
// Date only placeholders for timestamps
if ( is_array ( $this -> date_fields ))
{
foreach ( $this -> date_fields as $field )
{
if ( $value = $replacements [ '$$' . $field . '$$' ])
{
$replacements [ '$$' . $field . '/date$$' ] = egw_time :: to ( $value , true );
}
}
}
2010-02-22 00:15:05 +01:00
if ( $is_xml ) // zip'ed xml document (eg. OO)
2009-10-14 10:12:11 +02:00
{
2011-03-29 19:46:07 +02:00
// Numeric fields
$names = array ();
2011-07-20 01:32:01 +02:00
// Tags we can replace with the target document's version
$replace_tags = array ();
2011-09-12 12:51:21 +02:00
// only keep tags, if we have xsl extension available
2012-02-01 01:03:31 +01:00
if ( class_exists ( XSLTProcessor ) && class_exists ( DOMDocument ) && $this -> parse_html_styles )
2011-07-20 01:32:01 +02:00
{
2011-09-12 12:51:21 +02:00
switch ( $mimetype . $mso_application_progid )
{
2012-02-27 22:34:34 +01:00
case 'text/html' :
$replace_tags = array (
'<b>' , '<strong>' , '<i>' , '<em>' , '<u>' , '<span>' , '<ol>' , '<ul>' , '<li>' ,
2012-02-28 01:23:41 +01:00
'<table>' , '<tr>' , '<td>' , '<a>' ,
2012-02-27 22:34:34 +01:00
);
break ;
2011-09-12 12:51:21 +02:00
case 'application/vnd.oasis.opendocument.text' : // open office
case 'application/vnd.oasis.opendocument.spreadsheet' :
$replace_tags = array (
'<b>' , '<strong>' , '<i>' , '<em>' , '<u>' , '<span>' , '<ol>' , '<ul>' , '<li>' ,
2012-05-14 15:48:32 +02:00
'<table>' , '<tr>' , '<td>' , '<a>' ,
2011-09-12 12:51:21 +02:00
);
break ;
case 'application/xmlWord.Document' : // Word 2003*/
case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' : // ms office 2007
case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' :
$replace_tags = array (
'<b>' , '<strong>' , '<i>' , '<em>' , '<u>' , '<span>' , '<ol>' , '<ul>' , '<li>' ,
'<table>' , '<tr>' , '<td>' ,
);
break ;
}
2011-07-20 01:32:01 +02:00
}
2011-02-09 21:11:17 +01:00
// clean replacements from array values and html or html-entities, which mess up xml
2010-02-22 00:15:05 +01:00
foreach ( $replacements as $name => & $value )
{
2011-02-09 21:11:17 +01:00
// set unresolved array values to empty string
if ( is_array ( $value ))
{
$value = '' ;
continue ;
}
2010-02-22 00:15:05 +01:00
// decode html entities back to utf-8
2011-02-09 21:11:17 +01:00
if ( is_string ( $value ) && ( strpos ( $value , '&' ) !== false ))
2010-02-22 00:15:05 +01:00
{
$value = html_entity_decode ( $value , ENT_QUOTES , $charset );
2010-03-08 12:34:22 +01:00
2010-02-22 00:15:05 +01:00
// remove all non-decodable entities
if ( strpos ( $value , '&' ) !== false )
{
$value = preg_replace ( '/&[^; ]+;/' , '' , $value );
}
}
// remove all html tags, evtl. included
2011-02-09 21:11:17 +01:00
if ( is_string ( $value ) && ( strpos ( $value , '<' ) !== false ))
2010-02-22 00:15:05 +01:00
{
2011-09-05 22:24:10 +02:00
// Clean HTML, if it's being kept
2011-09-06 19:22:48 +02:00
if ( $replace_tags && extension_loaded ( 'tidy' )) {
2011-09-07 17:15:08 +02:00
$tidy = new tidy ();
$cleaned = $tidy -> repairString ( $value , self :: $tidy_config );
// Found errors. Strip it all so there's some output
if ( $tidy -> getStatus () == 2 )
{
error_log ( $tidy -> errorBuffer );
$value = strip_tags ( $value );
}
else
{
$value = $cleaned ;
}
2011-09-05 22:24:10 +02:00
}
2010-03-08 22:56:05 +01:00
// replace </p> and <br /> with CRLF (remove <p> and CRLF)
$value = str_replace ( array ( " \r " , " \n " , '<p>' , '</p>' , '<br />' ), array ( '' , '' , '' , " \r \n " , " \r \n " ), $value );
2011-07-20 01:32:01 +02:00
$value = strip_tags ( $value , implode ( '' , $replace_tags ));
2012-02-01 01:03:31 +01:00
// Change <tag>...\r\n</tag> to <tag>...</tag>\r\n or simplistic line break below will mangle it
// Loop to catch things like <b><span>Break:\r\n</span></b>
if ( $mso_application_progid )
{
$count = $i = 0 ;
do
{
$value = preg_replace ( '/<(b|strong|i|em|u|span)\b([^>]*?)>(.*?)' . " \r \n " . '<\/\1>/u' , '<$1$2>$3</$1>' . " \r \n " , $value , - 1 , $count );
$i ++ ;
} while ( $count > 0 && $i < 10 ); // Limit of 10 chosen arbitrarily just in case
}
2010-02-22 00:15:05 +01:00
}
2010-03-16 14:38:20 +01:00
// replace all control chars (C0+C1) but CR (\015), LF (\012) and TAB (\011) (eg. vertical tabulators) with space
2010-03-08 12:34:22 +01:00
// as they are not allowed in xml
2010-03-16 14:38:20 +01:00
$value = preg_replace ( '/[\000-\010\013\014\016-\037\177-\237]/u' , ' ' , $value );
2011-03-31 20:13:46 +02:00
if ( is_numeric ( $value ) && $name != '$$user/account_id$$' ) // account_id causes problems with the preg_replace below
{
$names [] = preg_quote ( $name , '/' );
}
2011-03-03 01:15:27 +01:00
}
2011-03-31 20:13:46 +02:00
2011-03-03 01:15:27 +01:00
// Look for numbers, set their value if needed
2011-03-29 19:46:07 +02:00
if ( $this -> numeric_fields || count ( $names ))
2011-03-18 16:14:55 +01:00
{
2011-03-31 20:13:46 +02:00
foreach (( array ) $this -> numeric_fields as $fieldname ) {
2011-03-14 23:06:23 +01:00
$names [] = preg_quote ( $fieldname , '/' );
}
2012-02-13 19:59:00 +01:00
$this -> format_spreadsheet_numbers ( $content , $names , $mimetype . $mso_application_progid );
}
2011-03-21 15:53:41 +01:00
2012-02-13 19:59:00 +01:00
// Look for dates, set their value if needed
if ( $this -> date_fields || count ( $names ))
{
$names = array ();
foreach (( array ) $this -> date_fields as $fieldname ) {
2012-04-16 17:54:22 +02:00
$names [] = $fieldname ;
2011-03-18 16:14:55 +01:00
}
2012-02-13 19:59:00 +01:00
$this -> format_spreadsheet_dates ( $content , $names , $replacements , $mimetype . $mso_application_progid );
2011-03-14 23:06:23 +01:00
}
2012-02-13 19:59:00 +01:00
2010-03-08 22:56:05 +01:00
// replace CRLF with linebreak tag of given type
switch ( $mimetype . $mso_application_progid )
{
case 'application/vnd.oasis.opendocument.text' : // open office writer
$break = '<text:line-break/>' ;
break ;
case 'application/vnd.oasis.opendocument.spreadsheet' : // open office calc
$break = '<text:line-break/>' ;
break ;
case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' : // ms word 2007
2011-07-20 01:32:01 +02:00
$break = '</w:t><w:br/><w:t>' ;
2010-03-08 22:56:05 +01:00
break ;
case 'application/xmlExcel.Sheet' : // Excel 2003
$break = ' ' ;
break ;
case 'application/xmlWord.Document' : // Word 2003*/
2011-08-23 21:56:46 +02:00
$break = '</w:t><w:br/><w:t>' ;
2010-03-08 22:56:05 +01:00
break ;
case 'text/html' :
$break = '<br/>' ;
break ;
case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' : // ms excel 2007
default :
$break = " \r \n " ;
break ;
}
2010-02-22 00:15:05 +01:00
// now decode &, < and >, which need to be encoded as entities in xml
2011-12-09 22:00:57 +01:00
// Check for encoded >< getting double-encoded
$replacements = str_replace ( array ( '&' , " \r " , " \n " , '&lt;' , '&gt;' ), array ( '&' , '' , $break , '<' , '>' ), $replacements );
2009-10-14 10:12:11 +02:00
}
return str_replace ( array_keys ( $replacements ), array_values ( $replacements ), $content );
}
2012-02-13 19:59:00 +01:00
/**
* Convert numeric values in spreadsheets into actual numeric values
*/
protected function format_spreadsheet_numbers ( & $content , $names , $mimetype )
{
foreach (( array ) $this -> numeric_fields as $fieldname ) {
$names [] = preg_quote ( $fieldname , '/' );
}
switch ( $mimetype )
{
case 'application/vnd.oasis.opendocument.spreadsheet' : // open office calc
$format = '/<table:table-cell([^>]+?)office:value-type="[^"]+"([^>]*?)>.?<([a-z].*?)[^>]*>(' . implode ( '|' , $names ) . ')<\/\3>.?<\/table:table-cell>/s' ;
$replacement = '<table:table-cell$1office:value-type="float" office:value="$4"$2>$4</table:table-cell>' ;
break ;
2012-06-26 18:21:20 +02:00
case 'application/vnd.oasis.opendocument.text' : // tables in open office writer
$format = '/<table:table-cell([^>]+?)office:value-type="[^"]+"([^>]*?)>.?<([a-z].*?)[^>]*>(' . implode ( '|' , $names ) . ')<\/\3>.?<\/table:table-cell>/s' ;
$replacement = '<table:table-cell$1office:value-type="float" office:value="$4"$2><text:p text:style-name="Standard">$4</text:p></table:table-cell>' ;
break ;
case 'application/vnd.oasis.opendocument.text' : // open office writer
2012-02-13 19:59:00 +01:00
case 'application/xmlExcel.Sheet' : // Excel 2003
$format = '/' . preg_quote ( '<Data ss:Type="String">' , '/' ) . '(' . implode ( '|' , $names ) . ')' . preg_quote ( '</Data>' , '/' ) . '/' ;
$replacement = '<Data ss:Type="Number">$1</Data>' ;
break ;
}
if ( $format && $names )
{
// Dealing with backtrack limit per AmigoJack 10-Jul-2010 comment on php.net preg-replace docs
$increase = 0 ;
while ( $increase < 10 ) {
$result = preg_replace ( $format , $replacement , $content , - 1 , $count );
if ( preg_last_error () == PREG_BACKTRACK_LIMIT_ERROR ) { // Only check on backtrack limit failure
ini_set ( 'pcre.backtrack_limit' , ( int ) ini_get ( 'pcre.backtrack_limit' ) + 10000 ); // Get current limit and increase
$increase ++ ; // Do not overkill the server
} else { // No fail
$content = $result ; // On failure $result would be NULL
break ; // Exit loop
}
}
if ( $increase == 10 ) {
error_log ( 'Backtrack limit exceeded @ ' . ini_get ( 'pcre.backtrack_limit' ) . ', some cells left as text.' );
}
}
}
/**
* Convert date / timestamp values in spreadsheets into actual date / timestamp values
*/
protected function format_spreadsheet_dates ( & $content , $names , & $values , $mimetype )
{
2012-02-25 00:29:58 +01:00
if ( ! in_array ( $mimetype , array (
'application/vnd.oasis.opendocument.spreadsheet' , // open office calc
'application/xmlExcel.Sheet' , // Excel 2003
//'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'//Excel WTF
))) return ;
2012-04-24 16:36:31 +02:00
// Some different formats dates could be in, depending what they've been through
$formats = array (
'!' . egw_time :: $user_dateformat . ' ' . egw_time :: $user_timeformat . ':s' ,
'!' . egw_time :: $user_dateformat . '* ' . egw_time :: $user_timeformat ,
'!' . egw_time :: $user_dateformat . '*' ,
2012-07-31 17:03:31 +02:00
'!' . egw_time :: $user_dateformat ,
2012-04-24 16:36:31 +02:00
);
2012-02-25 00:29:58 +01:00
// Properly format values for spreadsheet
2012-04-16 17:54:22 +02:00
foreach ( $names as $idx => & $field )
2012-02-25 00:29:58 +01:00
{
$key = '$$' . $field . '$$' ;
2012-04-16 17:54:22 +02:00
$field = preg_quote ( $field , '/' );
2012-02-25 00:29:58 +01:00
if ( $values [ $key ])
{
2012-04-23 18:32:34 +02:00
if ( ! is_numeric ( $values [ $key ]))
{
2012-04-24 16:36:31 +02:00
// Try the different formats, stop when one works
foreach ( $formats as $f )
{
try {
$date = egw_time :: createFromFormat (
$f ,
$values [ $key ],
egw_time :: $user_timezone
);
if ( $date ) break ;
} catch ( Exception $e ) {
}
}
if ( ! $date )
{
2012-04-23 18:32:34 +02:00
// Couldn't get a date out of it... skip it
trigger_error ( " Unable to parse date $key = ' { $values [ $key ] } ' - left as text " , E_USER_NOTICE );
2012-04-24 16:36:31 +02:00
unset ( $names [ $idx ]);
continue ;
}
2012-04-23 18:32:34 +02:00
}
else
{
$date = new egw_time ( $values [ $key ]);
}
2012-02-25 00:29:58 +01:00
if ( $mimetype == 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ) //Excel WTF
{
$interval = $date -> diff ( new egw_time ( '1900-01-00 0:00' ));
$values [ $key ] = $interval -> format ( '%a' ) + 1 ; // 1900-02-29 did not exist
// 1440 minutes in a day - fractional part
2012-03-04 14:33:10 +01:00
$values [ $key ] += ( $date -> format ( 'H' ) * 60 + $date -> format ( 'i' )) / 1440 ;
2012-02-25 00:29:58 +01:00
}
else
{
$values [ $key ] = date ( 'Y-m-d\TH:i:s' , egw_time :: to ( $date , 'ts' ));
}
}
else
{
unset ( $names [ $idx ]);
}
}
2012-04-16 17:54:22 +02:00
2012-02-13 19:59:00 +01:00
switch ( $mimetype )
{
case 'application/vnd.oasis.opendocument.spreadsheet' : // open office calc
2012-02-25 00:29:58 +01:00
$format = '/<table:table-cell([^>]+?)office:value-type="[^"]+"([^>]*?)>.?<([a-z].*?)[^>]*>\$\$(' . implode ( '|' , $names ) . ')\$\$<\/\3>.?<\/table:table-cell>/s' ;
2012-02-13 19:59:00 +01:00
$replacement = '<table:table-cell$1office:value-type="date" office:date-value="\$\$$4\$\$"$2><$3>\$\$$4\$\$</$3></table:table-cell>' ;
break ;
case 'application/xmlExcel.Sheet' : // Excel 2003
$format = '/' . preg_quote ( '<Data ss:Type="String">' , '/' ) . '..(' . implode ( '|' , $names ) . ')..' . preg_quote ( '</Data>' , '/' ) . '/' ;
2012-02-13 20:23:17 +01:00
$replacement = '<Data ss:Type="DateTime">\$\$$1\$\$</Data>' ;
2012-02-13 19:59:00 +01:00
2012-02-25 00:29:58 +01:00
break ;
case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' :
2012-02-13 19:59:00 +01:00
break ;
}
if ( $format && $names )
{
// Dealing with backtrack limit per AmigoJack 10-Jul-2010 comment on php.net preg-replace docs
$increase = 0 ;
while ( $increase < 10 ) {
$result = preg_replace ( $format , $replacement , $content , - 1 , $count );
if ( preg_last_error () == PREG_BACKTRACK_LIMIT_ERROR ) { // Only check on backtrack limit failure
ini_set ( 'pcre.backtrack_limit' , ( int ) ini_get ( 'pcre.backtrack_limit' ) + 10000 ); // Get current limit and increase
$increase ++ ; // Do not overkill the server
} else { // No fail
$content = $result ; // On failure $result would be NULL
break ; // Exit loop
}
}
if ( $increase == 10 ) {
error_log ( 'Backtrack limit exceeded @ ' . ini_get ( 'pcre.backtrack_limit' ) . ', some cells left as text.' );
}
}
}
2012-04-02 23:25:42 +02:00
/**
* Expand link_to custom fields with the merge replacements from the app
* but only if the template uses them .
*/
public function cf_link_to_expand ( $values , $content , & $replacements , $app = null )
{
if ( $app == null )
{
$app = str_replace ( '_merge' , '' , get_class ( $this ));
}
$cfs = config :: get_customfields ( $app );
// Cache, in case more than one sub-placeholder is used
$app_replacements = array ();
// Custom field placeholders look like {{#name}}
// Placeholders that need expanded will look like {{#name/placeholder}}
2012-04-11 22:03:11 +02:00
preg_match_all ( '/\${2}(([^\/#]*?\/)?)#([^$\/]+)\/(.*?)[$}]{2}/' , $content , $matches );
2012-04-02 23:25:42 +02:00
list ( $placeholders , $prefixes , $pre , $cf , $sub ) = $matches ;
foreach ( $cf as $index => $field )
{
if ( $cfs [ $field ])
{
// Get replacements for that application
$field_app = $cfs [ $field ][ 'type' ];
if ( ! $app_replacements [ $field_app ])
{
$classname = " { $field_app } _merge " ;
$class = new $classname ();
2012-04-18 18:21:24 +02:00
// If we send the real content, it can result in infinite loop of lookups
// This means you can't do {{#other_app/#other_app_cf/n_fn}}
$content = '' ;
$app_replacements [ $field_app ] = $class -> get_replacements ( $values [ '#' . $field ], $content );
2012-04-02 23:25:42 +02:00
}
$replacements [ $placeholders [ $index ]] = $app_replacements [ $field_app ][ '$$' . $sub [ $index ] . '$$' ];
}
}
}
2011-02-23 22:01:37 +01:00
/**
* Process special flags , such as IF or NELF
2011-03-18 16:14:55 +01:00
*
2011-02-23 22:01:37 +01:00
* @ param content Text to be examined and changed
* @ param replacements array of markers => replacement
*
* @ return changed content
*/
private function process_commands ( $content , $replacements )
{
if ( strpos ( $content , '$$IF' ))
{ //Example use to use: $$IF n_prefix~Herr~Sehr geehrter~Sehr geehrte$$
$this -> replacements =& $replacements ;
$content = preg_replace_callback ( '/\$\$IF ([0-9a-z_\/-]+)~(.*)~(.*)~(.*)\$\$/imU' , Array ( $this , 'replace_callback' ), $content );
unset ( $this -> replacements );
}
if ( strpos ( $content , '$$NELF' ))
{ //Example: $$NEPBR org_unit$$ sets a LF and value of org_unit, only if there is a value
$this -> replacements =& $replacements ;
$content = preg_replace_callback ( '/\$\$NELF ([0-9a-z_\/-]+)\$\$/imU' , Array ( $this , 'replace_callback' ), $content );
unset ( $this -> replacements );
}
if ( strpos ( $content , '$$NENVLF' ))
{ //Example: $$NEPBRNV org_unit$$ sets only a LF if there is a value for org_units, but did not add any value
$this -> replacements =& $replacements ;
$content = preg_replace_callback ( '/\$\$NENVLF ([0-9a-z_\/-]+)\$\$/imU' , Array ( $this , 'replace_callback' ), $content );
unset ( $this -> replacements );
}
if ( strpos ( $content , '$$LETTERPREFIX$$' ))
{ //Example use to use: $$LETTERPREFIX$$
$LETTERPREFIXCUSTOM = '$$LETTERPREFIXCUSTOM n_prefix title n_family$$' ;
$content = str_replace ( '$$LETTERPREFIX$$' , $LETTERPREFIXCUSTOM , $content );
}
if ( strpos ( $content , '$$LETTERPREFIXCUSTOM' ))
{ //Example use to use for a custom Letter Prefix: $$LETTERPREFIX n_prefix title n_family$$
$this -> replacements =& $replacements ;
$content = preg_replace_callback ( '/\$\$LETTERPREFIXCUSTOM ([0-9a-z_-]+)(.*)\$\$/imU' , Array ( $this , 'replace_callback' ), $content );
unset ( $this -> replacements );
}
return $content ;
}
2009-08-26 19:12:43 +02:00
/**
* Callback for preg_replace to process $$IF
*
* @ param array $param
* @ return string
*/
private function replace_callback ( $param )
{
if ( array_key_exists ( '$$' . $param [ 4 ] . '$$' , $this -> replacements )) $param [ 4 ] = $this -> replacements [ '$$' . $param [ 4 ] . '$$' ];
if ( array_key_exists ( '$$' . $param [ 3 ] . '$$' , $this -> replacements )) $param [ 3 ] = $this -> replacements [ '$$' . $param [ 3 ] . '$$' ];
$replace = preg_match ( '/' . $param [ 2 ] . '/' , $this -> replacements [ '$$' . $param [ 1 ] . '$$' ]) ? $param [ 3 ] : $param [ 4 ];
2011-03-28 22:36:15 +02:00
switch ( $this -> mimetype )
{
case 'application/vnd.oasis.opendocument.text' : // open office
case 'application/vnd.oasis.opendocument.spreadsheet' :
case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' : // ms office 2007
case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' :
case 'application/xml' :
case 'text/xml' :
case 'text/html' :
$is_xml = true ;
break ;
}
2009-10-01 21:37:16 +02:00
switch ( $this -> mimetype )
{
case 'application/rtf' :
case 'text/rtf' :
$LF = '}\par \pard\plain{' ;
break ;
case 'application/vnd.oasis.opendocument.text' :
case 'application/vnd.oasis.opendocument.spreadsheet' :
2011-03-03 19:22:49 +01:00
$LF = '<text:line-break/>' ;
2009-10-01 21:37:16 +02:00
break ;
2011-03-10 17:50:40 +01:00
case 'application/xmlExcel.Sheet' : // Excel 2003
$LF = ' ' ;
break ;
2009-10-01 21:37:16 +02:00
case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' :
case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' :
2011-07-29 17:34:12 +02:00
$LF = '</w:t></w:r></w:p><w:p><w:r><w:t>' ;
2009-10-01 21:37:16 +02:00
break ;
2011-03-10 18:23:28 +01:00
case 'application/xml' ;
$LF = '</w:t></w:r><w:r><w:br w:type="text-wrapping" w:clear="all"/></w:r><w:r><w:t>' ;
break ;
default :
$LF = " \n " ;
2009-10-01 21:37:16 +02:00
}
2011-03-28 22:36:15 +02:00
if ( $is_xml ) {
$this -> replacements = str_replace ( array ( '&' , '<' , '>' , " \r " , " \n " ), array ( '&' , '<' , '>' , '' , $LF ), $this -> replacements );
}
2009-09-23 15:00:59 +02:00
if ( strpos ( $param [ 0 ], '$$NELF' ) === 0 )
{ //sets a Pagebreak and value, only if the field has a value
if ( $this -> replacements [ '$$' . $param [ 1 ] . '$$' ] != '' ) $replace = $LF . $this -> replacements [ '$$' . $param [ 1 ] . '$$' ];
}
if ( strpos ( $param [ 0 ], '$$NENVLF' ) === 0 )
{ //sets a Pagebreak without any value, only if the field has a value
if ( $this -> replacements [ '$$' . $param [ 1 ] . '$$' ] != '' ) $replace = $LF ;
}
2009-11-27 17:37:54 +01:00
if ( strpos ( $param [ 0 ], '$$LETTERPREFIXCUSTOM' ) === 0 )
{ //sets a Letterprefix
2011-11-18 14:03:12 +01:00
$replaceprefixsort = array ();
2009-12-28 00:51:25 +01:00
// ToDo Stefan: $contentstart is NOT defined here!!!
2011-11-18 14:03:12 +01:00
$replaceprefix = explode ( ' ' , substr ( $param [ 0 ], 21 , - 2 ));
2009-11-27 17:37:54 +01:00
foreach ( $replaceprefix as $key => $nameprefix )
{
if ( $this -> replacements [ '$$' . $nameprefix . '$$' ] != '' ) $replaceprefixsort [] = $this -> replacements [ '$$' . $nameprefix . '$$' ];
}
$replace = implode ( $replaceprefixsort , ' ' );
}
2009-08-26 19:12:43 +02:00
return $replace ;
}
/**
* Download document merged with contact ( s )
*
* @ param string $document vfs - path of document
* @ param array $ids array with contact id ( s )
2011-04-08 21:32:17 +02:00
* @ param string $name = '' name to use for downloaded document
2011-06-14 12:48:15 +02:00
* @ param string $dirs comma or whitespace separated directories , used if $document is a relative path
2009-08-26 19:12:43 +02:00
* @ return string with error - message on error , otherwise it does NOT return
*/
2011-06-14 12:48:15 +02:00
public function download ( $document , $ids , $name = '' , $dirs = '' )
2009-08-26 19:12:43 +02:00
{
2011-06-30 11:47:29 +02:00
//error_log(__METHOD__."('$document', ".array2string($ids).", '$name', dirs='$dirs') ->".function_backtrace());
2011-06-14 12:48:15 +02:00
if (( $error = $this -> check_document ( $document , $dirs )))
{
return $error ;
}
2009-08-26 19:12:43 +02:00
$content_url = egw_vfs :: PREFIX . $document ;
2009-10-09 13:22:23 +02:00
switch (( $mimetype = egw_vfs :: mime_content_type ( $document )))
2009-08-26 19:12:43 +02:00
{
2011-06-30 11:47:29 +02:00
case 'message/rfc822' :
//error_log(__METHOD__."('$document', ".array2string($ids).", '$name', dirs='$dirs')=>$content_url ->".function_backtrace());
$bofelamimail = felamimail_bo :: getInstance ();
$bofelamimail -> openConnection ();
try
{
$msgs = $bofelamimail -> importMessageToMergeAndSend ( $this , $content_url , $ids , $_folder = '' , $importID = '' );
}
catch ( egw_exception_wrong_userinput $e )
{
// if this returns with an exeption, something failed big time
return $e -> getMessage ();
}
//error_log(__METHOD__.__LINE__.' Message after importMessageToMergeAndSend:'.array2string($msgs));
$retString = '' ;
if ( count ( $msgs [ 'success' ]) > 0 ) $retString .= count ( $msgs [ 'success' ]) . ' ' . lang ( 'Message(s) send ok.' ); //implode('<br />',$msgs['success']);
//if (strlen($retString)>0) $retString .= '<br />';
2011-06-30 19:35:52 +02:00
if ( count ( $msgs [ 'failed' ]) > 0 ) $retString .= count ( $msgs [ 'failed' ]) . ' ' . lang ( 'Message(s) send failed!' ) . '=>' . implode ( ', ' , $msgs [ 'failed' ]);
2011-06-30 11:47:29 +02:00
return $retString ;
break ;
2009-08-26 19:12:43 +02:00
case 'application/vnd.oasis.opendocument.text' :
2009-09-01 12:25:36 +02:00
case 'application/vnd.oasis.opendocument.spreadsheet' :
2009-10-09 13:22:23 +02:00
$ext = $mimetype == 'application/vnd.oasis.opendocument.text' ? '.odt' : '.ods' ;
2009-09-01 12:25:36 +02:00
$archive = tempnam ( $GLOBALS [ 'egw_info' ][ 'server' ][ 'temp_dir' ], basename ( $document , $ext ) . '-' ) . $ext ;
2009-08-26 19:12:43 +02:00
copy ( $content_url , $archive );
$content_url = 'zip://' . $archive . '#' . ( $content_file = 'content.xml' );
break ;
case 'application/vnd.openxmlformats-officedocument.wordprocessingml.d' : // mimetypes in vfs are limited to 64 chars
2009-10-09 13:22:23 +02:00
$mimetype = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' ;
2009-08-26 19:12:43 +02:00
case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' :
2009-10-09 16:39:55 +02:00
$archive = tempnam ( $GLOBALS [ 'egw_info' ][ 'server' ][ 'temp_dir' ], basename ( $document , '.docx' ) . '-' ) . '.docx' ;
2009-08-26 19:12:43 +02:00
copy ( $content_url , $archive );
$content_url = 'zip://' . $archive . '#' . ( $content_file = 'word/document.xml' );
$fix = array ( // regular expression to fix garbled placeholders
'/' . preg_quote ( '$$</w:t></w:r><w:proofErr w:type="spellStart"/><w:r><w:t>' , '/' ) . '([a-z0-9_]+)' .
preg_quote ( '</w:t></w:r><w:proofErr w:type="spellEnd"/><w:r><w:t>' , '/' ) . '/i' => '$$\\1$$' ,
'/' . preg_quote ( '$$</w:t></w:r><w:proofErr w:type="spellStart"/><w:r><w:rPr><w:lang w:val="' , '/' ) .
'([a-z]{2}-[A-Z]{2})' . preg_quote ( '"/></w:rPr><w:t>' , '/' ) . '([a-z0-9_]+)' .
preg_quote ( '</w:t></w:r><w:proofErr w:type="spellEnd"/><w:r><w:rPr><w:lang w:val="' , '/' ) .
'([a-z]{2}-[A-Z]{2})' . preg_quote ( '"/></w:rPr><w:t>$$' , '/' ) . '/i' => '$$\\2$$' ,
2009-09-13 14:06:40 +02:00
'/' . preg_quote ( '$</w:t></w:r><w:proofErr w:type="spellStart"/><w:r><w:t>' , '/' ) . '([a-z0-9_]+)' .
preg_quote ( '</w:t></w:r><w:proofErr w:type="spellEnd"/><w:r><w:t>' , '/' ) . '/i' => '$\\1$' ,
'/' . preg_quote ( '$ $</w:t></w:r><w:proofErr w:type="spellStart"/><w:r><w:t>' , '/' ) . '([a-z0-9_]+)' .
preg_quote ( '</w:t></w:r><w:proofErr w:type="spellEnd"/><w:r><w:t>' , '/' ) . '/i' => '$ $\\1$ $' ,
2009-08-26 19:12:43 +02:00
);
break ;
2009-12-28 00:51:25 +01:00
case 'application/xml' :
$fix = array ( // hack to get Excel 2003 to display additional rows in tables
'/ss:ExpandedRowCount="\d+"/' => 'ss:ExpandedRowCount="9999"' ,
);
break ;
2009-09-01 16:41:48 +02:00
case 'application/vnd.openxmlformats-officedocument.spreadsheetml.shee' :
2009-10-09 13:22:23 +02:00
$mimetype = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ;
2009-09-01 16:41:48 +02:00
case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' :
2009-12-28 00:51:25 +01:00
$fix = array ( // hack to get Excel 2007 to display additional rows in tables
'/ss:ExpandedRowCount="\d+"/' => 'ss:ExpandedRowCount="9999"' ,
);
2009-09-01 16:41:48 +02:00
$archive = tempnam ( $GLOBALS [ 'egw_info' ][ 'server' ][ 'temp_dir' ], basename ( $document , '.xlsx' ) . '-' ) . '.xlsx' ;
copy ( $content_url , $archive );
$content_url = 'zip://' . $archive . '#' . ( $content_file = 'xl/sharedStrings.xml' );
break ;
2009-08-26 19:12:43 +02:00
}
2011-01-27 18:08:40 +01:00
if ( ! ( $merged =& $this -> merge ( $content_url , $ids , $err , $mimetype , $fix )))
2009-08-26 19:12:43 +02:00
{
2011-06-16 13:36:15 +02:00
//error_log(__METHOD__."() !this->merge() err=$err");
2009-08-26 19:12:43 +02:00
return $err ;
}
2011-08-02 20:41:18 +02:00
// Apply HTML formatting to target document, if possible
2011-09-12 12:51:21 +02:00
// check if we can use the XSL extension, to not give a fatal error and rendering whole merge-print non-functional
2012-02-01 01:03:31 +01:00
if ( class_exists ( XSLTProcessor ) && class_exists ( DOMDocument ) && $this -> parse_html_styles )
2011-09-12 12:51:21 +02:00
{
2012-02-01 01:03:31 +01:00
try
{
$this -> apply_styles ( $merged , $mimetype );
}
catch ( Exception $e )
{
// Error converting HTML styles over
error_log ( $e -> getMessage ());
error_log ( " Target document: $content_url , IDs: " . array2string ( $ids ));
// Try again, but strip HTML so user gets something
$this -> parse_html_styles = false ;
if ( ! ( $merged =& $this -> merge ( $content_url , $ids , $err , $mimetype , $fix )))
{
return $err ;
}
}
2011-09-12 12:51:21 +02:00
}
2011-04-08 21:32:17 +02:00
if ( ! empty ( $name ))
{
if ( empty ( $ext ))
{
$ext = '.' . pathinfo ( $document , PATHINFO_EXTENSION );
}
$name .= $ext ;
}
else
{
$name = basename ( $document );
}
2009-08-26 19:12:43 +02:00
if ( isset ( $archive ))
{
$zip = new ZipArchive ;
2011-03-18 16:14:55 +01:00
if ( $zip -> open ( $archive , ZIPARCHIVE :: CHECKCONS ) !== true )
2011-01-13 11:57:20 +01:00
{
error_log ( __METHOD__ . __LINE__ . " !ZipArchive::open(' $archive ',ZIPARCHIVE::CHECKCONS) failed. Trying open without validating " );
if ( $zip -> open ( $archive ) !== true ) throw new Exception ( " !ZipArchive::open(' $archive ',|ZIPARCHIVE::CHECKCONS) " );
}
2009-08-26 19:12:43 +02:00
if ( $zip -> addFromString ( $content_file , $merged ) !== true ) throw new Exception ( " !ZipArchive::addFromString(' $content_file ', \$ merged) " );
if ( $zip -> close () !== true ) throw new Exception ( " !ZipArchive::close() " );
unset ( $zip );
unset ( $merged );
2009-10-09 16:39:55 +02:00
if ( substr ( $mimetype , 0 , 35 ) == 'application/vnd.oasis.opendocument.' && // only open office archives need that, ms word files brake
file_exists ( '/usr/bin/zip' ) && version_compare ( PHP_VERSION , '5.3.1' , '<' )) // fix broken zip archives generated by current php
2009-08-26 19:12:43 +02:00
{
exec ( '/usr/bin/zip -F ' . escapeshellarg ( $archive ));
}
2011-04-08 21:32:17 +02:00
html :: content_header ( $name , $mimetype , filesize ( $archive ));
2009-08-26 19:12:43 +02:00
readfile ( $archive , 'r' );
}
else
{
2009-11-14 17:08:26 +01:00
if ( $mimetype == 'application/xml' )
2009-11-03 12:55:12 +01:00
{
2009-11-14 17:08:26 +01:00
if ( strpos ( $merged , '<?mso-application progid="Word.Document"?>' ) !== false )
{
$mimetype = 'application/msword' ; // to open it automatically in word or oowriter
}
elseif ( strpos ( $merged , '<?mso-application progid="Excel.Sheet"?>' ) !== false )
{
$mimetype = 'application/vnd.ms-excel' ; // to open it automatically in excel or oocalc
}
2009-11-03 12:55:12 +01:00
}
2011-04-08 21:32:17 +02:00
ExecMethod2 ( 'phpgwapi.browser.content_header' , $name , $mimetype );
2009-08-26 19:12:43 +02:00
echo $merged ;
}
common :: egw_exit ();
}
2011-02-04 21:04:17 +01:00
/**
* Get a list of document actions / files from the given directory
*
2011-06-16 13:36:15 +02:00
* @ param string $dirs Directory ( s comma or space separated ) to search
2011-06-17 16:07:51 +02:00
* @ param string $prefix = 'document_' prefix for array keys
2011-06-17 17:16:40 +02:00
* @ param array | string $mime_filter = null allowed mime type ( s ), default all , negative filter if $mime_filter [ 0 ] === '!'
2011-06-17 16:07:51 +02:00
* @ return array List of documents , suitable for a selectbox . The key is document_ < filename >.
2011-02-04 21:04:17 +01:00
*/
2011-09-15 16:46:56 +02:00
public static function get_documents ( $dirs , $prefix = 'document_' , $mime_filter = null , $app = '' )
2011-04-16 23:45:13 +02:00
{
2011-09-16 15:03:46 +02:00
$export_limit = self :: getExportLimit ( $app );
2011-09-15 16:46:56 +02:00
if ( ! $dirs || ( ! self :: hasExportLimit ( $export_limit , 'ISALLOWED' ) && ! self :: is_export_limit_excepted ())) return array ();
2011-02-04 21:04:17 +01:00
2011-06-14 10:54:42 +02:00
// split multiple comma or whitespace separated directories
// to still allow space or comma in dirnames, we also use the trailing slash of all pathes to split
2011-06-16 13:36:15 +02:00
if ( count ( $dirs = preg_split ( '/[,\s]+\//' , $dirs )) > 1 )
2011-06-14 10:54:42 +02:00
{
2011-06-16 13:36:15 +02:00
foreach ( $dirs as $n => & $d ) if ( $n ) $d = '/' . $d ; // re-adding trailing slash removed by split
2011-06-14 10:54:42 +02:00
}
2011-06-17 17:16:40 +02:00
if ( $mime_filter && ( $negativ_filter = $mime_filter [ 0 ] === '!' ))
{
if ( is_array ( $mime_filter ))
{
unset ( $mime_filter [ 0 ]);
}
else
{
$mime_filter = substr ( $mime_filter , 1 );
}
}
2011-02-04 21:04:17 +01:00
$list = array ();
2011-06-16 13:36:15 +02:00
foreach ( $dirs as $dir )
2011-02-04 21:04:17 +01:00
{
2011-06-16 13:36:15 +02:00
if (( $files = egw_vfs :: find ( $dir , array ( 'need_mime' => true ), true )))
2011-02-04 21:04:17 +01:00
{
2011-06-16 13:36:15 +02:00
foreach ( $files as $file )
{
// return only the mime-types we support
2012-03-04 14:33:10 +01:00
$parts = explode ( '.' , $file [ 'name' ]);
if ( ! self :: is_implemented ( $file [ 'mime' ], '.' . array_pop ( $parts ))) continue ;
2011-06-17 17:16:40 +02:00
if ( $mime_filter && $negativ_filter === in_array ( $file [ 'mime' ], ( array ) $mime_filter )) continue ;
2011-06-16 13:36:15 +02:00
$list [ $prefix . $file [ 'name' ]] = egw_vfs :: decodePath ( $file [ 'name' ]);
}
2011-02-04 21:04:17 +01:00
}
}
return $list ;
}
2011-02-22 20:46:09 +01:00
2011-04-16 23:45:13 +02:00
/**
2011-04-25 13:30:37 +02:00
* From this number of documents , show them in submenus by mime type
*/
const SHOW_DOCS_BY_MIME_LIMIT = 10 ;
/**
* Get insert - in - document action with optional default document on top
*
2011-09-14 12:27:53 +02:00
* If more than SHOW_DOCS_BY_MIME_LIMIT = 10 documents found , they are displayed in submenus by mime type .
2011-04-16 23:45:13 +02:00
*
2011-06-16 13:36:15 +02:00
* @ param string $dirs Directory ( s comma or space separated ) to search
2011-04-16 23:45:13 +02:00
* @ param int $group see nextmatch_widget :: egw_actions
* @ param string $caption = 'Insert in document'
2011-04-25 13:30:37 +02:00
* @ param string $prefix = 'document_'
* @ param string $default_doc = '' full path to default document to show on top with action == 'document' !
2011-06-16 13:36:15 +02:00
* @ param int | string $export_limit = null export - limit , default $GLOBALS [ 'egw_info' ][ 'server' ][ 'export_limit' ]
2011-04-16 23:45:13 +02:00
* @ return array see nextmatch_widget :: egw_actions
*/
2011-06-16 13:36:15 +02:00
public static function document_action ( $dirs , $group = 0 , $caption = 'Insert in document' , $prefix = 'document_' , $default_doc = '' ,
$export_limit = null )
2011-04-16 23:45:13 +02:00
{
2011-04-25 13:30:37 +02:00
$documents = array ();
2011-09-16 15:03:46 +02:00
if ( $export_limit == null ) $export_limit = self :: getExportLimit (); // check if there is a globalsetting
2011-04-25 13:30:37 +02:00
if ( $default_doc && ( $file = egw_vfs :: stat ( $default_doc ))) // put default document on top
{
$documents [ 'document' ] = array (
'icon' => egw_vfs :: mime_icon ( $file [ 'mime' ]),
'caption' => egw_vfs :: decodePath ( egw_vfs :: basename ( $default_doc )),
'group' => 1 ,
);
}
2011-06-16 13:36:15 +02:00
$files = array ();
if ( $dirs )
2011-06-14 10:54:42 +02:00
{
2011-06-16 13:36:15 +02:00
// split multiple comma or whitespace separated directories
// to still allow space or comma in dirnames, we also use the trailing slash of all pathes to split
if ( count ( $dirs = preg_split ( '/[,\s]+\//' , $dirs )) > 1 )
{
foreach ( $dirs as $n => & $d ) if ( $n ) $d = '/' . $d ; // re-adding trailing slash removed by split
}
foreach ( $dirs as $dir )
{
$files += egw_vfs :: find ( $dir , array (
'need_mime' => true ,
'order' => 'fs_name' ,
'sort' => 'ASC' ,
), true );
}
2011-06-14 10:54:42 +02:00
}
2012-05-02 21:59:15 +02:00
$dircount = array ();
2011-06-16 13:36:15 +02:00
foreach ( $files as $key => $file )
2011-04-25 13:30:37 +02:00
{
2011-06-16 13:36:15 +02:00
// use only the mime-types we support
2012-03-04 14:33:10 +01:00
$parts = explode ( '.' , $file [ 'name' ]);
if ( ! self :: is_implemented ( $file [ 'mime' ], '.' . array_pop ( $parts )) ||
2011-06-16 13:36:15 +02:00
! egw_vfs :: check_access ( $file [ 'path' ], egw_vfs :: READABLE , $file ) || // remove files not readable by user
$file [ 'path' ] === $default_doc ) // default doc already added
2011-04-25 13:30:37 +02:00
{
2011-06-16 13:36:15 +02:00
unset ( $files [ $key ]);
2011-04-25 13:30:37 +02:00
}
2012-05-02 21:59:15 +02:00
else
{
$dirname = egw_vfs :: dirname ( $file [ 'path' ]);
if ( ! isset ( $dircount [ $dirname ]))
{
$dircount [ $dirname ] = 1 ;
}
else
{
$dircount [ $dirname ] ++ ;
}
}
2011-06-16 13:36:15 +02:00
}
foreach ( $files as $file )
{
2012-05-02 21:59:15 +02:00
if ( count ( $dircount ) > 1 )
{
$name_arr = explode ( '/' , $file [ 'name' ]);
$current_level = & $documents ;
for ( $count = 0 ; $count < count ( $name_arr ); $count ++ )
{
if ( $count == 0 )
{
$current_level = & $documents ;
}
else
{
$current_level = & $current_level [ $prefix . $name_arr [( $count - 1 )]][ 'children' ];
}
switch ( $count )
{
case ( count ( $name_arr ) - 1 ) :
$current_level [ $prefix . $file [ 'name' ]] = array (
'icon' => egw_vfs :: mime_icon ( $file [ 'mime' ]),
'caption' => egw_vfs :: decodePath ( $name_arr [ $count ]),
'group' => 2 ,
);
if ( $file [ 'mime' ] == 'message/rfc822' )
{
// does not work on children for some reason, now handled in felamimail_bo::importMessageToMergeAndSend
//$documents[$file['mime']]['allowOnMultiple'] = $GLOBALS['egw_info']['flags']['currentapp'] == 'addressbook';
// only need confirmation for multiple receipients for addressbook, as for others we can't do it anyway
if ( $GLOBALS [ 'egw_info' ][ 'flags' ][ 'currentapp' ] == 'addressbook' ) $current_level [ $prefix . $file [ 'name' ]][ 'confirm_multiple' ] = lang ( 'Do you want to send the message to all selected entries, WITHOUT further editing?' );
}
break ;
default :
if ( ! is_array ( $current_level [ $prefix . $name_arr [ $count ]]))
{
// create parent folder
$current_level [ $prefix . $name_arr [ $count ]] = array (
'icon' => 'phpgwapi/foldertree_folder' ,
'caption' => egw_vfs :: decodePath ( $name_arr [ $count ]),
'group' => 2 ,
'children' => array (),
);
}
break ;
}
}
}
else if ( count ( $files ) >= self :: SHOW_DOCS_BY_MIME_LIMIT )
2011-04-25 13:30:37 +02:00
{
2011-06-16 13:36:15 +02:00
if ( ! isset ( $documents [ $file [ 'mime' ]]))
2011-04-25 13:30:37 +02:00
{
2011-06-16 13:36:15 +02:00
$documents [ $file [ 'mime' ]] = array (
2011-04-25 13:30:37 +02:00
'icon' => egw_vfs :: mime_icon ( $file [ 'mime' ]),
2011-06-16 13:36:15 +02:00
'caption' => mime_magic :: mime2label ( $file [ 'mime' ]),
2011-04-25 13:30:37 +02:00
'group' => 2 ,
2011-06-16 13:36:15 +02:00
'children' => array (),
2011-04-25 13:30:37 +02:00
);
2011-06-30 19:35:52 +02:00
if ( $file [ 'mime' ] == 'message/rfc822' )
{
// does not work on children for some reason, now handled in felamimail_bo::importMessageToMergeAndSend
//$documents[$file['mime']]['allowOnMultiple'] = $GLOBALS['egw_info']['flags']['currentapp'] == 'addressbook';
// only need confirmation for multiple receipients for addressbook, as for others we can't do it anyway
if ( $GLOBALS [ 'egw_info' ][ 'flags' ][ 'currentapp' ] == 'addressbook' ) $documents [ $file [ 'mime' ]][ 'confirm_multiple' ] = lang ( 'Do you want to send the message to all selected entries, WITHOUT further editing?' );
}
2011-04-25 13:30:37 +02:00
}
2011-06-16 13:36:15 +02:00
$documents [ $file [ 'mime' ]][ 'children' ][ $prefix . $file [ 'name' ]] = egw_vfs :: decodePath ( $file [ 'name' ]);
}
else
{
$documents [ $prefix . $file [ 'name' ]] = array (
'icon' => egw_vfs :: mime_icon ( $file [ 'mime' ]),
'caption' => egw_vfs :: decodePath ( $file [ 'name' ]),
'group' => 2 ,
);
2011-06-30 19:35:52 +02:00
if ( $file [ 'mime' ] == 'message/rfc822' )
{
// does not work on children for some reason, now handled in felamimail_bo::importMessageToMergeAndSend
//$documents[$file['mime']]['allowOnMultiple'] = $GLOBALS['egw_info']['flags']['currentapp'] == 'addressbook';
// only need confirmation for multiple receipients for addressbook, as for others we can't do it anyway
if ( $GLOBALS [ 'egw_info' ][ 'flags' ][ 'currentapp' ] == 'addressbook' ) $documents [ $prefix . $file [ 'name' ]][ 'confirm_multiple' ] = lang ( 'Do you want to send the message to all selected entries, WITHOUT further editing?' );
}
2011-04-25 13:30:37 +02:00
}
}
2012-05-02 21:59:15 +02:00
2011-04-16 23:45:13 +02:00
return array (
'icon' => 'etemplate/merge' ,
'caption' => $caption ,
'children' => $documents ,
2011-06-16 13:36:15 +02:00
// disable action if no document or export completly forbidden for non-admins
2011-09-13 11:05:33 +02:00
'enabled' => ( boolean ) $documents && ( self :: hasExportLimit ( $export_limit , 'ISALLOWED' ) || self :: is_export_limit_excepted ()),
2011-06-16 13:36:15 +02:00
'hideOnDisabled' => true , // do not show 'Insert in document', if no documents defined or no export allowed
2011-04-16 23:45:13 +02:00
'group' => $group ,
);
}
2011-06-14 12:48:15 +02:00
/**
* Check if given document ( relative path from document_actions ()) exists in one of the given dirs
*
* @ param string & $document maybe relative path of document , on return true absolute path to existing document
* @ param string $dirs comma or whitespace separated directories
* @ return string | boolean false if document exists , otherwise string with error - message
*/
public static function check_document ( & $document , $dirs )
{
if ( $document [ 0 ] !== '/' )
{
// split multiple comma or whitespace separated directories
// to still allow space or comma in dirnames, we also use the trailing slash of all pathes to split
if ( $dirs && ( $dirs = preg_split ( '/[,\s]+\//' , $dirs )))
{
foreach ( $dirs as $n => $dir )
{
if ( $n ) $dir = '/' . $dir ; // re-adding trailing slash removed by split
if ( egw_vfs :: stat ( $dir . '/' . $document ) && egw_vfs :: is_readable ( $dir . '/' . $document ))
{
$document = $dir . '/' . $document ;
return false ;
}
}
}
}
2011-06-17 15:08:10 +02:00
elseif ( egw_vfs :: stat ( $document ) && egw_vfs :: is_readable ( $document ))
{
return false ;
}
//error_log(__METHOD__."('$document', dirs='$dirs') returning 'Document '$document' does not exist or is not readable for you!'");
2011-06-14 12:48:15 +02:00
return lang ( " Document '%1' does not exist or is not readable for you! " , $document );
}
2011-02-22 20:46:09 +01:00
/**
* Get a list of supported extentions
*/
2011-04-16 23:45:13 +02:00
public static function get_file_extensions ()
{
2011-06-30 11:47:29 +02:00
return array ( 'txt' , 'rtf' , 'odt' , 'ods' , 'docx' , 'xml' , 'eml' );
2011-02-22 20:46:09 +01:00
}
2011-02-28 21:31:56 +01:00
/**
* Format a number according to user prefs with decimal and thousands separator
*
* Reimplemented from etemplate to NOT use user prefs for Excel 2003 , which gives an xml error
*
* @ param int | float | string $number
* @ param int $num_decimal_places = 2
* @ param string $_mimetype = ''
* @ return string
*/
static public function number_format ( $number , $num_decimal_places = 2 , $_mimetype = '' )
{
if (( string ) $number === '' ) return '' ;
//error_log(__METHOD__.$_mimetype);
switch ( $_mimetype )
{
case 'application/xml' : // Excel 2003
2011-03-15 16:31:21 +01:00
case 'application/vnd.oasis.opendocument.spreadsheet' : // OO.o spreadsheet
2011-02-28 21:31:56 +01:00
return number_format ( str_replace ( ' ' , '' , $number ), $num_decimal_places , '.' , '' );
}
return etemplate :: number_format ( $number , $num_decimal_places );
}
2009-08-26 19:12:43 +02:00
}