rangeBeginRow > -1 ? $this->rangeBeginRow : 0; } public function getRangeEndRow() { return $this->rangeEndRow > -1 ? $this->rangeEndRow : $this->getRowCount(); } public function getRangeBeginCol() { return $this->rangeBeginCol > -1 ? $this->rangeBeginCol : 0; } public function getRangeEndCol() { return $this->rangeEndCol > -1 ? $this->rangeEndCol : $this->getColumnCount(); } /** getHandlerList * Returns an array containing the list of all valid * handlers for general file import/export. * @return array. * @static */ public function getHandlerList() { return [ 'TikiSheetCSVHandler', 'TikiSheetCSVExcelHandler', ]; } /** TikiSheet * Initializes the data container. */ public function __construct() { $this->dataGrid = []; $this->calcGrid = []; $this->cellInfo = []; $this->COLCHAR = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; $this->indexes = [ $this->COLCHAR[0] => 0 ]; $this->lastIndex = 0; $this->lastID = $this->COLCHAR[0]; $this->rowCount = INITIAL_ROW_COUNT; $this->columnCount = INITIAL_COL_COUNT; $this->headerRow = 0; $this->footerRow = 0; $this->parseValues = 'n'; $this->className = ''; } /** configureLayout * Assigns the different parameters for the output * @param $className String The class that will be assigned * to the table tag of the output. * If used for an other output than * HTML, it can be used as an identifier * for the type of layout. * @param $headerRow Integer The amount of rows that are considered * as part of the header. * @param $footerRow Integer The amount of rows that are considered * as part of the footer. * @param $parseValues String Parse cell values as wiki text if ='y' * when using output handler */ public function configureLayout($className, $headerRow = 0, $footerRow = 0, $parseValues = 'n', $metadata = '') { $this->cssName = $className; $this->headerRow = $headerRow; $this->footerRow = $footerRow; $this->parseValues = $parseValues; $this->metadata = json_decode($metadata); } /** getColumnIndex * Returns the index of the column from a cell ID. * @param $id String Cell ID in [A-Z]+[0-9]+ format. * @return Integer Zero-based column index. */ public function getColumnIndex($id) { if (! preg_match("/^([A-Z]+)([0-9]+)$/", $id, $parts)) { return false; } if (! isset($this->indexes[ $parts[1] ])) { while ($this->lastID != $parts[1]) { $this->lastID = $this->increment($this->lastID); $this->lastIndex++; $this->indexes[$this->lastID] = $this->lastIndex; } return $this->lastIndex; } else { return $this->indexes[ $parts[1] ]; } } /** getRowIndex * Returns the index of the row from a cell ID. * @param $id String Cell ID in [A-Z]+[0-9]+ format. * @return Integer Zero-based row index. */ public function getRowIndex($id) { if (! preg_match("/^([A-Z]+)([0-9]+)$/", $id, $parts)) { return false; } return $parts[2] - 1; } /** equals * Determines if the value, calculation and size are equal at * certain coordinates in the current and the given sheet. * @param $sheet TikiSheet The sheet to compare. * @param $rowIndex Integer The row coordinate. * @param $columnIndex Integer The column coordinate. * @return Boolean True if all values are equal. */ public function equals(TikiSheet &$sheet, $rowIndex, $columnIndex) { if (isset($this->dataGrid[$rowIndex][$columnIndex]) && ! isset($sheet->dataGrid[$rowIndex][$columnIndex])) { return false; } if (isset($this->calcGrid[$rowIndex][$columnIndex]) && ! isset($sheet->calcGrid[$rowIndex][$columnIndex])) { return false; } $dataGrid = $this->dataGrid[$rowIndex][$columnIndex]; $calcGrid = $this->calcGrid[$rowIndex][$columnIndex]; $cellInfo = $this->cellInfo[$rowIndex][$columnIndex]; $sheetDataGrid = $sheet->dataGrid[$rowIndex][$columnIndex]; $sheetCalcGrid = $sheet->calcGrid[$rowIndex][$columnIndex]; $sheetCellInfo = $sheet->cellInfo[$rowIndex][$columnIndex]; return ( $dataGrid == $sheetDataGrid && $calcGrid == $sheetCalcGrid && $cellInfo['value'] == $sheetCellInfo['value'] && $cellInfo['calculation'] == $sheetCellInfo['calculation'] && $cellInfo['width'] == $sheetCellInfo['width'] && $cellInfo['height'] == $sheetCellInfo['height'] && $cellInfo['format'] == $sheetCellInfo['format'] && $cellInfo['style'] == $sheetCellInfo['style'] && $cellInfo['class'] == $sheetCellInfo['class'] ); } /** export * Exports the content of the calculation sheet * to the given format handler. * @param $handler The format handler. * @return True on success. */ public function export(&$handler) { return $handler->_save($this); } /** * @param $incsubs boolean Include sub-sheets * @param $date * @return String */ public function getTableHtml($incsubs = true, $date = null) { global $prefs; $sheetlib = TikiLib::lib('sheet'); $filegallib = TikiLib::lib("filegal"); $handler = new TikiSheetOutputHandler(null, ($this->parseValues == 'y' && $_REQUEST['parse'] != 'n')); $this->export($handler); $data = $handler->output; if ($incsubs == true) { //get sheets from db first foreach ($sheetlib->get_related_sheet_ids($this->id) as $childSheetId) { $handler = new TikiSheetDatabaseHandler($childSheetId, $date); $childSheet = new TikiSheet(); $childSheet->import($handler); $childSheet->parseValues = true; $data .= $childSheet->getTableHtml(false); } } foreach ($sheetlib->get_related_file_ids($this->id) as $childFileId) { $fileInfo = $filegallib->get_file_info($childFileId); switch ($fileInfo['filetype']) { case 'text/csv': $handler = new TikiSheetCSVHandler($fileInfo); break; default: $handler = false; } if (! empty($handler)) { $childSheet = new TikiSheet(); $childSheet->import($handler); $data .= $childSheet->getTableHtml(); } } foreach ($sheetlib->get_related_tracker_ids($this->id) as $childTrackerId) { $handler = new TikiSheetTrackerHandler($childTrackerId); $childSheet = new TikiSheet(); $childSheet->import($handler); $data .= $childSheet->getTableHtml(); } return $data; } /** finalize * Analyses the content of the sheet and complete the * the load. */ public function finalize() { $maxRow = 0; $maxCol = 0; $this->finalizeGrid($this->dataGrid, $maxRow, $maxCol); $this->finalizeGrid($this->calcGrid, $maxRow, $maxCol); $this->finalizeGrid($this->cellInfo, $maxRow, $maxCol, true); $this->rowCount = ($maxRow >= INITIAL_ROW_COUNT || $maxRow > 0 ? $maxRow : INITIAL_ROW_COUNT); $this->columnCount = ($maxCol >= INITIAL_COL_COUNT || $maxCol > 0 ? $maxCol : INITIAL_COL_COUNT); $base = [ 'width' => 1, 'height' => 1, 'format' => null, 'style' => '', 'class' => '' ]; for ($y = 0; $this->rowCount > $y; $y++) { for ($x = 0; $this->columnCount > $x; $x++) { if (! isset($this->dataGrid[$y])) { $this->dataGrid[$y] = []; } if (! isset($this->calcGrid[$y])) { $this->calcGrid[$y] = []; } if (! isset($this->cellInfo[$y])) { $this->cellInfo[$y] = []; } if (! isset($this->dataGrid[$y][$x])) { $this->dataGrid[$y][$x] = ''; } if (! isset($this->calcGrid[$y][$x])) { $this->calcGrid[$y][$x] = ''; } if (! isset($this->cellInfo[$y][$x])) { $this->cellInfo[$y][$x] = $base; } $this->cellInfo[$y][$x] = array_merge($base, $this->cellInfo[$y][$x]); } } return true; } /** finalizeGrid * Locates the maximal values in a grid if they are above the initial ones. * @param $grid Array The grid to scan * @param $maxRow Integer The highest row index. * @param $maxCol Integer The highest column index. * @param $addIndex Boolean value, used for merged cells, determines * if the actual value should be added when calculating * the maximal values. As merged cells use more space, * they should be considered as more cells. */ public function finalizeGrid($grid, &$maxRow, &$maxCol, $addIndex = false) { foreach ($grid as $key => $row) { $this->finalizeRow($row, $maxRow, $maxCol, $key, $addIndex); } } /** finalizeRow * Identifies the largest key in an array and set it as the new maximum. * @param $row Integer The row to scan. * @param $maxRow Integer The current maximum value of the row. * @param $maxCol Integer The current maximum value of the column. * @param $rowIndex Integer * @param $addIndex Boolean Used for merged cells. Leave value blank (false) if the current scan is not on the * merged cell grid. Other possible values are 'width' and 'height' which should be used based on which side of the * grid is being scanned. */ public function finalizeRow($row, &$maxRow, &$maxCol, $rowIndex, $addIndex = false) { $localMax = max(array_keys($row)); $total = $localMax; if ($addIndex) { $total += $row[$localMax]['width']; } if ($total > $maxCol) { $maxCol = $total; } if ($addIndex) { foreach ($row as $info) { if (isset($info['height'])) { $total = $rowIndex + $info['height']; } if ($total > $maxRow) { $maxRow = $total; } } } else { if ($rowIndex > $maxRow) { $maxRow = $rowIndex; } } } /** getColumnCount * Returns the column count. */ public function getColumnCount() { return $this->columnCount == 0 ? INITIAL_COL_COUNT : $this->columnCount; } /** getRange * Reutrns an array containing the values located in * a given range (ex: A1:B9) */ public function getRange($range) { if (preg_match('/^([A-Z]+)([0-9]+):([A-Z]+)([0-9]+)$/', strtoupper($range), $parts)) { $beginRow = $parts[2] - 1; $endRow = $parts[4] - 1; $beginCol = $this->getColumnNumber($parts[1]); $endCol = $this->getColumnNumber($parts[3]); if ($beginRow > $endRow) { $a = $endRow; $endRow = $beginRow; $beginRow = $a; } if ($beginCol > $endCol) { $a = $endCol; $endCol = $beginCol; $beginCol = $a; } $data = []; for ($row = $beginRow; $endRow + 1 > $row; $row++) { for ($col = $beginCol; $endCol + 1 > $col; $col++) { if (isset($this->dataGrid[$row]) && isset($this->dataGrid[$row][$col])) { $data[] = $this->dataGrid[$row][$col]; } } } return $data; } else { return false; } } /** setRange * Limits display (so far) * a given range (ex: A1:B9) */ public function setRange($range) { if (preg_match('/^([A-Z]+)([0-9]+):([A-Z]+)([0-9]+)$/', strtoupper($range), $parts)) { $this->rangeBeginRow = (int)$parts[2] - 1; $this->rangeEndRow = (int)$parts[4]; $this->rangeBeginCol = $this->getColumnNumber($parts[1]); $this->rangeEndCol = $this->getColumnNumber($parts[3]) + 1; } } /** getRowCount * Returns the row count. */ public function getRowCount() { return $this->rowCount; } public function name() { return $this->name; } /** import * Fills the content of the calculation sheet with * data from the given handler. * @param $handler Object The format handler. * @return True on success. */ public function import(&$handler) { $this->name = $handler->name(); if (isset($handler->id)) { $this->id = $handler->id; } $this->type = $handler->type; $this->cssName = $handler->cssName; $this->rowCount = (isset($handler->rowCount) ? $handler->rowCount : $this->rowCount); $this->columnCount = (isset($handler->columnCount) ? $handler->columnCount : $this->columnCount); $this->dataGrid = []; $this->calcGrid = []; $this->cellInfo = []; $this->errorFlag = false; set_error_handler([ &$this, "error_handler" ]); if (! $handler->_load($this) || $this->errorFlag) { restore_error_handler(); return false; } restore_error_handler(); return $this->finalize(); } /** increment * Implementation of the column ID incrementation used * on client side. * @param $val String The value to increment. * @return Integer The incremented value. */ public function increment($val) { if (empty($val)) { return substr($this->COLCHAR, 0, 1); } $n = strpos($this->COLCHAR, substr($val, -1)) + 1; if ($n < strlen($this->COLCHAR)) { return substr($val, 0, -1) . substr($this->COLCHAR, $n, 1); } else { return $this->increment(substr($val, 0, -1)) . substr($this->COLCHAR, 0, 1); } } /** initCell * Indicates the next cell that will be filled. * @param $cellID Integer The Identifier of the cell or the row index * if there are 2 parameters. * @param $col Integer The index of the column. * @return True on success. */ public function initCell($cellID, $col = null) { if ($col === null) { $this->usedRow = $this->getRowIndex($cellID); $this->usedCol = $this->getColumnIndex($cellID); } else { $this->usedRow = $cellID; $this->usedCol = $col; } return $this->usedRow !== false && $this->usedCol !== false; } /** isEmpty * Determines if the value, calculation and size are equal at * certain coordinates in the current and the given sheet. * @param $rowIndex Integer The row coordinate. * @param $columnIndex Integer The column coordinate. * @return True if all values are empty. */ public function isEmpty($rowIndex, $columnIndex) { return $this->dataGrid[$rowIndex][$columnIndex] == '' && $this->calcGrid[$rowIndex][$columnIndex] == '' && ( $this->cellInfo[$rowIndex][$columnIndex]['width'] == '' || $this->cellInfo[$rowIndex][$columnIndex]['width'] == 1 ) && ( $this->cellInfo[$rowIndex][$columnIndex]['height'] == '' || $this->cellInfo[$rowIndex][$columnIndex]['height'] == 1 ); } /** setCalculation * Assigns a calculation to the currently initialized * cell. * @param $calculation String The calculation to set. */ public function setCalculation($calculation) { $this->calcGrid[$this->usedRow][$this->usedCol]['calculation'] = $calculation; } /** setFormat * Indicates the cell's data format during display. * The format is a text identifier that matches a function * name that will be executed. */ public function setFormat($format) { if (empty($format) || ! method_exists(new TikiSheetDataFormat(), $format)) { $format = null; } $this->cellInfo[$this->usedRow][$this->usedCol]['format'] = $format; } /** setRowSpan * Sets the cell's row span * @param $rowSpan Number row span */ public function setRowSpan($rowSpan) { $this->cellInfo[$this->usedRow][$this->usedCol]["height"] = $rowSpan; } /** setSize * Sets the size of the last initialized cell. * @param $colSpan Number col span */ public function setColSpan($colSpan) { $this->cellInfo[$this->usedRow][$this->usedCol]["width"] = $colSpan; } public function setDeadCells() { $usedRow = $this->usedRow; $usedCol = $this->usedCol; $cellInfo = $this->cellInfo[$this->usedRow][$this->usedCol]; for ($y = $usedRow; $usedRow + $cellInfo['height'] > $y; $y++) { for ($x = $usedCol; $usedCol + $cellInfo['width'] > $x; $x++) { if (! ($y == $usedRow && $x == $usedCol)) { $this->createDeadCell($x, $y); } } } } /** setValue * Assigns a value to the currently initialized * cell. * @param $value String The value to set. */ public function setValue($value) { $this->dataGrid[$this->usedRow][$this->usedCol]['value'] = $value; } /** setStyle * Sets html style,if any, to the currently initialized * cell. * @param $style String The value to set. */ public function setStyle($style = '') { $this->cellInfo[$this->usedRow][$this->usedCol]['style'] = $style; } /** setClass * Sets html class, if any, to the currently initialized * cell. * @param $class String The value to set. */ public function setClass($class = '') { $this->cellInfo[$this->usedRow][$this->usedCol]['class'] = $class; } /** createDeadCell * Assigns the cell as overlapped by a wide cell. * @param $x Integer Coordinate of the cell * @param $y Integer Coordinate of the cell */ public function createDeadCell($x, $y) { $this->dataGrid[$y][$x] = null; $this->cellInfo[$y][$x] = [ "width" => 0, "height" => 0, "format" => null, "style" => "", "class" => "" ]; } /** getClass * Returns the class of a the current cell if it exist. */ public function getClass() { if (isset($this->cellInfo[$this->usedRow][$this->usedCol]['class'])) { return $this->cellInfo[$this->usedRow][$this->usedCol]['class']; } else { return ""; } } /** getColumnNumber * Returns the column number from the letter-style. */ public function getColumnNumber($letter) { $val = 0; $len = strlen($letter); for ($i = 0; $len > $i; $i++) { $pow = pow(26, $len - $i - 1); $val += $pow * ( ord($letter[$i]) - 64 ); } $val--; return $val; } /** error_handler * Callback error handler function. Used by import. * @see http://ca.php.net/set_error_handler */ public function error_handler($errno, $errstr, $errfile, $errline) { echo $errstr . ': ' . $errfile . ' (' . $errline . ')'; $this->errorFlag = true; } } /** TikiSheetDataHandler * Base data handler to link the sheet to the data * source. Before being sent as an handler, the object * must know the target location of the data if they * are required. */ class TikiSheetDataHandler { public $maxrows = 300; public $maxcols = 26; public $output = ""; public $cssName; /** name * Identifies the handler in a readable form. * @return The name of the handler. */ public function name() { trigger_error(tr("Abstract method call. %0 not defined in %1", __FUNCTION__ . '()', get_class($this)), E_USER_ERROR); } /** supports * Function to indicate the features that are supported * by the handler. * @param The feature constant. * @return true if the feature is supported. * @static * @abstract */ public function supports($feature) { trigger_error(tr("Abstract method call. %0 not defined in %1", __FUNCTION__ . '()', get_class($this)), E_USER_ERROR); } /** version * Indicates the handler's version. * @return The version number as a string. */ public function version() { trigger_error(tr("Abstract method call. %0 not defined in %1", __FUNCTION__ . '()', get_class($this)), E_USER_ERROR); } } /** TikiSheetCSVHandler * Class that stores the sheet representation in a * standard text file as a serialized PHP object. */ class TikiSheetCSVHandler extends TikiSheetDataHandler { public $file = 'php://stdout'; public $lineLen; /** Constructor * Initializes the the serializer on a file. * @param $file The file path to save or load from. */ public function __construct($fileInfo = "php://stdout", $inputEncoding = '', $outputEncoding = '', $lineLen = 1024) { $this->lineLen = $lineLen; $this->type = "file"; if (is_array($fileInfo)) { // When loading from FileGalLib $this->data = strip_tags($fileInfo['data']); $this->name = $fileInfo['name']; $this->id = $fileInfo['fileId']; } else { // when FileInfo is in fact the name of a file $this->file = $fileInfo; if ($this->file === 'php://stdout' || $this->file === 'php://output') { $this->data = ''; } else { $this->data = strip_tags(file_get_contents($this->file)); } } $this->encoding = new Encoding($inputEncoding, $outputEncoding); } // _load public function _load(TikiSheet &$sheet) { $rows = explode("\n", $this->data); for ($i = 0; $i < count($rows) && $i < $this->maxrows; $i++) { $cols = str_getcsv($rows[$i]); for ($j = 0; $j < count($cols) && $j < $this->maxcols; $j++) { $sheet->initCell($i, $j); if (! empty($cols[$j])) { if ($cols[$j][0] == '=') { $sheet->setCalculation(substr($cols[$j], 1)); } else { $sheet->setValue($cols[$j]); } } else { $sheet->setValue(""); } $sheet->setRowSpan(1); $sheet->setColSpan(1); } } if ($i >= $this->maxrows || $j >= $this->maxcols) { $this->truncated = true; } return true; } // _save public function _save(&$sheet) { $total = []; ksort($sheet->dataGrid); foreach ($sheet->dataGrid as $row) { if (is_array($row)) { ksort($row); $values = (array)$row; array_walk($values, function (&$item) { if (is_array($item) && array_key_exists('value', $item)) { $item = $item['value']; } }); $total[] = implode(",", $values); } } if (is_array($total)) { $total = implode("\n", $total); } $total = $this->encoding->convert_encoding($total); if ($this->file === 'php://stdout' || $this->file === 'php://output') { $this->output = $total; return true; } else { if ($file = @fopen($this->file, "w")) { if (! @fwrite($file, $total)) { return false; } @fclose($file); return true; } else { return false; } } } // name public function name() { return tr("CSV File (commas)"); } // supports public function supports($type) { return ( ( TIKISHEET_SAVE_DATA | TIKISHEET_LOAD_DATA ) & $type ) > 0; } // version public function version() { return "1.0"; } } /** TikiSheetTrackerHandler * Class that stores the sheet representation in a * standard text file as a serialized PHP object. */ class TikiSheetTrackerHandler extends TikiSheetDataHandler { public $file; public $lineLen; /** Constructor * Initializes the the serializer on a file. * @param $file The file path to save or load from. */ public function __construct($trackerId) { $tikilib = TikiLib::lib('tiki'); $trklib = TikiLib::lib("trk"); $this->id = $trackerId; $this->def = Tracker_Definition::get($trackerId); $this->info = $this->def->getInformation(); $this->type = "tracker"; $this->cssName = 'readonly'; } // _load public function _load(&$sheet) { $tikilib = TikiLib::lib('tiki'); $i = 0; $trackerName = $this->info['name']; $tracker = Tracker_Query::tracker($trackerName) ->byName() ->excludeDetails() ->render(false) ->query(); foreach ($tracker as $item) { $j = 0; foreach ($item as $key => $field) { $sheet->initCell($i, $j); if (! empty($field[0]) && $field[0] == '=') { $sheet->setCalculation(substr($field, 1)); } $sheet->setValue($i == 0 ? $key : $field); $sheet->setColSpan(1, 1); $j++; } $i++; } return true; } // _save public function _save(&$sheet) { return false; } // name public function name() { return $this->info['name']; } // supports public function supports($type) { return ( ( TIKISHEET_LOAD_DATA ) & $type ) > 0; } // version public function version() { return "1.0"; } } /** TikiSheetTrackerHandler * Class that stores the sheet representation in a * standard text file as a serialized PHP object. */ class TikiSheetSimpleArrayHandler extends TikiSheetDataHandler { public $values = []; public function __construct($simpleArray = []) { $this->values = $simpleArray['values']; $this->name = $simpleArray['name']; $this->type = "simpleArray"; $this->cssName = 'readonly'; } // _load public function _load(&$sheet) { $i = 0; foreach ($this->values as $row) { $j = 0; foreach ($row as $key => $col) { $sheet->initCell($i, $j); if (! empty($col[0]) && $col[0] == '=') { $sheet->setCalculation(substr($col, 1)); } if (is_array($col)) { foreach ($col as $colKey => $val) { if (empty($val)) { array_splice($col, $colKey, 1); } } $col = implode(",", $col); } $col = htmlspecialchars($col); $sheet->setValue($i == 0 ? $key : $col); $sheet->setColSpan(1, 1); $j++; } $i++; } return true; } // _save public function _save(&$sheet) { return false; } // name public function name() { return $this->name; } // supports public function supports($type) { return ( ( TIKISHEET_LOAD_DATA ) & $type ) > 0; } // version public function version() { return "1.0"; } } /** TikiSheetCSVExcelHandler * Class that stores the sheet representation in a * standard text file as a serialized PHP object. The difference * betwen this and standard CSV is that fields here are separarated by ';' */ class TikiSheetCSVExcelHandler extends TikiSheetDataHandler { public $file; public $lineLen; /** Constructor * Initializes the the serializer on a file. * @param $file The file path to save or load from. */ public function __construct($file = "php://stdout", $inputEncoding = '', $outputEncoding = '', $lineLen = 1024) { $this->file = $file; $this->lineLen = $lineLen; $this->data = strip_tags(file_get_contents($this->file)); $this->encoding = new Encoding($inputEncoding, $outputEncoding); } // _load public function _load(&$sheet) { $rows = explode("\n", $this->data); for ($i = 0; $i < count($rows) && $i < $this->maxrows; $i++) { $cols = preg_split("/[,;](?!(?:[^\\\",;]|[^\\\"],[^\\\"])+\\\")/", $rows[$i]); for ($j = 0; $j < count($cols) && $j < $this->maxcols; $j++) { $sheet->initCell($i, $j); $sheet->setValue($cols[$j]); if (isset($cols[$j])) { if (strlen($cols[$j])) { if ($cols[$j][0] == '=') { $sheet->setCalculation(substr($cols[$j], 1)); } } } $sheet->setColSpan(1, 1); } } if ($i >= $this->maxrows || $j >= $this->maxcols) { $this->truncated = true; } return true; } // _save public function _save(&$sheet) { $total = []; foreach ($sheet->dataGrid as $row) { $values = (array)$row; array_walk($values, function (&$item) { if (is_array($item) && array_key_exists('value', $item)) { $item = $item['value']; } }); $total[] = implode(';', $values); } if (is_array($total)) { $total = implode("\n", $total); } $total = $this->encoding->convert_encoding($total); if ($this->file == "php://stdout") { $this->output = $total; return true; } else { if ($file = @fopen($this->file, "w")) { if (! @fwrite($file, $total)) { return false; } @fclose($file); return true; } else { return false; } } } // name public function name() { return tr("CSV-Excel File (semicolons)"); } // supports public function supports($type) { return ( ( TIKISHEET_SAVE_DATA | TIKISHEET_LOAD_DATA ) & $type ) > 0; } // version public function version() { return "1.0"; } private function fputcsvexcel($row, $fd = ';', $quot = '"', $limit = null) { $str = ''; $i = 0; foreach ($row as $col) { if ($i && $i < $limit) { $cell = ($col->formula ? $col->formula : $col->value); str_replace( [$quot, "\n"], [$quot . $quot, ''], $cell ); if (strchr($cell, $fd)) { $str .= $quot . $cell . $quot . $fd; } else { $str .= $cell . $fd; } } $i++; } return $str; } } /** TikiSheetDatabaseHandler * Class to handle transactions with the database. * The class and database structure allow data * rollbacks. The class does not allow to manipulate * the sheets themselves. The data will only be filled * and extracted based on the given sheet ID. As a default * value, the most recent entries will be read. * * The database loader will also select the appropriate * layout based on the timestamped database entries. Using * the database handler will not require to specify manually * using TikiSheet::configureLayout() as it is required by all * other known handler as this comment is being written. */ class TikiSheetDatabaseHandler extends TikiSheetDataHandler { public $id; public $readDate; public $rowCount; public $columnCount; public $metadata; /** Constructor * Assigns a sheet ID to the handler. * @param $id Integer The ID of the sheet in the database. * @param $date Integer The database link to use. */ public function __construct($id, $date = null, $metadata = null) { $sheetlib = TikiLib::lib('sheet'); $this->id = $id; $this->readDate = ( $date ? $date : time() ); $info = $sheetlib->get_sheet_info($this->id); $this->type = "sheet"; $this->name = $info['title']; $this->metadata = $metadata; } // _load public function _load(TikiSheet &$sheet) { $sheetlib = TikiLib::lib('sheet'); $tikilib = TikiLib::lib('tiki'); $result = $tikilib->query(" SELECT `rowIndex`, `columnIndex`, `value`, `calculation`, `width`, `height`, `format`, `style`, `class`, `user` FROM `tiki_sheet_values` WHERE `sheetId` = ? AND ? >= `begin` AND ( `end` IS NULL OR `end` > ? ) ", [ $this->id, (int)$this->readDate, (int)$this->readDate ]); while ($row = $result->fetchRow()) { $sheet->initCell($row['rowIndex'], $row['columnIndex']); $sheet->setValue($row['value']); $sheet->setCalculation($row['calculation']); $sheet->setColSpan($row['width']); $sheet->setRowSpan($row['height']); $sheet->setDeadCells(); $sheet->setFormat($row['format']); $sheet->setStyle($row['style']); $sheet->setClass($row['class']); } // Fetching the layout informations. $result2 = $tikilib->query(" SELECT `className`, `headerRow`, `footerRow`, `parseValues`, `metadata` FROM `tiki_sheet_layout` WHERE `sheetId` = ? AND ? >= `begin` AND ( `end` IS NULL OR `end` > ? ) ", [ $this->id, (int)$this->readDate, (int)$this->readDate ]); if ($row = $result2->fetchRow()) { $sheet->configureLayout($row['className'], $row['headerRow'], $row['footerRow'], $row['parseValues'], $row['metadata']); } return true; } public function name() { return $this->name; } // _save public function _save(TikiSheet &$sheet) { global $user, $prefs; $tikilib = TikiLib::lib('tiki'); // Load the current database state {{{3 $current = new TikiSheet(); $handler = new TikiSheetDatabaseHandler($this->id, null, $this->metadata); $current->import($handler); // Find differences {{{3 $mods = []; for ($row = 0; $sheet->getRowCount() > $row; $row++) { for ($col = 0; $sheet->getColumnCount() > $col; $col++) { if (! $sheet->equals($current, $row, $col)) { $mods[] = [ "row" => $row, "col" => $col ]; } } } $stamp = time(); $inserts = []; $updates = []; $updates[] = $stamp; $updates[] = $this->id; // Update the database {{{3 if (is_array($mods)) { foreach ($mods as $coord) { extract($coord); $value = $sheet->dataGrid[$row][$col]['value']; $calc = $sheet->calcGrid[$row][$col]['calculation']; $width = $sheet->cellInfo[$row][$col]['width']; $height = $sheet->cellInfo[$row][$col]['height']; $format = $sheet->cellInfo[$row][$col]['format']; $style = $sheet->cellInfo[$row][$col]['style']; $class = $sheet->cellInfo[$row][$col]['class']; $updates[] = $row; $updates[] = $col; //Now that sheets have styles, many things can change and the cell not have a value. //if ( !$sheet->isEmpty( $row, $col ) ) $inserts[] = [ (int)$this->id, $stamp, $row, $col, $value, $calc, $width, $height, $format, $style, $class, $user ]; } } $updates[] = $sheet->getRowCount(); $updates[] = $sheet->getColumnCount(); $conditions = str_repeat("( rowIndex = ? AND columnIndex = ? ) OR ", ( count($updates) - 4 ) / 2); if ($prefs['feature_actionlog'] == 'y') { // must keep the previous value to do the difference $query = "SELECT `rowIndex`, `columnIndex`, `value`, `style`, `class` FROM `tiki_sheet_values` WHERE `sheetId` = ? AND `end` IS NULL"; $result = $tikilib->query($query, [$this->id]); $old = []; while ($row = $result->fetchRow()) { $old[$row['rowIndex'] . '-' . $row['columnIndex']] = $row['value']; $old[$row['rowIndex'] . '-' . $row['columnIndex']]['style'] = $row['style']; $old[$row['rowIndex'] . '-' . $row['columnIndex']]['class'] = $row['class']; } $tikilib->query("UPDATE `tiki_sheet_layout` SET `metadata` = ? WHERE `sheetId` = ?", [$handler->metadata, $handler->id]); } $tikilib->query("UPDATE `tiki_sheet_values` SET `end` = ? WHERE `sheetId` = ? AND `end` IS NULL AND ( {$conditions}`rowIndex` >= ? OR `columnIndex` >= ? )", $updates); if (count($inserts) > 0) { foreach ($inserts as $values) { $tikilib->query("INSERT INTO `tiki_sheet_values` (`sheetId`, `begin`, `rowIndex`, `columnIndex`, `value`, `calculation`, `width`, `height`, `format`, `style`, `class`, `user` ) VALUES( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? )", $values); } } if ($prefs['feature_actionlog'] == 'y') { $logslib = TikiLib::lib('logs'); $add = 0; $del = 0; foreach ($inserts as $values) { $add += strlen($values[4]); if (! empty($old[$values[2] . '-' . $values[3]])) { $del += strlen($old[$values[2] . '-' . $values[3]]); } } if ($prefs['feature_contribution'] == 'y' && isset($_REQUEST['contributions'])) { $contributionlib = TikiLib::lib('contribution'); $contributionlib->assign_contributions($_REQUEST['contributions'], $this->id, 'sheet', '', '', ''); } if (isset($_REQUEST['contributions'])) { $logslib->add_action('Updated', $this->id, 'sheet', "add=$add&del=$del&sheetId=" . $this->id, '', '', '', '', $_REQUEST['contributions']); } else { $logslib->add_action('Updated', $this->id, 'sheet', "add=$add&del=$del&sheetId=" . $this->id); } } // }}}3 return true; } /** setReadDate * Modifies the instant at which the snapshot of the * database is taken. * @param $timestamp A unix timestamp. */ public function setReadDate($timestamp) { $this->readDate = $timestamp; } // supports public function supports($type) { return ( ( TIKISHEET_SAVE_DATA | TIKISHEET_SAVE_CALC | TIKISHEET_SAVE_CELL | TIKISHEET_SAVE_FORMAT | TIKISHEET_LOAD_DATA | TIKISHEET_LOAD_CALC | TIKISHEET_LOAD_CELL | TIKISHEET_LOAD_FORMAT ) & $type ) > 0; } // version public function version() { return "1.0"; } } /** TikiSheetOpenOfficeHandler * Class to generate OpenOffice sxc documents. */ class TikiSheetOpenOfficeHandler extends TikiSheetDataHandler { /** Constructor * Does nothing special. */ public function __construct($file = "php://stdout") { } // _save public function _save(&$sheet) { // Get rid of debug output ob_start(); APIC::import("org.apicnet.io.OOo.objOOo.OOoTable"); $OOoCalc = APIC::loadClass("org.apicnet.io.OOo.OOoDoc"); $OOoCalc->newCalc(); $OOoCalc->setName("export.sxc"); $OOoCalc->meta->setCreator("TikiSheet"); $OOoCalc->meta->setTitle(tr("TikiSheet Export")); $OOoCalc->content->addFeuille(); foreach ($sheet->dataGrid as $rowIndex => $row) { foreach ($row as $columnIndex => $value) { $OOoCalc->content->addcellData($rowIndex + 1, $columnIndex + 1, ["DATA" => $value['value']]); } } $OOoCalc->save(); $OOoCalc->close(); ob_end_clean(); $OOoCalc->download(); return true; } // name public function name() { return tr("OpenOffice.org File"); } // supports public function supports($type) { return ( ( TIKISHEET_SAVE_DATA ) & $type ) > 0; } // version public function version() { return "0.1-dev"; } } /** TikiSheetWikiTableHandler * Class that stores the sheet representation in a * standard text file as a serialized PHP object. */ class TikiSheetWikiTableHandler extends TikiSheetDataHandler { public $pageName; /** Constructor * Initializes the the serializer on a wiki page * @param $file The name of the wiki page to perform actions on. */ public function __construct($pageName) { $this->pageName = $pageName; } // _load public function _load(TikiSheet &$sheet) { $tikilib = TikiLib::lib('tiki'); $result = $tikilib->query("SELECT `data` FROM `tiki_pages` WHERE `pageName` = ?", [ $this->pageName ]); if ($row = $result->fetchRow()) { $tables = $this->getRawTables($row['data']); $row = 0; foreach ($tables as $table) { $table = explode("\n", $table); foreach ($table as $line) { $line = explode('|', trim($line)); foreach ($line as $col => $cellValue) { $value = $cellValue['value']; $sheet->initCell($row, $col); $sheet->setValue($value); if (isset($value)) { if (preg_match("/^::(.*)::$/", $value, $matches)) { $sheet->setClass($sheet->getClass() . " styleCenter"); $value = $matches[1]; $sheet->setValue($value); } if (preg_match("/^__(.*)__$/", $value, $matches)) { $sheet->setClass($sheet->getClass() . " styleBold"); $value = $matches[1]; $sheet->setValue($value); } if (preg_match("/^''(.*)''$/", $value, $matches)) { $sheet->setClass($sheet->getClass() . " styleItalic"); $value = $matches[1]; $sheet->setValue($value); } if (strlen($value)) { if ($value[0] == '=') { $sheet->setCalculation(substr($value, 1)); } else { $sheet->setCalculation($value); } } } $sheet->setRowSpan(1); $sheet->setColSpan(1); $sheet->setDeadCells(); } ++$row; } } return true; } else { return false; } } /** getRawTables * Returns an array containing all table-like structures * in the wiki-content. */ public function getRawTables($data) { $pos = 0; $tables = []; while (true) { // Keep looping if (( $begin = strpos($data, '||', $pos) ) === false) { break; }; if (( $end = strpos($data, '||', $begin + 2) ) === false) { break; } $pos = $end + 2; $content = substr($data, $begin + 2, $end - $begin - 2); if (strpos($content, '|') !== false) { $tables[] = $content; } } return $tables; } // name public function name() { return tr("CSV File"); } // supports public function supports($type) { return ( TIKISHEET_LOAD_DATA & $type ) > 0; } // version public function version() { return "0.1-dev"; } } /** TikiSheetOutputHandler * Class to output the data sheet as a standard HTML table. * Importing is not supported. */ class TikiSheetOutputHandler extends TikiSheetDataHandler { public $heading; public $parseOutput; /** Constructor * Identifies the caption of the table if it applies. * @param $heading The heading * @param $parseOutput Parse wiki markup in cells if parseValues=y in sheet layout */ public function __construct($heading = null, $parseOutput = true) { $this->heading = $heading; $this->parseOutput = $parseOutput; } // _save public function _save(TikiSheet &$sheet) { // if ( $sheet->headerRow + $sheet->footerRow > $sheet->getRowCount() ) // return false; $beginRow = $sheet->getRangeBeginRow(); $endRow = $sheet->getRangeEndRow(); $beginCol = $sheet->getRangeBeginCol(); $endCol = $sheet->getRangeEndCol(); if ( $beginRow > -1 && $beginRow == $endRow - 1 && $beginCol == $endCol - 1 ) { if (isset($sheet->dataGrid[$beginRow][$beginCol])) { $data = $sheet->dataGrid[$beginRow][$beginCol]; if ($sheet->parseValues == 'y' && mb_ereg_match('[^A-Za-z0-9\s]', $data)) { // needs to be multibyte regex here global $tikilib; $data = TikiLib::lib('parser')->parse_data($data, ['suppress_icons' => true]); } $this->output = $data; return true; } } $class = empty($sheet->cssName) ? "" : " class='{$sheet->cssName}'"; $id = empty($sheet->id) ? '' : " data-id='{$sheet->id}'"; $title = " title='" . htmlspecialchars($sheet->name(), ENT_QUOTES) . "'"; $type = (! empty($sheet->type) ? ' data-type="' . $sheet->type . '" ' : ''); $this->output = "\n"; if (! is_null($this->heading)) { $this->output .= " {$this->heading}\n"; } if ($sheet->headerRow > 0 && $beginRow < 0) { $this->output .= " \n"; $this->drawRows($sheet); $this->output .= " \n"; } $this->output .= " \n"; $this->drawCols($sheet); $this->output .= " \n"; $this->output .= " \n"; $this->drawRows($sheet); $this->output .= " \n"; if ($sheet->footerRow > 0 && $beginRow < 0) { $this->output .= " \n"; $this->drawRows($sheet); $this->output .= " \n"; } $this->output .= "\n"; return true; } /** drawRows * Draws out a defined set of rows from the sheet. * @param $sheet The data container. * @param $begin The index of the begining row. (included) * @param $end The index of the end row (excluded) */ public function drawRows(TikiSheet &$sheet) { $sheetlib = TikiLib::lib('sheet'); $beginRow = $sheet->getRangeBeginRow(); $endRow = $sheet->getRangeEndRow(); $beginCol = $sheet->getRangeBeginCol(); $endCol = $sheet->getRangeEndCol(); for ($i = $beginRow; $i < $endRow; $i++) { $td = ""; $trStyleHeight = ""; $trHeight = "20px"; $trHeightIsSet = false; for ($j = $beginCol; $j < $endCol; $j++) { $width = $height = ''; if (! empty($sheet->cellInfo[$i][$j])) { extract($sheet->cellInfo[$i][$j]); } $append = ''; if ($width > 1) { $append .= " colspan='{$width}'"; } if ($height > 1) { $append .= " rowspan='{$height}'"; } if (! empty($sheet->calcGrid[$i][$j])) { $append .= ' data-formula="=' . str_replace('"', "'", $sheet->calcGrid[$i][$j]['calculation']) . '"'; } if (isset($sheet->dataGrid[$i][$j]['value'])) { $data = $sheet->dataGrid[$i][$j]['value']; } else { $data = ''; } $format = $sheet->cellInfo[$i][$j]['format']; if (! empty($format)) { $data = TikiSheetDataFormat::$format($data); } $style = $sheet->cellInfo[$i][$j]['style']; if (! empty($style)) { //we have to sanitize the css style here $tdStyle = ""; $color = $sheetlib->get_attr_from_css_string($style, "color", ""); $bgColor = $sheetlib->get_attr_from_css_string($style, "background-color", ""); $tdHeight = ''; if ($trHeightIsSet == false) { $trHeight = $sheetlib->get_attr_from_css_string($style, "height", "20px"); $trHeightIsSet = true; } if ($color) { $tdStyle .= "color:$color;"; } if ($bgColor) { $tdStyle .= "background-color:$bgColor;"; } $tdHeight = $trHeight; if ($tdHeight) { $tdStyle .= "height:$tdHeight;"; $append .= " height='" . str_replace("px", "", $tdHeight) . "'"; } $append .= " style='$tdStyle'"; } $class = $sheet->cellInfo[$i][$j]['class']; if (! empty($class)) { $append .= ' class="' . $class . '"'; } if ($this->parseOutput && $sheet->parseValues == 'y') { global $tikilib; // only parse if we have non-alphanumeric or space chars if (mb_ereg_match('[^A-Za-z0-9\s]', $data)) { // needs to be multibyte regex here $data = TikiLib::lib('parser')->parse_data($data, ['suppress_icons' => true]); } if (strpos($data, '

') === 0) { // remove containing

tag $data = substr($data, 3); if (strrpos($data, '

') === strlen($data) - 4) { $data = substr($data, 0, -4); } } } $td .= " $data\n"; } if (! empty($td)) { $this->output .= " \n"; $this->output .= $td; $this->output .= " \n"; } } } /** drawCols * Draws out a defined set of rows from the sheet. * @param $sheet The data container. * @param $begin The index of the begining row. (included) * @param $end The index of the end row (excluded) */ public function drawCols(TikiSheet &$sheet) { $sheetlib = TikiLib::lib('sheet'); if (isset($sheet->metadata) && isset($sheet->metadata->widths)) { foreach ($sheet->metadata->widths as $width) { $this->output .= ""; } } else { $beginCol = $sheet->getRangeBeginCol(); $endCol = $sheet->getRangeEndCol(); for ($i = $beginCol; $i < $endCol; $i++) { $style = $sheet->cellInfo[0][$i]['style']; $width = $sheetlib->get_attr_from_css_string($style, "width", "118px"); $this->output .= "\n"; } } } // supports public function supports($type) { return ( ( TIKISHEET_SAVE_DATA | TIKISHEET_SAVE_CELL | TIKISHEET_SAVE_FORMAT ) & $type ) > 0; } // version public function version() { return "1.0"; } } /** TikiSheetLabeledOutputHandler * Class to output the data sheet as a standard HTML table. * Importing is not supported. */ class TikiSheetLabeledOutputHandler extends TikiSheetDataHandler { /** Constructor */ public function __construct() { } // _save public function _save(&$sheet) { $this->output = "\n"; $this->output .= " \n"; $this->output .= " \n"; $prev = 'A'; for ($j = 0; $sheet->getColumnCount() > $j; $j++) { $this->output .= " \n"; $prev = $sheet->increment($prev); } $this->output .= " \n"; $this->output .= " \n"; $this->output .= " \n"; $this->drawRows($sheet); $this->output .= " \n"; $this->output .= "
$prev
\n"; return true; } /** drawRows * Draws out a defined set of rows from the sheet. * @param $sheet The data container. * @param $begin The index of the begining row. (included) * @param $end The index of the end row (excluded) */ public function drawRows(&$sheet) { $sheetlib = TikiLib::lib('sheet'); $beginRow = $sheet->getRangeBeginRow(); $endRow = $sheet->getRangeEndRow(); $beginCol = $sheet->getRangeBeginCol(); $endCol = $sheet->getRangeEndCol(); for ($i = $beginRow; $i < $endRow; $i++) { $trHeight = "20px"; $td = ''; for ($j = $beginCol; $j < $endCol; $j++) { $width = $height = ""; extract($sheet->cellInfo[$i][$j]); $append = ""; if (empty($width) || empty($height) || $width == 0 || $height == 0) { continue; } if ($width > 1) { $append .= " colspan='{$width}'"; } if ($height > 1) { $append .= " rowspan='{$height}'"; } if (isset($sheet->dataGrid[$i][$j])) { $data = $sheet->dataGrid[$i][$j]; } else { $data = ''; } $format = $sheet->cellInfo[$i][$j]['format']; if (! empty($format)) { $data = TikiSheetDataFormat::$format($data); } $style = $sheet->cellInfo[$i][$j]['style']; if (! empty($style)) { $append .= " style='{$style}'"; $trHeight = $sheetlib->get_attr_from_css_string($style, "height", "20px"); } $class = $sheet->cellInfo[$i][$j]['class']; if (! empty($class)) { $append .= " class='{$class}'"; } $td .= " $data\n"; } $tr = " " . ($i + 1) . "\n"; $tr .= $td; $tr .= " \n"; $this->output .= $tr; } } // supports public function supports($type) { return ( ( TIKISHEET_SAVE_DATA | TIKISHEET_SAVE_CELL | TIKISHEET_SAVE_FORMAT ) & $type ) > 0; } // version public function version() { return "1.0"; } } /** TikiSheetHTMLTableHandler * Class that imports a sheet from an HTML table * Designed to be used with jQuery.sheet.save_sheet */ class TikiSheetHTMLTableHandler extends TikiSheetDataHandler { public $data; /** Constructor * Initializes the the serializer on a wiki page * @param $file The name of the wiki page to perform actions on. */ public function __construct($json) { $this->data = $json; } // _load public function _load(TikiSheet &$sheet) { $d = $this->data; foreach ($d->metadata->widths as $c => $width) { } foreach ($d->rows as $r => $row) { foreach ($row->columns as $c => $column) { $sheet->initCell($r, $c); //if cell has formula, use it, otherwise, use value, if value if blank, use '' if (! empty($column->formula)) { $sheet->setCalculation($column->formula); } else { $sheet->setValue(isset($column->value) ? $column->value : ''); } //Make cell able to span multi columns and rows $rowSpan = 1; $colSpan = 1; if (isset($column->rowspan)) { $rowSpan = $column->rowspan; } if (isset($column->colspan)) { $colSpan = $column->colspan; } $sheet->setRowSpan($rowSpan); $sheet->setColSpan($colSpan); $sheet->setDeadCells(); //setup cell css style if (! empty($column->style)) { $sheet->setStyle($column->style); } //setup cell html class if (isset($column->class)) { $sheet->setClass($column->class); } } } return true; } // name public function name() { return tr("HTML Table"); } // supports public function supports($type) { return ( TIKISHEET_LOAD_DATA & $type ) > 0; } // version public function version() { return "1.0"; } }