diff --git a/lib/classes/task/period/recordmover.class.php b/lib/classes/task/period/recordmover.class.php
new file mode 100644
index 0000000000..f0c02d5bc2
--- /dev/null
+++ b/lib/classes/task/period/recordmover.class.php
@@ -0,0 +1,820 @@
+get_parms(
+ "period", "logsql"
+ );
+ $dom = new DOMDocument();
+ $dom->preserveWhiteSpace = false;
+ $dom->formatOutput = true;
+ if ($dom->loadXML($oldxml)) {
+ $xmlchanged = false;
+ // foreach($parm2 as $pname=>$pvalue)
+ foreach (array(
+ "str:period",
+ "boo:logsql"
+ ) as $pname) {
+ $ptype = substr($pname, 0, 3);
+ $pname = substr($pname, 4);
+ $pvalue = $parm2[$pname];
+ if (($ns = $dom->getElementsByTagName($pname)->item(0))) {
+ // field did exists, remove thevalue
+ while (($n = $ns->firstChild))
+ $ns->removeChild($n);
+ } else {
+ // field did not exists, create it
+ $dom->documentElement->appendChild($dom->createTextNode("\t"));
+ $ns = $dom->documentElement->appendChild($dom->createElement($pname));
+ $dom->documentElement->appendChild($dom->createTextNode("\n"));
+ }
+ // set the value
+ switch ($ptype) {
+ case "str":
+ $ns->appendChild($dom->createTextNode($pvalue));
+ break;
+ case "boo":
+ $ns->appendChild($dom->createTextNode($pvalue ? '1' : '0'));
+ break;
+ }
+ $xmlchanged = true;
+ }
+ }
+
+ return $dom->saveXML();
+ }
+
+ /**
+ * must fill the gui (using js) from xml
+ *
+ * @param string $xml
+ * @param form-object $form
+ * @return string "" or error message
+ */
+ public function xml2graphic($xml, $form)
+ {
+ if (($sxml = simplexml_load_string($xml))) { // in fact XML IS always valid here...
+ // ... but we could check for safe values
+ if ((int) ($sxml->period) < 10)
+ $sxml->period = 10;
+ elseif ((int) ($sxml->period) > 1440) // 1 jour
+ $sxml->period = 1440;
+
+ if ((string) ($sxml->delay) == '')
+ $sxml->delay = 0;
+ ?>
+
+
+
+
+
+
+
+
+
+
+ maxrecs = 1000;
+ $this->sxTaskSettings = @simplexml_load_string($this->getSettings());
+ if ( ! $this->sxTaskSettings) {
+ return array();
+ }
+
+ $ret = array();
+
+ $logsql = (int) ($this->sxTaskSettings->logsql) > 0;
+
+ foreach ($this->sxTaskSettings->tasks->task as $sxtask) {
+
+ if ( ! $this->running) {
+ break;
+ }
+
+ $task = $this->calcSQL($sxtask);
+
+ if ( ! $task['active']) {
+ continue;
+ }
+
+ if ($logsql) {
+ $this->log(sprintf("playing task '%s' on base '%s'"
+ , $task['name']
+ , $task['basename'] ? $task['basename'] : ''));
+ }
+
+ try {
+ $connbas = connection::getPDOConnection($task['sbas_id']);
+ } catch (Exception $e) {
+ $this->log(sprintf("can't connect sbas %s", $task['sbas_id']));
+ continue;
+ }
+
+ $stmt = $connbas->prepare($task['sql']['real']['sql']);
+ if ($stmt->execute(array())) {
+ while ($this->running && ($row = $stmt->fetch(PDO::FETCH_ASSOC)) !== FALSE) {
+
+ $tmp = array('sbas_id' => $task['sbas_id'], 'record_id' => $row['record_id'], 'action' => $task['action']);
+
+ $rec = new record_adapter($task['sbas_id'], $row['record_id']);
+ switch ($task['action']) {
+
+ case 'UPDATE':
+
+ // change collection ?
+ if (($x = (int) ($sxtask->to->coll['id'])) > 0) {
+ $tmp['coll'] = $x;
+ }
+
+ // change sb ?
+ if (($x = $sxtask->to->status['mask'])) {
+ $tmp['sb'] = $x;
+ }
+ $ret[] = $tmp;
+ break;
+
+ case 'DELETE':
+ $tmp['deletechildren'] = false;
+ if ($sxtask['deletechildren'] && $rec->is_grouping()) {
+ $tmp['deletechildren'] = true;
+ }
+ $ret[] = $tmp;
+ break;
+ }
+ }
+ $stmt->closeCursor();
+ }
+ }
+
+ return $ret;
+ }
+
+ /**
+ * work on ONE record
+ *
+ * @param appbox $appbox
+ * @param array $row
+ * @return \task_period_workflow02
+ */
+ protected function processOneContent(appbox $appbox, Array $row)
+ {
+ $logsql = (int) ($this->sxTaskSettings->logsql) > 0;
+ $dbox = databox::get_instance($row['sbas_id']);
+ $rec = new record_adapter($row['sbas_id'], $row['record_id']);
+ switch ($row['action']) {
+
+ case 'UPDATE':
+
+ // change collection ?
+ if (array_key_exists('coll', $row)) {
+ $coll = collection::get_from_coll_id($dbox, $row['coll']);
+ $rec->move_to_collection($coll, $appbox);
+ if ($logsql) {
+ $this->log(sprintf("on sbas %s move rid %s to coll %s \n", $row['sbas_id'], $row['record_id'], $coll->get_coll_id()));
+ }
+ }
+
+ // change sb ?
+ if (array_key_exists('sb', $row)) {
+ $status = str_split($rec->get_status());
+ foreach (str_split(strrev($row['sb'])) as $bit => $val) {
+ if ($val == '0' || $val == '1') {
+ $status[63 - $bit] = $val;
+ }
+ }
+ $status = implode('', $status);
+ $rec->set_binary_status($status);
+ if ($logsql) {
+ $this->log(sprintf("on sbas %s set rid %s status to %s \n", $row['sbas_id'], $row['record_id'], $status));
+ }
+ }
+ break;
+
+ case 'DELETE':
+ if ($row['deletechildren'] && $rec->is_grouping()) {
+ foreach ($rec->get_children() as $child) {
+ $child->delete();
+ if ($logsql) {
+ $this->log(sprintf("on sbas %s delete (grp child) rid %s \n", $row['sbas_id'], $child->get_record_id()));
+ }
+ }
+ }
+ $rec->delete();
+ if ($logsql) {
+ $this->log(sprintf("on sbas %s delete rid %s \n", $row['sbas_id'], $rec->get_record_id()));
+ }
+ break;
+ }
+
+ return $this;
+ }
+
+ /**
+ * all work done on processOneContent, so nothing to do here
+ *
+ * @param appbox $appbox
+ * @param array $row
+ * @return \task_period_workflow02
+ */
+ protected function postProcessOneContent(appbox $appbox, Array $row)
+ {
+ return $this;
+ }
+
+ /**
+ * compute sql for a task ( entry in settings)
+ *
+ * @param simplexml $sxtask
+ * @param boolean $playTest
+ * @return array
+ */
+ private function calcSQL($sxtask, $playTest = false)
+ {
+ $appbox = appbox::get_instance(\bootstrap::getCore());
+
+ $sbas_id = (int) ($sxtask['sbas_id']);
+
+ $ret = array(
+ 'name' => $sxtask['name'] ? (string) $sxtask['name'] : 'sans nom',
+ 'name_htmlencoded' => htmlentities($sxtask['name'] ? $sxtask['name'] : 'sans nom'),
+ 'active' => trim($sxtask['active']) === '1',
+ 'sbas_id' => $sbas_id,
+ 'basename' => '',
+ 'basename_htmlencoded' => '',
+ 'action' => strtoupper($sxtask['action']),
+ 'sql' => NULL,
+ 'err' => '',
+ 'err_htmlencoded' => '',
+ );
+
+ try {
+ $dbox = $appbox->get_databox($sbas_id);
+
+ $ret['basename'] = $dbox->get_viewname();
+ $ret['basename_htmlencoded'] = htmlentities($ret['basename']);
+ switch ($ret['action']) {
+ case 'UPDATE':
+ $ret['sql'] = $this->calcUPDATE($sbas_id, $sxtask, $playTest);
+ break;
+ case 'DELETE':
+ $ret['sql'] = $this->calcDELETE($sbas_id, $sxtask, $playTest);
+ $ret['deletechildren'] = (int) ($sxtask['deletechildren']);
+ break;
+ default:
+ $ret['err'] = "bad action '" . $ret['action'] . "'";
+ $ret['err_htmlencoded'] = htmlentities($ret['err']);
+ break;
+ }
+ } catch (Exception $e) {
+ $ret['err'] = "bad sbas '" . $sbas_id . "'";
+ $ret['err_htmlencoded'] = htmlentities($ret['err']);
+ }
+
+ return $ret;
+ }
+
+ /**
+ * compute entry for a UPDATE query
+ *
+ * @param integer $sbas_id
+ * @param simplexml $sxtask
+ * @param boolean $playTest
+ * @return array
+ */
+ private function calcUPDATE($sbas_id, &$sxtask, $playTest)
+ {
+ $tws = array(); // NEGATION of updates, used to build the 'test' sql
+ //
+ // set coll_id ?
+ if (($x = (int) ($sxtask->to->coll['id'])) > 0) {
+ $tws[] = 'coll_id!=' . $x;
+ }
+
+ // set status ?
+ $x = $sxtask->to->status['mask'];
+ $mx = str_replace(' ', '0', ltrim(str_replace(array('0', 'x'), array(' ', ' '), $x)));
+ $ma = str_replace(' ', '0', ltrim(str_replace(array('x', '0'), array(' ', '1'), $x)));
+ if ($mx && $ma)
+ $tws[] = '((status ^ 0b' . $mx . ') & 0b' . $ma . ')!=0';
+ elseif ($mx)
+ $tws[] = '(status ^ 0b' . $mx . ')!=0';
+ elseif ($ma)
+ $tws[] = '(status & 0b' . $ma . ')!=0';
+
+ // compute the 'where' clause
+ list($tw, $join) = $this->calcWhere($sbas_id, $sxtask);
+
+ // ... complete the where to buid the TEST
+ if (count($tws) == 1)
+ $tw[] = $tws[0];
+ elseif (count($tws) > 1)
+ $tw[] = '(' . implode(') OR (', $tws) . ')';
+ if (count($tw) == 1)
+ $where = $tw[0];
+ if (count($tw) > 1)
+ $where = '(' . implode(') AND (', $tw) . ')';
+
+ // build the TEST sql (select)
+ $sql_test = 'SELECT record_id FROM record' . $join;
+ if (count($tw) > 0)
+ $sql_test .= ' WHERE ' . ((count($tw) == 1) ? $tw[0] : '(' . implode(') AND (', $tw) . ')');
+
+ // build the real sql (select)
+ $sql = 'SELECT record_id FROM record' . $join;
+ if (count($tw) > 0)
+ $sql .= ' WHERE ' . ((count($tw) == 1) ? $tw[0] : '(' . implode(') AND (', $tw) . ')');
+
+ $ret = array(
+ 'real' => array(
+ 'sql' => $sql,
+ 'sql_htmlencoded' => htmlentities($sql),
+ ),
+ 'test' => array(
+ 'sql' => $sql_test,
+ 'sql_htmlencoded' => htmlentities($sql_test),
+ 'result' => NULL,
+ 'err' => NULL
+ )
+ );
+
+ if ($playTest) {
+ $ret['test']['result'] = $this->playTest($sbas_id, $sql_test);
+ }
+
+ return $ret;
+ }
+
+ /**
+ * compute entry for a DELETE task
+ *
+ * @param integer $sbas_id
+ * @param simplexml $sxtask
+ * @param boolean $playTest
+ * @return array
+ */
+ private function calcDELETE($sbas_id, &$sxtask, $playTest)
+ {
+ // compute the 'where' clause
+ list($tw, $join) = $this->calcWhere($sbas_id, $sxtask);
+
+ // build the TEST sql (select)
+ $sql_test = 'SELECT SQL_CALC_FOUND_ROWS record_id FROM record' . $join;
+ if (count($tw) > 0)
+ $sql_test .= ' WHERE ' . ((count($tw) == 1) ? $tw[0] : '(' . implode(') AND (', $tw) . ')');
+ $sql_test .= ' LIMIT 10';
+
+ // build the real sql (select)
+ $sql = 'SELECT record_id FROM record' . $join;
+ if (count($tw) > 0)
+ $sql .= ' WHERE ' . ((count($tw) == 1) ? $tw[0] : '(' . implode(') AND (', $tw) . ')');
+
+ $ret = array(
+ 'real' => array(
+ 'sql' => $sql,
+ 'sql_htmlencoded' => htmlentities($sql),
+ ),
+ 'test' => array(
+ 'sql' => $sql_test,
+ 'sql_htmlencoded' => htmlentities($sql_test),
+ 'result' => NULL,
+ 'err' => NULL
+ )
+ );
+
+ if ($playTest) {
+ $ret['test']['result'] = $this->playTest($sbas_id, $sql_test);
+ }
+
+ return $ret;
+ }
+
+ /**
+ * compute the 'where' clause
+ * returns an array of clauses to be joined by 'and'
+ * and a 'join' to needed tables
+ *
+ * @param integer $sbas_id
+ * @param simplecms $sxtask
+ * @return array
+ */
+ private function calcWhere($sbas_id, &$sxtask)
+ {
+ $connbas = connection::getPDOConnection($sbas_id);
+
+ $tw = array();
+ $join = '';
+
+ $ijoin = 0;
+
+ // criteria
+ if (($x = $sxtask->from->type['type']) !== NULL) {
+ switch (strtoupper($x)) {
+ case 'RECORD':
+ $tw[] = 'parent_record_id!=record_id';
+ break;
+ case 'STORY':
+ $tw[] = 'parent_record_id=record_id';
+ break;
+ }
+ }
+
+ // criteria
+ foreach ($sxtask->from->text as $x) {
+ $ijoin ++;
+ $comp = strtoupper($x['compare']);
+ if (in_array($comp, array('<', '>', '<=', '>=', '=', '!='))) {
+ $s = 'p' . $ijoin . '.name=\'' . $x['field'] . '\' AND p' . $ijoin . '.value' . $comp;
+ $s .= '' . $connbas->quote($x['value']) . '';
+
+ $tw[] = $s;
+ $join .= ' INNER JOIN prop AS p' . $ijoin . ' USING(record_id)';
+ } else {
+ // bad comparison operator
+ }
+ }
+
+ // criteria
+ foreach ($sxtask->from->date as $x) {
+ $ijoin ++;
+ $s = 'p' . $ijoin . '.name=\'' . $x['field'] . '\' AND NOW()';
+ $s .= strtoupper($x['direction']) == 'BEFORE' ? '<' : '>=';
+ $delta = (int) ($x['delta']);
+ if ($delta > 0)
+ $s .= '(p' . $ijoin . '.value+INTERVAL ' . $delta . ' DAY)';
+ elseif ($delta < 0)
+ $s .= '(p' . $ijoin . '.value-INTERVAL ' . -$delta . ' DAY)';
+ else
+ $s .= 'p' . $ijoin . '.value';
+
+ $tw[] = $s;
+ $join .= ' INNER JOIN prop AS p' . $ijoin . ' USING(record_id)';
+ }
+
+ // criteria
+ if (($x = $sxtask->from->coll) !== NULL) {
+ $tcoll = explode(',', $x['id']);
+ foreach ($tcoll as $i => $c)
+ $tcoll[$i] = (int) $c;
+ if ($x['compare'] == '=') {
+ if (count($tcoll) == 1) {
+ $tw[] = 'coll_id = ' . $tcoll[0];
+ } else {
+ $tw[] = 'coll_id IN(' . implode(',', $tcoll) . ')';
+ }
+ } elseif ($x['compare'] == '!=') {
+ if (count($tcoll) == 1) {
+ $tw[] = 'coll_id != ' . $tcoll[0];
+ } else {
+ $tw[] = 'coll_id NOT IN(' . implode(',', $tcoll) . ')';
+ }
+ } else {
+ // bad operator
+ }
+ }
+
+ // criteria
+ $x = $sxtask->from->status['mask'];
+ $mx = str_replace(' ', '0', ltrim(str_replace(array('0', 'x'), array(' ', ' '), $x)));
+ $ma = str_replace(' ', '0', ltrim(str_replace(array('x', '0'), array(' ', '1'), $x)));
+ if ($mx && $ma) {
+ $tw[] = '((status^0b' . $mx . ')&0b' . $ma . ')=0';
+ } elseif ($mx) {
+ $tw[] = '(status^0b' . $mx . ')=0';
+ } elseif ($ma) {
+ $tw[] = '(status&0b' . $ma . ")=0";
+ }
+
+ if (count($tw) == 1) {
+ $where = $tw[0];
+ }
+ if (count($tw) > 1) {
+ $where = '(' . implode(') AND (', $tw) . ')';
+ }
+
+ return array($tw, $join);
+ }
+
+ /**
+ * play a 'test' sql on sbas, return the number of records and the 10 first rids
+ *
+ * @param integer $sbas_id
+ * @param string $sql
+ * @return array
+ */
+ private function playTest($sbas_id, $sql)
+ {
+ $connbas = connection::getPDOConnection($sbas_id);
+ $result = array('rids' => array(), 'err' => '', 'n' => null);
+
+ $result['n'] = $connbas->query('SELECT COUNT(*) AS n FROM (' . $sql . ') AS x')->fetchColumn();
+
+ $stmt = $connbas->prepare('SELECT record_id FROM (' . $sql . ') AS x LIMIT 10');
+ if ($stmt->execute(array())) {
+ while (($row = $stmt->fetch(PDO::FETCH_ASSOC))) {
+ $result['rids'][] = $row['record_id'];
+ }
+ $stmt->closeCursor();
+ } else {
+ $result['err'] = $connbas->last_error();
+ }
+
+ return $result;
+ }
+
+ /**
+ * facility called by xhttp/jquery from interface for ex. when switching interface from gui<->xml
+ */
+ public function facility()
+ {
+ $request = http_request::getInstance();
+
+ $parm2 = $request->get_parms(
+ 'ACT', 'xml'
+ );
+
+ $ret = array('tasks' => array());
+ switch ($parm2['ACT']) {
+ case 'CALCTEST':
+ $sxml = simplexml_load_string($parm2['xml']);
+ foreach ($sxml->tasks->task as $sxtask) {
+ $ret['tasks'][] = $this->calcSQL($sxtask, false);
+ }
+ break;
+ case 'PLAYTEST':
+ $sxml = simplexml_load_string($parm2['xml']);
+ foreach ($sxml->tasks->task as $sxtask) {
+ $ret['tasks'][] = $this->calcSQL($sxtask, true);
+ }
+ break;
+ case 'CALCSQL':
+ $xml = $this->graphic2xml('');
+ $sxml = simplexml_load_string($xml);
+ foreach ($sxml->tasks->task as $sxtask) {
+ $ret['tasks'][] = $this->calcSQL($sxtask, false);
+ }
+ break;
+ }
+
+ return json_encode($ret);
+ }
+}