Link to home
Start Free TrialLog in
Avatar of Dima Iva
Dima Iva

asked on

Need help debugging PHP code

Greetings!  We run an open source software called OJS (Open Journal Systems) in XAMPP on Windows 2016 server with PHP 7.2.11, and Apache 2.4.35. The site is supposed to show me the available plugins when I go to the Plugin Gallery page, but the page says "No items".  OJS folks explain that our server doesn't fetch the plugin list from their server. They point to that function in the /plugins/PluginGalleryDAO.inc.php as a ossible culprit:

private function _getDocument() {
    $doc = new DOMDocument('1.0');
    $gallery = FileWrapper::wrapper(PLUGIN_GALLERY_XML_URL);
    $doc->loadXML($gallery->contents());
    return $doc;

According to them, this is the function that fetches the plugin list from the PKP server (https://pkp.sfu.ca/ojs/xml/plugins.xml, defined at the head of that file). They suspect I’ll find that $gallery->contents() is failing to return the contents of that XML file.  The suggestion is to try to use error_log to log variables/results from within PHP code, but I am no sure how to do that.  Could someone guide me?
Avatar of Dave Baldwin
Dave Baldwin
Flag of United States of America image

There is a line or two about the error_log in your php.ini.  It is common to put it in the web root with read permissions for only the owner and not external users.  Although I'm not sure how that works with Apache on Windows.
Hi there,

To log errors in PHP, you use the error_log() function, so you could just add that to your code (after you've retrieved $gallery):

$doc = new DOMDocument('1.0');
$gallery = FileWrapper::wrapper(PLUGIN_GALLERY_XML_URL);

$logData = "GALLERY_CONTENT: " . print_r($gallery->contents(), true);
error_log($logData, 3, './error.log');

$doc->loadXML($gallery->contents());
return $doc;

Open in new window


Change the path to where you want to write the log file - the above example will write to error.log in the current directory (make sure you have write permission).

That should now output the results of the contents() call to your log file.
Have you checked your config file to see where this is pointing
PLUGIN_GALLERY_XML_URL
Are you sure that is set correctly.
Avatar of Dima Iva
Dima Iva

ASKER

Hi Julian,

It is set to:

define('PLUGIN_GALLERY_XML_URL', 'https://pkp.sfu.ca/ojs/xml/plugins.xml');

which, according to PKP people is correct.
Hi Chris,

I added your code with a path to a file in htdocs:


      private function _getDocument() {
            $doc = new DOMDocument('1.0');
            $gallery = FileWrapper::wrapper(PLUGIN_GALLERY_XML_URL);
            
            $logData = "GALLERY_CONTENT: " . print_r($gallery->contents(), true);
            error_log($logData, 3, 'C:\xampp\htdocs\error.log');
            
            $doc->loadXML($gallery->contents());
            return $doc;


Now, every time I re-load the Plugin Gallery page, this line appears in the new error.log:

GALLERY_CONTENT:
Here is a test script made from the components FileWrapper parts of the OJS Framework.

You can use this to debug the code and see where it is going wrong.

First step is to put this on your server somewhere - it does not matter where it has no dependencies - and call it.

Do you get any output
<?php
define('PLUGIN_GALLERY_XML_URL', 'https://pkp.sfu.ca/ojs/xml/plugins.xml');

class Config {
	public static function getVar($setting, $value) {
		return true;
	}
}

class HTTPFileWrapper extends FileWrapper {
	var $headers;
	var $defaultPort;
	var $defaultHost;
	var $defaultPath;
	var $redirects;

	var $proxyHost;
	var $proxyPort;
	var $proxyUsername;
	var $proxyPassword;

	function __construct($url, &$info, $redirects = 5) {
		parent::__construct($url, $info);
		$this->setDefaultPort(80);
		$this->setDefaultHost('localhost');
		$this->setDefaultPath('/');
		$this->redirects = 5;

		$this->proxyHost = Config::getVar('proxy', 'http_host');
		$this->proxyPort = Config::getVar('proxy', 'http_port');
		$this->proxyUsername = Config::getVar('proxy', 'proxy_username');
		$this->proxyPassword = Config::getVar('proxy', 'proxy_password');
	}

	function setDefaultPort($port) {
		$this->defaultPort = $port;
	}

	function setDefaultHost($host) {
		$this->defaultHost = $host;
	}

	function setDefaultPath($path) {
		$this->defaultPath = $path;
	}

	function addHeader($name, $value) {
		if (!isset($this->headers)) {
			$this->headers = array();
		}
		$this->headers[$name] = $value;
	}

	function open($mode = 'r') {
		$realHost = $host = isset($this->info['host']) ? $this->info['host'] : $this->defaultHost;
		$port = isset($this->info['port']) ? (int)$this->info['port'] : $this->defaultPort;
		$path = isset($this->info['path']) ? $this->info['path'] : $this->defaultPath;
		if (isset($this->info['query'])) $path .= '?' . $this->info['query'];

		if (!empty($this->proxyHost)) {
			$realHost = $host;
			$host = $this->proxyHost;
			$port = $this->proxyPort;
			if (!empty($this->proxyUsername)) {
				$this->headers['Proxy-Authorization'] = 'Basic ' . base64_encode($this->proxyUsername . ':' . $this->proxyPassword);
			}
		}

		if (!($this->fp = fsockopen($host, $port)))
			return false;

		$additionalHeadersString = '';
		if (is_array($this->headers)) foreach ($this->headers as $name => $value) {
			$additionalHeadersString .= "$name: $value\r\n";
		}

		$requestHost = preg_replace("!^.*://!", "", $realHost);
		$request = 'GET ' . (empty($this->proxyHost)?$path:$this->url) . " HTTP/1.0\r\n" .
			"Host: $requestHost\r\n" .
			$additionalHeadersString .
			"Connection: Close\r\n\r\n";
		fwrite($this->fp, $request);

		$response = fgets($this->fp, 4096);
		$rc = 0;
		sscanf($response, "HTTP/%*s %u %*[^\r\n]\r\n", $rc);
		if ($rc == 200) {
			while(fgets($this->fp, 4096) !== "\r\n");
			return true;
		}
		if(preg_match('!^3\d\d$!', $rc) && $this->redirects >= 1) {
			for($response = '', $time = time(); !feof($this->fp) && $time >= time() - 15; ) $response .= fgets($this->fp, 128);
			if (preg_match('!^(?:(?:Location)|(?:URI)|(?:location)): ([^\s]+)[\r\n]!m', $response, $matches)) {
				$this->close();
				$location = $matches[1];
				if (preg_match('!^[a-z]+://!', $location)) {
					$this->url = $location;
				} else {
					$newPath = ($this->info['path'] !== '' && strpos($location, '/') !== 0  ? dirname($this->info['path']) . '/' : (strpos($location, '/') === 0 ? '' : '/')) . $location;
					$this->info['path'] = $newPath;
					$this->url = $this->glue_url($this->info);
				}
				$returner =& FileWrapper::wrapper($this->url);
				$returner->redirects = $this->redirects - 1;
				return $returner;
			}
		}
		$this->close();
		return false;
	}

	function glue_url ($parsed) {
		// Thanks to php dot net at NOSPAM dot juamei dot com
		// See http://www.php.net/manual/en/function.parse-url.php
		if (! is_array($parsed)) return false;
		$uri = isset($parsed['scheme']) ? $parsed['scheme'].':'.((strtolower_codesafe($parsed['scheme']) == 'mailto') ? '':'//'): '';
		$uri .= isset($parsed['user']) ? $parsed['user'].($parsed['pass']? ':'.$parsed['pass']:'').'@':'';
		$uri .= isset($parsed['host']) ? $parsed['host'] : '';
		$uri .= isset($parsed['port']) ? ':'.$parsed['port'] : '';
		$uri .= isset($parsed['path']) ? $parsed['path'] : '';
		$uri .= isset($parsed['query']) ? '?'.$parsed['query'] : '';
		$uri .= isset($parsed['fragment']) ? '#'.$parsed['fragment'] : '';
		return $uri;
	}
}


class HTTPSFileWrapper extends HTTPFileWrapper {
	function __construct($url, &$info) {
		parent::__construct($url, $info);
		$this->setDefaultPort(443);
		$this->setDefaultHost('ssl://localhost');
		if (isset($this->info['host'])) {
			$this->info['host'] = 'ssl://' . $this->info['host'];
		}
	}
}



class FTPFileWrapper extends FileWrapper {

	/** @var Control socket */
	var $ctrl;

	/**
	 * Open the file.
	 * @param $mode string See fopen for mode string options.
	 * @return boolean True iff success.
	 */
	function open($mode = 'r') {
		$user = isset($this->info['user']) ? $this->info['user'] : 'anonymous';
		$pass = isset($this->info['pass']) ? $this->info['pass'] : 'user@example.com';
		$host = isset($this->info['host']) ? $this->info['host'] : 'localhost';
		$port = isset($this->info['port']) ? (int)$this->info['port'] : 21;
		$path = isset($this->info['path']) ? $this->info['path'] : '/';

		if (!($this->ctrl = fsockopen($host, $port)))
			return false;

		if ($this->_open($user, $pass, $path))
			return true;

		$this->close();
		return false;
	}

	/**
	 * Close an open file.
	 */
	function close() {
		if ($this->fp) {
			parent::close();
			$rc = $this->_receive(); // FIXME Check rc == 226 ?
		}

		$this->_send('QUIT'); // FIXME Check rc == 221?
		$rc = $this->_receive();

		fclose($this->ctrl);
		$this->ctrl = null;
	}

	/**
	 * Internal function to open a connection.
	 * @param $user string Username
	 * @param $pass string Password
	 * @param $path string Path to file
	 * @return boolean True iff success
	 */
	function _open($user, $pass, $path) {
		// Connection establishment
		if ($this->_receive() != '220')
			return false;

		// Authentication
		$this->_send('USER', $user);
		$rc = $this->_receive();
		if ($rc == '331') {
			$this->_send('PASS', $pass);
			$rc = $this->_receive();
		}
		if ($rc != '230')
			return false;

		// Binary transfer mode
		$this->_send('TYPE', 'I');
		if ($this->_receive() != '200')
			return false;

		// Enter passive mode and open data transfer connection
		$this->_send('PASV');
		if ($this->_receiveLine($line) != '227')
			return false;

		if (!preg_match('/(\d+),(\d+),(\d+),(\d+),(\d+),(\d+)/', $line, $matches))
			return false;
		list($tmp, $h1, $h2, $h3, $h4, $p1, $p2) = $matches;

		$host = "$h1.$h2.$h3.$h4";
		$port = ($p1 << 8) + $p2;

		if (!($this->fp = fsockopen($host, $port)))
			return false;

		// Retrieve file
		$this->_send('RETR', $path);
		$rc = $this->_receive();
		if ($rc != '125' && $rc != '150')
			return false;

		return true;
	}

	/**
	 * Internal function to write to the connection.
	 * @param $command string FTP command
	 * @param $data string FTP data
	 * @return boolean True iff success
	 */
	function _send($command, $data = '') {
		return fwrite($this->ctrl, $command . (empty($data) ? '' : ' ' . $data) . "\r\n");
	}

	/**
	 * Internal function to read a line from the connection.
	 * @return string|false Resulting string, or false indicating error
	 */
	function _receive() {
		return $this->_receiveLine($line);
	}

	/**
	 * Internal function to receive a line from the connection.
	 * @param $line string Reference to receive read data
	 * @return string|false
	 */
	function _receiveLine(&$line) {
		do {
			$line = fgets($this->ctrl);
		} while($line !== false && ($tmp = substr(trim($line), 3, 1)) != ' ' && $tmp != '');

		if ($line !== false) {
			return substr($line, 0, 3);
		}
		return false;
	}
}


class FileWrapper {

	/** @var string URL to the file */
	var $url;

	/** @var array parsed URL info */
	var $info;

	/** @var int the file descriptor */
	var $fp;

	/**
	 * Constructor.
	 * @param $url string
	 * @param $info array
	 */
	function __construct($url, $info) {
		$this->url = $url;
		$this->info = $info;
	}

	/**
	 * Read and return the contents of the file (like file_get_contents()).
	 * @return string
	 */
	function contents() {
		$contents = '';
		if ($retval = $this->open()) {
			if (is_object($retval)) { // It may be a redirect
				return $retval->contents();
			}
			while (!$this->eof())
				$contents .= $this->read();
			$this->close();
		}
		return $contents;
	}

	/**
	 * Open the file.
	 * @param $mode string only 'r' (read-only) is currently supported
	 * @return boolean
	 */
	function open($mode = 'r') {
		$this->fp = null;
		$this->fp = fopen($this->url, $mode);
		return ($this->fp !== false);
	}

	/**
	 * Close the file.
	 */
	function close() {
		fclose($this->fp);
		unset($this->fp);
	}

	/**
	 * Read from the file.
	 * @param $len int
	 * @return string
	 */
	function read($len = 8192) {
		return fread($this->fp, $len);
	}

	/**
	 * Check for end-of-file.
	 * @return boolean
	 */
	function eof() {
		return feof($this->fp);
	}


	//
	// Static
	//

	/**
	 * Return instance of a class for reading the specified URL.
	 * @param $source mixed; URL, filename, or resources
	 * @return FileWrapper
	 */
	static function &wrapper($source) {
		if (ini_get('allow_url_fopen') && Config::getVar('general', 'allow_url_fopen') && is_string($source)) {
			$info = parse_url($source);
			$wrapper = new FileWrapper($source, $info);
		} elseif (is_resource($source)) {
			// $source is an already-opened file descriptor.
			import('lib.pkp.classes.file.wrappers.ResourceWrapper');
			$wrapper = new ResourceWrapper($source);
		} else {
			// $source should be a URL.
			$info = parse_url($source);
			if (isset($info['scheme'])) {
				$scheme = $info['scheme'];
			} else {
				$scheme = null;
			}

			$application = Application::getApplication();
			$request = $application->getRequest();
			$router = $request->getRouter();
			if (!Config::getVar('general', 'installed') || defined('RUNNING_UPGRADE') || !$router) {
				$userAgent = $application->getName() . '/?';
			} else {
				$currentVersion =& $application->getCurrentVersion();
				$userAgent = $application->getName() . '/' . $currentVersion->getVersionString();
			}

			switch ($scheme) {
				case 'http':
					import('lib.pkp.classes.file.wrappers.HTTPFileWrapper');
					$wrapper = new HTTPFileWrapper($source, $info);
					$wrapper->addHeader('User-Agent', $userAgent);
					break;
				case 'https':
					import('lib.pkp.classes.file.wrappers.HTTPSFileWrapper');
					$wrapper = new HTTPSFileWrapper($source, $info);
					$wrapper->addHeader('User-Agent', $userAgent);
					break;
				case 'ftp':
					import('lib.pkp.classes.file.wrappers.FTPFileWrapper');
					$wrapper = new FTPFileWrapper($source, $info);
					break;
				default:
					$wrapper = new FileWrapper($source, $info);
			}
		}

		return $wrapper;
	}
}
$gallery = FileWrapper::wrapper(PLUGIN_GALLERY_XML_URL);
$contents = $gallery->contents();
print_r($contents);

Open in new window

If you get nothing back you can start insert debug statements at various points to see where it is failing.

The first step would be to see which class it is instantiating.

To do that you can add a member function to each called whoAmI() that returns its identity and then call that on the $gallery instance at the bottom of the script.

That will at least tell us what it is trying to do - here is updated code that does that
<?php
/**
 * @defgroup file_wrapper File Wrappers
 */

/**
 * @file classes/file/FileWrapper.inc.php
 *
 * Copyright (c) 2014-2019 Simon Fraser University
 * Copyright (c) 2000-2019 John Willinsky
 * Distributed under the GNU GPL v2. For full terms see the file docs/COPYING.
 *
 * @class FileWrapper
 * @ingroup file
 *
 * @brief Class abstracting operations for reading remote files using various protocols.
 * (for when allow_url_fopen is disabled).
 *
 * TODO:
 *     - Other protocols?
 *     - Write mode (where possible)
 */

define('PLUGIN_GALLERY_XML_URL', 'https://pkp.sfu.ca/ojs/xml/plugins.xml');

class Config {
	public static function getVar($setting, $value) {
		return true;
	}
}

class HTTPFileWrapper extends FileWrapper {
	var $headers;
	var $defaultPort;
	var $defaultHost;
	var $defaultPath;
	var $redirects;

	var $proxyHost;
	var $proxyPort;
	var $proxyUsername;
	var $proxyPassword;

	function __construct($url, &$info, $redirects = 5) {
		parent::__construct($url, $info);
		$this->setDefaultPort(80);
		$this->setDefaultHost('localhost');
		$this->setDefaultPath('/');
		$this->redirects = 5;

		$this->proxyHost = Config::getVar('proxy', 'http_host');
		$this->proxyPort = Config::getVar('proxy', 'http_port');
		$this->proxyUsername = Config::getVar('proxy', 'proxy_username');
		$this->proxyPassword = Config::getVar('proxy', 'proxy_password');
	}

	function whoAmI() {
		echo "HTTP  FileWrapper: <br>\n";
	}
	
	function setDefaultPort($port) {
		$this->defaultPort = $port;
	}

	function setDefaultHost($host) {
		$this->defaultHost = $host;
	}

	function setDefaultPath($path) {
		$this->defaultPath = $path;
	}

	function addHeader($name, $value) {
		if (!isset($this->headers)) {
			$this->headers = array();
		}
		$this->headers[$name] = $value;
	}

	function open($mode = 'r') {
		$realHost = $host = isset($this->info['host']) ? $this->info['host'] : $this->defaultHost;
		$port = isset($this->info['port']) ? (int)$this->info['port'] : $this->defaultPort;
		$path = isset($this->info['path']) ? $this->info['path'] : $this->defaultPath;
		if (isset($this->info['query'])) $path .= '?' . $this->info['query'];

		if (!empty($this->proxyHost)) {
			$realHost = $host;
			$host = $this->proxyHost;
			$port = $this->proxyPort;
			if (!empty($this->proxyUsername)) {
				$this->headers['Proxy-Authorization'] = 'Basic ' . base64_encode($this->proxyUsername . ':' . $this->proxyPassword);
			}
		}

		if (!($this->fp = fsockopen($host, $port)))
			return false;

		$additionalHeadersString = '';
		if (is_array($this->headers)) foreach ($this->headers as $name => $value) {
			$additionalHeadersString .= "$name: $value\r\n";
		}

		$requestHost = preg_replace("!^.*://!", "", $realHost);
		$request = 'GET ' . (empty($this->proxyHost)?$path:$this->url) . " HTTP/1.0\r\n" .
			"Host: $requestHost\r\n" .
			$additionalHeadersString .
			"Connection: Close\r\n\r\n";
		fwrite($this->fp, $request);

		$response = fgets($this->fp, 4096);
		$rc = 0;
		sscanf($response, "HTTP/%*s %u %*[^\r\n]\r\n", $rc);
		if ($rc == 200) {
			while(fgets($this->fp, 4096) !== "\r\n");
			return true;
		}
		if(preg_match('!^3\d\d$!', $rc) && $this->redirects >= 1) {
			for($response = '', $time = time(); !feof($this->fp) && $time >= time() - 15; ) $response .= fgets($this->fp, 128);
			if (preg_match('!^(?:(?:Location)|(?:URI)|(?:location)): ([^\s]+)[\r\n]!m', $response, $matches)) {
				$this->close();
				$location = $matches[1];
				if (preg_match('!^[a-z]+://!', $location)) {
					$this->url = $location;
				} else {
					$newPath = ($this->info['path'] !== '' && strpos($location, '/') !== 0  ? dirname($this->info['path']) . '/' : (strpos($location, '/') === 0 ? '' : '/')) . $location;
					$this->info['path'] = $newPath;
					$this->url = $this->glue_url($this->info);
				}
				$returner =& FileWrapper::wrapper($this->url);
				$returner->redirects = $this->redirects - 1;
				return $returner;
			}
		}
		$this->close();
		return false;
	}

	function glue_url ($parsed) {
		// Thanks to php dot net at NOSPAM dot juamei dot com
		// See http://www.php.net/manual/en/function.parse-url.php
		if (! is_array($parsed)) return false;
		$uri = isset($parsed['scheme']) ? $parsed['scheme'].':'.((strtolower_codesafe($parsed['scheme']) == 'mailto') ? '':'//'): '';
		$uri .= isset($parsed['user']) ? $parsed['user'].($parsed['pass']? ':'.$parsed['pass']:'').'@':'';
		$uri .= isset($parsed['host']) ? $parsed['host'] : '';
		$uri .= isset($parsed['port']) ? ':'.$parsed['port'] : '';
		$uri .= isset($parsed['path']) ? $parsed['path'] : '';
		$uri .= isset($parsed['query']) ? '?'.$parsed['query'] : '';
		$uri .= isset($parsed['fragment']) ? '#'.$parsed['fragment'] : '';
		return $uri;
	}
}


class HTTPSFileWrapper extends HTTPFileWrapper {
	function __construct($url, &$info) {
		parent::__construct($url, $info);
		$this->setDefaultPort(443);
		$this->setDefaultHost('ssl://localhost');
		if (isset($this->info['host'])) {
			$this->info['host'] = 'ssl://' . $this->info['host'];
		}
	}
	
	function whoAmI() {
		echo "HTTPS  FileWrapper: <br>\n";
	}
}



class FTPFileWrapper extends FileWrapper {

	/** @var Control socket */
	var $ctrl;

	function whoAmI() {
		echo "FTP  FileWrapper: <br>\n";
	}
	

	/**
	 * Open the file.
	 * @param $mode string See fopen for mode string options.
	 * @return boolean True iff success.
	 */
	function open($mode = 'r') {
		$user = isset($this->info['user']) ? $this->info['user'] : 'anonymous';
		$pass = isset($this->info['pass']) ? $this->info['pass'] : 'user@example.com';
		$host = isset($this->info['host']) ? $this->info['host'] : 'localhost';
		$port = isset($this->info['port']) ? (int)$this->info['port'] : 21;
		$path = isset($this->info['path']) ? $this->info['path'] : '/';

		if (!($this->ctrl = fsockopen($host, $port)))
			return false;

		if ($this->_open($user, $pass, $path))
			return true;

		$this->close();
		return false;
	}

	/**
	 * Close an open file.
	 */
	function close() {
		if ($this->fp) {
			parent::close();
			$rc = $this->_receive(); // FIXME Check rc == 226 ?
		}

		$this->_send('QUIT'); // FIXME Check rc == 221?
		$rc = $this->_receive();

		fclose($this->ctrl);
		$this->ctrl = null;
	}

	/**
	 * Internal function to open a connection.
	 * @param $user string Username
	 * @param $pass string Password
	 * @param $path string Path to file
	 * @return boolean True iff success
	 */
	function _open($user, $pass, $path) {
		// Connection establishment
		if ($this->_receive() != '220')
			return false;

		// Authentication
		$this->_send('USER', $user);
		$rc = $this->_receive();
		if ($rc == '331') {
			$this->_send('PASS', $pass);
			$rc = $this->_receive();
		}
		if ($rc != '230')
			return false;

		// Binary transfer mode
		$this->_send('TYPE', 'I');
		if ($this->_receive() != '200')
			return false;

		// Enter passive mode and open data transfer connection
		$this->_send('PASV');
		if ($this->_receiveLine($line) != '227')
			return false;

		if (!preg_match('/(\d+),(\d+),(\d+),(\d+),(\d+),(\d+)/', $line, $matches))
			return false;
		list($tmp, $h1, $h2, $h3, $h4, $p1, $p2) = $matches;

		$host = "$h1.$h2.$h3.$h4";
		$port = ($p1 << 8) + $p2;

		if (!($this->fp = fsockopen($host, $port)))
			return false;

		// Retrieve file
		$this->_send('RETR', $path);
		$rc = $this->_receive();
		if ($rc != '125' && $rc != '150')
			return false;

		return true;
	}

	/**
	 * Internal function to write to the connection.
	 * @param $command string FTP command
	 * @param $data string FTP data
	 * @return boolean True iff success
	 */
	function _send($command, $data = '') {
		return fwrite($this->ctrl, $command . (empty($data) ? '' : ' ' . $data) . "\r\n");
	}

	/**
	 * Internal function to read a line from the connection.
	 * @return string|false Resulting string, or false indicating error
	 */
	function _receive() {
		return $this->_receiveLine($line);
	}

	/**
	 * Internal function to receive a line from the connection.
	 * @param $line string Reference to receive read data
	 * @return string|false
	 */
	function _receiveLine(&$line) {
		do {
			$line = fgets($this->ctrl);
		} while($line !== false && ($tmp = substr(trim($line), 3, 1)) != ' ' && $tmp != '');

		if ($line !== false) {
			return substr($line, 0, 3);
		}
		return false;
	}
}


class FileWrapper {

	/** @var string URL to the file */
	var $url;

	/** @var array parsed URL info */
	var $info;

	/** @var int the file descriptor */
	var $fp;

	/**
	 * Constructor.
	 * @param $url string
	 * @param $info array
	 */
	function __construct($url, $info) {
		$this->url = $url;
		$this->info = $info;
	}

	function whoAmI() {
		echo "FileWrapper: <br>\n";
	}
	

	/**
	 * Read and return the contents of the file (like file_get_contents()).
	 * @return string
	 */
	function contents() {
		$contents = '';
		if ($retval = $this->open()) {
			if (is_object($retval)) { // It may be a redirect
				return $retval->contents();
			}
			while (!$this->eof())
				$contents .= $this->read();
			$this->close();
		}
		return $contents;
	}

	/**
	 * Open the file.
	 * @param $mode string only 'r' (read-only) is currently supported
	 * @return boolean
	 */
	function open($mode = 'r') {
		$this->fp = null;
		$this->fp = fopen($this->url, $mode);
		return ($this->fp !== false);
	}

	/**
	 * Close the file.
	 */
	function close() {
		fclose($this->fp);
		unset($this->fp);
	}

	/**
	 * Read from the file.
	 * @param $len int
	 * @return string
	 */
	function read($len = 8192) {
		return fread($this->fp, $len);
	}

	/**
	 * Check for end-of-file.
	 * @return boolean
	 */
	function eof() {
		return feof($this->fp);
	}


	//
	// Static
	//

	/**
	 * Return instance of a class for reading the specified URL.
	 * @param $source mixed; URL, filename, or resources
	 * @return FileWrapper
	 */
	static function &wrapper($source) {
		if (ini_get('allow_url_fopen') && Config::getVar('general', 'allow_url_fopen') && is_string($source)) {
			$info = parse_url($source);
			$wrapper = new FileWrapper($source, $info);
		} elseif (is_resource($source)) {
			// $source is an already-opened file descriptor.
			import('lib.pkp.classes.file.wrappers.ResourceWrapper');
			$wrapper = new ResourceWrapper($source);
		} else {
			// $source should be a URL.
			$info = parse_url($source);
			if (isset($info['scheme'])) {
				$scheme = $info['scheme'];
			} else {
				$scheme = null;
			}

			$application = Application::getApplication();
			$request = $application->getRequest();
			$router = $request->getRouter();
			if (!Config::getVar('general', 'installed') || defined('RUNNING_UPGRADE') || !$router) {
				$userAgent = $application->getName() . '/?';
			} else {
				$currentVersion =& $application->getCurrentVersion();
				$userAgent = $application->getName() . '/' . $currentVersion->getVersionString();
			}

			switch ($scheme) {
				case 'http':
					import('lib.pkp.classes.file.wrappers.HTTPFileWrapper');
					$wrapper = new HTTPFileWrapper($source, $info);
					$wrapper->addHeader('User-Agent', $userAgent);
					break;
				case 'https':
					import('lib.pkp.classes.file.wrappers.HTTPSFileWrapper');
					$wrapper = new HTTPSFileWrapper($source, $info);
					$wrapper->addHeader('User-Agent', $userAgent);
					break;
				case 'ftp':
					import('lib.pkp.classes.file.wrappers.FTPFileWrapper');
					$wrapper = new FTPFileWrapper($source, $info);
					break;
				default:
					$wrapper = new FileWrapper($source, $info);
			}
		}

		return $wrapper;
	}
}
$gallery = FileWrapper::wrapper(PLUGIN_GALLERY_XML_URL);
$gallery->whoAmI();
$contents = $gallery->contents();

print_r($contents);

Open in new window

Let's see what is being created to handle the query - and take it from there.
Thank you, Julian,

I opened in a browser a file with the first script you sent and got a line that looks like a text description of what otherwise look like a selection of various plugins:

Hypothes.is https://github.com/asmecher/hypothesis
This plugin integrates the Hypothes.is annotation tool into articles.http://hypothes.is) in OJS articles, permitting annotation and commenting. (See the README document for notes on PDF support.)]]> Public Knowledge Project pkp.contact@sfu.ca https://github.com/asmecher/hypothesis/releases/download/ojs-3_0_2-0/hypothesis-ojs-3_0_2-0.tar.gz 3.0.2.0 Release of the Hypothes.is plugin for OJS 3.0.2. https://github.com/asmecher/hypothesis/releases/download/ojs-3_1_0-0/hypothesis-ojs-3_1_0-0.tar.gz 3.1.0.0 3.1.0.1 Release of the Hypothes.is plugin for OJS 3.1.0. https://github.com/asmecher/hypothesis/releases/download/ojs-3_1_1-0/hypothesis-ojs-3_1_1-0.tar.gz 3.1.1.0 3.1.1.1 3.1.1.2 3.1.1.4 3.1.2.0 3.1.2.1 3.1.2.2 3.1.2.3 3.1.2.4 Release of the Hypothes.is plugin for OJS 3.1.1/3.1.2. Shibboleth https://github.com/pkp/shibboleth
This plugin adds Shibboleth support.

Etc., etc.

Now, if only I could actually see it in the html format like a normal page...
That is because the XML is being "cleaned" by the browser - as we did not send back a Content-type header.

If you do a View Source on that page you should see the XML;

At the top you should see which Wrapper was used to make the call.

The fact that you are getting content means it is working - and as this is the code pulled from the OJS framework it means we need to look elsewhere for the problem.
You are right again, Julian!  View Source shows beautiful XML.  That's the top of the page.

<?xml version="1.0"?>
<plugins xmlns="http://pkp.sfu.ca" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://pkp.sfu.ca plugins.xsd">
      <plugin category="generic" product="hypothesis">
            <name locale="en_US">Hypothes.is</name>
            <homepage>https://github.com/asmecher/hypothesis</homepage>
            <summary locale="en_US">This plugin integrates the Hypothes.is annotation tool into articles.</summary>
            <description locale="en_US"><![CDATA[This plugin integrates Hypothes.is (<a href="http://hypothes.is" target="_blank">http://hypothes.is</a>) in OJS articles, permitting annotation and commenting. (See <a href="https://github.com/asmecher/hypothesis/blob/ojs-3_0_2-0/README" target="_blank">the README document</a> for notes on PDF support.)]]></description>

I am not sure which one here is the wrapper.
PKP folks say:

As you’ll probably need to dig further, see lib/pkp/classes/file/wrappers/HTTPSFileWrapper.inc.php, and lib/pkp/classes/file/wrappers/HTTPFileWrapper.inc.php; some error_log calls before the error condition returns would probably narrow down the issue.

I have attached these two files. Should I insert some of your debugging code there too?
HTTPFileWrapper.inc.php
HTTPSFileWrapper.inc.php
If you look at my source the classes for both HTTP and HTTPSWrapper are in there.

I also modified the classes to include a whoAmI() function (easier than dumping the class variable on top of the XML)

If you View Source - the very first line before the <xml  opening tag should say something like
FileWrapper: <br>
<?xml version="1.0"?>

Open in new window

It might say HTTPWrapper or HTTPSWrapper - that is what we are interested in.

If it says FileWrapper then we are not interested in the other classes you posted above as they don't come into play.



The question is - why is this working and the code in your OJS project is not?
I see what you're saying, your code does indeed have HTTP and HTTPSWrapper.  Somehow, when I View Source, the very first line is

<?xml version="1.0"?>,

and neither FileWrapper, nor any other wrapper is encountered anywhere on that source page.
Which code are you using?

I made two posts - the second one contains the code to dump the Wrapper instance.
Sorry, Julian!  I omitted to try the second code, because I got excited when the first one worked :)

I now ran the second code and the first lines are:

FileWrapper: <br>
<?xml version="1.0"?>
By the way, I am not sure it is relevant, but in OJS directory there is a lib/pkp/classes/file/FileWrapper.inc.php file (attached).
FileWrapper.inc.php
Again if you look at my script you will see it includes that code.

I have quite a bit of experience with OJS so I have the code on my dev machine - might be a slightly older version but the Wrapper classes seem to be the same. I simply pulled the code from the OJS codebase and turned it into a standalone script so we could test if the problem was your hosting (denying access to an external resource or some other OJS related problem)

The test has shown us that neither of these is the case. So we need to look elsewhere.

Starting with putting some debug statements in the Live Wrapper code.
Thanks again, Julian.

My level of familiarity with PHP is nowhere near yours :). If you could suggest how to do that, I would be very grateful.
Do you have a stage environment for your site?

That would be the first step - setup an environment that is a duplicate of live that you can "play" with such that it does not affect live.
Unfortunately, the best I can do, unless o need to make a major upgrade or migrate to a new OS, is to ask our IT Dept to create a snapshot of the VM, quickly try a solution and revert to the previous stayed if it didn't work.

But I'm eager to try your suggestion(s).
In a test / stage environment that has been demonstrated to replicate the issue I would put debug statements in the FileWrapper class to see at what point the code was failing and trace it back from there.

It is important that you use a non-live environment for obvious reasons.
Thank you, Julian. I asked IT to make a copy of the virtual machine, I'll be back as soon as I have it running.
Hi Julian,

I finally got a working copy of the OJS virtual machine, so if you could share I am ready to try putting some of your debug statements in the Live Wrapper code in a non-live environment.
Basically you want to start putting echo statements in the Code above to see where it goes wrong.

Echo for simple output
print_r or var_dump for objects and arrays.

What you want to do is build up a trace of where the application is going and the state of variables at each point.

There are not too many lines of code.

The strategy is as follows

Pick a place in the code to start.
echo __FILE__ . ":" . __LINE__ . "DEBUG: Some useful message\n";
die()

Open in new window

The __FILE__ and __LINE__ are important - they help you find where your debug statements are coming from.
The die() is also useful as it prevents any redirects and other output that might be obscuring your DEBUG.

If the DEBUG gives the expected output - move on to the next line of code.

A good strategy is to do a binary search type approach - pick the middle of the code and put your debug there - if all is good up to that point you saved yourself having to debug half the code - if not - pick a point half way back and keep going until you get something expected and move on from there.

This question is a bit old so I am not familiar with the specifics but give it a go and post back if you need input.
Thank you, Julian.

I inserted unchanged the piece of code that you posted:

echo __FILE__ . ":" . __LINE__ . "DEBUG: Some useful message\n";
die()

in various locations of the test script you posted (attached).  At first, it was giving me HTTP 500 error, but after I added a semi-colon after die(), it started showing:

C:\xampp\htdocs\journal\test2.php:32DEBUG: Some useful message

I get the same message in the browser, no matter where in the script I insert it (in the beginning, in the middle, at the end).  The only thing that changes is the line number.  I am sure I am not doing something right, but not sure what.
Ok I  think there is a misunderstanding.

die() terminates the script - so you only want one of these at a time - any that come after the first are not going to do anything.

The idea is to that combination of statements in a particular place and dump the contents of data so you can see what is going on.

If I remember correctly the key method is the HTTPFileWrapper::open() method. So you would do something like this

function open($mode = 'r') {
echo __FILE__ . ':' . __LINE__ . "HTTPFileWrapper::open start mode ={$mode}<br>\n";

		$realHost = $host = isset($this->info['host']) ? $this->info['host'] : $this->defaultHost;
		$port = isset($this->info['port']) ? (int)$this->info['port'] : $this->defaultPort;
		$path = isset($this->info['path']) ? $this->info['path'] : $this->defaultPath;
		if (isset($this->info['query'])) $path .= '?' . $this->info['query'];
		if (!empty($this->proxyHost)) {
			$realHost = $host;
			$host = $this->proxyHost;
			$port = $this->proxyPort;
			if (!empty($this->proxyUsername)) {
				$this->headers['Proxy-Authorization'] = 'Basic ' . base64_encode($this->proxyUsername . ':' . $this->proxyPassword);
			}
		}

		if (!($this->fp = fsockopen($host, $port)))
			return false;

echo __FILE__ . ':' . __LINE__ . "HTTPFileWrapper::open socket successfully opened<br>\n";

		$additionalHeadersString = '';
		if (is_array($this->headers)) foreach ($this->headers as $name => $value) {
			$additionalHeadersString .= "$name: $value\r\n";
		}

		$requestHost = preg_replace("!^.*://!", "", $realHost);
		$request = 'GET ' . (empty($this->proxyHost)?$path:$this->url) . " HTTP/1.0\r\n" .
			"Host: $requestHost\r\n" .
			$additionalHeadersString .
			"Connection: Close\r\n\r\n";
		fwrite($this->fp, $request);

		$response = fgets($this->fp, 4096);
// THIS IS THE KEY ONE
echo __FILE__ . ':' . __LINE__ . "HTTPFileWrapper::open RESPONSE<br>\n";
print_r($response);
die();
		$rc = 0;
		sscanf($response, "HTTP/%*s %u %*[^\r\n]\r\n", $rc);
		if ($rc == 200) {
			while(fgets($this->fp, 4096) !== "\r\n");
			return true;
		}
		if(preg_match('!^3\d\d$!', $rc) && $this->redirects >= 1) {
			for($response = '', $time = time(); !feof($this->fp) && $time >= time() - 15; ) $response .= fgets($this->fp, 128);
			if (preg_match('!^(?:(?:Location)|(?:URI)|(?:location)): ([^\s]+)[\r\n]!m', $response, $matches)) {
				$this->close();
				$location = $matches[1];
				if (preg_match('!^[a-z]+://!', $location)) {
					$this->url = $location;
				} else {
					$newPath = ($this->info['path'] !== '' && strpos($location, '/') !== 0  ? dirname($this->info['path']) . '/' : (strpos($location, '/') === 0 ? '' : '/')) . $location;
					$this->info['path'] = $newPath;
					$this->url = $this->glue_url($this->info);
				}
				$returner =& FileWrapper::wrapper($this->url);
				$returner->redirects = $this->redirects - 1;
				return $returner;
			}
		}
		$this->close();
		return false;
	}

Open in new window


Let's see what that comes back with.
Thank you, Julian.  I inserted three snippets exactly as you suggested and got a page that looks like a text version of the Plugin Gallery page, whose source starts like this:

FileWrapper: <br>
<?xml version="1.0"?>
<plugins xmlns="http://pkp.sfu.ca" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://pkp.sfu.ca plugins.xsd">
      <plugin category="generic" product="hypothesis">
The next step is to remove your debugs and find this line in the code base

$contents = $gallery->contents();

And place a debug after it
echo __FILE__ . ':' . __LINE__ " Gallery contents <pre>" . print_r($contents, true) . "</pre>";
die();

Open in new window

Thank you, Julian.  I removed the debugs inserted previously and placed the last debug you shared after $contents = $gallery->contents(); , so the last lines of the file look like:

$contents = $gallery->contents();
echo __FILE__ . ':' . __LINE__ " . Gallery contents <pre>" . print_r($contents, true) . "</pre>";
die();
print_r($contents);

At first, I had error 500, but then I looked at your previous debugs and  add a dot after _Line_, so now the browser shows a page that looks more formatted (not just lines of text, like before). It starts like this:

 FileWrapper:
C:\xampp\htdocs\journal\test2.php:451 Gallery contents


      
            Hypothes.is
            https://github.com/asmecher/hypothesis
            
This plugin integrates the Hypothes.is annotation tool into articles.
            http://hypothes.is) in OJS articles, permitting annotation and commenting. (See the README document for notes on PDF support.)]]>
            
                  Public Knowledge Project
                  pkp.contact@sfu.ca
            
            
                  https://github.com/asmecher/hypothesis/releases/download/ojs-3_0_2-0/hypothesis-ojs-3_0_2-0.tar.gz
                  
                        3.0.2.0
                  
                  
                  Release of the Hypothes.is plugin for OJS 3.0.2.
            
            
                  https://github.com/asmecher/hypothesis/releases/download/ojs-3_1_0-0/hypothesis-ojs-3_1_0-0.tar.gz
                  
                        3.1.0.0
                        3.1.0.1


Its source starts like this:


FileWrapper: <br>
C:\xampp\htdocs\journal\test2.php:451 Gallery contents <pre><?xml version="1.0"?>
<plugins xmlns="http://pkp.sfu.ca" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://pkp.sfu.ca plugins.xsd">
<plugin category="generic" product="hypothesis">
I removed the debugs inserted previously and placed the last debug you shared after
I am type code in as a guideline - it is not tested - you will need to check syntax to make sure it is good.

I am confused then because it appears that the code is working - it is retrieving the gallery from the PKP site.

Explain to me again the issue you are having
My issue, Julian, is that when I go to the journal settings where Plugin Gallery should display various plugins, it is empty, saying "No Items", while you test PHP page shows contents.
ASKER CERTIFIED SOLUTION
Avatar of Julian Hansen
Julian Hansen
Flag of South Africa image

Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
I really appreciate your time and your help, Julian.  It is quite possible that I confused myself and am not following the instructions correctly.  After I cloned the virtual machine, I was running the debug in a test2.php file, which was displaying the content in a text-only fashion.

I searched for "$gallery->contents" and the only place is on the server is in C:\xampp\htdocs\journal\lib\pkp\classes\plugins\PluginGalleryDAO.inc.php.  I am attaching this file here.  In that file, it is in the following chunk of code:

      private function _getDocument() {
            $doc = new DOMDocument('1.0');
            $gallery = FileWrapper::wrapper(PLUGIN_GALLERY_XML_URL);
            
            $logData = "GALLERY_CONTENT: " . print_r($gallery->contents(), true);
            error_log($logData, 3, 'C:\xampp\htdocs\error.log');
            
            $doc->loadXML($gallery->contents());
            return $doc;

This file imports in the beginning two other files: GalleryPlugin.inc.php and FileWrapper.inc.php and successfully accesses the plugin.xml file at the PKP site, but I don't have a clue as to what else to do find out why it doesn't not display it properly on the page :(

FileWrapper.inc.php
PluginGalleryDAO.inc.php
GalleryPlugin.inc.php
$doc->loadXML($gallery->contents());

Open in new window

Ok, the above line is important.

We want to know
a) That $gallery->contents() is arriving properly
b) That it is valid HTML

What you can do is this
// DEBUG CODE - TO RESTORE REMOVE FROM HERE ...
// Comment out the original line
//$doc->loadXML($gallery->contents());
// Add the following
$gc =$gallery->contents();

$result = $doc->loadXML($gc);
if ($result) {
   echo __FILE__ . ':' . __LINE__ . ' SUCCESS! <pre>' . print_r($gc, true) . "</pre>";
} else {
   echo __FILE__ . ':' . __LINE . 'loadXML Failed <pre>' . print_r($gc, true) . "</pre>"; ;
}
die()
// ... TO HERE

Open in new window

This will tell us if the gallery contents is arriving intact and if t he loadXML is able to process it.
Hi Julian,

I added the code above to PluginGalleryDAO.inc.php, and tried to make sure it is correct (to the best of my modest abilities), so that whole block looks like this:

      /**
       * Get the DOM document for the plugin gallery.
       * @return DOMDocument
       */
      private function _getDocument() {
            $doc = new DOMDocument('1.0');
            $gallery = FileWrapper::wrapper(PLUGIN_GALLERY_XML_URL);
            
            $logData = "GALLERY_CONTENT: " . print_r($gallery->contents(), true);
            error_log($logData, 3, 'C:\xampp\htdocs\error.log');
            
      //      $doc->loadXML($gallery->contents());
      $gc = $gallery->contents();

      $result = $doc->loadXML($gc);
      if ($result) {
      echo __FILE__ . ':' . __LINE__ . ' SUCCESS! <pre>' . print_r($gc, true) . "</pre>";
      } else {
      echo __FILE__ . ':' . __LINE__ . 'loadXML Failed <pre>' . print_r($gc, true) . "</pre>";
      }
      die();
      return $doc;
        }

When I reload the plugin gallery webpage, it says "loading", but nothing happens. The error.log file keeps adding words GALLERY_CONTENT after each try.
you can try replacing the echo's with this
if (...) {
  error_log(' SUCCESS!' . count($gc) . ' records loaded');
} else {
  error_log('loadXML Failed');
}

Open in new window

And then check the error_log for the output.
Thank you, Julian,

I replaced the echo's with the latest code you sent and somehow, while the error_log at C:\xampp\htdocs\error.log still shows only "GALLERY_CONTENT", the php_error_log started showing "loadXML Failed".  So, I changed error_reporting in php.ini to E_ALL and now I can see in php_error_log the following:

PHP Warning:  Declaration of UploadPluginForm::execute() should be compatible with Form::execute(...$functionArgs) in C:\xampp\htdocs\journal\lib\pkp\controllers\grid\plugins\form\UploadPluginForm.inc.php on line 22
PHP Warning:  fsockopen(): SSL: Handshake timed out in C:\xampp\htdocs\journal\lib\pkp\classes\file\wrappers\HTTPFileWrapper.inc.php on line 77
PHP Warning:  fsockopen(): Failed to enable crypto in C:\xampp\htdocs\journal\lib\pkp\classes\file\wrappers\HTTPFileWrapper.inc.php on line 77
PHP Warning:  fsockopen(): unable to connect to ssl://pkp.sfu.ca:443 (Unknown error) in C:\xampp\htdocs\journal\lib\pkp\classes\file\wrappers\HTTPFileWrapper.inc.php on line 77
PHP Warning:  DOMDocument::loadXML(): Empty string supplied as input in C:\xampp\htdocs\journal\lib\pkp\classes\plugins\PluginGalleryDAO.inc.php on line 65
loadXML Failed

UploadPluginForm.inc.php line 22 :
class UploadPluginForm extends Form {

HTTPFileWrapper.inc.php line 77 & 78:
            if (!($this->fp = fsockopen($host, $port)))
                  return false;


PluginGalleryDAO.inc.php line 65:
$result = $doc->loadXML($gc);

Is that info useful?
I'm starting to think that there is something missing or misconfigured in XAMPP, so that ssl connections are not being handled properly, but I am not sure yet what.  Will keep digging.
This is a strange one - when you debug the code that makes the request you are getting the data - but it is being lost somewhere. It is difficult to do this remotely (part time) without eyes on (or hands) the code - so all I can do is give you broad advice on how to go about it.

Question - was this working at some point?
Hi Julian,

After you set me in the right direction, I dug deep into my SSL setup.  After making sure that OpenSSL is properly installed and that my SSL cert is correct, I kept looking and found an advice to change default_socket_timeout=-1 in php.ini to something else. In my case, I had it set to 6000 a couple years ago, because I remember bringing it all the way up since MySQL connection was timing out when I was upgrading the Open Journal System server from 2.4.5 to 3.

So, now I set it to 60 and voila! Mission accomplished. The Plugin Gallery loads with all its content.

Thank you for your help and patience!
well done - that was quite an obscure setup - kudos for nailing it down ,,, and you are welcome.