Klasse für Versionsabgleich
Version checking and publishing class
Version checks and auto-updates become more and more important for PHP applications and CMS.
The class presented in this article contains methods and static functions to parse and structure
(Java-Style) documentation comments of class- and interface definitions. Furthermore, MD5 and
SHA1 checksums are built for all files, as well as the (comment/whitespace) stripped source codes.
This allows to determine if an update is really necessary or if only changes in comments were made.
These features are used in two static main functions: Versioning::publishModule()
and
Versioning::checkModuleVersion()
. Versioning::publishModule()
compresses a package/module/library
(located in a specified folder) to a ZIP archive located in a publishing path. With this
download-archive, other information files are saved in the target directory, such as checksums,
the version
file, the readme
and license
info file. The "repository" folder of a published
module contains the following files:
- my-module.zip: Package contents (download file)
- my-module.zip.md5: MD5 checksum of the ZIP file
- my-module.zip.sha1: SHA1 checksum of the ZIP file
- my-module.checksum: Checksums of all recognized source codes in the package
- my-module.version: Copy of the file "version"
- my-module.license: Copy of the file "license"
- my-module.readme: Copy of the file "readme"
- my-module.content: Details the package contents in JSON format
Other packages have other file names, respectively. These information are used by the function
Versioning::checkModuleVersion()
, which performs a remote (HTTP) lookup in a published repository
to check for updates. First the package checksum is checked and compared with the checksum of the
local package. If the checksum is different, then other files, such as the version file and the
contents-JSON, are checked to determine if an update is "urgently" necessary or if only changes
in documentation or white-spaces are made.
Versionsprüfungen und Auto-Updates finden mehr und mehr in PHP-Applikationen und CMS
Anwendung. Die hier vorgestellte Klasse beinhaltet Methoden und statische Funktionen,
um PHP-Dateien nach (Java-Style) Dokumentationskommentaren von Klassen/Interface-Definitionen
zu durchsuchen und diese in eine übersichtliche Array-Struktur zu bringen. Weiterhin werden
MD5 und SHA1 Checksummen über alle Dateien gebildet. Auch über die Quelltexte ohne Kommentare
werden Checksummen gebildet, somit kann festgestellt werden, ob sich nur in der Dokumentation
etwas geändert hat und ein Update nicht zwingend notwendig ist.
Diese Features kommen in zwei statischen Methoden zum Einsatz: Versioning::checkModuleVersion()
und Versioning::publishModule()
. Mit Versioning::publishModule()
kann ein
Package/Module/Library-Verzeichnis in einem ZIP gepackt und in einem Zielpfad zum download
bereitgestellt werden. Zusätzlich werden dieser Datei die Checksummen, das Readme (readme
),
die Lizenzdatei (license
) und die Versionsdatei (version
), sowie die Download-Checksummen
(zum Prüfen der heruntergeladenen ZIP-Datei) hinzugefügt. Folgende Dateien umfasst ein
veröffentlichtes Modul:
- my-module.zip: Download-Archiv des Packages
- my-module.zip.md5: MD5 Checksumme der ZIP-Datei
- my-module.zip.sha1: SHA1 Checksumme der ZIP-Datei
- my-module.checksum: Checksumme über alle erkannten Quelltexte im Archiv
- my-module.version: Kopie der Datei "version"
- my-module.license: Kopie der Datei "license"
- my-module.readme: Kopie der Datei "readme"
- my-module.content: Details der Inhalte im JSON-Format
Für andere Packages sind die Namen entsprechend anders. Diese Information werden von der
Funktion Versioning::checkModuleVersion()
verwendet, um mithilfe eines HTTP-lookup der
Versionsdatei und der Checksumme zu prüfen, ob ein Update verfügbar ist. Falls ein Update
vorliegt wird geprüft, was sich geändert hat und ob ein Update wirklich notwendig ist
(oder nur die Dokumentation oder Whitespaces sich geändert haben).
Sample source code
Anwendungsbeispiel
<?php
require_once("swlib/swlib.class.php");
$_LIB_PATH = '/tmp/tst/swlib';
use sw\Versioning;
use sw\FileSystem;
// Instantiate the object
$ver = new Versioning();
// Get all files in library path of the SwLibrary (we use the swlib as example,
// there are plenty of classes in this path :)
$files = FileSystem::find(swlib::getLibPath());
// ... But we use only the first three files - it's the same for all other files
$files = array($files[0], $files[1], $files[2]);
// Scan all the files that you can scan
$ver->readFiles($files, $_LIB_PATH);
// Print the results we obtained:
print 'Classes = ' . print_r($ver->getClasses(), true) . "\n";
print 'SHA1 checksums = ' . print_r($ver->getFileSha1s(), true) . "\n";
print 'MD5 checksums = ' . print_r($ver->getFileMd5s(), true) . "\n";
print 'Stripped code checksums MD5) = ' . print_r($ver->getSourceCodeMd5s(), true) . "\n";
print 'File details = ' . print_r($ver->getFileDetails(), true) . "\n\n<hr><\n>";
// Example for publishing a package. This directory must be existing and writable.
Versioning::publishModule('swlib', $_LIB_PATH, dirname(__FILE__) . '/swlib');
// Check a version of a package
print_r(Versioning::checkModuleVersion('swlib', $_LIB_PATH, 'http://localhost' . dirname($_SERVER['PHP_SELF']) . '/swlib'));
Output
Ausgabe
Classes = Array (
[arrayfilter] => 1.0
[cache] => 0.1
[eexception] => 1.0
)
SHA1 checksums = Array (
[ArrayFilter.class.php] => d313140fe1fd6cceccbe6e15185d999436cbcb49
[Cache.class.php] => 0019328b74c32588bee35a8a0a2f837e95581f97
[EException.class.php] => 5cd19b2abf98121ed59b15b934b5f458e2c0cbe6
)
MD5 checksums = Array (
[ArrayFilter.class.php] => c1ea4599c43538fd84dbd7475a7a6789
[Cache.class.php] => e532897b6b92e1ae6f89da12a3236790
[EException.class.php] => 5ea9311e0cffb3fd1a0c6ffd941d8838
)
Stripped code checksums MD5) = Array (
[ArrayFilter.class.php] => 49d70bd2637838221feb0a6f130f3508
[Cache.class.php] => 8515aef2fda110d63aac3ec3c6e44924
[EException.class.php] => 971e89b8a1811240cafecabe97f214f2
)
File details = Array (
[ArrayFilter.class.php] => Array (
[file_sha1] => d313140fe1fd6cceccbe6e15185d999436cbcb49
[file_md5] => c1ea4599c43538fd84dbd7475a7a6789
[source_md5] => 49d70bd2637838221feb0a6f130f3508
[classes] => Array (
[arrayfilter] => Array (
[text] => Provides static functions for simple array filtering and finding tasks.
[package] => de.atwillys.sw.php.swLib
[author] => Stefan Wilhelm
[copyright] => Stefan Wilhelm, 2007-2010
[license] => GPL
[version] => 1.0
[type] => class
[name] => ArrayFilter
)
)
)
[Cache.class.php] => Array (
[file_sha1] => 0019328b74c32588bee35a8a0a2f837e95581f97
[file_md5] => e532897b6b92e1ae6f89da12a3236790
[source_md5] => 8515aef2fda110d63aac3ec3c6e44924
[classes] => Array (
[cache] => Array (
[text] => Cache management class with GZ compression and uncompress caching of
contents and files. Package caches are stored in sub directories. Each
resource is defined by the SHA1 checksum.
NOTE: THIS CLASS IS BEING DEVELOPED
[package] => de.atwillys.sw.php.swLib
[author] => Stefan Wilhelm
[copyright] => Stefan Wilhelm, 2010
[license] => GPL
[version] => 0.1
[type] => class
[name] => Cache
)
)
)
[EException.class.php] => Array (
[file_sha1] => 5cd19b2abf98121ed59b15b934b5f458e2c0cbe6
[file_md5] => 5ea9311e0cffb3fd1a0c6ffd941d8838
[source_md5] => 971e89b8a1811240cafecabe97f214f2
[classes] => Array(
[eexception] => Array(
[text] => Implements global error, assertion and exception handling. Errors, warnings
and messages are categorized and either thrown as exception or only traced
(e.g. warnings, messagses). A global exception handler catches uncaught
exceptions, traces the details and prints a HTML error text (without details,
as a MySqlException('You have an error near SELECT * form users where password=...')
is nothing to be seen by the user. Assertions are only traced.
[package] => de.atwillys.sw.php.swLib
[author] => Stefan Wilhelm
[copyright] => Stefan Wilhelm, 2006-2010
[license] => GPL
[version] => 1.0
[uses] => Array (
[exception] => Exception
[(optional) tracer] => (optional) Tracer
)
[type] => class
[name] => EException
[extends] => Exception
)
)
)
[...]
[...]
[...]
[...]
)
Class source code
Klassen-Quelltext
<?php
/**
* Library version checking and reporting class. Scans a package source
* directory for files, builds SHA1 and MD5 checksums for of each file and
* (if PHP) fetches additional information about documented classes and
* interfaces. The static function publishModule() allows to zip a package
* source directory and generate info files containing the checksums, details,
* version, readme and license and publish all this in a reporitory folder
* (which has to be writable). The function checkModuleVersion() performs a
* HTTP lookup to a repository generated by the function publishModule(),
* compares version, checksum and details and reports a recommendation if the
* the package has to be updated or not.
*
* @gpackage de.atwillys.sw.php.swLib
* @author Stefan Wilhelm
* @copyright Stefan Wilhelm, 2010
* @license GPL
* @version 1.0
*/
namespace sw;
final class Versioning {
/**
* Contains the SHA1 checksum over all scanned PHP code files.
* @var string
*/
private $checksum = '';
/**
* Contains the versions classes and interfaces that were found in the files.
* This array is an associative array, where the keys are the lower case
* classes and the values the version strings of these classes/interfaces.
* The versions are determined using the @version entry.
* @var array
*/
private $versions = array();
/**
* Contains the files that were checked
* @var array
*/
private $files = array();
/**
* Contains the exceptions
* @var array
*/
private $exceptions = array();
/**
* Constructor
*/
public final function __construct() {
}
/**
* Destructor
*/
public final function __destruct() {
}
/**
* Returns the files which were searched
* @return array
*/
public final function getFileDetails() {
return $this->files;
}
/**
* Returns the classes that were found in the searched files
* @return array
*/
public final function getClasses() {
return $this->versions;
}
/**
* Returns the SHA1 checksum of all scanned code files (*.php).
* @return string
*/
public final function getChecksum() {
return $this->checksum;
}
/**
* Returns the file-MD5 checksums of the scanned files as associative array,
* where the keys are the file paths/sub-paths and the values the checksums.
* @return array.
*/
public final function getFileMd5s() {
$cs = array();
foreach ($this->files as $file => $desc) {
$cs[$file] = $desc['file_md5'];
}
return $cs;
}
/**
* Returns the file-SHA1 checksums of the scanned files as associative array,
* where the keys are the file paths/sub-paths and the values the checksums.
* @return array.
*/
public final function getFileSha1s() {
$cs = array();
foreach ($this->files as $file => $desc) {
$cs[$file] = $desc['file_sha1'];
}
return $cs;
}
/**
* Returns the MD5 checksums of the whitespace-strupped codes of the scanned
* files as associative array, where the keys are the file paths/sub-paths
* and the values the checksums.
* @return array.
*/
public final function getSourceCodeMd5s() {
$cs = array();
foreach ($this->files as $file => $desc) {
$cs[$file] = $desc['source_md5'];
}
return $cs;
}
/**
* Returns the information about a selected class.
* @return array
*/
public final function getClass($class) {
return isset($this->versions[$class]) ? $this->versions[$class] : array();
}
/**
* Returns information about classes and interfaces in an assoc. array.
* The class names (lower case) are saved in the array keys, the array
* values are the information about the class:
* array(
* 'type' => Can be 'class' or 'interface'
* 'name' => The name of the class or interface
* <OTHER> => Dynamic entries dependent on the @-comments (like @author or
* @final etc). These values are automatically extended from
* the class/interface definition line, e.g:
*
* final class B extends A implements I1, I2
*
* would generate automatically without special comments:
*
* array (
* 'type' => 'class'
* 'name' => 'B'
* 'final' => true
* 'extends' => 'A'
* 'implements' => array('i1'=>'I1', 'i2'=>'I2')
* )
* )
* @param string $source
* @return array
*/
public final function getPhpClassDocumentations($source) {
// Conditionize
$source = str_replace(array("\r", "\t"), array("\n", ' '), $source);
while (strpos($source, "\n\n") !== false) {
$source = str_replace("\n\n", "\n", $source);
}
// Parse the source
$DOC_START = '/**';
$DOC_END = '*/';
$documented = array();
$p0 = strpos($source, $DOC_START);
while ($p0 !== false) {
$p1 = strpos($source, $DOC_END, $p0 + 1);
if ($p1 <= $p0)
break; // on not found
$doc = substr($source, $p0 + strlen($DOC_START), $p1 - $p0 - strlen($DOC_START) - strlen($DOC_END));
$doc = trim(preg_replace('/[\n\s]*\*([^\n]*)[\n]?/i', "$1\n", $doc), "\n ");
$p2 = $p1 + strlen($DOC_END);
while ($p2 < strlen($source) && ctype_space(substr($source, $p2, 1)))
$p2++;
if ($p2 >= strlen($source))
break; // on end of text
$p3 = strpos($source, "\n", $p2 + 1);
if ($p3 <= $p2)
break; // on not found
$ref = trim(substr($source, $p2, $p3 - $p2 + 1), "\n ");
$documented[] = array(
'for' => $ref,
'doc' => $doc
);
$p0 = strpos($source, $DOC_START, $p1 + 1);
}
// Filter information
$docs = array();
foreach ($documented as $key => $entry) {
$exp = array('text' => '');
// Parse the comment
$doc = explode("\n", str_replace("\r", "\n", $entry['doc']));
foreach ($doc as $line) {
$line = trim($line, "\t\n\r ");
if (!empty($line)) {
if (strpos($line, '@') === 0) {
$line = explode(' ', $line, 2);
$docKey = trim(strtolower(reset($line)), '@ ');
$docValue = count($line) > 1 ? trim(end($line)) : '';
switch ($docKey) {
case 'uses':
if (!isset($exp[$docKey])) {
$exp[$docKey] = array();
}
$exp[$docKey][strtolower($docValue)] = $docValue;
break;
case 'see':
case 'todo':
if (!isset($exp[$docKey])) {
$exp[$docKey] = array();
}
$exp[$docKey][] = $docValue;
break;
default:
$exp[$docKey] = $docValue;
}
} else {
$exp['text'] .= $line . "\n";
}
}
}
$exp['text'] = trim($exp['text'], "\n\r\t ");
// Parse the line
$for = $entry['for'];
if (strpos($for, '(') > 1) {
$for = substr($for, 0, strpos($for, '('));
}
$check = preg_split('/[\W]+/i', trim(strtolower($for), " ;{"), -1, PREG_SPLIT_NO_EMPTY);
$for = preg_split('/[\W]+/i', trim($for, " ;{"), -1, PREG_SPLIT_NO_EMPTY);
if (in_array('interface', $check)) {
$exp['type'] = 'interface';
$exp['name'] = end($for); // overwrite @name if specified
} else if (in_array('class', $check)) {
$exp['type'] = 'class';
while (!empty($for)) {
$desc = strtolower(array_shift($for));
switch ($desc) {
case 'abstract':
case 'final':
$exp[strtolower($desc)] = true;
break;
case 'class';
$exp['name'] = array_shift($for); // overwrite @name if specified
break;
case 'extends':
$exp['extends'] = array_shift($for);
break;
case 'implements':
$exp['implements'] = array();
while (!empty($for)) {
$desc = trim(array_shift($for), ' ,');
$exp['implements'][strtolower($desc)] = $desc;
}
break;
default:
Tracer::trace("Unrecognized descriptor: $desc");
}
}
}
// Add
if (isset($exp['name'])) {
$docs[strtolower($exp['name'])] = $exp;
}
}
return $docs;
}
/**
* Searches the files for documented classes and extracts information from
* the documentation. Undocumented classes will be ignored as they cannot
* be explicitly versioned. The method generates md5 and sha1 checksums for
* all file contents, where the documentations are ignored (as they do not
* influence the functionality.
* @param array $files
* @param string $rootDirectory
*/
public final function readFiles(array $files, $rootDirectory='') {
$rootDirectory = trim(rtrim($rootDirectory, ' /'));
$rootDirectory = rtrim($rootDirectory != '' ? $rootDirectory : $_SERVER['DOCUMENT_ROOT'], ' /') . '/';
$this->files = array();
$this->versions = array();
$this->exceptions = array();
$this->checksum = '';
foreach ($files as $file) {
$this->files[str_replace($rootDirectory, '', $file)] = array();
$f = &$this->files[str_replace($rootDirectory, '', $file)];
if (FileSystem::isFile($file) && FileSystem::isReadable($file)) {
$f['file_sha1'] = sha1_file($file);
$f['file_md5'] = md5_file($file);
$f['source_md5'] = md5(@php_strip_whitespace($file));
$f['classes'] = array();
$this->checksum .= basename($file) . $f['file_sha1'];
try {
if (FileSystem::getExtension($file) == 'php') {
$f['classes'] = $this->getPhpClassDocumentations(FileSystem::readFile($file));
foreach ($f['classes'] as $class => $desc) {
$this->versions[$class] = isset($desc['version']) ? $desc['version'] : '';
}
}
} catch (\Exception $e) {
$this->exceptions[$file] = $e->getMessage();
}
} else if (!FileSystem::isFile($file)) {
$this->exceptions[$file] = 'File does not exist';
} else {
$this->exceptions[$file] = 'File is not readable';
}
}
$this->checksum = sha1($this->checksum);
}
/**
* Returns information about version, readme, license, main checksum, file
* checksums, and file and class details.
* @param string $sourceDirectory
* @return array
*/
public static final function readLocalModuleVersion($sourceDirectory) {
$sourceDirectory = trim(rtrim($sourceDirectory, ' /'));
if (!FileSystem::isDirectory($sourceDirectory)) {
throw new LException('Source directory read does not exist: :dir', array(':dir' => $sourceDirectory));
} else {
$ver = new self();
$ver->readFiles(FileSystem::find($sourceDirectory), $sourceDirectory);
$return = array(
'version' => '0.0',
'readme' => '',
'license' => '',
'checksum' => $ver->getChecksum(),
'details' => $ver->getFileDetails()
);
if (FileSystem::isFile("$sourceDirectory/version")) {
$return['version'] = trim(FileSystem::readFile("$sourceDirectory/version"), "\n\r\t ");
} else if (FileSystem::isFile("$sourceDirectory/VERSION")) {
$return['version'] = trim(FileSystem::readFile("$sourceDirectory/VERSION"), "\n\r\t ");
}
if (FileSystem::isFile("$sourceDirectory/readme")) {
$return['readme'] = trim(FileSystem::readFile("$sourceDirectory/readme"), "\n\r");
} else if (FileSystem::isFile("$sourceDirectory/README")) {
$return['readme'] = trim(FileSystem::readFile("$sourceDirectory/README"), "\n\r");
}
if (FileSystem::isFile("$sourceDirectory/license")) {
$return['license'] = trim(FileSystem::readFile("$sourceDirectory/license"), "\n\r");
} else if (FileSystem::isFile("$sourceDirectory/LICENSE")) {
$return['license'] = trim(FileSystem::readFile("$sourceDirectory/LICENSE"), "\n\r");
}
}
return $return;
}
/**
* Creates a zip file containing all files in the source directory. The zip
* has the name $moduleName.zip. Additionally
* - a file $moduleName.version is created, which contains the main
* SHA1 checksum over all single files checksums
* - a file $moduleName.content is written, which contains details
* about documented PHP classes in JSON format
* - a file $moduleName.zip.sha1 is written, which contains the SHA1
* checksum of the zip file
* - a file $moduleName.zip.md5 is written, which contains the MD5
* checksum of the zip file
*
* @param <type> $moduleName
* @param <type> $sourceDirectory
* @param <type> $publicDirectory
*/
public static final function publishModule($moduleName, $sourceDirectory, $publicDirectory) {
$sourceDirectory = trim(rtrim($sourceDirectory, ' /'));
$publicDirectory = trim(rtrim($publicDirectory, ' /'));
$moduleName = strtolower(trim($moduleName));
if (empty($moduleName) || strpos($moduleName, '/') !== false || strpos($moduleName, ' ') !== false) {
throw new LException('Module name is invalid: ":name"', array(':name' => $moduleName));
} else if (empty($sourceDirectory)) {
throw new LException('No source directory specified to publish');
} else if (!FileSystem::isDirectory($sourceDirectory)) {
throw new LException('Source directory to publish does not exist: ":dir"', array(':dir' => $sourceDirectory));
} else if (empty($publicDirectory)) {
throw new LException('No public directory specified to publish');
} else if (!FileSystem::isDirectory($publicDirectory)) {
throw new LException('No such publishing directory: ":dir"', array(':dir' => $publicDirectory));
} else if (!FileSystem::isWritable($publicDirectory)) {
throw new LException('Directory to publish in is not writable: ":dir"', array(':dir' => $publicDirectory));
} else if (strpos($publicDirectory, $_SERVER['DOCUMENT_ROOT']) === false) {
throw new LException('Directory to publish in is not a sub-directory of this web server htdocs: ":dir"', array(':dir' => $publicDirectory));
} else {
$libzip = $publicDirectory . '/' . $moduleName . '.zip';
$chksum = $publicDirectory . '/' . $moduleName . '.checksum';
$version = $publicDirectory . '/' . $moduleName . '.version';
$content = $publicDirectory . '/' . $moduleName . '.content';
$license = $publicDirectory . '/' . $moduleName . '.license';
$readme = $publicDirectory . '/' . $moduleName . '.readme';
// Remove actual published files
if (FileSystem::isFile($libzip))
FileSystem::delete($libzip);
if (FileSystem::isFile($chksum))
FileSystem::delete($chksum);
if (FileSystem::isFile($content))
FileSystem::delete($content);
if (FileSystem::isFile($version))
FileSystem::delete($version);
if (FileSystem::isFile($license))
FileSystem::delete($license);
if (FileSystem::isFile($readme))
FileSystem::delete($readme);
// Write source folder contents as zip, checksum file, details and
// zip file verification checksums (for auto updater)
$ver = new self();
$ver->readFiles(FileSystem::find($sourceDirectory), $sourceDirectory);
ZipFile::compress($sourceDirectory, $libzip);
FileSystem::writeFile("$libzip.sha1", sha1_file($libzip));
FileSystem::writeFile("$libzip.md5", md5_file($libzip));
FileSystem::writeFile($chksum, $ver->getChecksum());
FileSystem::writeFile($content, json_encode($ver->getFileDetails()));
// Copy info file contents
if (FileSystem::isFile("$sourceDirectory/version")) {
FileSystem::writeFile($version, FileSystem::readFile("$sourceDirectory/version"));
} else if (FileSystem::isFile("$sourceDirectory/VERSION")) {
FileSystem::writeFile($version, FileSystem::readFile("$sourceDirectory/VERSION"));
}
if (FileSystem::isFile("$sourceDirectory/license")) {
FileSystem::writeFile($license, FileSystem::readFile("$sourceDirectory/license"));
} else if (FileSystem::isFile("$sourceDirectory/LICENSE")) {
FileSystem::writeFile($license, FileSystem::readFile("$sourceDirectory/LICENSE"));
}
if (FileSystem::isFile("$sourceDirectory/readme")) {
FileSystem::writeFile($readme, FileSystem::readFile("$sourceDirectory/readme"));
} else if (FileSystem::isFile("$sourceDirectory/README")) {
FileSystem::writeFile($readme, FileSystem::readFile("$sourceDirectory/README"));
}
}
}
/**
* Compares the version of a specified module with the version information
* files on a rempte server ("repository") using HTTP lookup request. The
* $sourceDirectory is the local folder containing the module source files,
* the $referenceUri is the URL where the module is published (conform to the
* Versioning::publishModule(...) method). Furthermore, a diff stat and
* details about all documented files is generated, each for local and remote.
* The following result types are possible:
*
* (1) Versions are identical
* (2) Update available. (If new files are added at the remote server or only
* documentation/whitespace has changed, but not the algorighms
* (3) Update recommended. At least one file is different in the interpreted
* source code - which could be a security bug fix.
*
* Results are presented in an associative array(
* 'checked' => true if version was successfully checked
* 'identical' => true if all files are identical
* 'code_identical' => true if the local algorighms are up to date
* 'text' => String representation of the result
* 'diff' => Array containing all files (keys) information if they are
* different
* 'local' => array containing details about the local versions
* 'remote' => array containing details about the remote versions
* )
*
* @param string $moduleName
* @param string $sourceDirectory
* @param string $referenceUri
* @return array
*/
public static final function checkModuleVersion($moduleName, $sourceDirectory, $referenceUri) {
$moduleName = strtolower(trim($moduleName));
$sourceDirectory = trim(rtrim($sourceDirectory, ' /'));
$return = array(
'checked' => false,
'version_identical' => false,
'update_recommended' => false,
'update_available' => false,
'new_features_available' => false,
'local_untracked_features' => false,
'identical' => false,
'code_identical' => false,
'text' => 'Checksum not checked: ',
'diff' => array(),
'local' => array('checksum' => '', 'version' => '0.0', 'details' => array()),
'remote' => array('checksum' => '', 'version' => '0.0', 'details' => array())
);
if (!FileSystem::isDirectory($sourceDirectory)) {
throw new LException("Local source directory does not exist: ':dir'", array(':dir' => $sourceDirectory));
} else if (empty($moduleName)) {
throw new LException("You did not specify a module name");
} else {
// Get the local statistics
$return['local'] = self::readLocalModuleVersion($sourceDirectory);
// Process checksum file
$rq = new HttpRequest();
if ($rq->request("$referenceUri/$moduleName.checksum")->getResponseStatus() != 200) {
$return['text'] .= 'HTTP lookup failed: ' . "$referenceUri/$moduleName.checksum";
} else {
$return['remote']['checksum'] = trim($rq->getOutput(), "\n\r\t ");
if ($return['remote']['checksum'] == $return['local']['checksum']) {
$return['checked'] = true;
$return['identical'] = true;
$return['code_identical'] = true;
$return['version_identical'] = true;
$return['text'] = 'Versions are identical';
$return['remote']['details'] = $return['local']['details'];
$return['remote']['version'] = $return['local']['version'];
} else if ($rq->request("$referenceUri/$moduleName.content")->getResponseStatus() != 200) {
$return['checked'] = true;
$return['identical'] = false;
$return['code_identical'] = false;
$return['text'] .= 'HTTP lookup failed:"' . "$referenceUri/$moduleName.content";
} else {
$return['checked'] = true;
$return['identical'] = false;
$return['code_identical'] = false;
$return['text'] = 'Update available';
$return['remote']['details'] = @json_decode($rq->getOutput(), true);
}
unset($ver, $rq);
if ($return['checked']) {
// Build diff
$fkeys = array_merge(array_keys($return['local']['details']), array_keys($return['remote']['details']));
$files = array();
foreach ($fkeys as $f) {
$files[strtolower($f)] = $f;
}
unset($fkeys);
$local = array_change_key_case($return['local']['details'], CASE_LOWER);
$remote = array_change_key_case($return['remote']['details'], CASE_LOWER);
$return['text'] = 'No update required';
if (!empty($remote) && !empty($local)) {
// Match local file against the remote files, shrink the
// arrays for each match
while (!empty($local)) {
reset($local);
$file = key($local);
$return['diff'][$files[$file]] = array(); // Use the original file name
$f = &$return['diff'][$files[$file]];
$f['local'] = true;
if (!isset($remote[$file])) {
$return['local_untracked_features'] = true;
$f['remote'] = false;
$f['identical'] = false;
$f['code_identical'] = false;
$f['text'] = 'Only local';
} else if ($remote[$file]['file_sha1'] == $local[$file]['file_sha1']) {
$f['remote'] = true;
$f['identical'] = true;
$f['code_identical'] = true;
$f['text'] = 'Files are identical';
} else if ($remote[$file]['source_md5'] == $local[$file]['source_md5']) {
$return['update_available'] = true;
$f['remote'] = true;
$f['identical'] = false;
$f['code_identical'] = true;
$f['text'] = 'Source codes are identical, but docs/whitespaces different';
} else {
$return['update_available'] = true;
$return['update_recommended'] = true;
$f['remote'] = true;
$f['identical'] = false;
$f['code_identical'] = false;
$f['text'] = 'Files are different';
}
unset($f); // unlink reference
array_shift($local); // implicit next local
if (isset($remote[$file]))
unset($remote[$file]);
}
// All other files are only remote ("new files")
foreach ($remote as $file => $desc) {
$return['update_available'] = true;
$return['new_features_available'] = true;
$return['diff'][$files[$file]] = array(
'local' => false,
'remote' => true,
'identical' => false,
'code_identical' => false,
'text' => 'Only remote'
);
}
unset($local, $remote, $f, $file, $desc);
// On updates check the remote version file
if ($return['update_available']) {
// Get version file
$rq = new HttpRequest();
if ($rq->request("$referenceUri/$moduleName.version")->getResponseStatus() == 200) {
$return['remote']['version'] = trim($rq->getOutput(), "\n\r\t ");
if ($return['remote']['version'] == $return['local']['version']) {
$return['version_identical'] = true;
}
}
}
// Final text specification
if ($return['update_recommended']) {
$return['text'] = "Update recommended to version " . $return['remote']['version'];
} else if ($return['update_available']) {
$t = array();
if (!$return['code_identical']) {
$t[] = 'some changes in documentation/whitespace';
}
if ($return['new_features_available']) {
$t[] = 'new features added';
}
if ($return['local_untracked_features']) {
$t[] = 'features removed or you added stuff locally';
}
$t = implode(', ', $t);
if (!empty($t))
$t = "($t)";
$return['text'] = trim("Update available (remote version {$return['remote']['version']}), $t");
} else if ($return['local_untracked_features']) {
$return['text'] = "No update required (Version is {$return['remote']['version']}, only a remote file was removed or you added a file locally)";
} else if ($return['identical']) {
$return['text'] = "No update available (Version is {$return['local']['version']}. The modules are absolutely identical)";
} else {
$return['text'] = "No update available (Version is {$return['local']['version']})";
}
}
}
}
}
return $return;
}
}
?>