diff --git a/README.md b/README.md index fb5992a..04999be 100755 --- a/README.md +++ b/README.md @@ -23,9 +23,9 @@ What is special to OpenTBS: * Works with both PHP 4 and PHP 5. ## Versions included -TinyButStrong - 3.9.0 +TinyButStrong - 3.10.1 -OpenTBS - 1.9.4 +OpenTBS - 1.9.7 ## Requirements @@ -35,22 +35,12 @@ OpenTBS - 1.9.4 ## Installation -### Step 1: Download the bundle using composer - -Add the following in your composer.json: +### Setp 0: [Install Composer](https://getcomposer.org/doc/00-intro.md#installation-linux-unix-osx) -```json -{ - "require": { - "mbence/opentbs-bundle": "dev-master" - } -} -``` - -Then download / update by running the command: +### Step 1: Download the bundle using composer ``` bash -$ php composer.phar update mbence/opentbs-bundle +> composer require mbence/opentbs-bundle ``` Composer will install the bundle to your project's `vendor/mbence/opentbs-bundle` directory. diff --git a/composer.json b/composer.json index e2b100e..130e383 100755 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "name": "mbence/opentbs-bundle", "type": "symfony-bundle", - "description": "OpenTBS Bundle for Symfony2 - create OpenOffice and Ms Office documents with Symfony", + "description": "OpenTBS Bundle for Symfony - create OpenOffice and Ms Office documents with Symfony", "keywords": ["symfony","TBS","OpenTBS", "document", "OpenOffice", "MS Office", "odt", "docx", "xlsx", "pptx"], "homepage": "https://github.com/mbence/OpenTBSBundle", "license": "MIT", diff --git a/lib/tbs_class.php b/lib/tbs_class.php index 91932dd..3581e16 100755 --- a/lib/tbs_class.php +++ b/lib/tbs_class.php @@ -1,19 +1,22 @@ TinyButStrong Error (PHP Version Check) : Your PHP version is '.PHP_VERSION.' while TinyButStrong needs PHP version 5.0 or higher. You should try with TinyButStrong Edition for PHP 4.'; +/* COMPAT#1 */ // Render flags define('TBS_NOTHING', 0); @@ -128,6 +131,12 @@ public function DataPrepare(&$SrcId,&$TBS) { $this->Type = 11; } elseif ($SrcId instanceof Zend_Db_Adapter_Abstract) { $this->Type = 12; + } elseif ($SrcId instanceof SQLite3) { + $this->Type = 13; $this->SubType = 1; + } elseif ($SrcId instanceof SQLite3Stmt) { + $this->Type = 13; $this->SubType = 2; + } elseif ($SrcId instanceof SQLite3Result) { + $this->Type = 13; $this->SubType = 3; } elseif (is_object($SrcId)) { $FctInfo = get_class($SrcId); $FctCat = 'o'; @@ -193,10 +202,10 @@ public function DataOpen(&$Query,$QryPrms=false) { case 0: // Array if (($this->SubType===1) && (is_string($Query))) $this->SubType = 2; if ($this->SubType===0) { - $this->RecSet = &$this->SrcId; + $this->RecSet = &$this->SrcId; /* COMPAT#2 */ } elseif ($this->SubType===1) { if (is_array($Query)) { - $this->RecSet = &$Query; + $this->RecSet = &$Query; /* COMPAT#3 */ } else { $this->DataAlert('type \''.gettype($Query).'\' not supported for the Query Parameter going with \'array\' Source Type.'); } @@ -220,7 +229,7 @@ public function DataOpen(&$Query,$QryPrms=false) { } } else { if (isset($this->TBS->VarRef[$Item0])) { - $Var = &$this->TBS->VarRef[$Item0]; + $Var = &$this->TBS->VarRef[$Item0]; /* COMPAT#4 */ $i = 1; } else { $i = $this->DataAlert('invalid query \''.$Query.'\' because VarRef item \''.$Item0.'\' is not found.'); @@ -372,6 +381,41 @@ public function DataOpen(&$Query,$QryPrms=false) { $this->DataAlert('Zend_DB_Adapter error message when opening the query: '.$e->getMessage()); } break; + case 13: // SQLite3 + try { + if ($this->SubType==3) { + $this->RecSet = $this->SrcId; + } elseif (($this->SubType==1) && (!is_array($QryPrms))) { + // SQL statement without parameters + $this->RecSet = $this->SrcId->query($Query); + } else { + if ($this->SubType==2) { + $stmt = $this->SrcId; + $prms = $Query; + } else { + // SQL statement with parameters + $stmt = $this->SrcId->prepare($Query); + $prms = $QryPrms; + } + // bind parameters + if (is_array($prms)) { + foreach ($prms as $p => $v) { + if (is_numeric($p)) { + $p = $p + 1; + } + if (is_array($v)) { + $stmt->bindValue($p, $v[0], $v[1]); + } else { + $stmt->bindValue($p, $v); + } + } + } + $this->RecSet = $stmt->execute(); + } + } catch (Exception $e) { + $this->DataAlert('SQLite3 error message when opening the query: '.$e->getMessage()); + } + break; } if (($this->Type===0) || ($this->Type===9)) { @@ -460,7 +504,7 @@ public function DataFetch() { $this->CurrRec = $this->SrcId->tbsdb_fetch($this->RecSet,$this->RecNum+1); break; case 7: // PostgreSQL - $this->CurrRec = pg_fetch_assoc($this->RecSet); + $this->CurrRec = pg_fetch_assoc($this->RecSet); /* COMPAT#5 */ break; case 8: // SQLite $this->CurrRec = sqlite_fetch_array($this->RecSet,SQLITE_ASSOC); @@ -484,6 +528,9 @@ public function DataFetch() { case 12: // Zend_DB_Adapater $this->CurrRec = $this->RecSet->fetch(Zend_Db::FETCH_ASSOC); break; + case 13: // SQLite3 + $this->CurrRec = $this->RecSet->fetchArray(SQLITE3_ASSOC); + break; } // Set the row count @@ -510,6 +557,11 @@ public function DataClose() { case 5: $this->SrcId->tbsdb_close($this->RecSet); break; case 7: pg_free_result($this->RecSet); break; case 10: $this->RecSet->free(); break; // MySQLi + case 13: // SQLite3 + if ($this->SubType!=3) { + $this->RecSet->finalize(); + } + break; //case 11: $this->RecSet->closeCursor(); break; // PDO } if ($this->RecSaving) { @@ -536,7 +588,7 @@ class clsTinyButStrong { public $ExtendedMethods = array(); public $ErrCount = 0; // Undocumented (can change at any version) -public $Version = '3.9.0'; +public $Version = '3.10.1'; public $Charset = ''; public $TurboBlock = true; public $VarPrefix = ''; @@ -595,10 +647,11 @@ function __construct($Options=null,$VarPrefix='',$FctPrefix='') { if (is_array($Options)) $this->SetOption($Options); // Links to global variables (cannot be converted to static yet because of compatibility) - global $_TBS_FormatLst, $_TBS_UserFctLst, $_TBS_BlockAlias, $_TBS_AutoInstallPlugIns; - if (!isset($_TBS_FormatLst)) $_TBS_FormatLst = array(); - if (!isset($_TBS_UserFctLst)) $_TBS_UserFctLst = array(); - if (!isset($_TBS_BlockAlias)) $_TBS_BlockAlias = array(); + global $_TBS_FormatLst, $_TBS_UserFctLst, $_TBS_BlockAlias, $_TBS_AutoInstallPlugIns, $_TBS_ParallelLst; + if (!isset($_TBS_FormatLst)) $_TBS_FormatLst = array(); + if (!isset($_TBS_UserFctLst)) $_TBS_UserFctLst = array(); + if (!isset($_TBS_BlockAlias)) $_TBS_BlockAlias = array(); + if (!isset($_TBS_ParallelLst)) $_TBS_ParallelLst = array(); $this->_UserFctLst = &$_TBS_UserFctLst; // Auto-installing plug-ins @@ -658,7 +711,7 @@ function SetOption($o, $v=false, $d=false) { function GetOption($o) { if ($o==='all') { - $x = explode(',', 'var_prefix,fct_prefix,noerr,auto_merge,onload,onshow,att_delim,protect,turbo_block,charset,chr_open,chr_close,tpl_frms,block_alias,include_path,render'); + $x = explode(',', 'var_prefix,fct_prefix,noerr,auto_merge,onload,onshow,att_delim,protect,turbo_block,charset,chr_open,chr_close,tpl_frms,block_alias,parallel_conf,include_path,render'); $r = array(); foreach ($x as $o) $r[$o] = $this->GetOption($o); return $r; @@ -684,6 +737,8 @@ function GetOption($o) { if ($o==='include_path') return $this->IncludePath; if ($o==='render') return $this->Render; if ($o==='methods_allowed') return $this->MethodsAllowed; + if ($o==='parallel_conf') return $GLOBALS['_TBS_ParallelLst']; + if ($o==='block_alias') return $GLOBALS['_TBS_BlockAlias']; return $this->meth_Misc_Alert('with GetOption() method','option \''.$o.'\' is not supported.');; } @@ -744,7 +799,6 @@ public function GetBlockSource($BlockName,$AsArray=false,$DefTags=true,$ReplaceW $P1 = false; $Mode = ($DefTags) ? 3 : 2; $PosBeg1 = 0; - $PosEndPrec = false; while ($Loc = $this->meth_Locator_FindBlockNext($this->Source,$BlockName,$Pos,'.',$Mode,$P1,$FieldOutside)) { $Nbr++; $Sep = ''; @@ -1007,11 +1061,12 @@ function &meth_Locator_SectionNewBDef(&$LocR,$BlockName,$Txt,$PrmLst,$Cache) { $Chk = true; $LocLst = array(); - $LocNbr = 0; - + $Pos = 0; + $Sort = false; + if ($this->_PlugIns_Ok && isset($this->_piOnCacheField)) { $pi = true; - $ArgLst = array(0=>$BlockName, 1=>false, 2=>&$Txt, 3=>array('att'=>true)); + $ArgLst = array(0=>$BlockName, 1=>false, 2=>&$Txt, 3=>array('att'=>true), 4=>&$LocLst, 5=>&$Pos); } else { $pi = false; } @@ -1021,15 +1076,26 @@ function &meth_Locator_SectionNewBDef(&$LocR,$BlockName,$Txt,$PrmLst,$Cache) { if ($Cache) { $Chk = false; - $Pos = 0; - $PrevEnd = -1; - $PrevIsAMF = false; // AMF means Attribute Moved Forward while ($Loc = $this->meth_Locator_FindTbs($Txt,$BlockName,$Pos,'.')) { + + $LocNbr = 1 + count($LocLst); + $LocLst[$LocNbr] = &$Loc; - $IsAMF = false; - $IsAtt = false; - $NewIdx = false; - + // Next search position : always ("original PosBeg" + 1). + // Must be done here because loc can be moved by the plug-in. + if ($Loc->Enlarged) { + // Enlarged + $Pos = $Loc->PosBeg0 + 1; + $Loc->Enlarged = false; + } else { + // Normal + $Pos = $Loc->PosBeg + 1; + } + + // Note: the plug-in may move, delete and add one or several locs. + // Move : backward or forward (will be sorted) + // Delete : add property DelMe=true + // Add : at the end of $LocLst (will be sorted) if ($pi) { $ArgLst[1] = &$Loc; $this->meth_Plugin_RunAll($this->_piOnCacheField,$ArgLst); @@ -1042,76 +1108,42 @@ function &meth_Locator_SectionNewBDef(&$LocR,$BlockName,$Txt,$PrmLst,$Cache) { } else { $Loc->IsRecInfo = false; } - - if ($Loc->PosBeg>$PrevEnd) { // No embedding - if (isset($Loc->PrmLst['att'])) { - $LocSrc = substr($Txt,$Loc->PosBeg,$Loc->PosEnd-$Loc->PosBeg+1); - if ($this->f_Xml_AttFind($Txt,$Loc,true,$this->AttDelim)) { + + // Process parameter att for new added locators. + $NewNbr = count($LocLst); + for ($i=$LocNbr;$i<=$NewNbr;$i++) { + $li = &$LocLst[$i]; + if (isset($li->PrmLst['att'])) { + $LocSrc = substr($Txt,$li->PosBeg,$li->PosEnd-$li->PosBeg+1); // for error message + if ($this->f_Xml_AttFind($Txt,$li,$LocLst,$this->AttDelim)) { if (isset($Loc->PrmLst['atttrue'])) { - $Loc->PrmLst['magnet'] = '#'; - $Loc->PrmLst['ope'] = (isset($Loc->PrmLst['ope'])) ? $Loc->PrmLst['ope'].',attbool' : 'attbool'; + $li->PrmLst['magnet'] = '#'; + $li->PrmLst['ope'] = (isset($li->PrmLst['ope'])) ? $li->PrmLst['ope'].',attbool' : 'attbool'; } - $IsAtt = true; - if ($Loc->AttForward) { - $IsAMF = true; - } else { - if ($Loc->AttInsLen>0) { - for ($i=$LocNbr;$i>0;$i--) { - if ($LocLst[$i]->PosEnd>=$Loc->PosBeg) { - $NewIdx = $i; - $li = $LocLst[$i]; - $li->PosBeg += $Loc->AttInsLen; - $li->PosEnd += $Loc->AttInsLen; - $LocLst[$i+1] = $li; - } else { - $i = 0; - } - } - } + if ($i==$LocNbr) { + $Pos = $Loc->DelPos; } } else { $this->meth_Misc_Alert('','TBS is not able to merge the field '.$LocSrc.' because the entity targeted by parameter \'att\' cannot be found.'); } - unset($Loc->PrmLst['att']); - } - $LocNbr++; - } else { - // The previous tag is embedding => no increment, then previous Loc is overwrited - $Chk = true; - if ($PrevIsAMF) { - $l = &$LocLst[$LocNbr]; - $this->meth_Misc_Alert('','TBS is not able to merge the field '.$LocSrc.' because parameter \'att\' makes this fied moving forward over another TBS field.'); } } - $PrevIsAMF = false; - if ($IsAtt) { - $Pos = $Loc->PrevPosBeg; - if ($IsAMF) $PrevIsAMF = true; - } elseif ($Loc->Enlarged) { - $Pos = $Loc->PosBeg0+1; - $Loc->Enlarged = false; - } else { - $Pos = $Loc->PosBeg+1; - } - - if ($NewIdx===false) { - $LocLst[$LocNbr] = $Loc; - $PrevEnd = $Loc->PosEnd; - } else { - $LocLst[$NewIdx] = $Loc; - $PrevEnd = $LocLst[$LocNbr]->PosEnd; - } - + unset($Loc); + } + // Re-order loc + $e = self::f_Loc_Sort($LocLst, true, 1); + $Chk = ($e > 0); + } // Create the object $o = (object) null; $o->Prm = $PrmLst; $o->LocLst = $LocLst; - $o->LocNbr = $LocNbr; + $o->LocNbr = count($LocLst); $o->Name = $BlockName; $o->Src = $Txt; $o->Chk = $Chk; @@ -1386,7 +1418,7 @@ function meth_Locator_Replace(&$Txt,&$Loc,&$Value,$SubStart) { case 15: $CurrVal = ($Loc->OpeUtf8) ? mb_convert_case($CurrVal, MB_CASE_UPPER, 'UTF-8') : strtoupper($CurrVal); break; case 16: $CurrVal = ($Loc->OpeUtf8) ? mb_convert_case($CurrVal, MB_CASE_LOWER, 'UTF-8') : strtolower($CurrVal); break; case 17: $CurrVal = ucfirst($CurrVal); break; - case 18: $CurrVal = ($Loc->OpeUtf8) ? mb_convert_case($CurrVal, MB_CASE_TITLE, 'UTF-8') : ucwords($CurrVal); break; + case 18: $CurrVal = ($Loc->OpeUtf8) ? mb_convert_case($CurrVal, MB_CASE_TITLE, 'UTF-8') : ucwords(strtolower($CurrVal)); break; } } } @@ -1938,17 +1970,15 @@ function meth_Locator_FindBlockLst(&$Txt,$BlockName,$Pos,$SpePrm) { function meth_Locator_FindParallel(&$Txt, $ZoneBeg, $ZoneEnd, $ConfId) { // Define configurations - global $_TBS_ParallelLst, $_TBS_BlockAlias; - - if (!isset($_TBS_ParallelLst)) $_TBS_ParallelLst = array(); + global $_TBS_ParallelLst; if ( ($ConfId=='tbs:table') && (!isset($_TBS_ParallelLst['tbs:table'])) ) { $_TBS_ParallelLst['tbs:table'] = array( 'parent' => 'table', - 'ignore' => array('!--', 'caption', 'thead', 'thbody', 'thfoot'), + 'ignore' => array('!--', 'caption', 'thead', 'tbody', 'tfoot'), 'cols' => array(), - 'rows' => array('tr'), - 'cells' => array('td'=>'colspan', 'th'=>'colspan'), + 'rows' => array('tr', 'colgroup'), + 'cells' => array('td'=>'colspan', 'th'=>'colspan', 'col'=>'span'), ); } @@ -2349,7 +2379,7 @@ function meth_Merge_BlockSections(&$Txt,&$LocR,&$Src,&$RecSpe) { $piOMG = false; if ($LocR->FooterFound) $Src->PrevRec = (object) null; } - // Plug-ins + // Plug-ins $piOMS = false; if ($this->_PlugIns_Ok) { if (isset($this->_piBeforeMergeBlock)) { @@ -2714,7 +2744,7 @@ function meth_Merge_SectionNormal(&$BDef,&$Src) { } } - // Unchached locators + // Uncached locators if ($BDef->Chk) { $BlockName = &$BDef->Name; $Pos = 0; @@ -2723,7 +2753,7 @@ function meth_Merge_SectionNormal(&$BDef,&$Src) { } else { - // Chached locators + // Cached locators for ($i=$iMax;$i>0;$i--) { if ($LocLst[$i]->PosBeg<$PosMax) { if ($LocLst[$i]->IsRecInfo) { @@ -4074,6 +4104,7 @@ static function f_Loc_EnlargeToTag(&$Txt,&$Loc,$TagStr,$RetInnerSrc) { $i = 0; $TagFct = array(); $TagLst = array(); + $TagBnd = array(); while ($TagStr!=='') { // get next tag $p = strpos($TagStr, '+'); @@ -4084,7 +4115,8 @@ static function f_Loc_EnlargeToTag(&$Txt,&$Loc,$TagStr,$RetInnerSrc) { $t = substr($TagStr,0,$p); $TagStr = substr($TagStr,$p+1); } - do { // Check parentheses, relative position and single tag + // Check parentheses, relative position and single tag + do { $t = trim($t); $e = strlen($t) - 1; // pos of last char if (($e>1) && ($t[0]==='(') && ($t[$e]===')')) { @@ -4096,19 +4128,36 @@ static function f_Loc_EnlargeToTag(&$Txt,&$Loc,$TagStr,$RetInnerSrc) { $e = false; } } while ($e!==false); + // Check for multiples + $p = strpos($t, '*'); + if ($p!==false) { + $n = intval(substr($t, 0, $p)); + $t = substr($t, $p + 1); + $n = max($n ,1); // prevent for error: minimum valu is 1 + $TagStr = str_repeat($t . '+', $n-1) . $TagStr; + } + // Reference + if (($t==='.') && ($Ref===0)) $Ref = $i; + // Take of the (!) prefix + $b = ''; + if (($t!=='') && ($t[0]==='!')) { + $t = substr($t, 1); + $b = '!'; + } + // Alias + $a = false; if (isset($AliasLst[$t])) { - $a = $AliasLst[$t]; + $a = $AliasLst[$t]; // a string or a function if (is_string($a)) { if ($i>999) return false; // prevent from circular alias - $TagStr = ($TagStr==='') ? $a : $a.'+'.$TagStr; - } else { - $TagLst[$i] = $t; - $TagFct[$i] = $a; - $i++; + $TagStr = $b . $a . (($TagStr==='') ? '' : '+') . $TagStr; + $t = false; } - } else { - $TagLst[$i] = $t; - $TagFct[$i] = false; + } + if ($t!==false) { + $TagLst[$i] = $t; // with prefix ! if specified + $TagFct[$i] = $a; + $TagBnd[$i] = ($b===''); $i++; } } @@ -4119,18 +4168,30 @@ static function f_Loc_EnlargeToTag(&$Txt,&$Loc,$TagStr,$RetInnerSrc) { if ($LevelStop===0) $LevelStop = 1; // First tag of reference - $TagO = self::f_Loc_Enlarge_Find($Txt,$TagLst[$Ref],$TagFct[$Ref],$Loc->PosBeg-1,false,$LevelStop); - if ($TagO===false) return false; - $PosBeg = $TagO->PosBeg; - $LevelStop += -$TagO->RightLevel; // RightLevel=1 only if the tag is single and embeds $Loc, otherwise it is 0 - if ($LevelStop>0) { - $TagC = self::f_Loc_Enlarge_Find($Txt,$TagLst[$Ref],$TagFct[$Ref],$Loc->PosEnd+1,true,-$LevelStop); - if ($TagC==false) return false; - $PosEnd = $TagC->PosEnd; - $InnerLim = $TagC->PosBeg; + if ($TagLst[$Ref] === '.') { + $TagO = new clsTbsLocator; + $TagO->PosBeg = $Loc->PosBeg; + $TagO->PosEnd = $Loc->PosEnd; + $PosBeg = $Loc->PosBeg; + $PosEnd = $Loc->PosEnd; } else { - $PosEnd = $TagO->PosEnd; - $InnerLim = $PosEnd + 1; + $TagO = self::f_Loc_Enlarge_Find($Txt,$TagLst[$Ref],$TagFct[$Ref],$Loc->PosBeg-1,false,$LevelStop); + if ($TagO===false) return false; + $PosBeg = $TagO->PosBeg; + $LevelStop += -$TagO->RightLevel; // RightLevel=1 only if the tag is single and embeds $Loc, otherwise it is 0 + if ($LevelStop>0) { + $TagC = self::f_Loc_Enlarge_Find($Txt,$TagLst[$Ref],$TagFct[$Ref],$Loc->PosEnd+1,true,-$LevelStop); + if ($TagC==false) return false; + $PosEnd = $TagC->PosEnd; + $InnerLim = $TagC->PosBeg; + if ((!$TagBnd[$Ref]) && ($TagMax==0)) { + $PosBeg = $TagO->PosEnd + 1; + $PosEnd = $TagC->PosBeg - 1; + } + } else { + $PosEnd = $TagO->PosEnd; + $InnerLim = $PosEnd + 1; + } } $RetVal = true; @@ -4145,8 +4206,11 @@ static function f_Loc_EnlargeToTag(&$Txt,&$Loc,$TagStr,$RetInnerSrc) { for ($i=$Ref+1;$i<=$TagMax;$i++) { $x = $TagLst[$i]; if (($x!=='') && ($TagC!==false)) { - $TagC = self::f_Loc_Enlarge_Find($Txt,$x,$TagFct[$i],$PosEnd+1,true,0); - if ($TagC!==false) $PosEnd = $TagC->PosEnd; + $level = ($TagBnd[$i]) ? 0 : 1; + $TagC = self::f_Loc_Enlarge_Find($Txt,$x,$TagFct[$i],$PosEnd+1,true,$level); + if ($TagC!==false) { + $PosEnd = ($TagBnd[$i]) ? $TagC->PosEnd : $TagC->PosBeg -1 ; + } } } @@ -4155,8 +4219,11 @@ static function f_Loc_EnlargeToTag(&$Txt,&$Loc,$TagStr,$RetInnerSrc) { for ($i=$Ref-1;$i>=0;$i--) { $x = $TagLst[$i]; if (($x!=='') && ($TagO!==false)) { - $TagO = self::f_Loc_Enlarge_Find($Txt,$x,$TagFct[$i],$PosBeg-1,false,0); - if ($TagO!==false) $PosBeg = $TagO->PosBeg; + $level = ($TagBnd[$i]) ? 0 : -1; + $TagO = self::f_Loc_Enlarge_Find($Txt,$x,$TagFct[$i],$PosBeg-1,false,$level); + if ($TagO!==false) { + $PosBeg = ($TagBnd[$i]) ? $TagO->PosBeg : $TagO->PosEnd + 1; + } } } @@ -4180,6 +4247,7 @@ static function f_Loc_Enlarge_Find($Txt, $Tag, $Fct, $Pos, $Forward, $LevelStop) } static function f_Loc_AttBoolean($CurrVal, $AttTrue, $AttName) { + // Return the good value for a boolean attribute if ($AttTrue===true) { if (self::meth_Misc_ToStr($CurrVal)==='') { @@ -4194,10 +4262,82 @@ static function f_Loc_AttBoolean($CurrVal, $AttTrue, $AttName) { } } -static function f_Xml_AttFind(&$Txt,&$Loc,$Move=false,$AttDelim=false) { +/** + * Affects the positions of a list of locators regarding to a specific moving locator. + */ +static function f_Loc_Moving(&$LocM, &$LocLst) { + foreach ($LocLst as &$Loc) { + if ($Loc !== $LocM) { + if ($Loc->PosBeg >= $LocM->InsPos) { + $Loc->PosBeg += $LocM->InsLen; + $Loc->PosEnd += $LocM->InsLen; + } + if ($Loc->PosBeg > $LocM->DelPos) { + $Loc->PosBeg -= $LocM->DelLen; + $Loc->PosEnd -= $LocM->DelLen; + } + } + } + return true; +} + +/** + * Sort the locators in the list. Apply the bubble algorithm. + * Deleted locators maked with DelMe. + * @param array $LocLst An array of locators. + * @param boolean $DelEmbd True to deleted locators that embded other ones. + * @param boolean $iFirst Index of the first item. + * @return integer Return the number of met embedding locators. + */ +static function f_Loc_Sort(&$LocLst, $DelEmbd, $iFirst = 0) { + + $iLast = $iFirst + count($LocLst) - 1; + $embd = 0; + + for ($i = $iLast ; $i>=$iFirst ; $i--) { + $Loc = $LocLst[$i]; + $d = (isset($Loc->DelMe) && $Loc->DelMe); + $b = $Loc->PosBeg; + $e = $Loc->PosEnd; + for ($j=$i+1; $j<=$iLast ; $j++) { + // If DelMe, then the loc will be put at the end and deleted + $jb = $LocLst[$j]->PosBeg; + if ($d || ($b > $jb)) { + $LocLst[$j-1] = $LocLst[$j]; + $LocLst[$j] = $Loc; + } elseif ($e > $jb) { + $embd++; + if ($DelEmbd) { + $d = true; + $j--; // replay the current position + } else { + $j = $iLast; // quit the loop + } + } else { + $j = $iLast; // quit the loop + } + } + if ($d) { + unset($LocLst[$iLast]); + $iLast--; + } + } + + return $embd; +} + +/** + * Prepare all informations to move a locator according to parameter "att". + * @param mixed $MoveLocLst true to simple move the loc, or an array of loc to rearrange the list after the move. + * Note: rearrange doest not work with PHP4. + */ +static function f_Xml_AttFind(&$Txt,&$Loc,$MoveLocLst=false,$AttDelim=false,$LocLst=false) { // att=div#class ; att=((div))#class ; att=+((div))#class $Att = $Loc->PrmLst['att']; + unset($Loc->PrmLst['att']); // prevent from processing the field twice + $Loc->PrmLst['att;'] = $Att; // for debug + $p = strrpos($Att,'#'); if ($p===false) { $TagLst = ''; @@ -4239,7 +4379,8 @@ static function f_Xml_AttFind(&$Txt,&$Loc,$Move=false,$AttDelim=false) { } if ($Att==='.') return false; } - + $Loc->AttName = $Att; + $AttLC = strtolower($Att); if (isset($LocO->PrmLst[$AttLC])) { // The attribute is existing @@ -4260,7 +4401,6 @@ static function f_Xml_AttFind(&$Txt,&$Loc,$Move=false,$AttDelim=false) { // The attribute is not yet existing $Loc->AttDelimCnt = 0; $Loc->AttBeg = false; - $Loc->AttName = $Att; } // Search for a delimitor @@ -4270,24 +4410,25 @@ static function f_Xml_AttFind(&$Txt,&$Loc,$Move=false,$AttDelim=false) { } } - if ($Move) return self::f_Xml_AttMove($Txt,$Loc,$AttDelim); + if ($MoveLocLst) return self::f_Xml_AttMove($Txt,$Loc,$AttDelim,$MoveLocLst); return true; } -static function f_Xml_AttMove(&$Txt, &$Loc, $AttDelim=false) { +static function f_Xml_AttMove(&$Txt, &$Loc, $AttDelim, &$MoveLocLst) { - if ($AttDelim===false) $AttDelim = $Loc->AttDelimChr; - if ($AttDelim===false) $AttDelim = '"'; + if ($AttDelim===false) $AttDelim = $Loc->AttDelimChr; + if ($AttDelim===false) $AttDelim = '"'; - $sz = $Loc->PosEnd - $Loc->PosBeg + 1; - $Txt = substr_replace($Txt,'',$Loc->PosBeg,$sz); // delete the current locator + $DelPos = $Loc->PosBeg; + $DelLen = $Loc->PosEnd - $Loc->PosBeg + 1; + $Txt = substr_replace($Txt,'',$DelPos,$DelLen); // delete the current locator if ($Loc->AttForward) { - $Loc->AttTagBeg += -$sz; - $Loc->AttTagEnd += -$sz; + $Loc->AttTagBeg += -$DelLen; + $Loc->AttTagEnd += -$DelLen; } elseif ($Loc->PosBeg<$Loc->AttTagEnd) { - $Loc->AttTagEnd += -$sz; + $Loc->AttTagEnd += -$DelLen; } $InsPos = false; @@ -4300,8 +4441,8 @@ static function f_Xml_AttMove(&$Txt, &$Loc, $AttDelim=false) { $Loc->AttBeg = $InsPos + 1; $Loc->AttValBeg = $InsPos + strlen($Ins1) - 1; } else { - if ($Loc->PosEnd<$Loc->AttBeg) $Loc->AttBeg += -$sz; - if ($Loc->PosEnd<$Loc->AttEnd) $Loc->AttEnd += -$sz; + if ($Loc->PosEnd<$Loc->AttBeg) $Loc->AttBeg += -$DelLen; + if ($Loc->PosEnd<$Loc->AttEnd) $Loc->AttEnd += -$DelLen; if ($Loc->AttValBeg===false) { $InsPos = $Loc->AttEnd+1; $Ins1 = '='.$AttDelim; @@ -4313,7 +4454,7 @@ static function f_Xml_AttMove(&$Txt, &$Loc, $AttDelim=false) { $Ins2 = ''; } else { // value already existing - if ($Loc->PosEnd<$Loc->AttValBeg) $Loc->AttValBeg += -$sz; + if ($Loc->PosEnd<$Loc->AttValBeg) $Loc->AttValBeg += -$DelLen; $PosBeg = $Loc->AttValBeg; $PosEnd = $Loc->AttEnd; if ($Loc->AttDelimCnt>0) {$PosBeg++; $PosEnd--;} @@ -4337,9 +4478,18 @@ static function f_Xml_AttMove(&$Txt, &$Loc, $AttDelim=false) { $Loc->PosBeg = $PosBeg; $Loc->PosEnd = $PosEnd; $Loc->AttBegM = ($Txt[$Loc->AttBeg-1]===' ') ? $Loc->AttBeg-1 : $Loc->AttBeg; // for magnet=# - $Loc->AttInsLen = $InsLen; // for CacheField - return min($Loc->PrevPosEnd,$Loc->PosEnd); // New position to continue the search. + // for CacheField + if (is_array($MoveLocLst)) { + $Loc->InsPos = $InsPos; + $Loc->InsLen = $InsLen; + $Loc->DelPos = $DelPos; + if ($Loc->InsPos < $Loc->DelPos) $Loc->DelPos += $InsLen; + $Loc->DelLen = $DelLen; + self::f_Loc_Moving($Loc, $MoveLocLst); + } + + return true; } @@ -4454,13 +4604,14 @@ static function f_Xml_GetPart(&$Txt, $TagLst, $AllIfNothing=false) { } +/** + * Find the start of an XML tag. Used by OpenTBS. + * $Case=false can be useful for HTML. + * $Tag='' should work and found the start of the first tag. + * $Tag='/' should work and found the start of the first closing tag. + * Encapsulation levels are not featured yet. + */ static function f_Xml_FindTagStart(&$Txt,$Tag,$Opening,$PosBeg,$Forward,$Case=true) { -/* Find the start of an XML tag. -$Case=false can be useful for HTML. -$Tag='' should work and found the start of the first tag. -$Tag='/' should work and found the start of the first closing tag. -Encapsulation levels are not feataured yet. -*/ if ($Txt==='') return false; @@ -4473,13 +4624,14 @@ static function f_Xml_FindTagStart(&$Txt,$Tag,$Opening,$PosBeg,$Forward,$Case=tr do { if ($Forward) $p = strpos($Txt,$x,$p+1); else $p = strrpos(substr($Txt,0,$p+1),$x); if ($p===false) return false; - if (substr($Txt,$p,$xl)!==$x) continue; // For PHP 4 only + /* COMPAT#6 */ $z = substr($Txt,$p+$xl,1); } while ( ($z!==' ') && ($z!=="\r") && ($z!=="\n") && ($z!=='>') && ($z!=='/') && ($Tag!=='/') && ($Tag!=='') ); } else { do { if ($Forward) $p = stripos($Txt,$x,$p+1); else $p = strripos(substr($Txt,0,$p+1),$x); if ($p===false) return false; + /* COMPAT#7 */ $z = substr($Txt,$p+$xl,1); } while ( ($z!==' ') && ($z!=="\r") && ($z!=="\n") && ($z!=='>') && ($z!=='/') && ($Tag!=='/') && ($Tag!=='') ); } @@ -4488,13 +4640,14 @@ static function f_Xml_FindTagStart(&$Txt,$Tag,$Opening,$PosBeg,$Forward,$Case=tr } +/** + * This function is a smart solution to find an XML tag. + * It allows to ignore full opening/closing couple of tags that could be inserted before the searched tag. + * It allows also to pass a number of encapsulations. + * To ignore encapsulation and opengin/closing just set $LevelStop=false. + * $Opening is used only when $LevelStop=false. + */ static function f_Xml_FindTag(&$Txt,$Tag,$Opening,$PosBeg,$Forward,$LevelStop,$WithPrm,$WithPos=false) { -/* This function is a smart solution to find an XML tag. -It allows to ignore full opening/closing couple of tags that could be inserted before the searched tag. -It allows also to pass a number of encapsulations. -To ignore encapsulation and opengin/closing just set $LevelStop=false. -$Opening is used only when $LevelStop=false. -*/ if ($Tag==='_') { // New line $p = self::f_Xml_FindNewLine($Txt,$PosBeg,$Forward,($LevelStop!==0)); @@ -4532,10 +4685,12 @@ static function f_Xml_FindTag(&$Txt,$Tag,$Opening,$PosBeg,$Forward,$LevelStop,$W // Check the name of the tag if (strcasecmp(substr($Txt,$Pos+1,$TagL),$Tag)==0) { + // It's an opening tag $PosX = $Pos + 1 + $TagL; // The next char $TagOk = true; $TagIsOpening = true; } elseif (strcasecmp(substr($Txt,$Pos+1,$TagClosingL),$TagClosing)==0) { + // It's a closing tag $PosX = $Pos + 1 + $TagClosingL; // The next char $TagOk = true; $TagIsOpening = false; @@ -4544,9 +4699,9 @@ static function f_Xml_FindTag(&$Txt,$Tag,$Opening,$PosBeg,$Forward,$LevelStop,$W if ($TagOk) { // Check the next char $x = $Txt[$PosX]; - if (($x===' ') || ($x==="\r") || ($x==="\n") || ($x==='>') || ($Tag==='/') || ($Tag==='')) { + if (($x===' ') || ($x==="\r") || ($x==="\n") || ($x==='>') || ($x==='/') || ($Tag==='/') || ($Tag==='')) { // Check the encapsulation count - if ($LevelStop===false) { // No encaplusation check + if ($LevelStop===false) { // No encapsulation check if ($TagIsOpening!==$Opening) $TagOk = false; } else { // Count the number of level if ($TagIsOpening) { diff --git a/lib/tbs_plugin_opentbs.php b/lib/tbs_plugin_opentbs.php index ad0566c..43dc62f 100755 --- a/lib/tbs_plugin_opentbs.php +++ b/lib/tbs_plugin_opentbs.php @@ -7,11 +7,11 @@ * This TBS plug-in can open a zip file, read the central directory, * and retrieve the content of a zipped file which is not compressed. * - * @version 1.9.4 - * @date 2015-02-11 + * @version 1.9.6 + * @date 2016-03-24 * @see http://www.tinybutstrong.com/plugins.php * @author Skrol29 http://www.tinybutstrong.com/onlyyou.html - * @license LGPL + * @license LGPL-3.0 */ /** @@ -29,8 +29,10 @@ define('OPENTBS_ADDFILE','clsOpenTBS.AddFile'); // command to add a new file in the archive define('OPENTBS_DELETEFILE','clsOpenTBS.DeleteFile'); // command to delete a file in the archive define('OPENTBS_REPLACEFILE','clsOpenTBS.ReplaceFile'); // command to replace a file in the archive +define('OPENTBS_EDIT_ENTITY','clsOpenTBS.EditEntity'); // command to force an attribute define('OPENTBS_FILEEXISTS','clsOpenTBS.FileExists'); -define('OPENTBS_CHART','clsOpenTBS.Chart'); // command to delete a file in the archive +define('OPENTBS_CHART','clsOpenTBS.Chart'); +define('OPENTBS_CHART_INFO','clsOpenTBS.ChartInfo'); define('OPENTBS_DEFAULT',''); // Charset define('OPENTBS_ALREADY_XML',false); define('OPENTBS_ALREADY_UTF8','already_utf8'); @@ -83,12 +85,13 @@ function OnInstall() { if (!isset($TBS->OtbsSpacePreserve)) $TBS->OtbsSpacePreserve = true; if (!isset($TBS->OtbsClearWriter)) $TBS->OtbsClearWriter = true; if (!isset($TBS->OtbsClearMsWord)) $TBS->OtbsClearMsWord = true; + if (!isset($TBS->OtbsDeleteObsoleteChartData)) $TBS->OtbsDeleteObsoleteChartData = true; if (!isset($TBS->OtbsMsExcelConsistent)) $TBS->OtbsMsExcelConsistent = true; if (!isset($TBS->OtbsMsExcelExplicitRef)) $TBS->OtbsMsExcelExplicitRef = true; if (!isset($TBS->OtbsClearMsPowerpoint)) $TBS->OtbsClearMsPowerpoint = true; if (!isset($TBS->OtbsGarbageCollector)) $TBS->OtbsGarbageCollector = true; if (!isset($TBS->OtbsMsExcelCompatibility)) $TBS->OtbsMsExcelCompatibility = true; - $this->Version = '1.9.4'; + $this->Version = '1.9.6'; $this->DebugLst = false; // deactivate the debug mode $this->ExtInfo = false; $TBS->TbsZip = &$this; // a shortcut @@ -100,6 +103,14 @@ function BeforeLoadTemplate(&$File,&$Charset) { $TBS =& $this->TBS; if ($TBS->_Mode!=0) return; // If we are in subtemplate mode, the we use the TBS default process + if ($File === false) { + // Close the current template if any + @$this->Close(); + // Save memory space + $this->TbsInitArchive(); + return false; + } + // Decompose the file path. The syntaxe is 'Archive.ext#subfile', or 'Archive.ext', or '#subfile' $FilePath = $File; $SubFileLst = false; @@ -196,9 +207,9 @@ function BeforeShow(&$Render, $File='') { $TBS->OtbsCurrFile = $this->TbsGetFileName($idx); // usefull for TbsPicAdd() $this->TbsCurrIdx = $idx; // usefull for debug mode if ($TbsShow && $onshow) $TBS->Show(TBS_NOTHING); - if ($this->ExtEquiv == 'docx') { - $this->MsWord_RenumDocPr($TBS->Source); - } + if ($this->ExtEquiv == 'docx') { + $this->MsWord_RenumDocPr($TBS->Source); + } if ($explicitRef && (!isset($this->MsExcel_KeepRelative[$idx])) ) { $this->MsExcel_ConvertToExplicit($TBS->Source); } @@ -287,7 +298,7 @@ function OnOperation($FieldName,&$Value,&$PrmLst,&$Txt,$PosBeg,$PosEnd,&$Loc) { $this->TbsPicAdd($Value, $PrmLst, $Txt, $Loc, 'ope=addpic'); } elseif ($ope==='changepic') { $this->TbsPicPrepare($Txt, $Loc, false); - $this->TbsPicAdd($Value, $PrmLst, $Txt, $Loc, 'ope=changepic'); + $this->TbsPicAdd($Value, $PrmLst, $Txt, $Loc, 'ope=changepic'); } elseif ($ope==='delcol') { $this->TbsDeleteColumns($Txt, $Value, $PrmLst, $PosBeg, $PosEnd); return false; // prevent TBS from merging the field @@ -388,7 +399,18 @@ function OnCommand($Cmd, $x1=null, $x2=null, $x3=null, $x4=null, $x5=null) { } else { return $this->OpenXML_ChartChangeSeries($ChartRef, $SeriesNameOrNum, $NewValues, $NewLegend); } + } elseif ($Cmd==OPENTBS_CHART_INFO) { + $ChartRef = $x1; + $Complete = $x2; + + if ($this->ExtType=='odf') { + return $this->OpenDoc_ChartReadSeries($ChartRef, $Complete); + } else { + return $this->OpenXML_ChartReadSeries($ChartRef, $Complete); + } + + } elseif ($Cmd==OPENTBS_DEBUG_XML_SHOW) { $this->TBS->Show(OPENTBS_DEBUG_XML); @@ -430,7 +452,7 @@ function OnCommand($Cmd, $x1=null, $x2=null, $x3=null, $x4=null, $x5=null) { // Only XLSX files have sheets in separated subfiles. if ($this->ExtEquiv==='xlsx') { - $o = $this->MsExcel_SheetGet($x1); + $o = $this->MsExcel_SheetGet($x1, $x2); if ($o===false) return; if ($o->file===false) return $this->RaiseError("($Cmd) Error with sheet '$x1'. The corresponding XML subfile is not referenced."); return $this->TbsLoadSubFileAsTemplate('xl/'.$o->file); @@ -509,18 +531,18 @@ function OnCommand($Cmd, $x1=null, $x2=null, $x3=null, $x4=null, $x5=null) { $code = $x1; $file = $x2; - $prms = array('default'=>'current', 'adjust' => 'inside'); - if (is_array($x3)) { - $prms = array_merge($prms, $x3); - } else { - // Compatibility v <= 1.9.0 - if (!is_null($x3)) $prms['default'] = $x3; - if (!is_null($x4)) $prms['adjust'] = $x4; - } - $prms_flat = array(); - foreach($prms as $p => $v) $prms_flat[] = $p.'='.$v; - $prms_flat = implode(';', $prms_flat); - + $prms = array('default'=>'current', 'adjust' => 'inside'); + if (is_array($x3)) { + $prms = array_merge($prms, $x3); + } else { + // Compatibility v <= 1.9.0 + if (!is_null($x3)) $prms['default'] = $x3; + if (!is_null($x4)) $prms['adjust'] = $x4; + } + $prms_flat = array(); + foreach($prms as $p => $v) $prms_flat[] = $p.'='.$v; + $prms_flat = implode(';', $prms_flat); + $UniqueId++; $name = 'OpenTBS_Change_Picture_'.$UniqueId; $tag = "[$name;ope=changepic;tagpos=inside;$prms_flat]"; @@ -564,11 +586,11 @@ function OnCommand($Cmd, $x1=null, $x2=null, $x3=null, $x4=null, $x5=null) { if ($this->ExtEquiv=='pptx') { $option = (is_null($x2)) ? OPENTBS_FIRST : $x2; - $returnFirstFound = (($option & TBS_ALL)!=TBS_ALL); + $returnFirstFound = (($option & OPENTBS_ALL)!=OPENTBS_ALL); $find = $this->MsPowerpoint_SearchInSlides($x1, $returnFirstFound); if ($returnFirstFound) { $slide = $find['key']; - if ( ($slide!==false) && (($option & TBS_GO)!=TBS_GO) ) $this->OnCommand(OPENTBS_SELECT_SLIDE, $slide); + if ( ($slide!==false) && (($option & OPENTBS_GO)==OPENTBS_GO) ) $this->OnCommand(OPENTBS_SELECT_SLIDE, $slide); return ($slide); } else { $res = array(); @@ -654,6 +676,12 @@ function OnCommand($Cmd, $x1=null, $x2=null, $x3=null, $x4=null, $x5=null) { } } return $KeepRelative; + + } elseif ($Cmd==OPENTBS_EDIT_ENTITY) { + + $AddElIfMissing = (boolean) $x5; + return $this->XML_ForceAtt($x1, $x2, $x3, $x4, $AddElIfMissing); + } } @@ -737,26 +765,28 @@ function TbsLoadSubFileAsTemplate($SubFileLst) { if ($this->ExtInfo!==false) { $i = $this->ExtInfo; $e = $this->ExtEquiv; - if (isset($i['rpl_what'])) { - // auto replace strings in the loaded file - $TBS->Source = str_replace($i['rpl_what'], $i['rpl_with'], $TBS->Source); - } - if (($e==='odt') && $TBS->OtbsClearWriter) { - $this->OpenDoc_CleanRsID($TBS->Source); - } - if (($e==='ods') && $TBS->OtbsMsExcelCompatibility) { - $this->OpenDoc_MsExcelCompatibility($TBS->Source); - } - if ($e==='docx') { - if ($TBS->OtbsSpacePreserve) $this->MsWord_CleanSpacePreserve($TBS->Source); - if ($TBS->OtbsClearMsWord) $this->MsWord_Clean($TBS->Source); - } - if (($e==='pptx') && $TBS->OtbsClearMsPowerpoint) { - $this->MsPowerpoint_Clean($TBS->Source); - } - if (($e==='xlsx') && $TBS->OtbsMsExcelConsistent) { - $this->MsExcel_DeleteFormulaResults($TBS->Source); - $this->MsExcel_ConvertToRelative($TBS->Source); + if ($this->TbsApplyOptim($TBS->Source, true)) { + if (isset($i['rpl_what'])) { + // auto replace strings in the loaded file + $TBS->Source = str_replace($i['rpl_what'], $i['rpl_with'], $TBS->Source); + } + if (($e==='odt') && $TBS->OtbsClearWriter) { + $this->OpenDoc_CleanRsID($TBS->Source); + } + if (($e==='ods') && $TBS->OtbsMsExcelCompatibility) { + $this->OpenDoc_MsExcelCompatibility($TBS->Source); + } + if ($e==='docx') { + if ($TBS->OtbsSpacePreserve) $this->MsWord_CleanSpacePreserve($TBS->Source); + if ($TBS->OtbsClearMsWord) $this->MsWord_Clean($TBS->Source); + } + if (($e==='pptx') && $TBS->OtbsClearMsPowerpoint) { + $this->MsPowerpoint_Clean($TBS->Source); + } + if (($e==='xlsx') && $TBS->OtbsMsExcelConsistent) { + $this->MsExcel_DeleteFormulaResults($TBS->Source); + $this->MsExcel_ConvertToRelative($TBS->Source); + } } } // apply default TBS behaviors on the uncompressed content: other plug-ins + [onload] fields @@ -908,6 +938,27 @@ function TbsGetFileName($idx) { } } + /** + * Tells if optimisation can be applied on the current content. + * @param boolean $mark true to mark the doc as done because optim will be processed. + * @return boolean True if the current doc is just been marked done. Null if the mark cannot be read. + */ + function TbsApplyOptim(&$Txt, $mark) { + if (substr($Txt, 0, 2) === ''); + if (substr($Txt, $p-1) === ' ') { + return false; + } else { + if ($mark) { + $Txt = substr_replace($Txt, ' ', $p, 0); + } + return true; + } + } else { + return null; + } + } + /** * Display the header of the debug mode (only once) */ @@ -1398,7 +1449,7 @@ function TbsPicAdd(&$Value, &$PrmLst, &$Txt, &$Loc, $Prm) { // parameter att already applied during Field caching $Value = substr($Txt, $Loc->PosBeg, $Loc->PosEnd - $Loc->PosBeg + 1); } - return false; + return false; } // set the name of the internal file @@ -1426,22 +1477,22 @@ function TbsPicAdd(&$Value, &$PrmLst, &$Txt, &$Loc, $Prm) { // preparation for others file in the archive $Rid = false; if ($this->ExtType==='odf') { - // OpenOffice document - $this->OpenDoc_ManifestChange($InternalPath,''); + // OpenOffice document + $this->OpenDoc_ManifestChange($InternalPath,''); } elseif ($this->ExtType==='openxml') { - // Microsoft Office document - $this->OpenXML_CTypesPrepareExt($InternalPath, ''); - $BackNbr = max(substr_count($TBS->OtbsCurrFile, '/') - 1, 0); // docx=>"media/img.png", xlsx & pptx=>"../media/img.png" - $TargetDir = str_repeat('../', $BackNbr).'media/'; - $FileName = basename($InternalPath); - $Rid = $this->OpenXML_Rels_AddNewRid($TBS->OtbsCurrFile, $TargetDir, $FileName); + // Microsoft Office document + $this->OpenXML_CTypesPrepareExt($InternalPath, ''); + $BackNbr = max(substr_count($TBS->OtbsCurrFile, '/') - 1, 0); // docx=>"media/img.png", xlsx & pptx=>"../media/img.png" + $TargetDir = str_repeat('../', $BackNbr).'media/'; + $FileName = basename($InternalPath); + $Rid = $this->OpenXML_Rels_AddNewRid($TBS->OtbsCurrFile, $TargetDir, $FileName); } // change the value of the field for the merging process if ($Rid===false) { - $Value = $InternalPath; + $Value = $InternalPath; } else { - $Value = $Rid; // the Rid is used instead of the file name for the merging + $Value = $Rid; // the Rid is used instead of the file name for the merging } // Change the dimensions of the picture @@ -1857,7 +1908,8 @@ function Ext_PrepareInfo($Ext=false) { } $ctype = 'application/vnd.openxmlformats-officedocument.'; if ($Ext==='docx') { - $i = array('br' => '', 'ctype' => $ctype . 'wordprocessingml.document', 'pic_path' => 'word/media/', 'rpl_what' => $x, 'rpl_with' => '\'', 'pic_entity'=>'w:drawing'); + // Notes: (1) '' works but '' enforce compatibility with Libre Office. (2) Line-breaks merged in attributes will corrupt the DOCX anyway. + $i = array('br' => '', 'ctype' => $ctype . 'wordprocessingml.document', 'pic_path' => 'word/media/', 'rpl_what' => $x, 'rpl_with' => '\'', 'pic_entity'=>'w:drawing'); $i['main'] = $this->OpenXML_MapGetMain('wordprocessingml.document.main+xml', 'word/document.xml'); $i['load'] = $this->OpenXML_MapGetFiles(array('wordprocessingml.header+xml', 'wordprocessingml.footer+xml')); $block_alias = array( @@ -1991,35 +2043,17 @@ function XML_FoundTagStart($Txt, $Tag, $PosBeg) { * @param {boolean} $OnlyInner Set to true to keep the content inside the element. Set to false to delete the entire element. Default is false. */ function XML_DeleteElements(&$Txt, $TagLst, $OnlyInner=false) { - $nbr_del = 0; + $nb = 0; + $Content = !$OnlyInner; foreach ($TagLst as $tag) { - $t_open = '<'.$tag; - $t_close = 'XML_FoundTagStart($Txt, $t_open, $p1))!==false) { - // get the end of the tag - $pe1 = strpos($Txt, '>', $p1); - if ($pe1===false) return false; // error in the XML formating - $p2 = false; - if (substr($Txt, $pe1-1, 1)=='/') { - $pe2 = $pe1; - } else { - // it's an opening+closing - $p2 = $this->XML_FoundTagStart($Txt, $t_close, $pe1); - if ($p2===false) return false; // error in the XML formating - $pe2 = strpos($Txt, '>', $p2); - } - if ($pe2===false) return false; // error in the XML formating - // delete the tag - if ($OnlyInner) { - if ($p2!==false) $Txt = substr_replace($Txt, '', $pe1+1, $p2-$pe1-1); - $p1 = $pe1; // for next search - } else { - $Txt = substr_replace($Txt, '', $p1, $pe2-$p1+1); - } - } + $p = 0; + while ($x = clsTbsXmlLoc::FindElement($Txt, $tag, $p)) { + $x->Delete($Content); + $p = $x->PosBeg; + $nb++; + } } - return $nbr_del; + return $nb; } /** @@ -2068,38 +2102,106 @@ function XML_DeleteColumnElements(&$Txt, $Tag, $SpanAtt, $ColLst, $ColMax) { } /** - * Delete attributes in an XML element. The XML element is located by $Pos. - * @param {string} $Txt Text containing XML elements. - * @param {int} $Pos Start of the XML element. - * @param {array} $AttLst List of attributes to search and delete - * @param {array} $StrLst List of strings to search and delete - * @return {int} The new end of the element. + * Change an attribute's value or an entity's value in the first element in a given sub-file. + * @param {mixed} $SubFile : the name or the index of the sub-file. Use value false to get the current sub-file. + * @param {string} $ElPath : path of the element. For example : 'w:document/w:body/w:p'. + * @param {string|boolean} $Att : the attribute, or false to replace the entity's value. + * @param {string|boolean} $NewVal : the new value, or false to delete the attribute. + * @return {boolean} True if the attribute is found and processed. False otherwise. */ - function XML_DeleteAttributes(&$Txt, $Pos, $AttLst, $StrLst) { - $end = strpos($Txt, '>', $Pos); // end of the element - if ($end===false) return (strlen($Txt)-1); - $x_len = $end - $Pos + 1; - $x = substr($Txt, $Pos, $x_len); - // delete attributes - foreach ($AttLst as $att) { - $a = ' '.$att.'="'; - $p1 = strpos($x, $a); - if ($p1!==false) { - $p2 = strpos($x, '"', $p1+strlen($a)); - if ($p2!==false) $x = substr_replace($x, '', $p1, $p2-$p1+1); - } - } - // Delete strings - foreach ($StrLst as $str) $x = str_replace('', $str, $x); - $x_len2 = strlen($x); - if ($x_len2!=$x_len) $Txt = substr_replace($Txt, $x, $Pos, $x_len); - return $Pos + $x_len2; - } + function XML_ForceAtt($SubFile, $ElPath, $Att, $NewVal, $AddElIfMissing = false) { + + // Find the file + $idx = $this->FileGetIdx($SubFile); + if ($idx === false) return false; + $Txt = $this->TbsStoreGet($idx, 'XML_ForceAtt'); + + // Find the element + $el_lst = explode('/', $ElPath); + $p = 0; + $el_idx = 0; + $el_nb = count($el_lst); + $end = $el_nb; + $loc = false; + $loc_prev = false; + while ($el_idx < $end) { + $loc_prev = $loc; + $loc = clsTbsXmlLoc::FindStartTag($Txt, $el_lst[$el_idx], $p); + if ($loc === false) { + if ($AddElIfMissing) { + // stop the loop + $end = $el_idx; + } else { + return false; + } + } else { + $p = $loc->PosEnd; + $el_idx++; + } + } + + if (($loc === false) && ($loc_prev === false)) return false; + + $save = true; + if ($el_idx < $el_nb) { + // One of the entities is not found => create entities + if ($NewVal === false) { + // Nothing to do + $save = false; + } else { + $before = ''; + $after = ''; + $i_end = ($end - 1); + for ($i = $el_idx ; $i < $i_end ; $i++) { + $before .= '<' . $el_lst[$i] . '>'; + $after = '' . $after; + } + if ($Att === false) { + $x = $before . '<' . $el_lst[$i] . '>' . $NewVal . '' . $after; + } else { + $x = $before . '<' . $el_lst[$i] . ' ' . $Att . '="' . $NewVal . '" />' . $after; + } + $loc_prev->FindEndTag(); + if ($loc_prev->pET_PosBeg === false) { + return $this->RaiseError("Cannot apply attribute because entity '" . $loc_prev->FindName() . "' has no ending tag in file [$SubFile]."); + } + $Txt = substr_replace($Txt, $x, $loc_prev->pET_PosBeg, 0); + } + } else { + // The last entity is found => force the attribute + if ($NewVal === false) { + if ($Att === false) { + // delete the entity + $loc->Delete(); + } else { + // delete the attribute + $loc->DeleteAtt($Att); + } + } else { + if ($Att === false) { + // change the entity's value + $loc->FindEndTag(); + $loc->ReplaceInnerSrc($NewVal); + } else { + // change the attribute's value + $loc->ReplaceAtt($Att, $NewVal, true); + } + } + } + // Save the file + if ($save) { + $this->TbsStorePut($idx, $Txt); + } + + return true; + + } + /** * Function used by Block Alias * The first start tag on the left is supposed to be the good one. - * Note: encapuslation is not yet supported in this version. + * Note: encapsulation is not yet supported in this version. */ function XML_BlockAlias_Prefix($TagPrefix, $Txt, $PosBeg, $Forward, $LevelStop) { @@ -2277,7 +2379,7 @@ function OpenXML_GetAbsolutePath($RelativePath, $RelativeTo) { // May be reltaive to the root if (substr($RelativePath, 0, 1) == '/') { - return substr($RelativePath, 1); + return substr($RelativePath, 1); } $rp = explode('/', $RelativePath); @@ -2328,7 +2430,7 @@ function OpenXML_GetInternalPicPath($Rid) { * Delete an XML file in the OpenXML archive. * The file is delete from the declaration file [Content_Types].xml and from the relationships of the specified files. * @param {string} $FullPath The full path of the file to delete. - * @param {array} $$RelatedTo List of the the full paths of the files than may have relationship with the file to delete. + * @param {array} $RelatedTo List of the the full paths of the files than may have relationship with the file to delete. * @return {mixed} False if it is not possible to delete the file, or the number of modifier relations ship in case of success (may be 0). */ function OpenXML_DeleteFile($FullPath, $RelatedTo) { @@ -2345,8 +2447,8 @@ function OpenXML_DeleteFile($FullPath, $RelatedTo) { $nb = 0; foreach ($RelatedTo as $file) { $target = $this->OpenXML_GetRelativePath($FullPath, $file); - $rels_file = $this->OpenXML_Rels_GetPath($file); - if ($this->OpenXML_Rels_ReplaceTarget($rels_file, $target, false)) { + $att = 'Target="' . $target . '"'; + if ($this->OpenXML_Rels_DeleteRel($file, $att)) { $nb++; } } @@ -2365,30 +2467,32 @@ function OpenXML_Rels_GetPath($DocPath) { } /** - * Replace or delete a target in a Rels file. - * The current function actually edit the Rels file. + * Delete an element in a Rels file. * Take car that there is another technic for listing and adding targets wish is working with a persistent object which is commit at the end of the merge.. - * @param {string} $RelsPath The path of the Rels file. - * @param {string} $OldTarget The target value to find. - * @param {string|boolean} $NewTarget The new target value, or false to delete the relation ship element. - * @return {boolean} True if the change is applied. + * @param string $DocPath The fullpath of the document file. + * @param string $AttExpr The target att expression to find. + * @param string|boolean $ReturnAttLst The list of att values to return. + * @return mixed $ReturnAttVal (or True) if the change is applied. */ - function OpenXML_Rels_ReplaceTarget($RelsPath, $OldTarget, $NewTarget) { + function OpenXML_Rels_DeleteRel($DocPath, $AttExpr, $ReturnAttLst = false) { + $RelsPath = $this->OpenXML_Rels_GetPath($DocPath); $idx = $this->FileGetIdx($RelsPath); if ($idx===false) $this->RaiseError("Cannot edit target in '$RelsPath' because the file is not found."); $txt = $this->TbsStoreGet($idx, 'Replace target in rels file'); - $att = 'Target="'.$OldTarget.'"'; - $loc = clsTbsXmlLoc::FindStartTagHavingAtt($txt, $att, 0); + $loc = clsTbsXmlLoc::FindElementHavingAtt($txt, $AttExpr, 0); if ($loc) { - if ($NewTarget === false) { - $loc->Delete(); - } else { - $loc->ReplaceAtt('Target',$NewTarget); + $ret = true; + if (is_array($ReturnAttLst)) { + $ret = array(); + foreach ($ReturnAttLst as $att) { + $ret[$att] = $loc->GetAttLazy($att); + } } + $loc->Delete(); $this->TbsStorePut($idx, $txt); - return true; + return $ret; } else { return false; } @@ -2748,19 +2852,23 @@ function OpenXML_MapGetMain($ShortType, $Default) { } } + /** + * Build the list of chart files. + */ function OpenXML_ChartInit() { $this->OpenXmlCharts = array(); foreach ($this->CdFileByName as $f => $i) { + // Note : some of liste files are style or color files, not chart. if (strpos($f, '/charts/')!==false) { - $f = explode('/',$f); - $n = count($f) -1; - if ( ($n>=2) && ($f[$n-1]==='charts') ) { - $f = $f[$n]; // name of the xml file - if (substr($f,-4)==='.xml') { - $f = substr($f,0,strlen($f)-4); - $this->OpenXmlCharts[$f] = array('idx'=>$i, 'clean'=>false, 'series'=>false); + $x = explode('/',$f); + $n = count($x) -1; + if ( ($n>=2) && ($x[$n-1]==='charts') ) { + $x = $x[$n]; // name of the xml file + if (substr($x,-4)==='.xml') { + $x = substr($x,0,strlen($x)-4); + $this->OpenXmlCharts[$x] = array('idx'=>$i, 'clean'=>false, 'series'=>false); } } } @@ -2816,38 +2924,44 @@ function OpenXML_ChartDebug($nl, $sep, $bull) { } + /** + * Search for the series in the chart definition + * @return mixed An Array if success, or a string if error. + */ function OpenXML_ChartSeriesFound(&$Txt, $SeriesNameOrNum, $OnlyBounds=false) { $IsNum = is_numeric($SeriesNameOrNum); if ($IsNum) { $p = strpos($Txt, ''); + if ($p===false) return "Number of the series not found."; } else { $SeriesNameOrNum = htmlentities($SeriesNameOrNum); $p = strpos($Txt, '>'.$SeriesNameOrNum.'<'); + if ($p===false) return "Name of the series not found."; + $p++; } - if ($p===false) return false; - if (!$IsNum) $p++; $res = array('p'=>$p); if ($OnlyBounds) { - $p1 = clsTinyButStrong::f_Xml_FindTagStart($Txt, 'c:ser', true, $p, false, true); - $x = ''; - $p2 = strpos($Txt, '', $p1); - if ($p2===false) return false; - $res['l'] = $p2 + strlen($x) - $p1; - $res['p'] = $p1; - return $res; + if ($loc = clsTbsXmlLoc::FindElement($Txt, 'c:ser', $p, false)) { + $res['p'] = $loc->PosBeg; + $res['l'] = $loc->PosEnd - $loc->PosBeg + 1; + return $res; + } else { + return "XML entity not found."; + } } + // faster than clsTbsXmlLoc::FindElement $end_tag = ''; - $end = strpos($Txt, '', $p); + $end = strpos($Txt, $end_tag, $p); $len = $end + strlen($end_tag) - $p; $res['l'] = $len; $x = substr($Txt, $p, $len); - // Legend, may be abensent + // Legend, may be absent $p = 0; if ($IsNum) { $p1 = strpos($x, ''); @@ -2869,11 +2983,12 @@ function OpenXML_ChartSeriesFound(&$Txt, $SeriesNameOrNum, $OnlyBounds=false) { } // Data X & Y, we assume that (X or Category) are always first and (Y or Value) are always second + // Some charts may not have categories, they cannot be merged :-( for ($i=1; $i<=2; $i++) { $p1 = strpos($x, '', $p1); // the closing tag can be or - if ($p2===false) return false; + if ($p2===false) return "Cached data not found for categories or values."; $p2 = $p2 - 7; $res['point'.$i.'_p'] = $p1; $res['point'.$i.'_l'] = $p2 - $p1; @@ -2884,34 +2999,64 @@ function OpenXML_ChartSeriesFound(&$Txt, $SeriesNameOrNum, $OnlyBounds=false) { } - function OpenXML_ChartChangeSeries($ChartRef, $SeriesNameOrNum, $NewValues, $NewLegend=false) { - + /** + * Find a chart in the template by its reference. + * Returns the OpenTBS's internal chart ref if found. + */ + function OpenXML_ChartFind($ChartRef, $ErrTitle) { + if ($this->OpenXmlCharts===false) $this->OpenXML_ChartInit(); - - // search the chart - $ref = ''.$ChartRef; - if (!isset($this->OpenXmlCharts[$ref])) $ref = 'chart'.$ref; // try with $ChartRef as number + + $ref = ''.$ChartRef; + // try with $ChartRef as number + if (!isset($this->OpenXmlCharts[$ref])) { + $ref = 'chart'.$ref; + } + // try with $ChartRef as name of the file if (!isset($this->OpenXmlCharts[$ref])) { - // try with $ChartRef as name of the file $charts = array(); + $idx = false; if ($this->ExtEquiv=='pptx') { // search in slides $find = $this->MsPowerpoint_SearchInSlides(' title="'.$ChartRef.'"'); - if ($find['idx']!==false) $charts = $this->OpenXML_ChartGetInfoFromFile($find['idx']); + $idx = $find['idx']; + } else { + $idx =$this->Ext_GetMainIdx(); + } + if ($idx !== false) { + $charts = $this->OpenXML_ChartGetInfoFromFile($idx); + } + // Search the chart having the title + foreach($charts as $c) { + if ($c['title']===$ChartRef) $ref = $c['name']; + } + if (isset($this->OpenXmlCharts[$ref])) { + $chart = &$this->OpenXmlCharts[$ref]; + $this->OpenXmlCharts[$ChartRef] = &$chart; + // For debug + $chart['parent_idx'] = $idx; } else { - $charts = $this->OpenXML_ChartGetInfoFromFile($this->Ext_GetMainIdx()); + return $this->RaiseError("($ErrTitle) : unable to found the chart corresponding to '".$ChartRef."'."); } - foreach($charts as $c) if ($c['title']===$ChartRef) $ref = $c['name']; // try with $ChartRef as title - if (!isset($this->OpenXmlCharts[$ref])) return $this->RaiseError("(ChartChangeSeries) : unable to found the chart corresponding to '".$ChartRef."'."); } + + return $ref; + + } + + function OpenXML_ChartChangeSeries($ChartRef, $SeriesNameOrNum, $NewValues, $NewLegend=false) { + + // Search the chart + $ref = $this->OpenXML_ChartFind($ChartRef, 'ChartChangeSeries'); + if ($ref===false) return false; + // Open the chart doc $chart =& $this->OpenXmlCharts[$ref]; $Txt = $this->TbsStoreGet($chart['idx'], 'ChartChangeSeries'); if ($Txt===false) return false; if (!$chart['clean']) { - // delete tags that refere to the XLSX file containing original data - //$this->XML_DeleteElements($Txt, array('c:externalData', 'c:f')); + $this->OpenXML_ChartUnlinklDataSheet($chart['idx'], $Txt, $this->TBS->OtbsDeleteObsoleteChartData); $chart['nbr'] = substr_count($Txt, ''); $chart['clean'] = true; } @@ -2919,7 +3064,7 @@ function OpenXML_ChartChangeSeries($ChartRef, $SeriesNameOrNum, $NewValues, $New $Delete = ($NewValues===false); if (is_array($SeriesNameOrNum)) return $this->RaiseError("(ChartChangeSeries) '$ChartRef' : The series reference is an array, a string or a number is expected. ".$ChartRef."'."); // usual mistake in arguments $ser = $this->OpenXML_ChartSeriesFound($Txt, $SeriesNameOrNum, $Delete); - if ($ser===false) return $this->RaiseError("(ChartChangeSeries) '$ChartRef' : unable to found series '".$SeriesNameOrNum."' in the chart '".$ref."'."); + if (!is_array($ser)) return $this->RaiseError("(ChartChangeSeries) '$ChartRef' : unable change series '".$SeriesNameOrNum."' in the chart '".$ref."' : ".$ser); if ($Delete) { @@ -2928,8 +3073,8 @@ function OpenXML_ChartChangeSeries($ChartRef, $SeriesNameOrNum, $NewValues, $New } else { - $point1 = ''; - $point2 = ''; + $point1 = ''; // category + $point2 = ''; // value $i = 0; $v = reset($NewValues); if (is_array($v)) { @@ -2952,14 +3097,16 @@ function OpenXML_ChartChangeSeries($ChartRef, $SeriesNameOrNum, $NewValues, $New $x = $v; $y = isset($val_lst[$k]) ? $val_lst[$k] : null; } + // a category should not be missing otherwise it caption may not be display if the series is the first one + $point1 .= ''.$x.''; + // a missing value is possible if ( (!is_null($y)) && ($y!==false) && ($y!=='') && ($y!=='NULL') ) { - $point1 .= ''.$x.''; $point2 .= ''.$y.''; - $i++; } + $i++; } $point1 = ''.$point1; - $point2 = ''.$point2; + $point2 = ''.$point2; // yes, the count is the same as point1 whenever missing values // change info in reverse order of placement in order to avoid exention problems $p = $ser['p']; @@ -3001,8 +3148,8 @@ function OpenXML_ChartGetInfoFromFile($idx, $Txt=false) { $name = false; $title = false; $descr = false; - $parent = clsTbsXmlLoc::FindStartTag($Txt, 'wp:inline', $t->PosBeg, false); // docx - if ($parent===false) $parent = clsTbsXmlLoc::FindStartTag($Txt, 'p:nvGraphicFramePr', $t->PosBeg, false); // pptx + $parent = clsTbsXmlLoc::FindStartTag($Txt, 'w:drawing', $t->PosBeg, false); // DOCX can embeds if inline with text, or otherwise + if ($parent===false) $parent = clsTbsXmlLoc::FindStartTag($Txt, 'p:nvGraphicFramePr', $t->PosBeg, false); // PPTX if ($parent!==false) { $parent->FindEndTag(); $src = $parent->GetInnerSrc(); @@ -3026,6 +3173,133 @@ function OpenXML_ChartGetInfoFromFile($idx, $Txt=false) { } + /** + * Unlink and eventually delete the data sheet from the chart. + * Each chart can have only 1 linked data sheet. It may be external or internal. + * Each internal data sheet can be linked to only 1 chart. So it is safe to delete the internal data sheet. + * If the chart stay linked to the old data sheet afert the merge, then the chart is automatically updated when the user attempt to edit it. This is not good. + * If the data sheet is simply unlinked, the user can open the data sheet from Word of Powerpoint. But that will not change the chart. + * If the data sheet is delete, the user cannot open the data sheet and cannot add a new data sheet. Data of the chart stay uneditable. + */ + function OpenXML_ChartUnlinklDataSheet($idx, &$Txt, $Delete) { + + if ($Delete) { + if ($loc = clsTbsXmlLoc::FindElement($Txt, 'c:externalData', 0)) { + // Delete the relationship + $rid = $loc->GetAttLazy('r:id'); + if ($rid) { + $doc = $this->TbsGetFileName($idx); + $att = 'Id="' . $rid . '"'; + $res = $this->OpenXML_Rels_DeleteRel($doc, $att, array('Target', 'TargetMode')); + // Delete the target file if embedded + if ($res && ($res['TargetMode'] != 'External')) { + $file = $this->OpenXML_GetAbsolutePath($res['Target'], $doc); + $this->FileReplace($file, false); + } + } + // Delete the element + $loc->Delete(); + } + } + + // Unlink the data sheet by deleting references + $this->XML_DeleteElements($Txt, array('c:f')); + } + + /** + * Return information and adata about all series in the chart. + */ + function OpenXML_ChartReadSeries($ChartRef, $Complete) { + + // Search the chart + $ref = $this->OpenXML_ChartFind($ChartRef, 'ChartReadSerials'); + if ($ref===false) return false; + + // Open the chart doc + $chart =& $this->OpenXmlCharts[$ref]; + + $Txt = $this->TbsStoreGet($chart['idx'], 'ChartReadSerials'); + if ($Txt===false) return false; + + // Prepare loops + $serials = array(); + + $loop_conf = array( + 'names' => array('parent' => 'c:tx', 'format' => false), + 'cat' => array('parent' => 'c:cat', 'format' => 'c:formatCode'), + 'val' => array('parent' => 'c:val', 'format' => 'c:formatCode'), + ); + + // Loop + $loop_res = array(); + $ser_p = 0; + while ($ser_loc = clsTbsXmlLoc::FindElement($Txt, 'c:ser', $ser_p)) { + $res = array(); + foreach ($loop_conf as $key => $conf) { + if ($loc_parent = clsTbsXmlLoc::FindElement($ser_loc, $conf['parent'], 0)) { + // Search format + $format = false; + if ($conf['format']) { + if ($loc = clsTbsXmlLoc::FindElement($loc_parent, $conf['format'], 0)) { + $format = $loc->GetInnerSrc(); + $res[$key . '_format'] = $format; + } + } + // Search items + // It is possible that a val item is missing for a cat idx + $items = array(); + $loc_p = 0; + while ($loc_pt = clsTbsXmlLoc::FindElement($loc_parent, 'c:pt', $loc_p)) { + $idx = $loc_pt->GetAttLazy('idx'); + $loc = clsTbsXmlLoc::FindElement($loc_pt, 'c:v', 0); + $items[$idx] = $loc->GetInnerSrc(); + $loc_p = $loc_pt->PosEnd; + } + $res[$key] = $items; + } else { + $res[$key] = false; + } + } + + // simplify name info + $names = $res['names']; + if (is_array($names) && isset($res['names'][0])) { + $res['name'] = $res['names'][0]; + } else { + $res['name'] = false; + } + if (is_array($names)) { + if (count($names) > 0) { + unset($res['names']); + } + } else { + unset($res['names']); + } + + $loop_res[] = $res; + $ser_p = $ser_loc->PosEnd; + } + + if ($Complete) { + return array( + 'file_idx' => $chart['idx'], + 'file_name' => $this->TbsGetFileName($chart['idx']), + 'parent_idx' => $chart['parent_idx'], + 'parent_name' => $this->TbsGetFileName($chart['parent_idx']), + 'series' => $loop_res, + ); + } else { + $series = array(); + foreach ($loop_res as $res) { + $series[$res['name']] = array($res['cat'], $res['val']); + } + return $series; + } + + return $loop_res; + + } + function OpenXML_SharedStrings_Prepare() { $file = 'xl/sharedStrings.xml'; @@ -3139,7 +3413,7 @@ function MsExcel_ConvertToRelative_Item(&$Txt, &$Loc, $Tag, $Att, $IsRow) { $p2 = $p + $tag_len + 2; // count the char '<' before and the char ' ' after $PosEnd = strpos($Txt, '>', $p2); clsTinyButStrong::f_Loc_PrmRead($Txt,$p2,true,'\'"','<','>',$Loc, $PosEnd, true); // read parameters - $Delete = false; + $Delete = false; if (isset($Loc->PrmPos[$Att])) { // attribute found $r = $Loc->PrmLst[$Att]; @@ -3151,10 +3425,10 @@ function MsExcel_ConvertToRelative_Item(&$Txt, &$Loc, $Tag, $Att, $IsRow) { $missing_nbr = $r - $item_num -1; if ($missing_nbr<0) { return $this->RaiseError('(Excel Consistency) error in counting items <'.$Tag.'>, found number '.$r.', previous was '.$item_num); - } elseif($IsRow && ($missing_nbr > $compat_limit_miss) && ($r >= $compat_limit_num)) { // Excel limit is 1048576 - // Useless final rows: LibreOffice add several final useless rows in the sheet when saving as XLSX. - $Delete = true; - $item_num++; + } elseif($IsRow && ($missing_nbr > $compat_limit_miss) && ($r >= $compat_limit_num)) { // Excel limit is 1048576 + // Useless final rows: LibreOffice add several final useless rows in the sheet when saving as XLSX. + $Delete = true; + $item_num++; } else { // delete the $Att attribute $pp = $Loc->PrmPos[$Att]; @@ -3173,20 +3447,20 @@ function MsExcel_ConvertToRelative_Item(&$Txt, &$Loc, $Tag, $Att, $IsRow) { $PosEnd = $PosEnd + $x_len; $x = ''; // empty the memory } - $item_num = $r; + $item_num = $r; } } else { // nothing to change the item is already relative $item_num++; } - if ($Delete) { - if (($Txt[$PosEnd-1]!=='/')) { - $x_p = strpos($Txt, $closing, $PosEnd); - if ($x_p===false) return $this->RaiseError('(Excel Consistency) closing row tag is not found.'); - $PosEnd = $x_p + strlen($closing) - 1; - } - $Txt = substr_replace($Txt, '', $p, $PosEnd - $p + 1); - } elseif ($IsRow && ($Txt[$PosEnd-1]!=='/')) { + if ($Delete) { + if (($Txt[$PosEnd-1]!=='/')) { + $x_p = strpos($Txt, $closing, $PosEnd); + if ($x_p===false) return $this->RaiseError('(Excel Consistency) closing row tag is not found.'); + $PosEnd = $x_p + strlen($closing) - 1; + } + $Txt = substr_replace($Txt, '', $p, $PosEnd - $p + 1); + } elseif ($IsRow && ($Txt[$PosEnd-1]!=='/')) { // It's a row item that may contain columns $x_p = strpos($Txt, $closing, $PosEnd); if ($x_p===false) return $this->RaiseError('(Excel Consistency) closing row tag is not found.'); @@ -3196,7 +3470,7 @@ function MsExcel_ConvertToRelative_Item(&$Txt, &$Loc, $Tag, $Att, $IsRow) { $Txt = substr_replace($Txt, $x, $PosEnd+1, $x_len0); $x_len = strlen($x); $p = $x_p + $x_len - $x_len0; - } else { + } else { $p = $PosEnd; } } @@ -3479,6 +3753,8 @@ function MsExcel_SheetInit() { $rels = array(); while ($loc=clsTbsXmlLoc::FindStartTag($Txt, 'sheet', $p, true) ) { $o = (object) null; + $o->num = $i + 1; + // SheetId is not the numbered sheet in the workbook. It may have a missing sheet id. $o->sheetId = $loc->GetAttLazy('sheetId'); $o->rid = $loc->GetAttLazy('r:id'); $o->name = $loc->GetAttLazy('name'); @@ -3505,13 +3781,17 @@ function MsExcel_SheetInit() { } - function MsExcel_SheetGet($IdOrName) { + function MsExcel_SheetGet($IdOrName, $bySheetId = false) { $this->MsExcel_SheetInit(); foreach($this->MsExcel_Sheets as $o) { if ($o->name==$IdOrName) return $o; - if ($o->sheetId==$IdOrName) return $o; + if ($bySheetId) { + if ($o->sheetId==$IdOrName) return $o; + } else { + if ($o->num==$IdOrName) return $o; + } } - return $this->RaiseError("($Caller) The sheet '$IdOrName' is not found inside the Workbook. Try command OPENTBS_DEBUG_INFO to check all sheets inside the current Workbook."); + return $this->RaiseError("(MsExcel_SheetInit) The sheet '$IdOrName' is not found inside the Workbook. Try command OPENTBS_DEBUG_INFO to check all sheets inside the current Workbook."); } /** @@ -3534,7 +3814,7 @@ function MsExcel_SheetDebug($nl, $sep, $bull) { echo $nl."-----------------------"; foreach ($this->MsExcel_Sheets as $o) { $name = str_replace(array('&','"','<','>'), array('&','"','<','>'), $o->name); - echo $bull."id: ".$o->sheetId.", name: [".$name."], state: ".$o->stateR.", file: xl/".$o->file; + echo $bull."num: ".$o->num.", id: ".$o->sheetId.", name: [".$name."], state: ".$o->stateR.", file: xl/".$o->file; } } @@ -3554,7 +3834,7 @@ function MsExcel_SheetDeleteAndDisplay() { // process sheet in reverse order of their positions foreach ($this->MsExcel_Sheets as $o) { - $zid = 'i:'.$o->sheetId; + $zid = 'i:'.$o->num; $zname = 'n:'.$o->name; // the value in the name attribute is XML protected if ( isset($this->OtbsSheetSlidesDelete[$zname]) || isset($this->OtbsSheetSlidesDelete[$zid]) ) { // Delete the sheet @@ -3708,9 +3988,14 @@ function MsPowerpoint_Clean(&$Txt) { } function MsPowerpoint_CleanRpr(&$Txt, $elem) { - $pe = 0; - while (($p=$this->XML_FoundTagStart($Txt, '<'.$elem, $pe))!==false) { - $pe = $this->XML_DeleteAttributes($Txt, $p, array('noProof', 'lang', 'err', 'smtClean', 'dirty'), array()); + $p = 0; + while ($x = clsTbsXmlLoc::FindStartTag($Txt, $elem, $p)) { + $x->DeleteAtt('noProof'); + $x->DeleteAtt('lang'); + $x->DeleteAtt('err'); + $x->DeleteAtt('smtClean'); + $x->DeleteAtt('dirty'); + $p = $x->PosEnd; } } @@ -3852,12 +4137,32 @@ function MsPowerpoint_SlideIsIt($FileName) { // Cleaning tags in MsWord function MsWord_Clean(&$Txt) { $Txt = str_replace('', '', $Txt); // faster + //$this->MsWord_CleanFallbacks($Txt); $this->XML_DeleteElements($Txt, array('w:proofErr', 'w:noProof', 'w:lang', 'w:lastRenderedPageBreak')); $this->MsWord_CleanSystemBookmarks($Txt); $this->MsWord_CleanRsID($Txt); $this->MsWord_CleanDuplicatedLayout($Txt); } + + /** + * entities may contains duplicated TBS fields and this may corrupt the merging. + * This function delete such entities if they seems to contain TBS fields. This make the DOCX content less compatible with previous Word versions. + * https://wiki.openoffice.org/wiki/OOXML/Markup_Compatibility_and_Extensibility + */ + function MsWord_CleanFallbacks(&$Txt) { + + $p = 0; + $nb = 0; + while ( ($loc = clsTbsXmlLoc::FindElement($Txt,'mc:Fallback',$p))!==false ) { + if (strpos($loc->GetSrc(), $this->TBS->_ChrOpen) !== false ) { + $loc->Delete(); + $nb++; + } + $p = $loc->PosEnd; + } + } + function MsWord_CleanSystemBookmarks(&$Txt) { // Delete GoBack hidden bookmarks that appear since Office 2010. Example: @@ -3970,30 +4275,30 @@ function MsWord_CleanDuplicatedLayout(&$Txt) { if ($wrc_p===false) return false; if ( ($wto_p<$wrc_p) && ($wtc_p<$wrc_p) ) { // if the is actually included in the element if ($first) { - // text that is concatened and can be simplified - $superflous = ''.substr($Txt, $wro_p, ($wto_p+$wto_len)-$wro_p); // without the last symbol, like: '....', '', $superflous); // tabs must not be deleted between parts => they nt be in the superflous string - $superflous_len = strlen($superflous); + // text that is concatenated and can be simplified + $superfluous = ''.substr($Txt, $wro_p, ($wto_p+$wto_len)-$wro_p); // without the last symbol, like: '....', '', $superfluous); // tabs must not be deleted between parts => they nt be in the superfluous string + $superfluous_len = strlen($superfluous); $first = false; $p_first_att = $wto_p+$wto_len; $p = strpos($Txt, '>', $wto_p); if ($p!==false) $first_att = substr($Txt, $p_first_att, $p-$p_first_att); } // if the layout is the same than the next , then we join them - $p_att = $wtc_p + $superflous_len; + $p_att = $wtc_p + $superfluous_len; $x = substr($Txt, $p_att, 1); // must be ' ' or '>' if the string is the superfluous AND the tag has or not attributes - if ( (($x===' ') || ($x==='>')) && (substr($Txt, $wtc_p, $superflous_len)===$superflous) ) { - $p_end = strpos($Txt, '>', $wtc_p+$superflous_len); // + if ( (($x===' ') || ($x==='>')) && (substr($Txt, $wtc_p, $superfluous_len)===$superfluous) ) { + $p_end = strpos($Txt, '>', $wtc_p+$superfluous_len); // if ($p_end===false) return false; // error in the structure of the tag $last_att = substr($Txt,$p_att,$p_end-$p_att); - $Txt = substr_replace($Txt, '', $wtc_p, $p_end-$wtc_p+1); // delete superflous part + attributes + $Txt = substr_replace($Txt, '', $wtc_p, $p_end-$wtc_p+1); // delete superfluous part + attributes $nbr++; $ok = true; } } } while ($ok); - // Recover the 'preserve' attribute if the last join element was having it. We check alo the first one because the attribute must not be twice. + // Recover the 'preserve' attribute if the last join element was having it. We check also the first one because the attribute must not be twice. if ( ($last_att!=='') && (strpos($first_att, $preserve)===false) && (strpos($last_att, $preserve)!==false) ) { $Txt = substr_replace($Txt, ' '.$preserve, $p_first_att, 0); } @@ -4728,15 +5033,19 @@ function OpenDoc_GetDraw($Tag, $Txt, $Pos, $Forward, $LevelStop) { return $this->XML_BlockAlias_Prefix('draw:', $Txt, $Pos, $Forward, $LevelStop); } - function OpenDoc_ChartChangeSeries($ChartRef, $SeriesNameOrNum, $NewValues, $NewLegend=false) { - + /** + * Find a chart in the template by its reference. + * Return an array of technical information about the sub-file. + */ + function OpenDoc_ChartFind($ChartRef, &$Txt, $ErrTitle) { + if ($this->OpenDocCharts===false) $this->OpenDoc_ChartInit(); // Find the chart if (is_numeric($ChartRef)) { $ChartCaption = 'number '.$ChartRef; $idx = intval($ChartRef) -1; - if (!isset($this->OpenDocCharts[$idx])) return $this->RaiseError("(ChartChangeSeries) : unable to found the chart $ChartCaption."); + if (!isset($this->OpenDocCharts[$idx])) return $this->RaiseError("($ErrTitle) : unable to found the chart $ChartCaption."); } else { $ChartCaption = 'with title "'.$ChartRef.'"'; $idx = false; @@ -4744,7 +5053,7 @@ function OpenDoc_ChartChangeSeries($ChartRef, $SeriesNameOrNum, $NewValues, $New foreach($this->OpenDocCharts as $i=>$c) { if ($c['title']==$x) $idx = $i; } - if ($idx===false) return $this->RaiseError("(ChartChangeSeries) : unable to found the chart $ChartCaption."); + if ($idx===false) return $this->RaiseError("($ErrTitle) : unable to found the chart $ChartCaption."); } $this->_ChartCaption = $ChartCaption; // for error messages @@ -4753,15 +5062,30 @@ function OpenDoc_ChartChangeSeries($ChartRef, $SeriesNameOrNum, $NewValues, $New if ($chart['to_clear']) $this->OpenDoc_ChartClear($chart); // Retrieve the XML of the data - $data_idx = $this->FileGetIdx($chart['href'].'/content.xml'); - if ($data_idx===false) return $this->RaiseError("(ChartChangeSeries) : unable to found the data in the chart $ChartCaption."); - $Txt = $this->TbsStoreGet($data_idx, 'OpenDoc_ChartChangeSeries'); + $file_name = $chart['href'] . '/content.xml'; + $file_idx = $this->FileGetIdx($file_name); + if ($file_idx===false) return $this->RaiseError("($ErrTitle) : unable to found the data in the chart $ChartCaption."); + $chart['file_name'] = $file_name; + $chart['file_idx'] = $file_idx; + + $Txt = $this->TbsStoreGet($file_idx, 'OpenDoc_ChartChangeSeries'); // Found all chart series if (!isset($chart['series'])) { $ok = $this->OpenDoc_ChartFindSeries($chart, $Txt); - if (!$ok) return; + if (!$ok) return false; } + + return $chart; + + } + + function OpenDoc_ChartChangeSeries($ChartRef, $SeriesNameOrNum, $NewValues, $NewLegend=false) { + + $Txt = false; + $chart = $this->OpenDoc_ChartFind($ChartRef, $Txt, 'ChartChangeSeries'); + if ($chart === false) return; + $series = &$chart['series']; // Found the asked series @@ -4878,7 +5202,7 @@ function OpenDoc_ChartChangeSeries($ChartRef, $SeriesNameOrNum, $NewValues, $New if ($x!=='') $Txt = substr_replace($Txt, $x, $p, 0); // Save the result - $this->TbsStorePut($data_idx, $Txt); + $this->TbsStorePut($chart['file_idx'], $Txt); } @@ -4963,9 +5287,9 @@ function OpenDoc_ChartFindSeries(&$chart, $Txt) { // Column of main value $col = $this->OpenDoc_ChartFindCol($cols, $elSeries, 'chart:values-cell-range-address', $s_idx); $s_cols[$col] = true; - // Column of series's name + // Column's num that contains the name of the series $col_name = $this->OpenDoc_ChartFindCol($cols, $elSeries, 'chart:label-cell-address', $s_idx); - // Columns for other values + // List of column's nums for other values $src = $elSeries->GetInnerSrc(); $p2 = 0; while($elDom = clsTbsXmlLoc::FindStartTag($src, 'chart:domain', $p2)) { @@ -4975,11 +5299,16 @@ function OpenDoc_ChartFindSeries(&$chart, $Txt) { } // rearrange col numbers ksort($s_cols); - $s_cols = array_keys($s_cols); // nedded for havinf first col on index 0 + $s_cols = array_keys($s_cols); // nedded for having first col on index 0 // Attribute to re-find the series $ref = $elSeries->GetAttLazy('chart:label-cell-address'); // Add the series - $series[$s_idx] = array('name'=>false, 'col_name'=>$col_name, 'cols'=>$s_cols, 'ref'=>$ref); + $series[$s_idx] = array( + 'name' => false, // name of the series + 'col_name' => $col_name, + 'cols' => $s_cols, + 'ref' => $ref, + ); $cols_name[$col_name] = $s_idx; $p = $elSeries->PosEnd; $s_idx++; @@ -5047,7 +5376,7 @@ function OpenDoc_ChartRenameSeries(&$Txt, &$series, $NewName) { $elP = clsTbsXmlLoc::FindElement($elCell, 'text:p', 0); if ($elP===false) { - $elCell->ReplaceInnerSrc($elCell->InnerSrc.''.$NewName.''); + $elCell->ReplaceInnerSrc($elCell->GetInnerSrc().''.$NewName.''); } else { if($elP->SelfClosing) { $elP->ReplaceSrc(''.$NewName.''); @@ -5059,6 +5388,81 @@ function OpenDoc_ChartRenameSeries(&$Txt, &$series, $NewName) { } + /** + * Return information and data about all series in the chart. + */ + function OpenDoc_ChartReadSeries($ChartRef, $Complete) { + + $Txt = false; + $chart = $this->OpenDoc_ChartFind($ChartRef, $Txt, 'ChartReadSeries'); + if ($chart === false) return; + + // Read the data table + $table = array(); + $rows = clsTbsXmlLoc::FindElement($Txt, 'table:table-rows', 0); + $pr = 0; + while ($r = clsTbsXmlLoc::FindElement($rows, 'table:table-row', $pr)) { + $pr = $r->PosEnd; + $pc = 0; + $row = array(); + while ($c = clsTbsXmlLoc::FindElement($r, 'table:table-cell', $pc)) { + $pc = $c->PosEnd; + $val = $c->getAttLazy('office:value'); + if ($val == 'NaN') { // Not a Number, happens when the cell is empty + $val = false; + $txt = ''; + } else { + if ($x = clsTbsXmlLoc::FindElement($c, 'text:p', 0)) { + $txt = $x->GetInnerSrc(); + } else { + $txt = false; + }; + } + $row[] = array('val' => $val, 'txt' => $txt); + } + $table[] = $row; + } + + // Format series information + $series = array(); + $cat_idx = $chart['col_cat'] - 1; + foreach ($chart['series'] as $idx => $info) { + $cat = array(); + $val = array(); + $col_idx = $info['cols'][0] - 1; + foreach ($table as $row) { + $val[] = $row[$col_idx]['val']; + $cat[] = $row[$cat_idx]['txt']; + } + $series[] = array( + 'name' => $info['name'], + 'cat' => $cat, + 'val' => $val, + ); + } + + if ($Complete) { + // Complete information about the chart + $main_idx = $this->Ext_GetMainIdx(); + return array( + 'file_idx' => $chart['file_idx'], + 'file_name' => $chart['file_name'], + 'parent_idx' => $main_idx, + 'parent_name' => $this->TbsGetFileName($main_idx), + 'series' => $series, + ); + } else { + // Simple information about data + $simple = array(); + foreach ($series as $s) { + $name = $s['name']; + $simple[$name] = array($s['cat'], $s['val']); + } + return $simple; + } + + } + function OpenDoc_ChartDebug($nl, $sep, $bull) { if ($this->OpenDocCharts===false) $this->OpenDoc_ChartInit(); @@ -5162,7 +5566,7 @@ class clsTbsXmlLoc { var $pST_PosEnd = false; // start tag: position of the end var $pST_Src = false; // start tag: source - var $pET_PosBeg = false; // end tag: position of the begining + var $pET_PosBeg = false; // end tag: position of the beginning var $Parent = false; // parent object @@ -5272,12 +5676,22 @@ function GetInnerSrc() { // Replace the inner source of the locator in the TXT contents. Update the locator's positions. // Assume FindEndTag() is previously called. + // Convert a self-closing entity to a start+end entity if needed. function ReplaceInnerSrc($new) { - $len = $this->GetInnerLen(); - if ($len===false) return false; - $this->Txt = substr_replace($this->Txt, $new, $this->pST_PosEnd + 1, $len); - $this->PosEnd += strlen($new) - $len; - $this->pET_PosBeg += strlen($new) - $len; + if ($this->SelfClosing) { + $end = '>' . $new . 'FindName() . '>'; + $this->Txt = substr_replace($this->Txt, $end, $this->PosEnd - 1, 2); + $this->SelfClosing = false; + $this->pST_PosEnd = $this->PosEnd - 1; + $this->pET_PosBeg = $this->pST_PosEnd + strlen($new) + 1; + $this->PosEnd = $this->pST_PosEnd + strlen($end) - 1; + } else { + $len = $this->GetInnerLen(); + if ($len===false) return false; + $this->Txt = substr_replace($this->Txt, $new, $this->pST_PosEnd + 1, $len); + $this->PosEnd += strlen($new) - $len; + $this->pET_PosBeg += strlen($new) - $len; + } } // Update the parent object, if any. @@ -5333,6 +5747,9 @@ function Delete($Contents=true) { } } + /** + * Return true if the attribute existed and is deleted, otherwise return false. + */ function DeleteAtt($Att) { $z = $this->_GetAttValPos($Att); if ($z===false) return false; @@ -5463,13 +5880,13 @@ static function FindStartTagByPrefix(&$Txt, $TagPrefix, $PosBeg, $Forward=true) } - // Search an element in the TXT contents, and return an object if it is found. + // Search an element in the TXT contents, and return an object if it's found. static function FindElement(&$TxtOrObj, $Tag, $PosBeg, $Forward=true) { $XmlLoc = clsTbsXmlLoc::FindStartTag($TxtOrObj, $Tag, $PosBeg, $Forward); if ($XmlLoc===false) return false; - $XmlLoc->FindEndTag('dc:creator'); + $XmlLoc->FindEndTag(); return $XmlLoc; }