/forum/Sources/Subs-Package.php
PHP | 2950 lines | 2167 code | 379 blank | 404 comment | 615 complexity | 82c4eea80a692588747286a46af49446 MD5 | raw file
Large files files are truncated, but you can click here to view the full file
- <?php
- /**********************************************************************************
- * Subs-Package.php *
- ***********************************************************************************
- * SMF: Simple Machines Forum *
- * Open-Source Project Inspired by Zef Hemel (zef@zefhemel.com) *
- * =============================================================================== *
- * Software Version: SMF 2.0 RC2 *
- * Software by: Simple Machines (http://www.simplemachines.org) *
- * Copyright 2006-2009 by: Simple Machines LLC (http://www.simplemachines.org) *
- * 2001-2006 by: Lewis Media (http://www.lewismedia.com) *
- * Support, News, Updates at: http://www.simplemachines.org *
- ***********************************************************************************
- * This program is free software; you may redistribute it and/or modify it under *
- * the terms of the provided license as published by Simple Machines LLC. *
- * *
- * This program is distributed in the hope that it is and will be useful, but *
- * WITHOUT ANY WARRANTIES; without even any implied warranty of MERCHANTABILITY *
- * or FITNESS FOR A PARTICULAR PURPOSE. *
- * *
- * See the "license.txt" file for details of the Simple Machines license. *
- * The latest version can always be found at http://www.simplemachines.org. *
- **********************************************************************************/
- if (!defined('SMF'))
- die('Hacking attempt...');
- /* This file's central purpose of existence is that of making the package
- manager work nicely. It contains functions for handling tar.gz and zip
- files, as well as a simple xml parser to handle the xml package stuff.
- Not to mention a few functions to make file handling easier.
- array read_tgz_file(string filename, string destination,
- bool single_file = false, bool overwrite = false, array files_to_extract = null)
- - reads a .tar.gz file, filename, in and extracts file(s) from it.
- - essentially just a shortcut for read_tgz_data().
- array read_tgz_data(string data, string destination,
- bool single_file = false, bool overwrite = false, array files_to_extract = null)
- - extracts a file or files from the .tar.gz contained in data.
- - detects if the file is really a .zip file, and if so returns the
- result of read_zip_data
- - if destination is null, returns a list of files in the archive.
- - if single_file is true, returns the contents of the file specified
- by destination, if it exists, or false.
- - if single_file is true, destination can start with * and / to
- signify that the file may come from any directory.
- - destination should not begin with a / if single_file is true.
- - overwrites existing files with newer modification times if and
- only if overwrite is true.
- - creates the destination directory if it doesn't exist, and is
- is specified.
- - requires zlib support be built into PHP.
- - returns an array of the files extracted.
- - if files_to_extract is not equal to null only extracts file within this array.
- array read_zip_data(string data, string destination,
- bool single_file = false, bool overwrite = false, array files_to_extract = null)
- - extracts a file or files from the .zip contained in data.
- - if destination is null, returns a list of files in the archive.
- - if single_file is true, returns the contents of the file specified
- by destination, if it exists, or false.
- - if single_file is true, destination can start with * and / to
- signify that the file may come from any directory.
- - destination should not begin with a / if single_file is true.
- - overwrites existing files with newer modification times if and
- only if overwrite is true.
- - creates the destination directory if it doesn't exist, and is
- is specified.
- - requires zlib support be built into PHP.
- - returns an array of the files extracted.
- - if files_to_extract is not equal to null only extracts file within this array.
- bool url_exists(string url)
- - checks to see if url is valid, and returns a 200 status code.
- - will return false if the file is "moved permanently" or similar.
- - returns true if the remote url exists.
- array loadInstalledPackages()
- - loads and returns an array of installed packages.
- - gets this information from Packages/installed.list.
- - returns the array of data.
- array getPackageInfo(string filename)
- - loads a package's information and returns a representative array.
- - expects the file to be a package in Packages/.
- - returns a error string if the package-info is invalid.
- - returns a basic array of id, version, filename, and similar
- information.
- - in the array returned, an xmlArray is available in 'xml'.
- void packageRequireFTP(string destination_url, array files = none, bool return = false)
- // !!!
- array parsePackageInfo(xmlArray &package, bool testing_only = true,
- string method = 'install', string previous_version = '')
- - parses the actions in package-info.xml files from packages.
- - package should be an xmlArray with package-info as its base.
- - testing_only should be true if the package should not actually be
- applied.
- - method is upgrade, install, or uninstall. Its default is install.
- - previous_version should be set to the previous installed version
- of this package, if any.
- - does not handle failure terribly well; testing first is always
- better.
- - returns an array of those changes made.
- bool matchPackageVersion(string version, string versions)
- - checks if version matches any of the versions in versions.
- - supports comma separated version numbers, with or without
- whitespace.
- - supports lower and upper bounds. (1.0-1.2)
- - returns true if the version matched.
- string parse_path(string path)
- - parses special identifiers out of the specified path.
- - returns the parsed path.
- void deltree(string path, bool delete_directory = true)
- - deletes a directory, and all the files and direcories inside it.
- - requires access to delete these files.
- bool mktree(string path, int mode)
- - creates the specified tree structure with the mode specified.
- - creates every directory in path until it finds one that already
- exists.
- - returns true if successful, false otherwise.
- void copytree(string source, string destination)
- - copies one directory structure over to another.
- - requires the destination to be writable.
- void listtree(string path, string sub_path = none)
- // !!!
- array parseModification(string file, bool testing = true, bool undo = false, array theme_paths = array())
- - parses a xml-style modification file (file).
- - testing tells it the modifications shouldn't actually be saved.
- - undo specifies that the modifications the file requests should be
- undone; this doesn't work with everything (regular expressions.)
- - returns an array of those changes made.
- array parseBoardMod(string file, bool testing = true, bool undo = false, array theme_paths = array())
- - parses a boardmod-style modification file (file).
- - testing tells it the modifications shouldn't actually be saved.
- - undo specifies that the modifications the file requests should be
- undone.
- - returns an array of those changes made.
- // !!!
- int package_put_contents(string filename, string data)
- - writes data to a file, almost exactly like the file_put_contents()
- function.
- - uses FTP to create/chmod the file when necessary and available.
- - uses text mode for text mode file extensions.
- - returns the number of bytes written.
- void package_chmod(string filename)
- // !!!
- string package_crypt(string password)
- // !!!
- string fetch_web_data(string url, string post_data = '',
- bool keep_alive = false)
- // !!!
- Creating your own package server:
- ---------------------------------------------------------------------------
- // !!!
- Creating your own package:
- ---------------------------------------------------------------------------
- // !!!
- */
- // Get the data from the file and extract it.
- function read_tgz_file($gzfilename, $destination, $single_file = false, $overwrite = false, $files_to_extract = null)
- {
- if (substr($gzfilename, 0, 7) == 'http://')
- {
- $data = fetch_web_data($gzfilename);
- if ($data === false)
- return false;
- }
- else
- {
- $data = @file_get_contents($gzfilename);
- if ($data === false)
- return false;
- }
- return read_tgz_data($data, $destination, $single_file, $overwrite, $files_to_extract);
- }
- // Extract tar.gz data. If destination is null, return a listing.
- function read_tgz_data($data, $destination, $single_file = false, $overwrite = false, $files_to_extract = null)
- {
- // This function sorta needs gzinflate!
- if (!function_exists('gzinflate'))
- fatal_lang_error('package_no_zlib', 'critical');
- umask(0);
- if (!$single_file && $destination !== null && !file_exists($destination))
- mktree($destination, 0777);
- // No signature?
- if (strlen($data) < 2)
- return false;
- $id = unpack('H2a/H2b', substr($data, 0, 2));
- if (strtolower($id['a'] . $id['b']) != '1f8b')
- {
- // Okay, this ain't no tar.gz, but maybe it's a zip file.
- if (substr($data, 0, 2) == 'PK')
- return read_zip_data($data, $destination, $single_file, $overwrite, $files_to_extract);
- else
- return false;
- }
- $flags = unpack('Ct/Cf', substr($data, 2, 2));
- // Not deflate!
- if ($flags['t'] != 8)
- return false;
- $flags = $flags['f'];
- $offset = 10;
- $octdec = array('mode', 'uid', 'gid', 'size', 'mtime', 'checksum', 'type');
- // "Read" the filename and comment. // !!! Might be mussed.
- if ($flags & 12)
- {
- while ($flags & 8 && $data{$offset++} != "\0")
- continue;
- while ($flags & 4 && $data{$offset++} != "\0")
- continue;
- }
- $crc = unpack('Vcrc32/Visize', substr($data, strlen($data) - 8, 8));
- $data = @gzinflate(substr($data, $offset, strlen($data) - 8 - $offset));
- // smf_crc32 and crc32 may not return the same results, so we accept either.
- if ($crc['crc32'] != smf_crc32($data) && $crc['crc32'] != crc32($data))
- return false;
- $blocks = strlen($data) / 512 - 1;
- $offset = 0;
- $return = array();
- while ($offset < $blocks)
- {
- $header = substr($data, $offset << 9, 512);
- $current = unpack('a100filename/a8mode/a8uid/a8gid/a12size/a12mtime/a8checksum/a1type/a100linkname/a6magic/a2version/a32uname/a32gname/a8devmajor/a8devminor/a155path', $header);
- foreach ($current as $k => $v)
- {
- if (in_array($k, $octdec))
- $current[$k] = octdec(trim($v));
- else
- $current[$k] = trim($v);
- }
- $checksum = 256;
- for ($i = 0; $i < 148; $i++)
- $checksum += ord($header{$i});
- for ($i = 156; $i < 512; $i++)
- $checksum += ord($header{$i});
- if ($current['checksum'] != $checksum)
- return $return;
- $size = ceil($current['size'] / 512);
- $current['data'] = substr($data, ++$offset << 9, $current['size']);
- $offset += $size;
- // Not a directory and doesn't exist already...
- if (substr($current['filename'], -1, 1) != '/' && !file_exists($destination . '/' . $current['filename']))
- $write_this = true;
- // File exists... check if it is newer.
- elseif (substr($current['filename'], -1, 1) != '/')
- $write_this = $overwrite || filemtime($destination . '/' . $current['filename']) < $current['mtime'];
- // Folder... create.
- elseif ($destination !== null && !$single_file)
- {
- // Protect from accidental parent directory writing...
- $current['filename'] = strtr($current['filename'], array('../' => '', '/..' => ''));
- if (!file_exists($destination . '/' . $current['filename']))
- mktree($destination . '/' . $current['filename'], 0777);
- $write_this = false;
- }
- else
- $write_this = false;
- if ($write_this && $destination !== null)
- {
- if (strpos($current['filename'], '/') !== false && !$single_file)
- mktree($destination . '/' . dirname($current['filename']), 0777);
- // Is this the file we're looking for?
- if ($single_file && ($destination == $current['filename'] || $destination == '*/' . basename($current['filename'])))
- return $current['data'];
- // If we're looking for another file, keep going.
- elseif ($single_file)
- continue;
- // Looking for restricted files?
- elseif ($files_to_extract !== null && !in_array($current['filename'], $files_to_extract))
- continue;
- package_put_contents($destination . '/' . $current['filename'], $current['data']);
- }
- if (substr($current['filename'], -1, 1) != '/')
- $return[] = array(
- 'filename' => $current['filename'],
- 'md5' => md5($current['data']),
- 'preview' => substr($current['data'], 0, 100),
- 'size' => $current['size'],
- 'skipped' => false
- );
- }
- if ($destination !== null && !$single_file)
- package_flush_cache();
- if ($single_file)
- return false;
- else
- return $return;
- }
- // Extract zip data. If destination is null, return a listing.
- function read_zip_data($data, $destination, $single_file = false, $overwrite = false, $files_to_extract = null)
- {
- umask(0);
- if ($destination !== null && !file_exists($destination) && !$single_file)
- mktree($destination, 0777);
- // Look for the PK header...
- if (substr($data, 0, 2) != 'PK')
- return false;
- // Find the central whosamawhatsit at the end; if there's a comment it's a pain.
- if (substr($data, -22, 4) == 'PK' . chr(5) . chr(6))
- $p = -22;
- else
- {
- // Have to find where the comment begins, ugh.
- for ($p = -22; $p > -strlen($data); $p--)
- {
- if (substr($data, $p, 4) == 'PK' . chr(5) . chr(6))
- break;
- }
- }
- $return = array();
- // Get the basic zip file info.
- $zip_info = unpack('vfiles/Vsize/Voffset', substr($data, $p + 10, 10));
- $p = $zip_info['offset'];
- for ($i = 0; $i < $zip_info['files']; $i++)
- {
- // Make sure this is a file entry...
- if (substr($data, $p, 4) != 'PK' . chr(1) . chr(2))
- return false;
- // Get all the important file information.
- $file_info = unpack('Vcrc/Vcompressed_size/Vsize/vfilename_len/vextra_len/vcomment_len/vdisk/vinternal/Vexternal/Voffset', substr($data, $p + 16, 30));
- $file_info['filename'] = substr($data, $p + 46, $file_info['filename_len']);
- // Skip all the information we don't care about anyway.
- $p += 46 + $file_info['filename_len'] + $file_info['extra_len'] + $file_info['comment_len'];
- // If this is a file, and it doesn't exist.... happy days!
- if (substr($file_info['filename'], -1, 1) != '/' && !file_exists($destination . '/' . $file_info['filename']))
- $write_this = true;
- // If the file exists, we may not want to overwrite it.
- elseif (substr($file_info['filename'], -1, 1) != '/')
- $write_this = $overwrite;
- // This is a directory, so we're gonna want to create it. (probably...)
- elseif ($destination !== null && !$single_file)
- {
- // Just a little accident prevention, don't mind me.
- $file_info['filename'] = strtr($file_info['filename'], array('../' => '', '/..' => ''));
- if (!file_exists($destination . '/' . $file_info['filename']))
- mktree($destination . '/' . $file_info['filename'], 0777);
- $write_this = false;
- }
- else
- $write_this = false;
- // Check that the data is there and does exist.
- if (substr($data, $file_info['offset'], 4) != 'PK' . chr(3) . chr(4))
- return false;
- // Get the actual compressed data.
- $file_info['data'] = substr($data, $file_info['offset'] + 30 + $file_info['filename_len'] + $file_info['extra_len'], $file_info['compressed_size']);
- // Only inflate it if we need to ;).
- if ($file_info['compressed_size'] != $file_info['size'])
- $file_info['data'] = @gzinflate($file_info['data']);
- // Okay! We can write this file, looks good from here...
- if ($write_this && $destination !== null)
- {
- if (strpos($file_info['filename'], '/') !== false && !$single_file)
- mktree($destination . '/' . dirname($file_info['filename']), 0777);
- // If we're looking for a specific file, and this is it... ka-bam, baby.
- if ($single_file && ($destination == $file_info['filename'] || $destination == '*/' . basename($file_info['filename'])))
- return $file_info['data'];
- // Oh? Another file. Fine. You don't like this file, do you? I know how it is. Yeah... just go away. No, don't apologize. I know this file's just not *good enough* for you.
- elseif ($single_file)
- continue;
- // Don't really want this?
- elseif ($files_to_extract !== null && !in_array($file_info['filename'], $files_to_extract))
- continue;
- package_put_contents($destination . '/' . $file_info['filename'], $file_info['data']);
- }
- if (substr($file_info['filename'], -1, 1) != '/')
- $return[] = array(
- 'filename' => $file_info['filename'],
- 'md5' => md5($file_info['data']),
- 'preview' => substr($file_info['data'], 0, 100),
- 'size' => $file_info['size'],
- 'skipped' => false
- );
- }
- if ($destination !== null && !$single_file)
- package_flush_cache();
- if ($single_file)
- return false;
- else
- return $return;
- }
- // Checks the existence of a remote file since file_exists() does not do remote.
- function url_exists($url)
- {
- $a_url = parse_url($url);
- if (!isset($a_url['scheme']))
- return false;
- // Attempt to connect...
- $temp = '';
- $fid = fsockopen($a_url['host'], !isset($a_url['port']) ? 80 : $a_url['port'], $temp, $temp, 8);
- if (!$fid)
- return false;
- fputs($fid, 'HEAD ' . $a_url['path'] . ' HTTP/1.0' . "\r\n" . 'Host: ' . $a_url['host'] . "\r\n\r\n");
- $head = fread($fid, 1024);
- fclose($fid);
- return preg_match('~^HTTP/.+\s+(20[01]|30[127])~i', $head) == 1;
- }
- // Load the installed packages.
- function loadInstalledPackages()
- {
- global $boarddir, $smcFunc;
- // First, check that the database is valid, installed.list is still king.
- $install_file = implode('', file($boarddir . '/Packages/installed.list'));
- if (trim($install_file) == '')
- {
- $smcFunc['db_query']('', '
- UPDATE {db_prefix}log_packages
- SET install_state = {int:not_installed}',
- array(
- 'not_installed' => 0,
- )
- );
- // Don't have anything left, so send an empty array.
- return array();
- }
- // Load the packages from the database - note this is ordered by install time to ensure latest package uninstalled first.
- $request = $smcFunc['db_query']('', '
- SELECT id_install, package_id, filename, name, version
- FROM {db_prefix}log_packages
- WHERE install_state != {int:not_installed}
- ORDER BY time_installed DESC',
- array(
- 'not_installed' => 0,
- )
- );
- $installed = array();
- $found = array();
- while ($row = $smcFunc['db_fetch_assoc']($request))
- {
- // Already found this? If so don't add it twice!
- if (in_array($row['package_id'], $found))
- continue;
- $found[] = $row['package_id'];
- $installed[] = array(
- 'id' => $row['id_install'],
- 'name' => $row['name'],
- 'filename' => $row['filename'],
- 'package_id' => $row['package_id'],
- 'version' => $row['version'],
- );
- }
- $smcFunc['db_free_result']($request);
- return $installed;
- }
- function getPackageInfo($gzfilename)
- {
- global $boarddir;
- // Extract package-info.xml from downloaded file. (*/ is used because it could be in any directory.)
- if (strpos($gzfilename, 'http://') !== false)
- $packageInfo = read_tgz_data(fetch_web_data($gzfilename, '', true), '*/package-info.xml', true);
- else
- {
- if (!file_exists($boarddir . '/Packages/' . $gzfilename))
- return 'package_get_error_not_found';
- if (is_file($boarddir . '/Packages/' . $gzfilename))
- $packageInfo = read_tgz_file($boarddir . '/Packages/' . $gzfilename, '*/package-info.xml', true);
- elseif (file_exists($boarddir . '/Packages/' . $gzfilename . '/package-info.xml'))
- $packageInfo = file_get_contents($boarddir . '/Packages/' . $gzfilename . '/package-info.xml');
- else
- return 'package_get_error_missing_xml';
- }
- // Nothing?
- if (empty($packageInfo))
- return 'package_get_error_is_zero';
- // Parse package-info.xml into an xmlArray.
- loadClassFile('Class-Package.php');
- $packageInfo = new xmlArray($packageInfo);
- // !!! Error message of some sort?
- if (!$packageInfo->exists('package-info[0]'))
- return 'package_get_error_packageinfo_corrupt';
- $packageInfo = $packageInfo->path('package-info[0]');
- $package = $packageInfo->to_array();
- $package['xml'] = $packageInfo;
- $package['filename'] = $gzfilename;
- if (!isset($package['type']))
- $package['type'] = 'modification';
- return $package;
- }
- // Create a chmod control for chmoding files.
- function create_chmod_control($chmodFiles = array(), $chmodOptions = array(), $restore_write_status = false)
- {
- global $context, $modSettings, $package_ftp, $boarddir, $txt, $sourcedir, $scripturl;
- // If we're restoring the status of existing files prepare the data.
- if ($restore_write_status && isset($_SESSION['pack_ftp']) && !empty($_SESSION['pack_ftp']['original_perms']))
- {
- function list_restoreFiles($dummy1, $dummy2, $dummy3, $do_change)
- {
- global $txt;
- $restore_files = array();
- foreach ($_SESSION['pack_ftp']['original_perms'] as $file => $perms)
- {
- // Check the file still exists, and the permissions were indeed different than now.
- $file_permissions = @fileperms($file);
- if (!file_exists($file) || $file_permissions == $perms)
- {
- unset($_SESSION['pack_ftp']['original_perms'][$file]);
- continue;
- }
- // Are we wanting to change the permission?
- if ($do_change && isset($_POST['restore_files']) && in_array($file, $_POST['restore_files']))
- {
- // Use FTP if we have it.
- if (!empty($package_ftp))
- {
- $ftp_file = strtr($file, array($_SESSION['pack_ftp']['root'] => ''));
- $package_ftp->chmod($ftp_file, $perms);
- }
- else
- @chmod($file, $perms);
- $new_permissions = @fileperms($file);
- $result = $new_permissions == $perms ? 'success' : 'failure';
- unset($_SESSION['pack_ftp']['original_perms'][$file]);
- }
- elseif ($do_change)
- {
- $new_permissions = '';
- $result = 'skipped';
- unset($_SESSION['pack_ftp']['original_perms'][$file]);
- }
- // Record the results!
- $restore_files[] = array(
- 'path' => $file,
- 'old_perms_raw' => $perms,
- 'old_perms' => substr(sprintf('%o', $perms), -4),
- 'cur_perms' => substr(sprintf('%o', $file_permissions), -4),
- 'new_perms' => isset($new_permissions) ? substr(sprintf('%o', $new_permissions), -4) : '',
- 'result' => isset($result) ? $result : '',
- 'writable_message' => '<span style="color: ' . (@is_writable($file) ? 'green' : 'red') . '">' . (@is_writable($file) ? $txt['package_file_perms_writable'] : $txt['package_file_perms_not_writable']) . '</span>',
- );
- }
- return $restore_files;
- }
- $listOptions = array(
- 'id' => 'restore_file_permissions',
- 'title' => $txt['package_restore_permissions'],
- 'get_items' => array(
- 'function' => 'list_restoreFiles',
- 'params' => array(
- !empty($_POST['restore_perms']),
- ),
- ),
- 'columns' => array(
- 'path' => array(
- 'header' => array(
- 'value' => $txt['package_restore_permissions_filename'],
- ),
- 'data' => array(
- 'db' => 'path',
- 'class' => 'smalltext',
- ),
- ),
- 'old_perms' => array(
- 'header' => array(
- 'value' => $txt['package_restore_permissions_orig_status'],
- ),
- 'data' => array(
- 'db' => 'old_perms',
- 'class' => 'smalltext',
- ),
- ),
- 'cur_perms' => array(
- 'header' => array(
- 'value' => $txt['package_restore_permissions_cur_status'],
- ),
- 'data' => array(
- 'function' => create_function('$rowData', '
- global $txt;
- $formatTxt = $rowData[\'result\'] == \'\' || $rowData[\'result\'] == \'skipped\' ? $txt[\'package_restore_permissions_pre_change\'] : $txt[\'package_restore_permissions_post_change\'];
- return sprintf($formatTxt, $rowData[\'cur_perms\'], $rowData[\'new_perms\'], $rowData[\'writable_message\']);
- '),
- 'class' => 'smalltext',
- ),
- ),
- 'check' => array(
- 'header' => array(
- 'value' => '<input type="checkbox" onclick="invertAll(this, this.form);" class="input_check" />',
- ),
- 'data' => array(
- 'sprintf' => array(
- 'format' => '<input type="checkbox" name="restore_files[]" value="%1$s" class="input_check" />',
- 'params' => array(
- 'path' => false,
- ),
- ),
- 'style' => 'text-align: center',
- ),
- ),
- 'result' => array(
- 'header' => array(
- 'value' => $txt['package_restore_permissions_result'],
- ),
- 'data' => array(
- 'function' => create_function('$rowData', '
- global $txt;
- return $txt[\'package_restore_permissions_action_\' . $rowData[\'result\']];
- '),
- 'class' => 'smalltext',
- ),
- ),
- ),
- 'form' => array(
- 'href' => !empty($chmodOptions['destination_url']) ? $chmodOptions['destination_url'] : $scripturl . '?action=admin;area=packages;sa=perms;restore;' . $context['session_var'] . '=' . $context['session_id'],
- ),
- 'additional_rows' => array(
- array(
- 'position' => 'below_table_data',
- 'value' => '<input type="submit" name="restore_perms" value="' . $txt['package_restore_permissions_restore'] . '" class="button_submit" />',
- 'class' => 'titlebg',
- 'style' => 'text-align: right;',
- ),
- array(
- 'position' => 'after_title',
- 'value' => '<span class="smalltext">' . $txt['package_restore_permissions_desc'] . '</span>',
- 'class' => 'windowbg2',
- ),
- ),
- );
- // Work out what columns and the like to show.
- if (!empty($_POST['restore_perms']))
- {
- $listOptions['additional_rows'][1]['value'] = sprintf($txt['package_restore_permissions_action_done'], $scripturl . '?action=admin;area=packages;sa=perms;' . $context['session_var'] . '=' . $context['session_id']);
- unset($listOptions['columns']['check'], $listOptions['form'], $listOptions['additional_rows'][0]);
- $context['sub_template'] = 'show_list';
- $context['default_list'] = 'restore_file_permissions';
- }
- else
- {
- unset($listOptions['columns']['result']);
- }
- // Create the list for display.
- require_once($sourcedir . '/Subs-List.php');
- createList($listOptions);
- // If we just restored permissions then whereever we are, we are now done and dusted.
- if (!empty($_POST['restore_perms']))
- obExit();
- }
- // Otherwise, it's entirely irrelevant?
- elseif ($restore_write_status)
- return true;
- // This is where we report what we got up to.
- $return_data = array(
- 'files' => array(
- 'writable' => array(),
- 'notwritable' => array(),
- ),
- );
- // If we have some FTP information already, then let's assume it was required and try to get ourselves connected.
- if (!empty($_SESSION['pack_ftp']['connected']))
- {
- // Load the file containing the ftp_connection class.
- loadClassFile('Class-Package.php');
- $package_ftp = new ftp_connection($_SESSION['pack_ftp']['server'], $_SESSION['pack_ftp']['port'], $_SESSION['pack_ftp']['username'], package_crypt($_SESSION['pack_ftp']['password']));
- }
- // Just got a submission did we?
- if (empty($package_ftp) && isset($_POST['ftp_username']))
- {
- loadClassFile('Class-Package.php');
- $ftp = new ftp_connection($_POST['ftp_server'], $_POST['ftp_port'], $_POST['ftp_username'], $_POST['ftp_password']);
- // We're connected, jolly good!
- if ($ftp->error === false)
- {
- // Common mistake, so let's try to remedy it...
- if (!$ftp->chdir($_POST['ftp_path']))
- {
- $ftp_error = $ftp->last_message;
- $ftp->chdir(preg_replace('~^/home[2]?/[^/]+?~', '', $_POST['ftp_path']));
- }
- if (!in_array($_POST['ftp_path'], array('', '/')))
- {
- $ftp_root = strtr($boarddir, array($_POST['ftp_path'] => ''));
- if (substr($ftp_root, -1) == '/' && ($_POST['ftp_path'] == '' || substr($_POST['ftp_path'], 0, 1) == '/'))
- $ftp_root = substr($ftp_root, 0, -1);
- }
- else
- $ftp_root = $boarddir;
- $_SESSION['pack_ftp'] = array(
- 'server' => $_POST['ftp_server'],
- 'port' => $_POST['ftp_port'],
- 'username' => $_POST['ftp_username'],
- 'password' => package_crypt($_POST['ftp_password']),
- 'path' => $_POST['ftp_path'],
- 'root' => $ftp_root,
- 'connected' => true,
- );
- if (!isset($modSettings['package_path']) || $modSettings['package_path'] != $_POST['ftp_path'])
- updateSettings(array('package_path' => $_POST['ftp_path']));
- // This is now the primary connection.
- $package_ftp = $ftp;
- }
- }
- // Now try to simply make the files writable, with whatever we might have.
- if (!empty($chmodFiles))
- {
- foreach ($chmodFiles as $k => $file)
- {
- // Sometimes this can somehow happen maybe?
- if (empty($file))
- unset($chmodFiles[$k]);
- // Already writable?
- elseif (@is_writable($file))
- $return_data['files']['writable'][] = $file;
- else
- {
- // Now try to change that.
- $return_data['files'][package_chmod($file, 'writable', true) ? 'writable' : 'notwritable'][] = $file;
- }
- }
- }
- // Have we still got nasty files which ain't writable? Dear me we need more FTP good sir.
- if (empty($package_ftp) && (!empty($return_data['files']['notwritable']) || !empty($chmodOptions['force_find_error'])))
- {
- if (!isset($ftp) || $ftp->error !== false)
- {
- if (!isset($ftp))
- {
- loadClassFile('Class-Package.php');
- $ftp = new ftp_connection(null);
- }
- elseif ($ftp->error !== false && !isset($ftp_error))
- $ftp_error = $ftp->last_message === null ? '' : $ftp->last_message;
- list ($username, $detect_path, $found_path) = $ftp->detect_path($boarddir);
- if ($found_path)
- $_POST['ftp_path'] = $detect_path;
- elseif (!isset($_POST['ftp_path']))
- $_POST['ftp_path'] = isset($modSettings['package_path']) ? $modSettings['package_path'] : $detect_path;
- if (!isset($_POST['ftp_username']))
- $_POST['ftp_username'] = $username;
- }
- $context['package_ftp'] = array(
- 'server' => isset($_POST['ftp_server']) ? $_POST['ftp_server'] : (isset($modSettings['package_server']) ? $modSettings['package_server'] : 'localhost'),
- 'port' => isset($_POST['ftp_port']) ? $_POST['ftp_port'] : (isset($modSettings['package_port']) ? $modSettings['package_port'] : '21'),
- 'username' => isset($_POST['ftp_username']) ? $_POST['ftp_username'] : (isset($modSettings['package_username']) ? $modSettings['package_username'] : ''),
- 'path' => $_POST['ftp_path'],
- 'error' => empty($ftp_error) ? null : $ftp_error,
- 'destination' => !empty($chmodOptions['destination_url']) ? $chmodOptions['destination_url'] : '',
- );
- // Which files failed?
- if (!isset($context['notwritable_files']))
- $context['notwritable_files'] = array();
- $context['notwritable_files'] = array_merge($context['notwritable_files'], $return_data['files']['notwritable']);
- // Sent here to die?
- if (!empty($chmodOptions['crash_on_error']))
- {
- $context['page_title'] = $txt['package_ftp_necessary'];
- $context['sub_template'] = 'ftp_required';
- obExit();
- }
- }
- return $return_data;
- }
- function packageRequireFTP($destination_url, $files = null, $return = false)
- {
- global $context, $modSettings, $package_ftp, $boarddir, $txt;
- // Try to make them writable the manual way.
- if ($files !== null)
- {
- foreach ($files as $k => $file)
- {
- // If this file doesn't exist, then we actually want to look at the directory, no?
- if (!file_exists($file))
- $file = dirname($file);
- // This looks odd, but it's an attempt to work around PHP suExec.
- if (!@is_writable($file))
- @chmod($file, 0755);
- if (!@is_writable($file))
- @chmod($file, 0777);
- if (!@is_writable(dirname($file)))
- @chmod($file, 0755);
- if (!@is_writable(dirname($file)))
- @chmod($file, 0777);
- $fp = is_dir($file) ? @opendir($file) : @fopen($file, 'rb');
- if (@is_writable($file) && $fp)
- {
- unset($files[$k]);
- if (!is_dir($file))
- fclose($fp);
- else
- closedir($fp);
- }
- }
- // No FTP required!
- if (empty($files))
- return array();
- }
- // They've opted to not use FTP, and try anyway.
- if (isset($_SESSION['pack_ftp']) && $_SESSION['pack_ftp'] == false)
- {
- if ($files === null)
- return array();
- foreach ($files as $k => $file)
- {
- // This looks odd, but it's an attempt to work around PHP suExec.
- if (!file_exists($file))
- {
- mktree(dirname($file), 0755);
- @touch($file);
- @chmod($file, 0755);
- }
- if (!@is_writable($file))
- @chmod($file, 0777);
- if (!@is_writable(dirname($file)))
- @chmod(dirname($file), 0777);
- if (@is_writable($file))
- unset($files[$k]);
- }
- return $files;
- }
- elseif (isset($_SESSION['pack_ftp']))
- {
- // Load the file containing the ftp_connection class.
- loadClassFile('Class-Package.php');
- $package_ftp = new ftp_connection($_SESSION['pack_ftp']['server'], $_SESSION['pack_ftp']['port'], $_SESSION['pack_ftp']['username'], package_crypt($_SESSION['pack_ftp']['password']));
- if ($files === null)
- return array();
- foreach ($files as $k => $file)
- {
- $ftp_file = strtr($file, array($_SESSION['pack_ftp']['root'] => ''));
- // This looks odd, but it's an attempt to work around PHP suExec.
- if (!file_exists($file))
- {
- mktree(dirname($file), 0755);
- $package_ftp->create_file($ftp_file);
- $package_ftp->chmod($ftp_file, 0755);
- }
- if (!@is_writable($file))
- $package_ftp->chmod($ftp_file, 0777);
- if (!@is_writable(dirname($file)))
- $package_ftp->chmod(dirname($ftp_file), 0777);
- if (@is_writable($file))
- unset($files[$k]);
- }
- return $files;
- }
- if (isset($_POST['ftp_none']))
- {
- $_SESSION['pack_ftp'] = false;
- $files = packageRequireFTP($destination_url, $files, $return);
- return $files;
- }
- elseif (isset($_POST['ftp_username']))
- {
- loadClassFile('Class-Package.php');
- $ftp = new ftp_connection($_POST['ftp_server'], $_POST['ftp_port'], $_POST['ftp_username'], $_POST['ftp_password']);
- if ($ftp->error === false)
- {
- // Common mistake, so let's try to remedy it...
- if (!$ftp->chdir($_POST['ftp_path']))
- {
- $ftp_error = $ftp->last_message;
- $ftp->chdir(preg_replace('~^/home[2]?/[^/]+?~', '', $_POST['ftp_path']));
- }
- }
- }
- if (!isset($ftp) || $ftp->error !== false)
- {
- if (!isset($ftp))
- {
- loadClassFile('Class-Package.php');
- $ftp = new ftp_connection(null);
- }
- elseif ($ftp->error !== false && !isset($ftp_error))
- $ftp_error = $ftp->last_message === null ? '' : $ftp->last_message;
- list ($username, $detect_path, $found_path) = $ftp->detect_path($boarddir);
- if ($found_path)
- $_POST['ftp_path'] = $detect_path;
- elseif (!isset($_POST['ftp_path']))
- $_POST['ftp_path'] = isset($modSettings['package_path']) ? $modSettings['package_path'] : $detect_path;
- if (!isset($_POST['ftp_username']))
- $_POST['ftp_username'] = $username;
- $context['package_ftp'] = array(
- 'server' => isset($_POST['ftp_server']) ? $_POST['ftp_server'] : (isset($modSettings['package_server']) ? $modSettings['package_server'] : 'localhost'),
- 'port' => isset($_POST['ftp_port']) ? $_POST['ftp_port'] : (isset($modSettings['package_port']) ? $modSettings['package_port'] : '21'),
- 'username' => isset($_POST['ftp_username']) ? $_POST['ftp_username'] : (isset($modSettings['package_username']) ? $modSettings['package_username'] : ''),
- 'path' => $_POST['ftp_path'],
- 'error' => empty($ftp_error) ? null : $ftp_error,
- 'destination' => $destination_url,
- );
- // If we're returning dump out here.
- if ($return)
- return $files;
- $context['page_title'] = $txt['package_ftp_necessary'];
- $context['sub_template'] = 'ftp_required';
- obExit();
- }
- else
- {
- if (!in_array($_POST['ftp_path'], array('', '/')))
- {
- $ftp_root = strtr($boarddir, array($_POST['ftp_path'] => ''));
- if (substr($ftp_root, -1) == '/' && ($_POST['ftp_path'] == '' || substr($_POST['ftp_path'], 0, 1) == '/'))
- $ftp_root = substr($ftp_root, 0, -1);
- }
- else
- $ftp_root = $boarddir;
- $_SESSION['pack_ftp'] = array(
- 'server' => $_POST['ftp_server'],
- 'port' => $_POST['ftp_port'],
- 'username' => $_POST['ftp_username'],
- 'password' => package_crypt($_POST['ftp_password']),
- 'path' => $_POST['ftp_path'],
- 'root' => $ftp_root,
- );
- if (!isset($modSettings['package_path']) || $modSettings['package_path'] != $_POST['ftp_path'])
- updateSettings(array('package_path' => $_POST['ftp_path']));
- $files = packageRequireFTP($destination_url, $files, $return);
- }
- return $files;
- }
- // Parses a package-info.xml file - method can be 'install', 'upgrade', or 'uninstall'.
- function parsePackageInfo(&$packageXML, $testing_only = true, $method = 'install', $previous_version = '')
- {
- global $boarddir, $forum_version, $context, $temp_path, $language;
- // Mayday! That action doesn't exist!!
- if (empty($packageXML) || !$packageXML->exists($method))
- return array();
- // We haven't found the package script yet...
- $script = false;
- $the_version = strtr($forum_version, array('SMF ' => ''));
- // Emulation support...
- if (!empty($_SESSION['version_emulate']))
- $the_version = $_SESSION['version_emulate'];
- // Get all the versions of this method and find the right one.
- $these_methods = $packageXML->set($method);
- foreach ($these_methods as $this_method)
- {
- // They specified certain versions this part is for.
- if ($this_method->exists('@for'))
- {
- // Don't keep going if this won't work for this version of SMF.
- if (!matchPackageVersion($the_version, $this_method->fetch('@for')))
- continue;
- }
- // Upgrades may go from a certain old version of the mod.
- if ($method == 'upgrade' && $this_method->exists('@from'))
- {
- // Well, this is for the wrong old version...
- if (!matchPackageVersion($previous_version, $this_method->fetch('@from')))
- continue;
- }
- // We've found it!
- $script = $this_method;
- break;
- }
- // Bad news, a matching script wasn't found!
- if ($script === false)
- return array();
- // Find all the actions in this method - in theory, these should only be allowed actions. (* means all.)
- $actions = $script->set('*');
- $return = array();
- $temp_auto = 0;
- $temp_path = $boarddir . '/Packages/temp/' . (isset($context['base_path']) ? $context['base_path'] : '');
- $context['readmes'] = array();
- // This is the testing phase... nothing shall be done yet.
- foreach ($actions as $action)
- {
- $actionType = $action->name();
- if ($actionType == 'readme' || $actionType == 'code' || $actionType == 'database' || $actionType == 'modification' || $actionType == 'redirect')
- {
- // Allow for translated readme files.
- if ($actionType == 'readme')
- {
- if ($action->exists('@lang'))
- {
- // Auto-select a readme language based on either request variable or current language.
- if ((isset($_REQUEST['readme']) && $action->fetch('@lang') == $_REQUEST['readme']) || (!isset($_REQUEST['readme']) && $action->fetch('@lang') == $language))
- {
- // In case the user put the readme blocks in the wrong order.
- if (isset($context['readmes']['selected']) && $context['readmes']['selected'] == 'default')
- $context['readmes'][] = 'default';
- $context['readmes']['selected'] = htmlspecialchars($action->fetch('@lang'));
- }
- else
- {
- // We don't want this readme now, but we'll allow the user to select to read it.
- $context['readmes'][] = htmlspecialchars($action->fetch('@lang'));
- continue;
- }
- }
- // Fallback readme. Without lang parameter.
- else
- {
- // Already selected a readme.
- if (isset($context['readmes']['selected']))
- {
- $context['readmes'][] = 'default';
- continue;
- }
- else
- $context['readmes']['selected'] = 'default';
- }
- }
- // !!! TODO: Make sure the file actually exists? Might not work when testing?
- if ($action->exists('@type') && $action->fetch('@type') == 'inline')
- {
- $filename = $temp_path . '$auto_' . $temp_auto++ . ($actionType == 'readme' || $actionType == 'redirect' ? '.txt' : ($actionType == 'code' || $actionType == 'database' ? '.php' : '.mod'));
- package_put_contents($filename, $action->fetch('.'));
- $filename = strtr($filename, array($temp_path => ''));
- }
- else
- $filename = $action->fetch('.');
- $return[] = array(
- 'type' => $actionType,
- 'filename' => $filename,
- 'description' => '',
- 'reverse' => $action->exists('@reverse') && $action->fetch('@reverse') == 'true',
- 'boardmod' => $action->exists('@format') && $action->fetch('@format') == 'boardmod',
- 'redirect_url' => $action->exists('@url') ? $action->fetch('@url') : '',
- 'redirect_timeout' => $action->exists('@timeout') ? (int) $action->fetch('@timeout') : '',
- 'parse_bbc' => $action->exists('@parsebbc') && $action->fetch('@parsebbc') == 'true',
- 'language' => ($actionType == 'readme' && $action->exists('@lang') && $action->fetch('@lang') == $language) ? $language : '',
- );
- continue;
- }
- elseif ($actionType == 'error')
- {
- $return[] = array(
- 'type' => 'error',
- );
- }
- $this_action = &$return[];
- $this_action = array(
- 'type' => $actionType,
- 'filename' => $action->fetch('@name'),
- 'description' => $action->fetch('.')
- );
- // If there is a destination, make sure it makes sense.
- if (substr($actionType, 0, 6) != 'remove')
- {
- $this_action['unparsed_destination'] = $action->fetch('@destination');
- $this_action['destination'] = parse_path($action->fetch('@destination')) . '/' . basename($this_action['filename']);
- }
- else
- {
- $this_action['unparsed_filename'] = $this_action['filename'];
- $this_action['filename'] = parse_path($this_action['filename']);
- }
- // If we're moving or requiring (copying) a file.
- if (substr($actionType, 0, 4) == 'move' || substr($actionType, 0, 7) == 'require')
- {
- if ($action->exists('@from'))
- $this_action['source'] = parse_path($action->fetch('@from'));
- else
- $this_action['source'] = $temp_path . $this_action['filename'];
- }
- // Check if these things can be done. (chmod's etc.)
- if ($actionType == 'create-dir')
- {
- if (!mktree($this_action['destination'], false))
- {
- $temp = $this_action['destination'];
- while (!file_exists($temp) && strlen($temp) > 1)
- $temp = dirname($temp);
- $return[] = array(
- 'type' => 'chmod',
- 'filename' => $temp
- );
- }
- }
- elseif ($actionType == 'create-file')
- {
- if (!mktree(dirname($this_action['destination']), false))
- {
- $temp = dirname($this_action['destination']);
- while (!file_exists($temp) && strlen($temp) > 1)
- $temp = dirname($temp);
- $return[] = array(
- 'type' => 'chmod',
- 'filename' => $temp
- );
- }
- if (!is_writable($this_action['destination']) && (file_exists($this_action['destination']) || !is_writable(dirname($this_action['destination']))))
- $return[] = array(
- 'type' => 'chmod',
- 'filename' => $this_action['destination']
- );
- }
- elseif ($actionType == 'require-dir')
- {
- if (!mktree($this_action['destination'], false))
- {
- $temp = $this_action['destination'];
- while (!file_exists($temp) && strlen($temp) > 1)
- $temp = dirname($temp);
- $return[] = array(
- 'type' => 'chmod',
- 'filename' => $temp
- );
- }
- }
- elseif ($actionType == 'require-file')
- {
- if ($action->exists('@theme'))
- $this_action['theme_action'] = $action->fetch('@theme');
- if (!mktree(dirname($this_action['destination']), false))
- {
- $temp = dirname($this_action['destination']);
- while (!file_exists($temp) && strlen($temp) > 1)
- $temp = dirname($temp);
- $return[] = array(
- 'type' => 'chmod',
- 'filename' => $temp
- );
- }
- if (!is_writable($this_action['destination']) && (file_exists($this_action['destination']) || !is_writable(dirname($this_action['destination']))))
- $return[] = array(
- 'type' => 'chmod',
- 'filename' => $this_action['destination']
- );
- }
- elseif ($actionType == 'move-dir' || $actionType == 'move-file')
- {
- if (!mktree(dirname($this_action['destination']), false))
- {
- $temp = dirname($this_action['destination']);
- while (!file_exists($temp) && strlen($temp) > 1)
- $temp = dirname($temp);
- $return[] = array(
- 'type' => 'chmod',
- 'filename' => $temp
- );
- }
- if (!is_writable($this_action['destination']) && (file_exists($this_action['destination']) || !is_writable(dirname($this_action['destination']))))
- $return[] = array(
- 'type' => 'chmod',
- 'filename' => $this_action['destination']
- );
- }
- elseif ($actionType == 'remove-dir')
- {
- if (!is_writable($this_action['filename']) && file_exists($this_action['destination']))
- $return[] = array(
- 'type' => 'chmod',
- 'filename' => $this_action['filename']
- );
- }
- elseif ($actionType == 'remove-file')
- {
- if (!is_writable($this_action['filename']) && file_exists($this_action['filename']))
- $return[] = array(
- 'type' => 'chmod',
- 'filename' => $this_action['filename']
- );
- }
- }
- // Only testing - just return a list of things to be done.
- if ($testing_only)
- return $return;
- umask(0);
- $failure = false;
- $not_done = array(array('type' => '!'));
- foreach ($return as $action)
- {
- if ($action['type'] == 'modification' || $action['type'] == 'code' || $action['type'] == 'database' || $action['type'] == 'redirect')
- $not_done[] = $action;
- if ($action['type'] == 'create-dir')
- {
- if (!mktree($action['destination'], 0755) || !is_writable($action['destination']))
- $failure |= !mktree($action['destination'], 0777);
- }
- elseif ($action['type'] == 'create-file')
- {
- if (!mktree(dirname($action['destination']), 0755) || !is_writable(dirname($action['destination'])))
- $failure |= !mktree(dirname($action['destination']), 0777);
- // Create an empty file.
- package_put_contents($action['destination'], package_get_contents($action['source']), $testing_only);
- if (!file_exists($action['destination']))
- $failure = true;
- }
- elseif ($action['type'] == 'require-dir')
- {
- copytree($action['source'], $action['destination']);
- // Any other theme folders?
- if (!empty($context['theme_copies']) && !empty($context['theme_copies'][$action['type']][$action['destination']]))
- foreach ($context['theme_copies'][$action['type']][$action['destination']] as $theme_destination)
- copytree($action['source'], $theme_destination);
- }
- elseif ($action['type'] == 'require-file')
- {
- if (!mktree(dirname($action['destination']), 0755) || !is_writable(dirname($action['destination'])))
- $failure |= !mktree(dirname($action['destination']), 0777);
- package_put_contents($action['destination'], package_get_contents($action['source']), $testing_only);
- $failure |= !copy($action['source'], $action['destination']);
- // Any other theme files?
- if (!empty($context['theme_copies']) && !empty($context['theme_copies'][$action['type']][$action['destination']]))
- foreach ($context['theme_copies'][$action['type']][$action['destination']] as $theme_destination)
- {
- if (!mktree(dirname($theme_destination), 0755) || !is_writable(dirname($theme_destination)))
- $failure |= !mktree(dirname($theme_destination), 0777);
- package_put_contents($theme_destination, package_get_contents($action['source']), $testing_only);
- $failure |= !copy($action['source'], $theme_destination);
- }
- }
- elseif ($action['type'] == 'move-file')
- {
- if (!mktree(dirname($action['destination']), 0755) || !is_writable(dirname($action['destination'])))
- $failure |= !mktree(dirname($action['destination']), 0777);
- $failure |= !rename($action['source'], $action['destination']);
- }
- elseif ($action['type'] == 'move-dir')
- {
- if (!mktree($action['destination'], 0755) || !is_writable($action['destination']))
- $failure |= !mktree($action['destination'], 0777);
- $failure |= !rename($action['source'], $action['destination']);
- }
- elseif ($action['type'] == 'remove-dir')
- {
- deltree($action['filename']);
- // Any other theme folders?
- if (!empty($context['theme_copies']) && !empty($context['theme_copies'][$action['type']][$action['filename']]))
- foreach ($context['theme_copies'][$action['type']][$action['filename']] as $theme_destination)
- deltree($theme_destination);
- }
- elseif ($action['type'] == 'remove-file')
- {
- // Make sure the file exists before deleting it.
- if (file_exists($action['filename']))
- {
- package_chmod($action['filename']);
- $failure |= !unlink($action['filename']);
- }
- // The file that was supposed to be deleted couldn't be found.
- else
- $failure = true;
- // Any other theme folders?
- if (!empty($context['theme_copies']) && !empty($context['theme_copies'][$action['type']][$action['filename']]))
- foreach ($context['theme_copies'][$action['type']][$action['filename']] as $theme_destination)
- if (file_exists($theme_destination))
- $failure |= !unlink($theme_destination);
- else
- $failure = true;
- }
- }
- return $not_done;
- }
- // This is such a pain I created a function for it :P.
- function matchPackageVersion($version, $versions)
- {
- $version = strtolower($version);
- $for = explode(',', strtolower($versions));
- // Trim them all!
- for ($i = 0, $n = count($for); $i < $n; $i++)
- $for[$i] = trim($for[$i]);
- // The version is explicitly defined... too easy.
- if (in_array($version, $for) || in_array('all', $for))
- return true;
- foreach ($for as $list)
- {
- if (substr($list, -1) == '*' && strpos($list, '-') === false)
- {
- // "Nothing" is the lowest alphanumeric character, z the highest.
- $list = substr($list, 0, -1) . '-' . substr($list, 0, -1) . 'z';
- }
- // Look for a version specification like "1.0-1.2".
- elseif (strpos($list, '-') === false)
- continue;
- list ($lower, $upper) = explode('-', $list);
- $lower = explode('.', $lower);
- $upper = explode('.', $upper);
- $version = explode('.', $version);
- foreach ($upper as $key => $high)
- {
- // Let's check that this is at or below the upper... o…
Large files files are truncated, but you can click here to view the full file