diff --git a/data-sources/datasource.remote.php b/data-sources/datasource.remote.php
index b628381..39a2612 100644
--- a/data-sources/datasource.remote.php
+++ b/data-sources/datasource.remote.php
@@ -1,810 +1,831 @@
dsParamNAMESPACES;
- $settings[self::getClass()]['url'] = $this->dsParamURL;
- $settings[self::getClass()]['xpath'] = isset($this->dsParamXPATH) ? $this->dsParamXPATH : '/';
- $settings[self::getClass()]['cache'] = isset($this->dsParamCACHE) ? $this->dsParamCACHE : 30;
- $settings[self::getClass()]['format'] = isset($this->dsParamFORMAT) ? $this->dsParamFORMAT : 'xml';
- $settings[self::getClass()]['timeout'] = isset($this->dsParamTIMEOUT) ? $this->dsParamTIMEOUT : 6;
-
- return $settings;
- }
-
- /**
- * This methods allows custom remote data source to set other
- * properties on the HTTP gateway, like Authentication or other
- * parameters. This method is call just before the `exec` method.
- *
- * @param Gateway $gateway
- * the Gateway object that will be use for the current HTTP request
- * passed by reference
- */
- public static function prepareGateway(&$gateway) {}
-
- /**
- * This methods allows custom remote data source to read the returned
- * data before it becomes only available in the XML.
- *
- * @since Remote Datasource 2.0
- * @param string $data
- * the parsed xml string data returned by the Gateway by reference
- */
- public function exposeData(&$data) {}
-
- /*-------------------------------------------------------------------------
- Utilities
- -------------------------------------------------------------------------*/
-
- /**
- * Returns the source value for display in the Datasources index
- *
- * @param string $file
- * The path to the Datasource file
- * @return string
- */
- public static function getSourceColumn($handle) {
- $datasource = DatasourceManager::create($handle, array(), false);
-
- if(isset($datasource->dsParamURL)) {
- return Widget::Anchor(str_replace('http://www.', '', $datasource->dsParamURL), $datasource->dsParamURL);
- }
- else {
- return 'Remote Datasource';
- }
- }
-
- /**
- * Given a `$url` and `$timeout`, this function will use the `Gateway`
- * class to determine that it is a valid URL and returns successfully
- * before the `$timeout`. If it does not, an error message will be
- * returned, otherwise true.
- *
- * @todo This function is a bit messy, could be revisited.
- * @param string $url
- * @param integer $timeout
- * If not provided, this will default to 6 seconds
- * @param boolean $fetch_URL
- * Defaults to false, but when set to true, this function will use the
- * `Gateway` class to attempt to validate the URL's existence and it
- * returns before the `$timeout`
- * @param string $format
- * The format that the URL will return, either JSON or XML. Defaults
- * to 'xml' which will send the appropriate ACCEPTs header.
- * @return string|array
- * Returns an array with the 'data' if it is a valid URL, otherwise a string
- * containing an error message.
- */
- public static function isValidURL($url, $timeout = 6, $format = 'xml', $fetch_URL = false) {
- // Check that URL was provided
- if(trim($url) == '') {
- return __('This is a required field');
- }
- // Check to see the URL works.
- else if ($fetch_URL === true) {
- $gateway = new Gateway;
- $gateway->init($url);
- $gateway->setopt('TIMEOUT', $timeout);
-
- // Set the approtiate Accept: headers depending on the format of the URL.
- if($format == 'xml') {
- $gateway->setopt('HTTPHEADER', array('Accept: text/xml, */*'));
- }
- else if($format == 'json') {
- $gateway->setopt('HTTPHEADER', array('Accept: application/json, */*'));
- }
- else if($format == 'csv') {
- $gateway->setopt('HTTPHEADER', array('Accept: text/csv, */*'));
- }
-
- static::prepareGateway($gateway);
-
- $data = $gateway->exec();
- $info = $gateway->getInfoLast();
-
- // 28 is CURLE_OPERATION_TIMEOUTED
- if(isset($info['curl_error']) && $info['curl_error'] == 28) {
- return __('Request timed out. %d second limit reached.', array($timeout));
- }
- else if($data === false || $info['http_code'] != 200) {
- return __('Failed to load URL, status code %d was returned.', array($info['http_code']));
- }
- }
-
- return array('data' => $data);
- }
-
- /**
- * Builds the namespaces out to be saved in the Datasource file
- *
- * @param array $namespaces
- * An associative array of where the key is the namespace prefix
- * and the value is the namespace URL.
- * @param string $template
- * The template file, as defined by `getTemplate()`
- * @return string
- * The template injected with the Namespaces (if any).
- */
- public static function injectNamespaces(array $namespaces, &$template) {
- if(empty($namespaces)) return;
-
- $placeholder = '';
- $string = 'public $dsParamNAMESPACES = array(' . PHP_EOL;
-
- foreach($namespaces as $key => $val){
- if(trim($val) == '') continue;
- $string .= "\t\t\t'$key' => '" . addslashes($val) . "'," . PHP_EOL;
- }
-
- $string .= "\t\t);" . PHP_EOL . "\t\t" . $placeholder;
- $template = str_replace($placeholder, trim($string), $template);
- }
-
- /**
- * Given either the Datasource object or an array of settings for a
- * Remote Datasource, this function will return it's cache ID, which
- * is stored in tbl_cache.
- *
- * @since 1.1
- * @param array|object $settings
- */
- public static function buildCacheID($settings) {
- $cache_id = null;
-
- if(is_object($settings)) {
- $cache_id = md5(
- $settings->dsParamURL .
- serialize($settings->dsParamNAMESPACES) .
- $settings->dsParamXPATH .
- $settings->dsParamFORMAT
- );
- }
- else if(is_array($settings)) {
- // Namespaces come through empty, or as an array, so normalise
- // to ensure the cache key stays the same.
- if(is_array($settings['namespaces']) && empty($settings['namespaces'])) {
- $settings['namespaces'] = null;
- }
-
- $cache_id = md5(
- $settings['url'] .
- serialize($settings['namespaces']) .
- stripslashes($settings['xpath']) .
- $settings['format']
- );
- }
-
- return $cache_id;
- }
-
- /**
- * Helper function to build Cache information block
- *
- * @param XMLElement $wrapper
- * @param Cacheable $cache
- * @param string $cache_id
- */
- public static function buildCacheInformation(XMLElement $wrapper, Cacheable $cache, $cache_id) {
- $cachedData = $cache->check($cache_id);
-
- if(is_array($cachedData) && !empty($cachedData) && (time() < $cachedData['expiry'])) {
- $a = Widget::Anchor(__('Clear now'), SYMPHONY_URL . getCurrentPage() . 'clear_cache/');
- $wrapper->appendChild(
- new XMLElement('p', __('Cache expires in %d minutes. %s', array(
- ($cachedData['expiry'] - time()) / 60,
- $a->generate(false)
- )), array('class' => 'help'))
- );
- }
- else {
- $wrapper->appendChild(
- new XMLElement('p', __('Cache has expired or does not exist.'), array('class' => 'help'))
- );
- }
- }
-
- /*-------------------------------------------------------------------------
- Editor
- -------------------------------------------------------------------------*/
-
- public static function buildEditor(XMLElement $wrapper, array &$errors = array(), array $settings = null, $handle = null) {
- if(!is_null($handle) && isset($settings[self::getClass()])) {
- $cache = Symphony::ExtensionManager()->getCacheProvider('remotedatasource');
- $cache_id = self::buildCacheID($settings[self::getClass()]);
- }
-
- // If `clear_cache` is set, clear it..
- if(isset($cache_id) && in_array('clear_cache', Administration::instance()->Page->getContext())) {
- $cache->forceExpiry($cache_id);
- Administration::instance()->Page->pageAlert(
- __('Data source cache cleared at %s.', array(Widget::Time()->generate()))
- . ''
- . __('View all Data sources')
- . ''
- , Alert::SUCCESS);
- }
-
- $fieldset = new XMLElement('fieldset');
- $fieldset->setAttribute('class', 'settings contextual ' . __CLASS__);
- $fieldset->setAttribute('data-context', Lang::createHandle(self::getName()));
- $fieldset->appendChild(new XMLElement('legend', self::getName()));
- $p = new XMLElement('p',
- __('Use %s syntax to specify dynamic portions of the URL.', array(
- '{' . __('$param') . '}
'
- ))
- );
- $p->setAttribute('class', 'help');
- $fieldset->appendChild($p);
-
- // URL
- $label = Widget::Label(__('URL'));
- $url = isset($settings[self::getClass()]['url'])
- ? General::sanitize($settings[self::getClass()]['url'])
- : null;
-
- $label->appendChild(Widget::Input('fields[' . self::getClass() . '][url]', $url, 'text', array('placeholder' => 'http://')));
-
- if(isset($errors[self::getClass()]['url'])) {
- $fieldset->appendChild(Widget::Error($label, $errors[self::getClass()]['url']));
- }
- else {
- $fieldset->appendChild($label);
- }
-
- // Included Elements
- $label = Widget::Label(__('Included Elements'));
-
- $help = new XMLElement('i', __('xPath expression'));
- $label->appendChild($help);
-
- $xpath = isset($settings[self::getClass()]['xpath'])
- ? stripslashes($settings[self::getClass()]['xpath'])
- : null;
-
- $label->appendChild(
- Widget::Input('fields[' . self::getClass() . '][xpath]', $xpath, 'text', array('placeholder' => '/'))
- );
- if(isset($errors[self::getClass()]['xpath'])) {
- $fieldset->appendChild(Widget::Error($label, $errors[self::getClass()]['xpath']));
- }
- else {
- $fieldset->appendChild($label);
- }
-
- // Timeout
- $group = new XMLElement('div', null, array('class' => 'three columns'));
- $fieldset->appendChild($group);
-
- $label = Widget::Label(__('Timeout'));
- $label->setAttribute('class', 'column');
-
- $help = new XMLElement('i', __('in minutes'));
- $label->appendChild($help);
-
- $timeout_time = isset($settings[self::getClass()]['timeout'])
- ? max(1, intval($settings[self::getClass()]['timeout']))
- : 6;
-
- $label->appendChild(
- Widget::Input('fields[' . self::getClass() . '][timeout]', (string)$timeout_time, 'text')
- );
- if(isset($errors[self::getClass()]['timeout'])) {
- $group->appendChild(Widget::Error($label, $errors[self::getClass()]['timeout']));
- }
- else {
- $group->appendChild($label);
- }
-
- // Caching
- $label = Widget::Label(__('Cache expiration'));
- $label->setAttribute('class', 'column');
-
- $help = new XMLElement('i', __('in minutes'));
- $label->appendChild($help);
-
- $cache_time = isset($settings[self::getClass()]['cache'])
- ? max(0, intval($settings[self::getClass()]['cache']))
- : 5;
-
- $input = Widget::Input('fields[' . self::getClass() . '][cache]', (string)$cache_time);
- $label->appendChild($input);
- if(isset($errors[self::getClass()]['cache'])) $group->appendChild(Widget::Error($label, $errors[self::getClass()]['cache']));
- else $group->appendChild($label);
-
- // Format
- $label = Widget::Label(__('Format'));
- $label->setAttribute('class', 'column');
-
- $format = isset($settings[self::getClass()]['format'])
- ? $settings[self::getClass()]['format']
- : null;
-
- $label->appendChild(
- Widget::Select('fields[' . self::getClass() . '][format]', array(
- array('xml', $settings[self::getClass()]['format'] == 'xml', 'XML'),
- array('json', $settings[self::getClass()]['format'] == 'json', 'JSON'),
- array('csv', $settings[self::getClass()]['format'] == 'csv', 'CSV')
- ), array(
- 'class' => 'picker'
- ))
- );
- if(isset($errors[self::getClass()]['format'])) {
- $group->appendChild(Widget::Error($label, $errors[self::getClass()]['format']));
- }
- else {
- $group->appendChild($label);
- }
-
- // Namespaces
- $div = new XMLElement('div', false, array(
- 'id' => 'xml',
- 'class' => 'pickable'
- ));
- $p = new XMLElement('p', __('Namespace Declarations'));
- $p->appendChild(new XMLElement('i', __('optional')));
- $p->setAttribute('class', 'label');
- $div->appendChild($p);
-
- $frame = new XMLElement('div', null, array('class' => 'frame filters-duplicator'));
- $frame->setAttribute('data-interactive', 'data-interactive');
-
- $ol = new XMLElement('ol');
- $ol->setAttribute('data-add', __('Add namespace'));
- $ol->setAttribute('data-remove', __('Remove namespace'));
-
- if(isset($settings[self::getClass()], $settings[self::getClass()]['namespaces']) && is_array($settings[self::getClass()]['namespaces']) && !empty($settings[self::getClass()]['namespaces'])){
- $ii = 0;
- foreach($settings[self::getClass()]['namespaces'] as $name => $uri) {
- // Namespaces get saved to the file as $name => $uri, however in
- // the $_POST they are represented as $index => array. This loop
- // patches the difference.
- if(is_array($uri)) {
- $name = $uri['name'];
- $uri = $uri['uri'];
- }
-
- $li = new XMLElement('li');
- $li->setAttribute('class', 'instance');
- $header = new XMLElement('header');
- $header->appendChild(
- new XMLElement('h4', __('Namespace'))
- );
- $li->appendChild($header);
-
- $group = new XMLElement('div');
- $group->setAttribute('class', 'two columns');
-
- $label = Widget::Label(__('Name'));
- $label->setAttribute('class', 'column');
- $label->appendChild(Widget::Input("fields[" . self::getClass() . "][namespaces][$ii][name]", General::sanitize($name)));
- $group->appendChild($label);
-
- $label = Widget::Label(__('URI'));
- $label->setAttribute('class', 'column');
- $label->appendChild(Widget::Input("fields[" . self::getClass() . "][namespaces][$ii][uri]", General::sanitize($uri)));
- $group->appendChild($label);
-
- $li->appendChild($group);
- $ol->appendChild($li);
- $ii++;
- }
- }
-
- $li = new XMLElement('li');
- $li->setAttribute('class', 'template');
- $li->setAttribute('data-type', 'namespace');
- $header = new XMLElement('header');
- $header->appendChild(
- new XMLElement('h4', __('Namespace'))
- );
- $li->appendChild($header);
-
- $group = new XMLElement('div');
- $group->setAttribute('class', 'two columns');
-
- $label = Widget::Label(__('Name'));
- $label->setAttribute('class', 'column');
- $label->appendChild(Widget::Input('fields[' . self::getClass() . '][namespaces][-1][name]'));
- $group->appendChild($label);
-
- $label = Widget::Label(__('URI'));
- $label->setAttribute('class', 'column');
- $label->appendChild(Widget::Input('fields[' . self::getClass() . '][namespaces][-1][uri]'));
- $group->appendChild($label);
-
- $li->appendChild($group);
- $ol->appendChild($li);
-
- $frame->appendChild($ol);
- $div->appendChild($frame);
- $fieldset->appendChild($div);
-
- // Check for existing Cache objects
- if(isset($cache_id)) {
- self::buildCacheInformation($fieldset, $cache, $cache_id);
- }
-
- $wrapper->appendChild($fieldset);
- }
-
- public static function validate(array &$settings, array &$errors) {
- // Use the TIMEOUT that was specified by the user for a real world indication
- $timeout = isset($settings[self::getClass()]['timeout'])
- ? (int)$settings[self::getClass()]['timeout']
- : 6;
-
- // Check cache value is numeric
- if(!is_numeric($settings[self::getClass()]['cache'])) {
- $errors[self::getClass()]['cache'] = __('Must be a valid number');
- }
-
- // Make sure that XPath has been filled out
- if(trim($settings[self::getClass()]['xpath']) == '') {
- $errors[self::getClass()]['xpath'] = __('This is a required field');
- }
-
- // Ensure we have a URL
- if(trim($settings[self::getClass()]['url']) == '') {
- $errors[self::getClass()]['url'] = __('This is a required field');
- }
- // If there is a parameter in the URL, we can't validate the existence of the URL
- // as we don't have the environment details of where this datasource is going
- // to be executed.
- else if(!preg_match('@{([^}]+)}@i', $settings[self::getClass()]['url'])) {
- $valid_url = self::isValidURL($settings[self::getClass()]['url'], $timeout, $settings[self::getClass()]['format'], true);
-
- // If url was valid, `isValidURL` will return an array of data
- if(is_array($valid_url)) {
- self::$url_result = $valid_url['data'];
- }
- // Otherwise it'll return a string, which is an error
- else {
- $errors[self::getClass()]['url'] = $valid_url;
- }
- }
-
- return empty($errors[self::getClass()]);
- }
-
- public static function prepare(array $settings, array $params, $template) {
- $settings = $settings[self::getClass()];
-
- // Automatically detect namespaces
- if(!is_null(self::$url_result)) {
- preg_match_all('/xmlns:([a-z][a-z-0-9\-]*)="([^\"]+)"/i', self::$url_result, $matches);
-
- if(!is_array($settings['namespaces'])) {
- $settings['namespaces'] = array();
- }
-
- if (isset($matches[2][0])) {
- $detected_namespaces = array();
-
- foreach ($settings['namespaces'] as $index => $namespace) {
- $detected_namespaces[] = $namespace['name'];
- $detected_namespaces[] = $namespace['uri'];
- }
-
- foreach ($matches[2] as $index => $uri) {
- $name = $matches[1][$index];
-
- if (in_array($name, $detected_namespaces) or in_array($uri, $detected_namespaces)) continue;
-
- $detected_namespaces[] = $name;
- $detected_namespaces[] = $uri;
-
- $settings['namespaces'][] = array(
- 'name' => $name,
- 'uri' => $uri
- );
- }
- }
- }
-
- $namespaces = array();
- if(is_array($settings['namespaces'])) {
- foreach($settings['namespaces'] as $index => $data) {
- $namespaces[$data['name']] = $data['uri'];
- }
- }
- self::injectNamespaces($namespaces, $template);
-
- $timeout = isset($settings['timeout'])
- ? (int)$settings['timeout']
- : 6;
-
- // If there is valid data, save it to cache so that it is available
- // immediately to the frontend
- if(!is_null(self::$url_result)) {
- $settings['namespaces'] = $namespaces;
- $cache = Symphony::ExtensionManager()->getCacheProvider('remotedatasource');
- $cache_id = self::buildCacheID($settings);
- $cache->write($cache_id, self::$url_result, $settings['cache']);
- }
-
- return sprintf($template,
- $params['rootelement'], // rootelement
- $settings['url'], // url
- $settings['format'], // format
- addslashes($settings['xpath']), // xpath
- $settings['cache'], // cache
- $timeout// timeout
- );
- }
-
- /*-------------------------------------------------------------------------
- Execution
- -------------------------------------------------------------------------*/
-
- public function execute(array &$param_pool = null) {
- $result = new XMLElement($this->dsParamROOTELEMENT);
-
- // When DS is called out of the Frontend context, this will enable
- // {$root} and {$workspace} parameters to be evaluated
- if(empty($this->_env)) {
- $this->_env['env']['pool'] = array(
- 'root' => URL,
- 'workspace' => WORKSPACE
- );
- }
-
- try {
- require_once(TOOLKIT . '/class.gateway.php');
- require_once(TOOLKIT . '/class.xsltprocess.php');
- require_once(CORE . '/class.cacheable.php');
-
- $this->dsParamURL = $this->parseParamURL($this->dsParamURL);
-
- if(isset($this->dsParamXPATH)) {
- $this->dsParamXPATH = $this->__processParametersInString(stripslashes($this->dsParamXPATH), $this->_env);
- }
-
- // Builds a Default Stylesheet to transform the resulting XML with
- $stylesheet = new XMLElement('xsl:stylesheet');
- $stylesheet->setAttributeArray(array('version' => '1.0', 'xmlns:xsl' => 'http://www.w3.org/1999/XSL/Transform'));
-
- $output = new XMLElement('xsl:output');
- $output->setAttributeArray(array('method' => 'xml', 'version' => '1.0', 'encoding' => 'utf-8', 'indent' => 'yes', 'omit-xml-declaration' => 'yes'));
- $stylesheet->appendChild($output);
-
- $template = new XMLElement('xsl:template');
- $template->setAttribute('match', '/');
-
- $instruction = new XMLElement('xsl:copy-of');
-
- // Namespaces
- if(isset($this->dsParamNAMESPACES) && is_array($this->dsParamNAMESPACES)){
- foreach($this->dsParamNAMESPACES as $name => $uri) {
- $instruction->setAttribute('xmlns' . ($name ? ":{$name}" : NULL), $uri);
- }
- }
-
- // XPath
- $instruction->setAttribute('select', $this->dsParamXPATH);
-
- $template->appendChild($instruction);
- $stylesheet->appendChild($template);
- $stylesheet->setIncludeHeader(true);
-
- $xsl = $stylesheet->generate(true);
-
- // Check for an existing Cache for this Datasource
- $cache_id = self::buildCacheID($this);
- $cache = Symphony::ExtensionManager()->getCacheProvider('remotedatasource');
- $cachedData = $cache->check($cache_id);
- $writeToCache = null;
- $isCacheValid = true;
- $creation = DateTimeObj::get('c');
-
- // Execute if the cache doesn't exist, or if it is old.
- if(
- (!is_array($cachedData) || empty($cachedData)) // There's no cache.
- || (time() - $cachedData['creation']) > ($this->dsParamCACHE * 60) // The cache is old.
- ){
- if(Mutex::acquire($cache_id, $this->dsParamTIMEOUT, TMP)) {
- $ch = new Gateway;
- $ch->init($this->dsParamURL);
- $ch->setopt('TIMEOUT', $this->dsParamTIMEOUT);
-
- // Set the approtiate Accept: headers depending on the format of the URL.
- if($this->dsParamFORMAT == 'xml') {
- $ch->setopt('HTTPHEADER', array('Accept: text/xml, */*'));
- }
- else if($this->dsParamFORMAT == 'json') {
- $ch->setopt('HTTPHEADER', array('Accept: application/json, */*'));
- }
- else if($this->dsParamFORMAT == 'csv') {
- $ch->setopt('HTTPHEADER', array('Accept: text/csv, */*'));
- }
-
- $this->prepareGateway($ch);
-
- $data = $ch->exec();
- $info = $ch->getInfoLast();
-
- Mutex::release($cache_id, TMP);
-
- $data = trim($data);
- $writeToCache = true;
-
- // Handle any response that is not a 200, or the content type does not include XML, JSON, plain or text
- if((int)$info['http_code'] != 200 || !preg_match('/(xml|json|csv|plain|text)/i', $info['content_type'])){
- $writeToCache = false;
-
- $result->setAttribute('valid', 'false');
-
- // 28 is CURLE_OPERATION_TIMEOUTED
- if($info['curl_error'] == 28) {
- $result->appendChild(
- new XMLElement('error',
- sprintf('Request timed out. %d second limit reached.', $timeout)
- )
- );
- }
- else{
- $result->appendChild(
- new XMLElement('error',
- sprintf('Status code %d was returned. Content-type: %s', $info['http_code'], $info['content_type'])
- )
- );
- }
-
- return $result;
- }
-
- // Handle where there is `$data`
- else if(strlen($data) > 0) {
- // If it's JSON, convert it to XML
- if($this->dsParamFORMAT == 'json') {
- try {
- require_once TOOLKIT . '/class.json.php';
- $data = JSON::convertToXML($data);
- }
- catch (Exception $ex) {
- $writeToCache = false;
- $errors = array(
- array('message' => $ex->getMessage())
- );
- }
- }
- else if($this->dsParamFORMAT == 'csv') {
- try {
- require_once EXTENSIONS . '/remote_datasource/lib/class.csv.php';
- $data = CSV::convertToXML($data);
- }
- catch (Exception $ex) {
- $writeToCache = false;
- $errors = array(
- array('message' => $ex->getMessage())
- );
- }
- }
- // If the XML doesn't validate..
- else if(!General::validateXML($data, $errors, false, new XsltProcess)) {
- $writeToCache = false;
- }
-
- // If the `$data` is invalid, return a result explaining why
- if($writeToCache === false) {
- $error = new XMLElement('errors');
- $error->setAttribute('valid', 'false');
-
- $error->appendChild(new XMLElement('error', __('Data returned is invalid.')));
-
- foreach($errors as $e) {
- if(strlen(trim($e['message'])) == 0) continue;
- $error->appendChild(new XMLElement('item', General::sanitize($e['message'])));
- }
-
- $result->appendChild($error);
-
- return $result;
- }
- }
- // If `$data` is empty, set the `force_empty_result` to true.
- else if(strlen($data) == 0){
- $this->_force_empty_result = true;
- }
- }
-
- // Failed to acquire a lock
- else {
- $result->appendChild(
- new XMLElement('error', __('The %s class failed to acquire a lock.', array('Mutex
')))
- );
- }
- }
-
- // The cache is good, use it!
- else {
- $data = trim($cachedData['data']);
- $creation = DateTimeObj::get('c', $cachedData['creation']);
- }
-
- // Visit the data
- $this->exposeData($data);
-
- // If `$writeToCache` is set to false, invalidate the old cache if it existed.
- if(is_array($cachedData) && !empty($cachedData) && $writeToCache === false) {
- $data = trim($cachedData['data']);
- $isCacheValid = false;
- $creation = DateTimeObj::get('c', $cachedData['creation']);
-
- if(empty($data)) $this->_force_empty_result = true;
- }
-
- // If `force_empty_result` is false and `$result` is an instance of
- // XMLElement, build the `$result`.
- if(!$this->_force_empty_result && is_object($result)) {
- $proc = new XsltProcess;
- $ret = $proc->process($data, $xsl);
-
- if($proc->isErrors()) {
- $result->setAttribute('valid', 'false');
- $error = new XMLElement('error', __('Transformed XML is invalid.'));
- $result->appendChild($error);
- $errors = new XMLElement('errors');
- foreach($proc->getError() as $e) {
- if(strlen(trim($e['message'])) == 0) continue;
- $errors->appendChild(new XMLElement('item', General::sanitize($e['message'])));
- }
- $result->appendChild($errors);
- $result->appendChild(
- new XMLElement('raw-data', General::wrapInCDATA($data))
- );
- }
-
- else if(strlen(trim($ret)) == 0) {
- $this->_force_empty_result = true;
- }
-
- else {
- if($this->dsParamCACHE > 0 && $writeToCache) $cache->write($cache_id, $data, $this->dsParamCACHE);
-
- $result->setValue(PHP_EOL . str_repeat("\t", 2) . preg_replace('/([\r\n]+)/', "$1\t", $ret));
- $result->setAttribute('status', ($isCacheValid === true ? 'fresh' : 'stale'));
- $result->setAttribute('cache-id', $cache_id);
- $result->setAttribute('creation', $creation);
- }
- }
- }
- catch(Exception $e){
- $result->appendChild(new XMLElement('error', $e->getMessage()));
- }
-
- if($this->_force_empty_result) $result = $this->emptyXMLSet();
-
- $result->setAttribute('url', General::sanitize($this->dsParamURL));
-
- return $result;
- }
- }
-
- return 'RemoteDatasource';
+require_once TOOLKIT . '/class.datasource.php';
+require_once FACE . '/interface.datasource.php';
+
+class RemoteDatasource extends DataSource implements iDatasource
+{
+
+ private static $url_result = null;
+
+ private static $cacheable = null;
+
+ public static function getName()
+ {
+ return __('Remote Datasource');
+ }
+
+ public static function getClass()
+ {
+ return __CLASS__;
+ }
+
+ public function getSource()
+ {
+ return self::getClass();
+ }
+
+ public static function getTemplate()
+ {
+ return EXTENSIONS . '/remote_datasource/templates/blueprints.datasource.tpl';
+ }
+
+ public function settings()
+ {
+ $settings = array();
+
+ $settings[self::getClass()]['namespaces'] = $this->dsParamNAMESPACES;
+ $settings[self::getClass()]['url'] = $this->dsParamURL;
+ $settings[self::getClass()]['xpath'] = isset($this->dsParamXPATH) ? $this->dsParamXPATH : '/';
+ $settings[self::getClass()]['cache'] = isset($this->dsParamCACHE) ? $this->dsParamCACHE : 30;
+ $settings[self::getClass()]['format'] = isset($this->dsParamFORMAT) ? $this->dsParamFORMAT : 'xml';
+ $settings[self::getClass()]['timeout'] = isset($this->dsParamTIMEOUT) ? $this->dsParamTIMEOUT : 6;
+
+ return $settings;
+ }
+
+ /**
+ * This methods allows custom remote data source to set other
+ * properties on the HTTP gateway, like Authentication or other
+ * parameters. This method is call just before the `exec` method.
+ *
+ * @param Gateway $gateway
+ * the Gateway object that will be use for the current HTTP request
+ * passed by reference
+ */
+ public static function prepareGateway(&$gateway)
+ {
+
+ }
+
+ /**
+ * This methods allows custom remote data source to read the returned
+ * data before it becomes only available in the XML.
+ *
+ * @since Remote Datasource 2.0
+ * @param string $data
+ * the parsed xml string data returned by the Gateway by reference
+ */
+ public function exposeData(&$data)
+ {
+
+ }
+
+/*-------------------------------------------------------------------------
+ Utilities
+-------------------------------------------------------------------------*/
+
+ /**
+ * Returns the source value for display in the Datasources index
+ *
+ * @param string $file
+ * The path to the Datasource file
+ * @return string
+ */
+ public static function getSourceColumn($handle)
+ {
+ $datasource = DatasourceManager::create($handle, array(), false);
+
+ if (isset($datasource->dsParamURL)) {
+ return Widget::Anchor(str_replace('http://www.', '', $datasource->dsParamURL), $datasource->dsParamURL);
+ } else {
+ return 'Remote Datasource';
+ }
+ }
+
+ /**
+ * Given a `$url` and `$timeout`, this function will use the `Gateway`
+ * class to determine that it is a valid URL and returns successfully
+ * before the `$timeout`. If it does not, an error message will be
+ * returned, otherwise true.
+ *
+ * @todo This function is a bit messy, could be revisited.
+ * @param string $url
+ * @param integer $timeout
+ * If not provided, this will default to 6 seconds
+ * @param boolean $fetch_URL
+ * Defaults to false, but when set to true, this function will use the
+ * `Gateway` class to attempt to validate the URL's existence and it
+ * returns before the `$timeout`
+ * @param string $format
+ * The format that the URL will return, either JSON or XML. Defaults
+ * to 'xml' which will send the appropriate ACCEPTs header.
+ * @return string|array
+ * Returns an array with the 'data' if it is a valid URL, otherwise a string
+ * containing an error message.
+ */
+ public static function isValidURL($url, $timeout = 6, $format = 'xml', $fetch_URL = false)
+ {
+ if (trim($url) == '') {
+ return __('This is a required field');
+ } elseif ($fetch_URL === true) {
+ $gateway = new Gateway;
+ $gateway->init($url);
+ $gateway->setopt('TIMEOUT', $timeout);
+
+ // Set the approtiate Accept: headers depending on the format of the URL.
+ if ($format == 'xml') {
+ $gateway->setopt('HTTPHEADER', array('Accept: text/xml, */*'));
+ } elseif ($format == 'json') {
+ $gateway->setopt('HTTPHEADER', array('Accept: application/json, */*'));
+ } elseif ($format == 'csv') {
+ $gateway->setopt('HTTPHEADER', array('Accept: text/csv, */*'));
+ }
+
+ static::prepareGateway($gateway);
+
+ $data = $gateway->exec();
+ $info = $gateway->getInfoLast();
+
+ // 28 is CURLE_OPERATION_TIMEOUTED
+ if (isset($info['curl_error']) && $info['curl_error'] == 28) {
+ return __('Request timed out. %d second limit reached.', array($timeout));
+ } elseif ($data === false || $info['http_code'] != 200) {
+ return __('Failed to load URL, status code %d was returned.', array($info['http_code']));
+ }
+ }
+
+ return array('data' => $data);
+ }
+
+ /**
+ * Builds the namespaces out to be saved in the Datasource file
+ *
+ * @param array $namespaces
+ * An associative array of where the key is the namespace prefix
+ * and the value is the namespace URL.
+ * @param string $template
+ * The template file, as defined by `getTemplate()`
+ * @return string
+ * The template injected with the Namespaces (if any).
+ */
+ public static function injectNamespaces(array $namespaces, &$template)
+ {
+ if (empty($namespaces)) {
+ return;
+ }
+
+ $placeholder = '';
+ $string = 'public $dsParamNAMESPACES = array(' . PHP_EOL;
+
+ foreach ($namespaces as $key => $val) {
+ if (trim($val) == '') {
+ continue;
+ }
+
+ $string .= "\t\t\t'$key' => '" . addslashes($val) . "'," . PHP_EOL;
+ }
+
+ $string .= "\t\t);" . PHP_EOL . "\t\t" . $placeholder;
+ $template = str_replace($placeholder, trim($string), $template);
+ }
+
+ /**
+ * Given either the Datasource object or an array of settings for a
+ * Remote Datasource, this function will return it's cache ID, which
+ * is stored in tbl_cache.
+ *
+ * @since 1.1
+ * @param array|object $settings
+ */
+ public static function buildCacheID($settings)
+ {
+ $cache_id = null;
+
+ if (is_object($settings)) {
+ $cache_id = md5(
+ $settings->dsParamURL .
+ serialize($settings->dsParamNAMESPACES) .
+ $settings->dsParamXPATH .
+ $settings->dsParamFORMAT
+ );
+ } elseif (is_array($settings)) {
+ // Namespaces come through empty, or as an array, so normalise
+ // to ensure the cache key stays the same.
+ if (is_array($settings['namespaces']) && empty($settings['namespaces'])) {
+ $settings['namespaces'] = null;
+ }
+
+ $cache_id = md5(
+ $settings['url'] .
+ serialize($settings['namespaces']) .
+ stripslashes($settings['xpath']) .
+ $settings['format']
+ );
+ }
+
+ return $cache_id;
+ }
+
+ /**
+ * Helper function to build Cache information block
+ *
+ * @param XMLElement $wrapper
+ * @param Cacheable $cache
+ * @param string $cache_id
+ */
+ public static function buildCacheInformation(XMLElement $wrapper, Cacheable $cache, $cache_id)
+ {
+ $cachedData = $cache->check($cache_id);
+
+ if (is_array($cachedData) && !empty($cachedData) && (time() < $cachedData['expiry'])) {
+ $a = Widget::Anchor(__('Clear now'), SYMPHONY_URL . getCurrentPage() . 'clear_cache/');
+ $wrapper->appendChild(
+ new XMLElement('p', __('Cache expires in %d minutes. %s', array(
+ ($cachedData['expiry'] - time()) / 60,
+ $a->generate(false)
+ )), array('class' => 'help'))
+ );
+ } else {
+ $wrapper->appendChild(
+ new XMLElement('p', __('Cache has expired or does not exist.'), array('class' => 'help'))
+ );
+ }
+ }
+
+/*-------------------------------------------------------------------------
+ Editor
+-------------------------------------------------------------------------*/
+
+ public static function buildEditor(XMLElement $wrapper, array &$errors = array(), array $settings = null, $handle = null)
+ {
+ if (!is_null($handle) && isset($settings[self::getClass()])) {
+ $cache = Symphony::ExtensionManager()->getCacheProvider('remotedatasource');
+ $cache_id = self::buildCacheID($settings[self::getClass()]);
+ }
+
+ // If `clear_cache` is set, clear it..
+ if (isset($cache_id) && in_array('clear_cache', Administration::instance()->Page->getContext())) {
+ $cache->forceExpiry($cache_id);
+ Administration::instance()->Page->pageAlert(
+ __('Data source cache cleared at %s.', array(Widget::Time()->generate()))
+ . ''
+ . __('View all Data sources')
+ . '',
+ Alert::SUCCESS
+ );
+ }
+
+ $fieldset = new XMLElement('fieldset');
+ $fieldset->setAttribute('class', 'settings contextual ' . __CLASS__);
+ $fieldset->setAttribute('data-context', Lang::createHandle(self::getName()));
+ $fieldset->appendChild(new XMLElement('legend', self::getName()));
+ $p = new XMLElement(
+ 'p',
+ __('Use %s syntax to specify dynamic portions of the URL.', array(
+ '{' . __('$param') . '}
'
+ ))
+ );
+ $p->setAttribute('class', 'help');
+ $fieldset->appendChild($p);
+
+ // URL
+ $label = Widget::Label(__('URL'));
+ $url = isset($settings[self::getClass()]['url'])
+ ? General::sanitize($settings[self::getClass()]['url'])
+ : null;
+
+ $label->appendChild(Widget::Input('fields[' . self::getClass() . '][url]', $url, 'text', array('placeholder' => 'http://')));
+
+ if (isset($errors[self::getClass()]['url'])) {
+ $fieldset->appendChild(Widget::Error($label, $errors[self::getClass()]['url']));
+ } else {
+ $fieldset->appendChild($label);
+ }
+
+ // Included Elements
+ $label = Widget::Label(__('Included Elements'));
+
+ $help = new XMLElement('i', __('xPath expression'));
+ $label->appendChild($help);
+
+ $xpath = isset($settings[self::getClass()]['xpath'])
+ ? stripslashes($settings[self::getClass()]['xpath'])
+ : null;
+
+ $label->appendChild(
+ Widget::Input('fields[' . self::getClass() . '][xpath]', $xpath, 'text', array('placeholder' => '/'))
+ );
+ if (isset($errors[self::getClass()]['xpath'])) {
+ $fieldset->appendChild(Widget::Error($label, $errors[self::getClass()]['xpath']));
+ } else {
+ $fieldset->appendChild($label);
+ }
+
+ // Timeout
+ $group = new XMLElement('div', null, array('class' => 'three columns'));
+ $fieldset->appendChild($group);
+
+ $label = Widget::Label(__('Timeout'));
+ $label->setAttribute('class', 'column');
+
+ $help = new XMLElement('i', __('in minutes'));
+ $label->appendChild($help);
+
+ $timeout_time = isset($settings[self::getClass()]['timeout'])
+ ? max(1, intval($settings[self::getClass()]['timeout']))
+ : 6;
+
+ $label->appendChild(
+ Widget::Input('fields[' . self::getClass() . '][timeout]', (string) $timeout_time, 'text')
+ );
+ if (isset($errors[self::getClass()]['timeout'])) {
+ $group->appendChild(Widget::Error($label, $errors[self::getClass()]['timeout']));
+ } else {
+ $group->appendChild($label);
+ }
+
+ // Caching
+ $label = Widget::Label(__('Cache expiration'));
+ $label->setAttribute('class', 'column');
+
+ $help = new XMLElement('i', __('in minutes'));
+ $label->appendChild($help);
+
+ $cache_time = isset($settings[self::getClass()]['cache'])
+ ? max(0, intval($settings[self::getClass()]['cache']))
+ : 5;
+
+ $input = Widget::Input('fields[' . self::getClass() . '][cache]', (string) $cache_time);
+ $label->appendChild($input);
+ if (isset($errors[self::getClass()]['cache'])) {
+ $group->appendChild(Widget::Error($label, $errors[self::getClass()]['cache']));
+ } else {
+ $group->appendChild($label);
+ }
+
+ // Format
+ $label = Widget::Label(__('Format'));
+ $label->setAttribute('class', 'column');
+
+ $format = isset($settings[self::getClass()]['format'])
+ ? $settings[self::getClass()]['format']
+ : null;
+
+ $label->appendChild(
+ Widget::Select('fields[' . self::getClass() . '][format]', array(
+ array('xml', $settings[self::getClass()]['format'] == 'xml', 'XML'),
+ array('json', $settings[self::getClass()]['format'] == 'json', 'JSON'),
+ array('csv', $settings[self::getClass()]['format'] == 'csv', 'CSV')
+ ), array(
+ 'class' => 'picker'
+ ))
+ );
+ if (isset($errors[self::getClass()]['format'])) {
+ $group->appendChild(Widget::Error($label, $errors[self::getClass()]['format']));
+ } else {
+ $group->appendChild($label);
+ }
+
+ // Namespaces
+ $div = new XMLElement('div', false, array(
+ 'id' => 'xml',
+ 'class' => 'pickable'
+ ));
+ $p = new XMLElement('p', __('Namespace Declarations'));
+ $p->appendChild(new XMLElement('i', __('optional')));
+ $p->setAttribute('class', 'label');
+ $div->appendChild($p);
+
+ $frame = new XMLElement('div', null, array('class' => 'frame filters-duplicator'));
+ $frame->setAttribute('data-interactive', 'data-interactive');
+
+ $ol = new XMLElement('ol');
+ $ol->setAttribute('data-add', __('Add namespace'));
+ $ol->setAttribute('data-remove', __('Remove namespace'));
+
+ if (isset($settings[self::getClass()], $settings[self::getClass()]['namespaces']) && is_array($settings[self::getClass()]['namespaces']) && !empty($settings[self::getClass()]['namespaces'])) {
+ $ii = 0;
+ foreach ($settings[self::getClass()]['namespaces'] as $name => $uri) {
+ // Namespaces get saved to the file as $name => $uri, however in
+ // the $_POST they are represented as $index => array. This loop
+ // patches the difference.
+ if (is_array($uri)) {
+ $name = $uri['name'];
+ $uri = $uri['uri'];
+ }
+
+ $li = new XMLElement('li');
+ $li->setAttribute('class', 'instance');
+ $header = new XMLElement('header');
+ $header->appendChild(
+ new XMLElement('h4', __('Namespace'))
+ );
+ $li->appendChild($header);
+
+ $group = new XMLElement('div');
+ $group->setAttribute('class', 'two columns');
+
+ $label = Widget::Label(__('Name'));
+ $label->setAttribute('class', 'column');
+ $label->appendChild(Widget::Input("fields[" . self::getClass() . "][namespaces][$ii][name]", General::sanitize($name)));
+ $group->appendChild($label);
+
+ $label = Widget::Label(__('URI'));
+ $label->setAttribute('class', 'column');
+ $label->appendChild(Widget::Input("fields[" . self::getClass() . "][namespaces][$ii][uri]", General::sanitize($uri)));
+ $group->appendChild($label);
+
+ $li->appendChild($group);
+ $ol->appendChild($li);
+ $ii++;
+ }
+ }
+
+ $li = new XMLElement('li');
+ $li->setAttribute('class', 'template');
+ $li->setAttribute('data-type', 'namespace');
+ $header = new XMLElement('header');
+ $header->appendChild(
+ new XMLElement('h4', __('Namespace'))
+ );
+ $li->appendChild($header);
+
+ $group = new XMLElement('div');
+ $group->setAttribute('class', 'two columns');
+
+ $label = Widget::Label(__('Name'));
+ $label->setAttribute('class', 'column');
+ $label->appendChild(Widget::Input('fields[' . self::getClass() . '][namespaces][-1][name]'));
+ $group->appendChild($label);
+
+ $label = Widget::Label(__('URI'));
+ $label->setAttribute('class', 'column');
+ $label->appendChild(Widget::Input('fields[' . self::getClass() . '][namespaces][-1][uri]'));
+ $group->appendChild($label);
+
+ $li->appendChild($group);
+ $ol->appendChild($li);
+
+ $frame->appendChild($ol);
+ $div->appendChild($frame);
+ $fieldset->appendChild($div);
+
+ // Check for existing Cache objects
+ if (isset($cache_id)) {
+ self::buildCacheInformation($fieldset, $cache, $cache_id);
+ }
+
+ $wrapper->appendChild($fieldset);
+ }
+
+ public static function validate(array &$settings, array &$errors)
+ {
+ // Use the TIMEOUT that was specified by the user for a real world indication
+ $timeout = isset($settings[self::getClass()]['timeout'])
+ ? (int) $settings[self::getClass()]['timeout']
+ : 6;
+
+ // Check cache value is numeric
+ if (!is_numeric($settings[self::getClass()]['cache'])) {
+ $errors[self::getClass()]['cache'] = __('Must be a valid number');
+ }
+
+ // Make sure that XPath has been filled out
+ if (trim($settings[self::getClass()]['xpath']) == '') {
+ $errors[self::getClass()]['xpath'] = __('This is a required field');
+ }
+
+ // Ensure we have a URL
+ if (trim($settings[self::getClass()]['url']) == '') {
+ $errors[self::getClass()]['url'] = __('This is a required field');
+ } elseif (!preg_match('@{([^}]+)}@i', $settings[self::getClass()]['url'])) {
+
+ // If there is a parameter in the URL, we can't validate the existence of the URL
+ // as we don't have the environment details of where this datasource is going
+ // to be executed.
+ $valid_url = self::isValidURL($settings[self::getClass()]['url'], $timeout, $settings[self::getClass()]['format'], true);
+
+ // If url was valid, `isValidURL` will return an array of data
+ // Otherwise it'll return a string, which is an error
+ if (is_array($valid_url)) {
+ self::$url_result = $valid_url['data'];
+ } else {
+ $errors[self::getClass()]['url'] = $valid_url;
+ }
+ }
+
+ return empty($errors[self::getClass()]);
+ }
+
+ public static function prepare(array $settings, array $params, $template)
+ {
+ $settings = $settings[self::getClass()];
+
+ // Automatically detect namespaces
+ if (!is_null(self::$url_result)) {
+ preg_match_all('/xmlns:([a-z][a-z-0-9\-]*)="([^\"]+)"/i', self::$url_result, $matches);
+
+ if (!is_array($settings['namespaces'])) {
+ $settings['namespaces'] = array();
+ }
+
+ if (isset($matches[2][0])) {
+ $detected_namespaces = array();
+
+ foreach ($settings['namespaces'] as $index => $namespace) {
+ $detected_namespaces[] = $namespace['name'];
+ $detected_namespaces[] = $namespace['uri'];
+ }
+
+ foreach ($matches[2] as $index => $uri) {
+ $name = $matches[1][$index];
+
+ if (in_array($name, $detected_namespaces) || in_array($uri, $detected_namespaces)) {
+ continue;
+ }
+
+ $detected_namespaces[] = $name;
+ $detected_namespaces[] = $uri;
+
+ $settings['namespaces'][] = array(
+ 'name' => $name,
+ 'uri' => $uri
+ );
+ }
+ }
+ }
+
+ $namespaces = array();
+ if (is_array($settings['namespaces'])) {
+ foreach ($settings['namespaces'] as $index => $data) {
+ $namespaces[$data['name']] = $data['uri'];
+ }
+ }
+ self::injectNamespaces($namespaces, $template);
+
+ $timeout = isset($settings['timeout'])
+ ? (int) $settings['timeout']
+ : 6;
+
+ // If there is valid data, save it to cache so that it is available
+ // immediately to the frontend
+ if (!is_null(self::$url_result)) {
+ $settings['namespaces'] = $namespaces;
+ $cache = Symphony::ExtensionManager()->getCacheProvider('remotedatasource');
+ $cache_id = self::buildCacheID($settings);
+ $cache->write($cache_id, self::$url_result, $settings['cache']);
+ }
+
+ return sprintf(
+ $template,
+ $params['rootelement'], // rootelement
+ $settings['url'], // url
+ $settings['format'], // format
+ addslashes($settings['xpath']), // xpath
+ $settings['cache'], // cache
+ $timeout// timeout
+ );
+ }
+
+/*-------------------------------------------------------------------------
+ Execution
+-------------------------------------------------------------------------*/
+
+ public function execute(array &$param_pool = null)
+ {
+ $result = new XMLElement($this->dsParamROOTELEMENT);
+
+ // When DS is called out of the Frontend context, this will enable
+ // {$root} and {$workspace} parameters to be evaluated
+ if (empty($this->_env)) {
+ $this->_env['env']['pool'] = array(
+ 'root' => URL,
+ 'workspace' => WORKSPACE
+ );
+ }
+
+ try {
+ require_once(TOOLKIT . '/class.gateway.php');
+ require_once(TOOLKIT . '/class.xsltprocess.php');
+ require_once(CORE . '/class.cacheable.php');
+
+ $this->dsParamURL = $this->parseParamURL($this->dsParamURL);
+
+ if (isset($this->dsParamXPATH)) {
+ $this->dsParamXPATH = $this->__processParametersInString(stripslashes($this->dsParamXPATH), $this->_env);
+ }
+
+ // Builds a Default Stylesheet to transform the resulting XML with
+ $stylesheet = new XMLElement('xsl:stylesheet');
+ $stylesheet->setAttributeArray(array('version' => '1.0', 'xmlns:xsl' => 'http://www.w3.org/1999/XSL/Transform'));
+
+ $output = new XMLElement('xsl:output');
+ $output->setAttributeArray(array('method' => 'xml', 'version' => '1.0', 'encoding' => 'utf-8', 'indent' => 'yes', 'omit-xml-declaration' => 'yes'));
+ $stylesheet->appendChild($output);
+
+ $template = new XMLElement('xsl:template');
+ $template->setAttribute('match', '/');
+
+ $instruction = new XMLElement('xsl:copy-of');
+
+ // Namespaces
+ if (isset($this->dsParamNAMESPACES) && is_array($this->dsParamNAMESPACES)) {
+ foreach ($this->dsParamNAMESPACES as $name => $uri) {
+ $instruction->setAttribute('xmlns' . ($name ? ":{$name}" : null), $uri);
+ }
+ }
+
+ // XPath
+ $instruction->setAttribute('select', $this->dsParamXPATH);
+
+ $template->appendChild($instruction);
+ $stylesheet->appendChild($template);
+ $stylesheet->setIncludeHeader(true);
+
+ $xsl = $stylesheet->generate(true);
+
+ // Check for an existing Cache for this Datasource
+ $cache_id = self::buildCacheID($this);
+ $cache = Symphony::ExtensionManager()->getCacheProvider('remotedatasource');
+ $cachedData = $cache->check($cache_id);
+ $writeToCache = null;
+ $isCacheValid = true;
+ $creation = DateTimeObj::get('c');
+
+ // Execute if the cache doesn't exist, or if it is old.
+ if (
+ (!is_array($cachedData) || empty($cachedData)) // There's no cache.
+ || (time() - $cachedData['creation']) > ($this->dsParamCACHE * 60) // The cache is old.
+ ) {
+ if (Mutex::acquire($cache_id, $this->dsParamTIMEOUT, TMP)) {
+ $ch = new Gateway;
+ $ch->init($this->dsParamURL);
+ $ch->setopt('TIMEOUT', $this->dsParamTIMEOUT);
+
+ // Set the approtiate Accept: headers depending on the format of the URL.
+ if ($this->dsParamFORMAT == 'xml') {
+ $ch->setopt('HTTPHEADER', array('Accept: text/xml, */*'));
+ } elseif ($this->dsParamFORMAT == 'json') {
+ $ch->setopt('HTTPHEADER', array('Accept: application/json, */*'));
+ } elseif ($this->dsParamFORMAT == 'csv') {
+ $ch->setopt('HTTPHEADER', array('Accept: text/csv, */*'));
+ }
+
+ $this->prepareGateway($ch);
+
+ $data = $ch->exec();
+ $info = $ch->getInfoLast();
+
+ Mutex::release($cache_id, TMP);
+
+ $data = trim($data);
+ $writeToCache = true;
+
+ // Handle any response that is not a 200, or the content type does not include XML, JSON, plain or text
+ if ((int) $info['http_code'] != 200 || !preg_match('/(xml|json|csv|plain|text)/i', $info['content_type'])) {
+ $writeToCache = false;
+
+ $result->setAttribute('valid', 'false');
+
+ // 28 is CURLE_OPERATION_TIMEOUTED
+ if ($info['curl_error'] == 28) {
+ $result->appendChild(
+ new XMLElement(
+ 'error',
+ sprintf('Request timed out. %d second limit reached.', $timeout)
+ )
+ );
+ } else {
+ $result->appendChild(
+ new XMLElement(
+ 'error',
+ sprintf('Status code %d was returned. Content-type: %s', $info['http_code'], $info['content_type'])
+ )
+ );
+ }
+
+ return $result;
+ } else if (strlen($data) > 0) {
+
+ // Handle where there is `$data`
+
+ // If it's JSON, convert it to XML
+ if ($this->dsParamFORMAT == 'json') {
+ try {
+ require_once TOOLKIT . '/class.json.php';
+ $data = JSON::convertToXML($data);
+ } catch (Exception $ex) {
+ $writeToCache = false;
+ $errors = array(
+ array('message' => $ex->getMessage())
+ );
+ }
+ } elseif ($this->dsParamFORMAT == 'csv') {
+ try {
+ require_once EXTENSIONS . '/remote_datasource/lib/class.csv.php';
+ $data = CSV::convertToXML($data);
+ } catch (Exception $ex) {
+ $writeToCache = false;
+ $errors = array(
+ array('message' => $ex->getMessage())
+ );
+ }
+ } elseif (!General::validateXML($data, $errors, false, new XsltProcess)) {
+
+ // If the XML doesn't validate..
+ $writeToCache = false;
+ }
+
+ // If the `$data` is invalid, return a result explaining why
+ if ($writeToCache === false) {
+ $error = new XMLElement('errors');
+ $error->setAttribute('valid', 'false');
+
+ $error->appendChild(new XMLElement('error', __('Data returned is invalid.')));
+
+ foreach ($errors as $e) {
+ if (strlen(trim($e['message'])) == 0) {
+ continue;
+ }
+
+ $error->appendChild(new XMLElement('item', General::sanitize($e['message'])));
+ }
+
+ $result->appendChild($error);
+
+ return $result;
+ }
+ } elseif (strlen($data) == 0) {
+
+ // If `$data` is empty, set the `force_empty_result` to true.
+ $this->_force_empty_result = true;
+ }
+ } else {
+
+ // Failed to acquire a lock
+ $result->appendChild(
+ new XMLElement('error', __('The %s class failed to acquire a lock.', array('Mutex
')))
+ );
+ }
+ } else {
+
+ // The cache is good, use it!
+ $data = trim($cachedData['data']);
+ $creation = DateTimeObj::get('c', $cachedData['creation']);
+ }
+
+ // Visit the data
+ $this->exposeData($data);
+
+ // If `$writeToCache` is set to false, invalidate the old cache if it existed.
+ if (is_array($cachedData) && !empty($cachedData) && $writeToCache === false) {
+ $data = trim($cachedData['data']);
+ $isCacheValid = false;
+ $creation = DateTimeObj::get('c', $cachedData['creation']);
+
+ if (empty($data)) {
+ $this->_force_empty_result = true;
+ }
+ }
+
+ // If `force_empty_result` is false and `$result` is an instance of
+ // XMLElement, build the `$result`.
+ if (!$this->_force_empty_result && is_object($result)) {
+ $proc = new XsltProcess;
+ $ret = $proc->process($data, $xsl);
+
+ if ($proc->isErrors()) {
+ $result->setAttribute('valid', 'false');
+ $error = new XMLElement('error', __('Transformed XML is invalid.'));
+ $result->appendChild($error);
+ $errors = new XMLElement('errors');
+ foreach ($proc->getError() as $e) {
+ if (strlen(trim($e['message'])) == 0) {
+ continue;
+ }
+
+ $errors->appendChild(new XMLElement('item', General::sanitize($e['message'])));
+ }
+ $result->appendChild($errors);
+ $result->appendChild(
+ new XMLElement('raw-data', General::wrapInCDATA($data))
+ );
+ } elseif (strlen(trim($ret)) == 0) {
+ $this->_force_empty_result = true;
+ } else {
+ if ($this->dsParamCACHE > 0 && $writeToCache) {
+ $cache->write($cache_id, $data, $this->dsParamCACHE);
+ }
+
+ $result->setValue(PHP_EOL . str_repeat("\t", 2) . preg_replace('/([\r\n]+)/', "$1\t", $ret));
+ $result->setAttribute('status', ($isCacheValid === true ? 'fresh' : 'stale'));
+ $result->setAttribute('cache-id', $cache_id);
+ $result->setAttribute('creation', $creation);
+ }
+ }
+ } catch (Exception $e) {
+ $result->appendChild(new XMLElement('error', $e->getMessage()));
+ }
+
+ if ($this->_force_empty_result) {
+ $result = $this->emptyXMLSet();
+ }
+
+ $result->setAttribute('url', General::sanitize($this->dsParamURL));
+
+ return $result;
+ }
+}
+
+return 'RemoteDatasource';
diff --git a/extension.driver.php b/extension.driver.php
index 7d5b5b6..beaff9a 100644
--- a/extension.driver.php
+++ b/extension.driver.php
@@ -1,54 +1,62 @@
array(
- 'RemoteDatasource' => RemoteDatasource::getName()
- )
- );
-
- return true;
- }
-
- public static function providerOf($type = null) {
- self::registerProviders();
-
- if(is_null($type)) return self::$provides;
-
- if(!isset(self::$provides[$type])) return array();
-
- return self::$provides[$type];
- }
-
- public function getSubscribedDelegates(){
- return array(
- array(
- 'page' => '/system/preferences/',
- 'delegate' => 'AddCachingOpportunity',
- 'callback' => 'addCachingOpportunity'
- )
- );
- }
-
- public function addCachingOpportunity($context) {
- $current_cache = Symphony::Configuration()->get('remotedatasource', 'caching');
- $label = Widget::Label(__('Remote Datasource'));
-
- $options = array();
- foreach($context['available_caches'] as $handle => $cache_name) {
- $options[] = array($handle, ($current_cache == $handle || (!isset($current_cache) && $handle === 'database')), $cache_name);
- }
-
- $select = Widget::Select('settings[caching][remotedatasource]', $options, array('class' => 'picker'));
- $label->appendChild($select);
-
- $context['wrapper']->appendChild($label);
- }
-
- }
+require_once EXTENSIONS . '/remote_datasource/data-sources/datasource.remote.php';
+
+class Extension_Remote_Datasource extends Extension
+{
+
+ private static $provides = array();
+
+ public static function registerProviders()
+ {
+ self::$provides = array(
+ 'data-sources' => array(
+ 'RemoteDatasource' => RemoteDatasource::getName()
+ )
+ );
+
+ return true;
+ }
+
+ public static function providerOf($type = null)
+ {
+ self::registerProviders();
+
+ if (is_null($type)) {
+ return self::$provides;
+ }
+
+ if (!isset(self::$provides[$type])) {
+ return array();
+ }
+
+ return self::$provides[$type];
+ }
+
+ public function getSubscribedDelegates()
+ {
+ return array(
+ array(
+ 'page' => '/system/preferences/',
+ 'delegate' => 'AddCachingOpportunity',
+ 'callback' => 'addCachingOpportunity'
+ )
+ );
+ }
+
+ public function addCachingOpportunity($context)
+ {
+ $current_cache = Symphony::Configuration()->get('remotedatasource', 'caching');
+ $label = Widget::Label(__('Remote Datasource'));
+
+ $options = array();
+ foreach ($context['available_caches'] as $handle => $cache_name) {
+ $options[] = array($handle, ($current_cache == $handle || (!isset($current_cache) && $handle === 'database')), $cache_name);
+ }
+
+ $select = Widget::Select('settings[caching][remotedatasource]', $options, array('class' => 'picker'));
+ $label->appendChild($select);
+
+ $context['wrapper']->appendChild($label);
+ }
+}
diff --git a/extension.meta.xml b/extension.meta.xml
index 4e031ec..e619675 100644
--- a/extension.meta.xml
+++ b/extension.meta.xml
@@ -9,7 +9,10 @@
-
+
+ - Clean-up
+
+
- Add support for Symphony 2.4
- Support CSV data format
- Allow `$gateway` to be manipulated and `$data` to previewed
@@ -17,15 +20,15 @@
- Allow no cache to be set
- Sanitize XPath to allow for more complex queries
-
+
- Officially release the extension
- Add `url` to the resulting XML result so you can see what URL was actually fetched
- Fix bug where a result would always be `stale`
- Allow timeout to be user configurable in the Data Source Editor
- Various PHP E_NOTICE fixes
-
+
- Initial release
-
\ No newline at end of file
+
diff --git a/lib/class.csv.php b/lib/class.csv.php
index ae8bced..4d09cf8 100644
--- a/lib/class.csv.php
+++ b/lib/class.csv.php
@@ -1,63 +1,68 @@
formatOutput = true;
-
- $root = $doc->createElement('data');
- $doc->appendChild($root);
-
- foreach(str_getcsv($data, PHP_EOL) as $i => $row) {
- if(empty($row)) continue;
-
- if($i == 0) {
- foreach(str_getcsv($row) as $i => $head) {
- if(class_exists('Lang')) {
- $head = Lang::createHandle($head);
- }
- $headers[] = $head;
- }
- }
- else {
- self::addRow($doc, $root, str_getcsv($row), $headers);
- }
- }
-
- $output = $doc->saveXML($doc->documentElement);
- return trim($output);
- }
-
- /**
- * @param DOMDocument $doc
- * @param DOMElement $root
- * @param array $row
- * @param array $headers
- */
- public static function addRow(DOMDocument $doc, DOMElement $root, $row, $headers) {
- foreach($row as $column) {
- // Create
- $entry = $doc->createElement('entry');
-
- foreach ($headers as $i => $header) {
- $col = $doc->createElement($header);
- $col = $entry->appendChild($col);
-
- $value = $doc->createTextNode($row[$i]);
- $value = $col->appendChild($value);
- }
-
- $root->appendChild($entry);
- }
- }
- }
+class CSV
+{
+
+ /**
+ * Given a CSV file, generate a resulting XML tree
+ *
+ * @param string $data
+ * @return string
+ */
+ public static function convertToXML($data)
+ {
+ $headers = array();
+
+ // DOMDocument
+ $doc = new DOMDocument('1.0', 'utf-8');
+ $doc->formatOutput = true;
+
+ $root = $doc->createElement('data');
+ $doc->appendChild($root);
+
+ foreach (str_getcsv($data, PHP_EOL) as $i => $row) {
+ if (empty($row)) {
+ continue;
+ }
+
+ if ($i == 0) {
+ foreach (str_getcsv($row) as $i => $head) {
+ if (class_exists('Lang')) {
+ $head = Lang::createHandle($head);
+ }
+ $headers[] = $head;
+ }
+ } else {
+ self::addRow($doc, $root, str_getcsv($row), $headers);
+ }
+ }
+
+ $output = $doc->saveXML($doc->documentElement);
+
+ return trim($output);
+ }
+
+ /**
+ * @param DOMDocument $doc
+ * @param DOMElement $root
+ * @param array $row
+ * @param array $headers
+ */
+ public static function addRow(DOMDocument $doc, DOMElement $root, $row, $headers)
+ {
+ foreach ($row as $column) {
+ // Create
+ $entry = $doc->createElement('entry');
+
+ foreach ($headers as $i => $header) {
+ $col = $doc->createElement($header);
+ $col = $entry->appendChild($col);
+
+ $value = $doc->createTextNode($row[$i]);
+ $value = $col->appendChild($value);
+ }
+
+ $root->appendChild($entry);
+ }
+ }
+}
diff --git a/templates/blueprints.datasource.tpl b/templates/blueprints.datasource.tpl
index c97e6d9..8529be6 100644
--- a/templates/blueprints.datasource.tpl
+++ b/templates/blueprints.datasource.tpl
@@ -1,37 +1,40 @@
extends RemoteDatasource {
-
- public $dsParamROOTELEMENT = '%s';
- public $dsParamURL = '%s';
- public $dsParamFORMAT = '%s';
- public $dsParamXPATH = '%s';
- public $dsParamCACHE = %d;
- public $dsParamTIMEOUT = %d;
-
-
-
- public function __construct($env=NULL, $process_params=true){
- parent::__construct($env, $process_params);
- $this->_dependencies = array();
- }
-
- public function about(){
- return array(
- 'name' => '',
- 'author' => array(
- 'name' => '',
- 'website' => '',
- 'email' => ''),
- 'version' => '',
- 'release-date' => ''
- );
- }
-
- public function allowEditorToParse(){
- return true;
- }
-
- }
+require_once(EXTENSIONS . '/remote_datasource/data-sources/datasource.remote.php');
+
+class datasource extends RemoteDatasource {
+
+ public $dsParamROOTELEMENT = '%s';
+ public $dsParamURL = '%s';
+ public $dsParamFORMAT = '%s';
+ public $dsParamXPATH = '%s';
+ public $dsParamCACHE = %d;
+ public $dsParamTIMEOUT = %d;
+
+
+
+ public function __construct($env=NULL, $process_params=true)
+ {
+ parent::__construct($env, $process_params);
+ $this->_dependencies = array();
+ }
+
+ public function about()
+ {
+ return array(
+ 'name' => '',
+ 'author' => array(
+ 'name' => '',
+ 'website' => '',
+ 'email' => ''),
+ 'version' => '',
+ 'release-date' => ''
+ );
+ }
+
+ public function allowEditorToParse()
+ {
+ return true;
+ }
+
+}