Version 4.1.2
This commit is contained in:
1 parent
3ba79295ee
commit
28bd025b35
1131 files changed
+63658
-20579
No files matched your search
+54
@@ -0,0 +1,54 @@
|
||||
# Contributions Welcome!
|
||||
|
||||
Pull Requests and Community Contributions are the bread and butter of open source software. Every contribution- from bug
|
||||
reports to feature requests, typos to full new features- are greatly appreciated.
|
||||
|
||||
|
||||
## Important Guidelines
|
||||
|
||||
* One Item Per Pull Request or Issue. This makes it much easier to review code and merge it back in, and prevents issues
|
||||
with one request from blocking another.
|
||||
|
||||
* Code Coverage is extremely important, and pull requests are much more likely to be accepted if testing is also improved.
|
||||
New code should be properly tested, and all tests must pass.
|
||||
|
||||
* Read the LICENSE document and make sure you understand it, because your code is going to be released under it.
|
||||
|
||||
* Be prepared to make revisions. Don't be discouraged if you're asked to make changes, as that is just another step
|
||||
towards refining the code and getting it merged back in.
|
||||
|
||||
* Remember to add the relevant documentation, particular the docblock comments.
|
||||
|
||||
|
||||
## Code Styling
|
||||
|
||||
This project follows the PSR standards set forth by the [PHP Framework Interop Group](http://www.php-fig.org/).
|
||||
|
||||
* [PSR-0: Class and file naming conventions](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-0.md)
|
||||
* [PSR-1: Basic coding standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-1-basic-coding-standard.md)
|
||||
* [PSR-2: Coding style guide](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)
|
||||
|
||||
All code most follow these standards to be accepted. The easiest way to accomplish this is to run php-cs-fixer once the
|
||||
new changes are finished. The php-cs-fixer package is installed as a development dependency of this project.
|
||||
|
||||
composer install --dev
|
||||
vendor/bin/php-cs-fixer fix ./ --level="all" -vv
|
||||
|
||||
|
||||
## Running the test suite
|
||||
|
||||
First install dependencies using Composer. It's important to include the dev packages:
|
||||
|
||||
composer install --dev
|
||||
|
||||
The "runTests.sh" script runs the full test suite- phpunit, php-cs-fixer, as well as any environmental setup:
|
||||
|
||||
tests/runTests.sh
|
||||
|
||||
To call phpunit directly:
|
||||
|
||||
vendor/bin/phpunit
|
||||
|
||||
To call php-cs-fixer directly:
|
||||
|
||||
vendor/bin/php-cs-fixer fix ./ --level="all" -vv --dry-run
|
||||
Vendored
+24
@@ -0,0 +1,24 @@
|
||||
Copyright (c) 2009, Robert Hafner
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of the Stash Project nor the
|
||||
names of its contributors may be used to endorse or promote products
|
||||
derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL Robert Hafner BE LIABLE FOR ANY
|
||||
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
+587
@@ -0,0 +1,587 @@
|
||||
<?php
|
||||
/*
|
||||
* This file is part of the JShrink package.
|
||||
*
|
||||
* (c) Robert Hafner <tedivm@tedivm.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
/**
|
||||
* JShrink
|
||||
*
|
||||
*
|
||||
* @package JShrink
|
||||
* @author Robert Hafner <tedivm@tedivm.com>
|
||||
*/
|
||||
|
||||
namespace JShrink;
|
||||
|
||||
/**
|
||||
* Minifier
|
||||
*
|
||||
* Usage - Minifier::minify($js);
|
||||
* Usage - Minifier::minify($js, $options);
|
||||
* Usage - Minifier::minify($js, array('flaggedComments' => false));
|
||||
*
|
||||
* @package JShrink
|
||||
* @author Robert Hafner <tedivm@tedivm.com>
|
||||
* @license http://www.opensource.org/licenses/bsd-license.php BSD License
|
||||
*/
|
||||
class Minifier
|
||||
{
|
||||
/**
|
||||
* The input javascript to be minified.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $input;
|
||||
|
||||
/**
|
||||
* The location of the character (in the input string) that is next to be
|
||||
* processed.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $index = 0;
|
||||
|
||||
/**
|
||||
* The first of the characters currently being looked at.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $a = '';
|
||||
|
||||
/**
|
||||
* The next character being looked at (after a);
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $b = '';
|
||||
|
||||
/**
|
||||
* This character is only active when certain look ahead actions take place.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $c;
|
||||
|
||||
/**
|
||||
* Contains the options for the current minification process.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $options;
|
||||
|
||||
/**
|
||||
* Contains the default options for minification. This array is merged with
|
||||
* the one passed in by the user to create the request specific set of
|
||||
* options (stored in the $options attribute).
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected static $defaultOptions = array('flaggedComments' => true);
|
||||
|
||||
/**
|
||||
* Contains lock ids which are used to replace certain code patterns and
|
||||
* prevent them from being minified
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $locks = array();
|
||||
|
||||
/**
|
||||
* Takes a string containing javascript and removes unneeded characters in
|
||||
* order to shrink the code without altering it's functionality.
|
||||
*
|
||||
* @param string $js The raw javascript to be minified
|
||||
* @param array $options Various runtime options in an associative array
|
||||
* @throws \Exception
|
||||
* @return bool|string
|
||||
*/
|
||||
public static function minify($js, $options = array())
|
||||
{
|
||||
try {
|
||||
ob_start();
|
||||
|
||||
$jshrink = new Minifier();
|
||||
$js = $jshrink->lock($js);
|
||||
$jshrink->minifyDirectToOutput($js, $options);
|
||||
|
||||
// Sometimes there's a leading new line, so we trim that out here.
|
||||
$js = ltrim(ob_get_clean());
|
||||
$js = $jshrink->unlock($js);
|
||||
unset($jshrink);
|
||||
|
||||
return $js;
|
||||
|
||||
} catch (\Exception $e) {
|
||||
|
||||
if (isset($jshrink)) {
|
||||
// Since the breakdownScript function probably wasn't finished
|
||||
// we clean it out before discarding it.
|
||||
$jshrink->clean();
|
||||
unset($jshrink);
|
||||
}
|
||||
|
||||
// without this call things get weird, with partially outputted js.
|
||||
ob_end_clean();
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes a javascript string and outputs only the required characters,
|
||||
* stripping out all unneeded characters.
|
||||
*
|
||||
* @param string $js The raw javascript to be minified
|
||||
* @param array $options Various runtime options in an associative array
|
||||
*/
|
||||
protected function minifyDirectToOutput($js, $options)
|
||||
{
|
||||
$this->initialize($js, $options);
|
||||
$this->loop();
|
||||
$this->clean();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes internal variables, normalizes new lines,
|
||||
*
|
||||
* @param string $js The raw javascript to be minified
|
||||
* @param array $options Various runtime options in an associative array
|
||||
*/
|
||||
protected function initialize($js, $options)
|
||||
{
|
||||
$this->options = array_merge(static::$defaultOptions, $options);
|
||||
$js = str_replace("\r\n", "\n", $js);
|
||||
$js = str_replace('/**/', '', $js);
|
||||
$this->input = str_replace("\r", "\n", $js);
|
||||
|
||||
// We add a newline to the end of the script to make it easier to deal
|
||||
// with comments at the bottom of the script- this prevents the unclosed
|
||||
// comment error that can otherwise occur.
|
||||
$this->input .= PHP_EOL;
|
||||
|
||||
// Populate "a" with a new line, "b" with the first character, before
|
||||
// entering the loop
|
||||
$this->a = "\n";
|
||||
$this->b = $this->getReal();
|
||||
}
|
||||
|
||||
/**
|
||||
* The primary action occurs here. This function loops through the input string,
|
||||
* outputting anything that's relevant and discarding anything that is not.
|
||||
*/
|
||||
protected function loop()
|
||||
{
|
||||
while ($this->a !== false && !is_null($this->a) && $this->a !== '') {
|
||||
|
||||
switch ($this->a) {
|
||||
// new lines
|
||||
case "\n":
|
||||
// if the next line is something that can't stand alone preserve the newline
|
||||
if (strpos('(-+{[@', $this->b) !== false) {
|
||||
echo $this->a;
|
||||
$this->saveString();
|
||||
break;
|
||||
}
|
||||
|
||||
// if B is a space we skip the rest of the switch block and go down to the
|
||||
// string/regex check below, resetting $this->b with getReal
|
||||
if($this->b === ' ')
|
||||
break;
|
||||
|
||||
// otherwise we treat the newline like a space
|
||||
|
||||
case ' ':
|
||||
if(static::isAlphaNumeric($this->b))
|
||||
echo $this->a;
|
||||
|
||||
$this->saveString();
|
||||
break;
|
||||
|
||||
default:
|
||||
switch ($this->b) {
|
||||
case "\n":
|
||||
if (strpos('}])+-"\'', $this->a) !== false) {
|
||||
echo $this->a;
|
||||
$this->saveString();
|
||||
break;
|
||||
} else {
|
||||
if (static::isAlphaNumeric($this->a)) {
|
||||
echo $this->a;
|
||||
$this->saveString();
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case ' ':
|
||||
if(!static::isAlphaNumeric($this->a))
|
||||
break;
|
||||
|
||||
default:
|
||||
// check for some regex that breaks stuff
|
||||
if ($this->a === '/' && ($this->b === '\'' || $this->b === '"')) {
|
||||
$this->saveRegex();
|
||||
continue;
|
||||
}
|
||||
|
||||
echo $this->a;
|
||||
$this->saveString();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// do reg check of doom
|
||||
$this->b = $this->getReal();
|
||||
|
||||
if(($this->b == '/' && strpos('(,=:[!&|?', $this->a) !== false))
|
||||
$this->saveRegex();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets attributes that do not need to be stored between requests so that
|
||||
* the next request is ready to go. Another reason for this is to make sure
|
||||
* the variables are cleared and are not taking up memory.
|
||||
*/
|
||||
protected function clean()
|
||||
{
|
||||
unset($this->input);
|
||||
$this->index = 0;
|
||||
$this->a = $this->b = '';
|
||||
unset($this->c);
|
||||
unset($this->options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the next string for processing based off of the current index.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getChar()
|
||||
{
|
||||
// Check to see if we had anything in the look ahead buffer and use that.
|
||||
if (isset($this->c)) {
|
||||
$char = $this->c;
|
||||
unset($this->c);
|
||||
|
||||
// Otherwise we start pulling from the input.
|
||||
} else {
|
||||
$char = substr($this->input, $this->index, 1);
|
||||
|
||||
// If the next character doesn't exist return false.
|
||||
if (isset($char) && $char === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Otherwise increment the pointer and use this char.
|
||||
$this->index++;
|
||||
}
|
||||
|
||||
// Normalize all whitespace except for the newline character into a
|
||||
// standard space.
|
||||
if($char !== "\n" && ord($char) < 32)
|
||||
|
||||
return ' ';
|
||||
|
||||
return $char;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function gets the next "real" character. It is essentially a wrapper
|
||||
* around the getChar function that skips comments. This has significant
|
||||
* performance benefits as the skipping is done using native functions (ie,
|
||||
* c code) rather than in script php.
|
||||
*
|
||||
*
|
||||
* @return string Next 'real' character to be processed.
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
protected function getReal()
|
||||
{
|
||||
$startIndex = $this->index;
|
||||
$char = $this->getChar();
|
||||
|
||||
// Check to see if we're potentially in a comment
|
||||
if ($char !== '/') {
|
||||
return $char;
|
||||
}
|
||||
|
||||
$this->c = $this->getChar();
|
||||
|
||||
if ($this->c === '/') {
|
||||
return $this->processOneLineComments($startIndex);
|
||||
|
||||
} elseif ($this->c === '*') {
|
||||
return $this->processMultiLineComments($startIndex);
|
||||
}
|
||||
|
||||
return $char;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removed one line comments, with the exception of some very specific types of
|
||||
* conditional comments.
|
||||
*
|
||||
* @param int $startIndex The index point where "getReal" function started
|
||||
* @return string
|
||||
*/
|
||||
protected function processOneLineComments($startIndex)
|
||||
{
|
||||
$thirdCommentString = substr($this->input, $this->index, 1);
|
||||
|
||||
// kill rest of line
|
||||
$this->getNext("\n");
|
||||
|
||||
if ($thirdCommentString == '@') {
|
||||
$endPoint = $this->index - $startIndex;
|
||||
unset($this->c);
|
||||
$char = "\n" . substr($this->input, $startIndex, $endPoint);
|
||||
} else {
|
||||
// first one is contents of $this->c
|
||||
$this->getChar();
|
||||
$char = $this->getChar();
|
||||
}
|
||||
|
||||
return $char;
|
||||
}
|
||||
|
||||
/**
|
||||
* Skips multiline comments where appropriate, and includes them where needed.
|
||||
* Conditional comments and "license" style blocks are preserved.
|
||||
*
|
||||
* @param int $startIndex The index point where "getReal" function started
|
||||
* @return bool|string False if there's no character
|
||||
* @throws \RuntimeException Unclosed comments will throw an error
|
||||
*/
|
||||
protected function processMultiLineComments($startIndex)
|
||||
{
|
||||
$this->getChar(); // current C
|
||||
$thirdCommentString = $this->getChar();
|
||||
|
||||
// kill everything up to the next */ if it's there
|
||||
if ($this->getNext('*/')) {
|
||||
|
||||
$this->getChar(); // get *
|
||||
$this->getChar(); // get /
|
||||
$char = $this->getChar(); // get next real character
|
||||
|
||||
// Now we reinsert conditional comments and YUI-style licensing comments
|
||||
if (($this->options['flaggedComments'] && $thirdCommentString === '!')
|
||||
|| ($thirdCommentString === '@') ) {
|
||||
|
||||
// If conditional comments or flagged comments are not the first thing in the script
|
||||
// we need to echo a and fill it with a space before moving on.
|
||||
if ($startIndex > 0) {
|
||||
echo $this->a;
|
||||
$this->a = " ";
|
||||
|
||||
// If the comment started on a new line we let it stay on the new line
|
||||
if ($this->input[($startIndex - 1)] === "\n") {
|
||||
echo "\n";
|
||||
}
|
||||
}
|
||||
|
||||
$endPoint = ($this->index - 1) - $startIndex;
|
||||
echo substr($this->input, $startIndex, $endPoint);
|
||||
|
||||
return $char;
|
||||
}
|
||||
|
||||
} else {
|
||||
$char = false;
|
||||
}
|
||||
|
||||
if($char === false)
|
||||
throw new \RuntimeException('Unclosed multiline comment at position: ' . ($this->index - 2));
|
||||
|
||||
// if we're here c is part of the comment and therefore tossed
|
||||
if(isset($this->c))
|
||||
unset($this->c);
|
||||
|
||||
return $char;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pushes the index ahead to the next instance of the supplied string. If it
|
||||
* is found the first character of the string is returned and the index is set
|
||||
* to it's position.
|
||||
*
|
||||
* @param string $string
|
||||
* @return string|false Returns the first character of the string or false.
|
||||
*/
|
||||
protected function getNext($string)
|
||||
{
|
||||
// Find the next occurrence of "string" after the current position.
|
||||
$pos = strpos($this->input, $string, $this->index);
|
||||
|
||||
// If it's not there return false.
|
||||
if($pos === false)
|
||||
|
||||
return false;
|
||||
|
||||
// Adjust position of index to jump ahead to the asked for string
|
||||
$this->index = $pos;
|
||||
|
||||
// Return the first character of that string.
|
||||
return substr($this->input, $this->index, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* When a javascript string is detected this function crawls for the end of
|
||||
* it and saves the whole string.
|
||||
*
|
||||
* @throws \RuntimeException Unclosed strings will throw an error
|
||||
*/
|
||||
protected function saveString()
|
||||
{
|
||||
$startpos = $this->index;
|
||||
|
||||
// saveString is always called after a gets cleared, so we push b into
|
||||
// that spot.
|
||||
$this->a = $this->b;
|
||||
|
||||
// If this isn't a string we don't need to do anything.
|
||||
if ($this->a !== "'" && $this->a !== '"') {
|
||||
return;
|
||||
}
|
||||
|
||||
// String type is the quote used, " or '
|
||||
$stringType = $this->a;
|
||||
|
||||
// Echo out that starting quote
|
||||
echo $this->a;
|
||||
|
||||
// Loop until the string is done
|
||||
while (true) {
|
||||
|
||||
// Grab the very next character and load it into a
|
||||
$this->a = $this->getChar();
|
||||
|
||||
switch ($this->a) {
|
||||
|
||||
// If the string opener (single or double quote) is used
|
||||
// output it and break out of the while loop-
|
||||
// The string is finished!
|
||||
case $stringType:
|
||||
break 2;
|
||||
|
||||
// New lines in strings without line delimiters are bad- actual
|
||||
// new lines will be represented by the string \n and not the actual
|
||||
// character, so those will be treated just fine using the switch
|
||||
// block below.
|
||||
case "\n":
|
||||
throw new \RuntimeException('Unclosed string at position: ' . $startpos );
|
||||
break;
|
||||
|
||||
// Escaped characters get picked up here. If it's an escaped new line it's not really needed
|
||||
case '\\':
|
||||
|
||||
// a is a slash. We want to keep it, and the next character,
|
||||
// unless it's a new line. New lines as actual strings will be
|
||||
// preserved, but escaped new lines should be reduced.
|
||||
$this->b = $this->getChar();
|
||||
|
||||
// If b is a new line we discard a and b and restart the loop.
|
||||
if ($this->b === "\n") {
|
||||
break;
|
||||
}
|
||||
|
||||
// echo out the escaped character and restart the loop.
|
||||
echo $this->a . $this->b;
|
||||
break;
|
||||
|
||||
|
||||
// Since we're not dealing with any special cases we simply
|
||||
// output the character and continue our loop.
|
||||
default:
|
||||
echo $this->a;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When a regular expression is detected this function crawls for the end of
|
||||
* it and saves the whole regex.
|
||||
*
|
||||
* @throws \RuntimeException Unclosed regex will throw an error
|
||||
*/
|
||||
protected function saveRegex()
|
||||
{
|
||||
echo $this->a . $this->b;
|
||||
|
||||
while (($this->a = $this->getChar()) !== false) {
|
||||
if($this->a === '/')
|
||||
break;
|
||||
|
||||
if ($this->a === '\\') {
|
||||
echo $this->a;
|
||||
$this->a = $this->getChar();
|
||||
}
|
||||
|
||||
if($this->a === "\n")
|
||||
throw new \RuntimeException('Unclosed regex pattern at position: ' . $this->index);
|
||||
|
||||
echo $this->a;
|
||||
}
|
||||
$this->b = $this->getReal();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks to see if a character is alphanumeric.
|
||||
*
|
||||
* @param string $char Just one character
|
||||
* @return bool
|
||||
*/
|
||||
protected static function isAlphaNumeric($char)
|
||||
{
|
||||
return preg_match('/^[\w\$\pL]$/', $char) === 1 || $char == '/';
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace patterns in the given string and store the replacement
|
||||
*
|
||||
* @param string $js The string to lock
|
||||
* @return bool
|
||||
*/
|
||||
protected function lock($js)
|
||||
{
|
||||
/* lock things like <code>"asd" + ++x;</code> */
|
||||
$lock = '"LOCK---' . crc32(time()) . '"';
|
||||
|
||||
$matches = array();
|
||||
preg_match('/([+-])(\s+)([+-])/S', $js, $matches);
|
||||
if (empty($matches)) {
|
||||
return $js;
|
||||
}
|
||||
|
||||
$this->locks[$lock] = $matches[2];
|
||||
|
||||
$js = preg_replace('/([+-])\s+([+-])/S', "$1{$lock}$2", $js);
|
||||
/* -- */
|
||||
|
||||
return $js;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace "locks" with the original characters
|
||||
*
|
||||
* @param string $js The string to unlock
|
||||
* @return bool
|
||||
*/
|
||||
protected function unlock($js)
|
||||
{
|
||||
if (empty($this->locks)) {
|
||||
return $js;
|
||||
}
|
||||
|
||||
foreach ($this->locks as $lock => $replacement) {
|
||||
$js = str_replace($lock, $replacement, $js);
|
||||
}
|
||||
|
||||
return $js;
|
||||
}
|
||||
|
||||
}
|
||||
Vendored
+61
@@ -0,0 +1,61 @@
|
||||
# JShrink [](https://travis-ci.org/tedivm/JShrink)
|
||||
|
||||
[](https://github.com/tedivm/JShrink/blob/master/LICENSE)
|
||||
[](https://packagist.org/packages/tedivm/JShrink)
|
||||
[](https://coveralls.io/r/tedivm/JShrink?branch=master)
|
||||
[](https://packagist.org/packages/tedivm/JShrink)
|
||||
|
||||
|
||||
JShrink is a php class that minifies javascript so that it can be delivered to the client quicker. This code can be used
|
||||
by any product looking to minify their javascript on the fly (although caching the results is suggested for performance
|
||||
reasons). Unlike many other products this is not a port into php but a native application, resulting in better
|
||||
performance.
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
Minifying your code is simple call to a static function-
|
||||
|
||||
```php
|
||||
<?php
|
||||
include('vendor/autoload.php');
|
||||
|
||||
// Basic (default) usage.
|
||||
$minifiedCode = \JShrink\Minifier::minify($js);
|
||||
|
||||
// Disable YUI style comment preservation.
|
||||
$minifiedCode = \JShrink\Minifier::minify($js, array('flaggedComments' => false));
|
||||
```
|
||||
|
||||
|
||||
## Results
|
||||
|
||||
* Raw - 586,990
|
||||
* Gzip - 151,301
|
||||
* JShrink - 371,982
|
||||
* JShrink and Gzip - 93,507
|
||||
|
||||
|
||||
## Installing
|
||||
|
||||
### Composer
|
||||
|
||||
Installing JShrink can be done through a variety of methods, although Composer is
|
||||
recommended.
|
||||
|
||||
```yaml
|
||||
"require": {
|
||||
"tedivm/jshrink": "~1.0"
|
||||
}
|
||||
```
|
||||
|
||||
### Github
|
||||
|
||||
Releases of JShrink are available on [Github](https://github.com/tedious/JShrink/releases).
|
||||
|
||||
|
||||
## License
|
||||
|
||||
JShrink is licensed under the BSD License. See the LICENSE file for details.
|
||||
|
||||
In the spirit of open source, use of this library for evil is discouraged but not prohibited.
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "tedivm/jshrink",
|
||||
"description": "Javascript Minifier built in PHP",
|
||||
"keywords": ["minifier","javascript"],
|
||||
"homepage": "http://github.com/tedious/JShrink",
|
||||
"type": "library",
|
||||
"license": "BSD-3-Clause",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Robert Hafner",
|
||||
"email": "tedivm@tedivm.com"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=5.3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "4.0.*",
|
||||
"fabpot/php-cs-fixer": "0.4.0",
|
||||
"satooshi/php-coveralls": "dev-master"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-0": {"JShrink": "src/"}
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<phpunit bootstrap="./tests/bootstrap.php" colors="true">
|
||||
<testsuites>
|
||||
<testsuite name="JShrink Test Suite">
|
||||
<directory suffix="Test.php">./tests/JShrink/</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
<groups>
|
||||
<exclude>
|
||||
<group>requests</group>
|
||||
<group>development</group>
|
||||
</exclude>
|
||||
</groups>
|
||||
<filter>
|
||||
<whitelist>
|
||||
<directory suffix=".php">./src/JShrink/</directory>
|
||||
</whitelist>
|
||||
</filter>
|
||||
<logging>
|
||||
<log type="coverage-text" target="php://stdout" showUncoveredFiles="false"/>
|
||||
</logging>
|
||||
</phpunit>
|
||||
Vendored
+504
@@ -0,0 +1,504 @@
|
||||
GNU LESSER GENERAL PUBLIC LICENSE
|
||||
Version 2.1, February 1999
|
||||
|
||||
Copyright (C) 1991, 1999 Free Software Foundation, Inc.
|
||||
59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
[This is the first released version of the Lesser GPL. It also counts
|
||||
as the successor of the GNU Library Public License, version 2, hence
|
||||
the version number 2.1.]
|
||||
|
||||
Preamble
|
||||
|
||||
The licenses for most software are designed to take away your
|
||||
freedom to share and change it. By contrast, the GNU General Public
|
||||
Licenses are intended to guarantee your freedom to share and change
|
||||
free software--to make sure the software is free for all its users.
|
||||
|
||||
This license, the Lesser General Public License, applies to some
|
||||
specially designated software packages--typically libraries--of the
|
||||
Free Software Foundation and other authors who decide to use it. You
|
||||
can use it too, but we suggest you first think carefully about whether
|
||||
this license or the ordinary General Public License is the better
|
||||
strategy to use in any particular case, based on the explanations below.
|
||||
|
||||
When we speak of free software, we are referring to freedom of use,
|
||||
not price. Our General Public Licenses are designed to make sure that
|
||||
you have the freedom to distribute copies of free software (and charge
|
||||
for this service if you wish); that you receive source code or can get
|
||||
it if you want it; that you can change the software and use pieces of
|
||||
it in new free programs; and that you are informed that you can do
|
||||
these things.
|
||||
|
||||
To protect your rights, we need to make restrictions that forbid
|
||||
distributors to deny you these rights or to ask you to surrender these
|
||||
rights. These restrictions translate to certain responsibilities for
|
||||
you if you distribute copies of the library or if you modify it.
|
||||
|
||||
For example, if you distribute copies of the library, whether gratis
|
||||
or for a fee, you must give the recipients all the rights that we gave
|
||||
you. You must make sure that they, too, receive or can get the source
|
||||
code. If you link other code with the library, you must provide
|
||||
complete object files to the recipients, so that they can relink them
|
||||
with the library after making changes to the library and recompiling
|
||||
it. And you must show them these terms so they know their rights.
|
||||
|
||||
We protect your rights with a two-step method: (1) we copyright the
|
||||
library, and (2) we offer you this license, which gives you legal
|
||||
permission to copy, distribute and/or modify the library.
|
||||
|
||||
To protect each distributor, we want to make it very clear that
|
||||
there is no warranty for the free library. Also, if the library is
|
||||
modified by someone else and passed on, the recipients should know
|
||||
that what they have is not the original version, so that the original
|
||||
author's reputation will not be affected by problems that might be
|
||||
introduced by others.
|
||||
|
||||
Finally, software patents pose a constant threat to the existence of
|
||||
any free program. We wish to make sure that a company cannot
|
||||
effectively restrict the users of a free program by obtaining a
|
||||
restrictive license from a patent holder. Therefore, we insist that
|
||||
any patent license obtained for a version of the library must be
|
||||
consistent with the full freedom of use specified in this license.
|
||||
|
||||
Most GNU software, including some libraries, is covered by the
|
||||
ordinary GNU General Public License. This license, the GNU Lesser
|
||||
General Public License, applies to certain designated libraries, and
|
||||
is quite different from the ordinary General Public License. We use
|
||||
this license for certain libraries in order to permit linking those
|
||||
libraries into non-free programs.
|
||||
|
||||
When a program is linked with a library, whether statically or using
|
||||
a shared library, the combination of the two is legally speaking a
|
||||
combined work, a derivative of the original library. The ordinary
|
||||
General Public License therefore permits such linking only if the
|
||||
entire combination fits its criteria of freedom. The Lesser General
|
||||
Public License permits more lax criteria for linking other code with
|
||||
the library.
|
||||
|
||||
We call this license the "Lesser" General Public License because it
|
||||
does Less to protect the user's freedom than the ordinary General
|
||||
Public License. It also provides other free software developers Less
|
||||
of an advantage over competing non-free programs. These disadvantages
|
||||
are the reason we use the ordinary General Public License for many
|
||||
libraries. However, the Lesser license provides advantages in certain
|
||||
special circumstances.
|
||||
|
||||
For example, on rare occasions, there may be a special need to
|
||||
encourage the widest possible use of a certain library, so that it becomes
|
||||
a de-facto standard. To achieve this, non-free programs must be
|
||||
allowed to use the library. A more frequent case is that a free
|
||||
library does the same job as widely used non-free libraries. In this
|
||||
case, there is little to gain by limiting the free library to free
|
||||
software only, so we use the Lesser General Public License.
|
||||
|
||||
In other cases, permission to use a particular library in non-free
|
||||
programs enables a greater number of people to use a large body of
|
||||
free software. For example, permission to use the GNU C Library in
|
||||
non-free programs enables many more people to use the whole GNU
|
||||
operating system, as well as its variant, the GNU/Linux operating
|
||||
system.
|
||||
|
||||
Although the Lesser General Public License is Less protective of the
|
||||
users' freedom, it does ensure that the user of a program that is
|
||||
linked with the Library has the freedom and the wherewithal to run
|
||||
that program using a modified version of the Library.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow. Pay close attention to the difference between a
|
||||
"work based on the library" and a "work that uses the library". The
|
||||
former contains code derived from the library, whereas the latter must
|
||||
be combined with the library in order to run.
|
||||
|
||||
GNU LESSER GENERAL PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. This License Agreement applies to any software library or other
|
||||
program which contains a notice placed by the copyright holder or
|
||||
other authorized party saying it may be distributed under the terms of
|
||||
this Lesser General Public License (also called "this License").
|
||||
Each licensee is addressed as "you".
|
||||
|
||||
A "library" means a collection of software functions and/or data
|
||||
prepared so as to be conveniently linked with application programs
|
||||
(which use some of those functions and data) to form executables.
|
||||
|
||||
The "Library", below, refers to any such software library or work
|
||||
which has been distributed under these terms. A "work based on the
|
||||
Library" means either the Library or any derivative work under
|
||||
copyright law: that is to say, a work containing the Library or a
|
||||
portion of it, either verbatim or with modifications and/or translated
|
||||
straightforwardly into another language. (Hereinafter, translation is
|
||||
included without limitation in the term "modification".)
|
||||
|
||||
"Source code" for a work means the preferred form of the work for
|
||||
making modifications to it. For a library, complete source code means
|
||||
all the source code for all modules it contains, plus any associated
|
||||
interface definition files, plus the scripts used to control compilation
|
||||
and installation of the library.
|
||||
|
||||
Activities other than copying, distribution and modification are not
|
||||
covered by this License; they are outside its scope. The act of
|
||||
running a program using the Library is not restricted, and output from
|
||||
such a program is covered only if its contents constitute a work based
|
||||
on the Library (independent of the use of the Library in a tool for
|
||||
writing it). Whether that is true depends on what the Library does
|
||||
and what the program that uses the Library does.
|
||||
|
||||
1. You may copy and distribute verbatim copies of the Library's
|
||||
complete source code as you receive it, in any medium, provided that
|
||||
you conspicuously and appropriately publish on each copy an
|
||||
appropriate copyright notice and disclaimer of warranty; keep intact
|
||||
all the notices that refer to this License and to the absence of any
|
||||
warranty; and distribute a copy of this License along with the
|
||||
Library.
|
||||
|
||||
You may charge a fee for the physical act of transferring a copy,
|
||||
and you may at your option offer warranty protection in exchange for a
|
||||
fee.
|
||||
|
||||
2. You may modify your copy or copies of the Library or any portion
|
||||
of it, thus forming a work based on the Library, and copy and
|
||||
distribute such modifications or work under the terms of Section 1
|
||||
above, provided that you also meet all of these conditions:
|
||||
|
||||
a) The modified work must itself be a software library.
|
||||
|
||||
b) You must cause the files modified to carry prominent notices
|
||||
stating that you changed the files and the date of any change.
|
||||
|
||||
c) You must cause the whole of the work to be licensed at no
|
||||
charge to all third parties under the terms of this License.
|
||||
|
||||
d) If a facility in the modified Library refers to a function or a
|
||||
table of data to be supplied by an application program that uses
|
||||
the facility, other than as an argument passed when the facility
|
||||
is invoked, then you must make a good faith effort to ensure that,
|
||||
in the event an application does not supply such function or
|
||||
table, the facility still operates, and performs whatever part of
|
||||
its purpose remains meaningful.
|
||||
|
||||
(For example, a function in a library to compute square roots has
|
||||
a purpose that is entirely well-defined independent of the
|
||||
application. Therefore, Subsection 2d requires that any
|
||||
application-supplied function or table used by this function must
|
||||
be optional: if the application does not supply it, the square
|
||||
root function must still compute square roots.)
|
||||
|
||||
These requirements apply to the modified work as a whole. If
|
||||
identifiable sections of that work are not derived from the Library,
|
||||
and can be reasonably considered independent and separate works in
|
||||
themselves, then this License, and its terms, do not apply to those
|
||||
sections when you distribute them as separate works. But when you
|
||||
distribute the same sections as part of a whole which is a work based
|
||||
on the Library, the distribution of the whole must be on the terms of
|
||||
this License, whose permissions for other licensees extend to the
|
||||
entire whole, and thus to each and every part regardless of who wrote
|
||||
it.
|
||||
|
||||
Thus, it is not the intent of this section to claim rights or contest
|
||||
your rights to work written entirely by you; rather, the intent is to
|
||||
exercise the right to control the distribution of derivative or
|
||||
collective works based on the Library.
|
||||
|
||||
In addition, mere aggregation of another work not based on the Library
|
||||
with the Library (or with a work based on the Library) on a volume of
|
||||
a storage or distribution medium does not bring the other work under
|
||||
the scope of this License.
|
||||
|
||||
3. You may opt to apply the terms of the ordinary GNU General Public
|
||||
License instead of this License to a given copy of the Library. To do
|
||||
this, you must alter all the notices that refer to this License, so
|
||||
that they refer to the ordinary GNU General Public License, version 2,
|
||||
instead of to this License. (If a newer version than version 2 of the
|
||||
ordinary GNU General Public License has appeared, then you can specify
|
||||
that version instead if you wish.) Do not make any other change in
|
||||
these notices.
|
||||
|
||||
Once this change is made in a given copy, it is irreversible for
|
||||
that copy, so the ordinary GNU General Public License applies to all
|
||||
subsequent copies and derivative works made from that copy.
|
||||
|
||||
This option is useful when you wish to copy part of the code of
|
||||
the Library into a program that is not a library.
|
||||
|
||||
4. You may copy and distribute the Library (or a portion or
|
||||
derivative of it, under Section 2) in object code or executable form
|
||||
under the terms of Sections 1 and 2 above provided that you accompany
|
||||
it with the complete corresponding machine-readable source code, which
|
||||
must be distributed under the terms of Sections 1 and 2 above on a
|
||||
medium customarily used for software interchange.
|
||||
|
||||
If distribution of object code is made by offering access to copy
|
||||
from a designated place, then offering equivalent access to copy the
|
||||
source code from the same place satisfies the requirement to
|
||||
distribute the source code, even though third parties are not
|
||||
compelled to copy the source along with the object code.
|
||||
|
||||
5. A program that contains no derivative of any portion of the
|
||||
Library, but is designed to work with the Library by being compiled or
|
||||
linked with it, is called a "work that uses the Library". Such a
|
||||
work, in isolation, is not a derivative work of the Library, and
|
||||
therefore falls outside the scope of this License.
|
||||
|
||||
However, linking a "work that uses the Library" with the Library
|
||||
creates an executable that is a derivative of the Library (because it
|
||||
contains portions of the Library), rather than a "work that uses the
|
||||
library". The executable is therefore covered by this License.
|
||||
Section 6 states terms for distribution of such executables.
|
||||
|
||||
When a "work that uses the Library" uses material from a header file
|
||||
that is part of the Library, the object code for the work may be a
|
||||
derivative work of the Library even though the source code is not.
|
||||
Whether this is true is especially significant if the work can be
|
||||
linked without the Library, or if the work is itself a library. The
|
||||
threshold for this to be true is not precisely defined by law.
|
||||
|
||||
If such an object file uses only numerical parameters, data
|
||||
structure layouts and accessors, and small macros and small inline
|
||||
functions (ten lines or less in length), then the use of the object
|
||||
file is unrestricted, regardless of whether it is legally a derivative
|
||||
work. (Executables containing this object code plus portions of the
|
||||
Library will still fall under Section 6.)
|
||||
|
||||
Otherwise, if the work is a derivative of the Library, you may
|
||||
distribute the object code for the work under the terms of Section 6.
|
||||
Any executables containing that work also fall under Section 6,
|
||||
whether or not they are linked directly with the Library itself.
|
||||
|
||||
6. As an exception to the Sections above, you may also combine or
|
||||
link a "work that uses the Library" with the Library to produce a
|
||||
work containing portions of the Library, and distribute that work
|
||||
under terms of your choice, provided that the terms permit
|
||||
modification of the work for the customer's own use and reverse
|
||||
engineering for debugging such modifications.
|
||||
|
||||
You must give prominent notice with each copy of the work that the
|
||||
Library is used in it and that the Library and its use are covered by
|
||||
this License. You must supply a copy of this License. If the work
|
||||
during execution displays copyright notices, you must include the
|
||||
copyright notice for the Library among them, as well as a reference
|
||||
directing the user to the copy of this License. Also, you must do one
|
||||
of these things:
|
||||
|
||||
a) Accompany the work with the complete corresponding
|
||||
machine-readable source code for the Library including whatever
|
||||
changes were used in the work (which must be distributed under
|
||||
Sections 1 and 2 above); and, if the work is an executable linked
|
||||
with the Library, with the complete machine-readable "work that
|
||||
uses the Library", as object code and/or source code, so that the
|
||||
user can modify the Library and then relink to produce a modified
|
||||
executable containing the modified Library. (It is understood
|
||||
that the user who changes the contents of definitions files in the
|
||||
Library will not necessarily be able to recompile the application
|
||||
to use the modified definitions.)
|
||||
|
||||
b) Use a suitable shared library mechanism for linking with the
|
||||
Library. A suitable mechanism is one that (1) uses at run time a
|
||||
copy of the library already present on the user's computer system,
|
||||
rather than copying library functions into the executable, and (2)
|
||||
will operate properly with a modified version of the library, if
|
||||
the user installs one, as long as the modified version is
|
||||
interface-compatible with the version that the work was made with.
|
||||
|
||||
c) Accompany the work with a written offer, valid for at
|
||||
least three years, to give the same user the materials
|
||||
specified in Subsection 6a, above, for a charge no more
|
||||
than the cost of performing this distribution.
|
||||
|
||||
d) If distribution of the work is made by offering access to copy
|
||||
from a designated place, offer equivalent access to copy the above
|
||||
specified materials from the same place.
|
||||
|
||||
e) Verify that the user has already received a copy of these
|
||||
materials or that you have already sent this user a copy.
|
||||
|
||||
For an executable, the required form of the "work that uses the
|
||||
Library" must include any data and utility programs needed for
|
||||
reproducing the executable from it. However, as a special exception,
|
||||
the materials to be distributed need not include anything that is
|
||||
normally distributed (in either source or binary form) with the major
|
||||
components (compiler, kernel, and so on) of the operating system on
|
||||
which the executable runs, unless that component itself accompanies
|
||||
the executable.
|
||||
|
||||
It may happen that this requirement contradicts the license
|
||||
restrictions of other proprietary libraries that do not normally
|
||||
accompany the operating system. Such a contradiction means you cannot
|
||||
use both them and the Library together in an executable that you
|
||||
distribute.
|
||||
|
||||
7. You may place library facilities that are a work based on the
|
||||
Library side-by-side in a single library together with other library
|
||||
facilities not covered by this License, and distribute such a combined
|
||||
library, provided that the separate distribution of the work based on
|
||||
the Library and of the other library facilities is otherwise
|
||||
permitted, and provided that you do these two things:
|
||||
|
||||
a) Accompany the combined library with a copy of the same work
|
||||
based on the Library, uncombined with any other library
|
||||
facilities. This must be distributed under the terms of the
|
||||
Sections above.
|
||||
|
||||
b) Give prominent notice with the combined library of the fact
|
||||
that part of it is a work based on the Library, and explaining
|
||||
where to find the accompanying uncombined form of the same work.
|
||||
|
||||
8. You may not copy, modify, sublicense, link with, or distribute
|
||||
the Library except as expressly provided under this License. Any
|
||||
attempt otherwise to copy, modify, sublicense, link with, or
|
||||
distribute the Library is void, and will automatically terminate your
|
||||
rights under this License. However, parties who have received copies,
|
||||
or rights, from you under this License will not have their licenses
|
||||
terminated so long as such parties remain in full compliance.
|
||||
|
||||
9. You are not required to accept this License, since you have not
|
||||
signed it. However, nothing else grants you permission to modify or
|
||||
distribute the Library or its derivative works. These actions are
|
||||
prohibited by law if you do not accept this License. Therefore, by
|
||||
modifying or distributing the Library (or any work based on the
|
||||
Library), you indicate your acceptance of this License to do so, and
|
||||
all its terms and conditions for copying, distributing or modifying
|
||||
the Library or works based on it.
|
||||
|
||||
10. Each time you redistribute the Library (or any work based on the
|
||||
Library), the recipient automatically receives a license from the
|
||||
original licensor to copy, distribute, link with or modify the Library
|
||||
subject to these terms and conditions. You may not impose any further
|
||||
restrictions on the recipients' exercise of the rights granted herein.
|
||||
You are not responsible for enforcing compliance by third parties with
|
||||
this License.
|
||||
|
||||
11. If, as a consequence of a court judgment or allegation of patent
|
||||
infringement or for any other reason (not limited to patent issues),
|
||||
conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot
|
||||
distribute so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you
|
||||
may not distribute the Library at all. For example, if a patent
|
||||
license would not permit royalty-free redistribution of the Library by
|
||||
all those who receive copies directly or indirectly through you, then
|
||||
the only way you could satisfy both it and this License would be to
|
||||
refrain entirely from distribution of the Library.
|
||||
|
||||
If any portion of this section is held invalid or unenforceable under any
|
||||
particular circumstance, the balance of the section is intended to apply,
|
||||
and the section as a whole is intended to apply in other circumstances.
|
||||
|
||||
It is not the purpose of this section to induce you to infringe any
|
||||
patents or other property right claims or to contest validity of any
|
||||
such claims; this section has the sole purpose of protecting the
|
||||
integrity of the free software distribution system which is
|
||||
implemented by public license practices. Many people have made
|
||||
generous contributions to the wide range of software distributed
|
||||
through that system in reliance on consistent application of that
|
||||
system; it is up to the author/donor to decide if he or she is willing
|
||||
to distribute software through any other system and a licensee cannot
|
||||
impose that choice.
|
||||
|
||||
This section is intended to make thoroughly clear what is believed to
|
||||
be a consequence of the rest of this License.
|
||||
|
||||
12. If the distribution and/or use of the Library is restricted in
|
||||
certain countries either by patents or by copyrighted interfaces, the
|
||||
original copyright holder who places the Library under this License may add
|
||||
an explicit geographical distribution limitation excluding those countries,
|
||||
so that distribution is permitted only in or among countries not thus
|
||||
excluded. In such case, this License incorporates the limitation as if
|
||||
written in the body of this License.
|
||||
|
||||
13. The Free Software Foundation may publish revised and/or new
|
||||
versions of the Lesser General Public License from time to time.
|
||||
Such new versions will be similar in spirit to the present version,
|
||||
but may differ in detail to address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the Library
|
||||
specifies a version number of this License which applies to it and
|
||||
"any later version", you have the option of following the terms and
|
||||
conditions either of that version or of any later version published by
|
||||
the Free Software Foundation. If the Library does not specify a
|
||||
license version number, you may choose any version ever published by
|
||||
the Free Software Foundation.
|
||||
|
||||
14. If you wish to incorporate parts of the Library into other free
|
||||
programs whose distribution conditions are incompatible with these,
|
||||
write to the author to ask for permission. For software which is
|
||||
copyrighted by the Free Software Foundation, write to the Free
|
||||
Software Foundation; we sometimes make exceptions for this. Our
|
||||
decision will be guided by the two goals of preserving the free status
|
||||
of all derivatives of our free software and of promoting the sharing
|
||||
and reuse of software generally.
|
||||
|
||||
NO WARRANTY
|
||||
|
||||
15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
|
||||
WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
|
||||
EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
|
||||
OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
|
||||
KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
|
||||
LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
|
||||
THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
|
||||
WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
|
||||
AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
|
||||
FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
|
||||
CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
|
||||
LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
|
||||
RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
|
||||
FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
|
||||
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
|
||||
DAMAGES.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Libraries
|
||||
|
||||
If you develop a new library, and you want it to be of the greatest
|
||||
possible use to the public, we recommend making it free software that
|
||||
everyone can redistribute and change. You can do so by permitting
|
||||
redistribution under these terms (or, alternatively, under the terms of the
|
||||
ordinary General Public License).
|
||||
|
||||
To apply these terms, attach the following notices to the library. It is
|
||||
safest to attach them to the start of each source file to most effectively
|
||||
convey the exclusion of warranty; and each file should have at least the
|
||||
"copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the library's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License as published by the Free Software Foundation; either
|
||||
version 2.1 of the License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with this library; if not, write to the Free Software
|
||||
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or your
|
||||
school, if any, to sign a "copyright disclaimer" for the library, if
|
||||
necessary. Here is a sample; alter the names:
|
||||
|
||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the
|
||||
library `Frob' (a library for tweaking knobs) written by James Random Hacker.
|
||||
|
||||
<signature of Ty Coon>, 1 April 1990
|
||||
Ty Coon, President of Vice
|
||||
|
||||
That's all there is to it!
|
||||
|
||||
|
||||
+5694
File diff suppressed because it is too large.
Load diff
Vendored
+421
@@ -0,0 +1,421 @@
|
||||
// --------------------------------------------------------------------------------
|
||||
// PclZip 2.8.2 - readme.txt
|
||||
// --------------------------------------------------------------------------------
|
||||
// License GNU/LGPL - August 2009
|
||||
// Vincent Blavet - vincent@phpconcept.net
|
||||
// http://www.phpconcept.net
|
||||
// --------------------------------------------------------------------------------
|
||||
// $Id: readme.txt,v 1.60 2009/09/30 20:35:21 vblavet Exp $
|
||||
// --------------------------------------------------------------------------------
|
||||
|
||||
|
||||
|
||||
0 - Sommaire
|
||||
============
|
||||
1 - Introduction
|
||||
2 - What's new
|
||||
3 - Corrected bugs
|
||||
4 - Known bugs or limitations
|
||||
5 - License
|
||||
6 - Warning
|
||||
7 - Documentation
|
||||
8 - Author
|
||||
9 - Contribute
|
||||
|
||||
1 - Introduction
|
||||
================
|
||||
|
||||
PclZip is a library that allow you to manage a Zip archive.
|
||||
|
||||
Full documentation about PclZip can be found here : http://www.phpconcept.net/pclzip
|
||||
|
||||
2 - What's new
|
||||
==============
|
||||
|
||||
Version 2.8.2 :
|
||||
- PCLZIP_CB_PRE_EXTRACT and PCLZIP_CB_POST_EXTRACT are now supported with
|
||||
extraction as a string (PCLZIP_OPT_EXTRACT_AS_STRING). The string
|
||||
can also be modified in the post-extract call back.
|
||||
**Bugs correction :
|
||||
- PCLZIP_OPT_REMOVE_ALL_PATH was not working correctly
|
||||
- Remove use of eval() and do direct call to callback functions
|
||||
- Correct support of 64bits systems (Thanks to WordPress team)
|
||||
|
||||
Version 2.8.1 :
|
||||
- Move option PCLZIP_OPT_BY_EREG to PCLZIP_OPT_BY_PREG because ereg() is
|
||||
deprecated in PHP 5.3. When using option PCLZIP_OPT_BY_EREG, PclZip will
|
||||
automatically replace it by PCLZIP_OPT_BY_PREG.
|
||||
|
||||
Version 2.8 :
|
||||
- Improve extraction of zip archive for large files by using temporary files
|
||||
This feature is working like the one defined in r2.7.
|
||||
Options are renamed : PCLZIP_OPT_TEMP_FILE_ON, PCLZIP_OPT_TEMP_FILE_OFF,
|
||||
PCLZIP_OPT_TEMP_FILE_THRESHOLD
|
||||
- Add a ratio constant PCLZIP_TEMPORARY_FILE_RATIO to configure the auto
|
||||
sense of temporary file use.
|
||||
- Bug correction : Reduce filepath in returned file list to remove ennoying
|
||||
'.//' preambule in file path.
|
||||
|
||||
Version 2.7 :
|
||||
- Improve creation of zip archive for large files :
|
||||
PclZip will now autosense the configured memory and use temporary files
|
||||
when large file is suspected.
|
||||
This feature can also ne triggered by manual options in create() and add()
|
||||
methods. 'PCLZIP_OPT_ADD_TEMP_FILE_ON' force the use of temporary files,
|
||||
'PCLZIP_OPT_ADD_TEMP_FILE_OFF' disable the autosense technic,
|
||||
'PCLZIP_OPT_ADD_TEMP_FILE_THRESHOLD' allow for configuration of a size
|
||||
threshold to use temporary files.
|
||||
Using "temporary files" rather than "memory" might take more time, but
|
||||
might give the ability to zip very large files :
|
||||
Tested on my win laptop with a 88Mo file :
|
||||
Zip "in-memory" : 18sec (max_execution_time=30, memory_limit=180Mo)
|
||||
Zip "tmporary-files" : 23sec (max_execution_time=30, memory_limit=30Mo)
|
||||
- Replace use of mktime() by time() to limit the E_STRICT error messages.
|
||||
- Bug correction : When adding files with full windows path (drive letter)
|
||||
PclZip is now working. Before, if the drive letter is not the default
|
||||
path, PclZip was not able to add the file.
|
||||
|
||||
Version 2.6 :
|
||||
- Code optimisation
|
||||
- New attributes PCLZIP_ATT_FILE_COMMENT gives the ability to
|
||||
add a comment for a specific file. (Don't really know if this is usefull)
|
||||
- New attribute PCLZIP_ATT_FILE_CONTENT gives the ability to add a string
|
||||
as a file.
|
||||
- New attribute PCLZIP_ATT_FILE_MTIME modify the timestamp associated with
|
||||
a file.
|
||||
- Correct a bug. Files archived with a timestamp with 0h0m0s were extracted
|
||||
with current time
|
||||
- Add CRC value in the informations returned back for each file after an
|
||||
action.
|
||||
- Add missing closedir() statement.
|
||||
- When adding a folder, and removing the path of this folder, files were
|
||||
incorrectly added with a '/' at the beginning. Which means files are
|
||||
related to root in unix systems. Corrected.
|
||||
- Add conditional if before constant definition. This will allow users
|
||||
to redefine constants without changing the file, and then improve
|
||||
upgrade of pclzip code for new versions.
|
||||
|
||||
Version 2.5 :
|
||||
- Introduce the ability to add file/folder with individual properties (file descriptor).
|
||||
This gives for example the ability to change the filename of a zipped file.
|
||||
. Able to add files individually
|
||||
. Able to change full name
|
||||
. Able to change short name
|
||||
. Compatible with global options
|
||||
- New attributes : PCLZIP_ATT_FILE_NAME, PCLZIP_ATT_FILE_NEW_SHORT_NAME, PCLZIP_ATT_FILE_NEW_FULL_NAME
|
||||
- New error code : PCLZIP_ERR_INVALID_ATTRIBUTE_VALUE
|
||||
- Add a security control feature. PclZip can extract any file in any folder
|
||||
of a system. People may use this to upload a zip file and try to override
|
||||
a system file. The PCLZIP_OPT_EXTRACT_DIR_RESTRICTION will give the
|
||||
ability to forgive any directory transversal behavior.
|
||||
- New PCLZIP_OPT_EXTRACT_DIR_RESTRICTION : check extraction path
|
||||
- New error code : PCLZIP_ERR_DIRECTORY_RESTRICTION
|
||||
- Modification in PclZipUtilPathInclusion() : dir and path beginning with ./ will be prepend
|
||||
by current path (getcwd())
|
||||
|
||||
Version 2.4 :
|
||||
- Code improvment : try to speed up the code by removing unusefull call to pack()
|
||||
- Correct bug in delete() : delete() should be called with no argument. This was not
|
||||
the case in 2.3. This is corrected in 2.4.
|
||||
- Correct a bug in path_inclusion function. When the path has several '../../', the
|
||||
result was bad.
|
||||
- Add a check for magic_quotes_runtime configuration. If enabled, PclZip will
|
||||
disable it while working and det it back to its original value.
|
||||
This resolve a lots of bad formated archive errors.
|
||||
- Bug correction : PclZip now correctly unzip file in some specific situation,
|
||||
when compressed content has same size as uncompressed content.
|
||||
- Bug correction : When selecting option 'PCLZIP_OPT_REMOVE_ALL_PATH',
|
||||
directories are not any more created.
|
||||
- Code improvment : correct unclosed opendir(), better handling of . and .. in
|
||||
loops.
|
||||
|
||||
|
||||
Version 2.3 :
|
||||
- Correct a bug with PHP5 : affecting the value 0xFE49FFE0 to a variable does not
|
||||
give the same result in PHP4 and PHP5 ....
|
||||
|
||||
Version 2.2 :
|
||||
- Try development of PCLZIP_OPT_CRYPT .....
|
||||
However this becomes to a stop. To crypt/decrypt I need to multiply 2 long integers,
|
||||
the result (greater than a long) is not supported by PHP. Even the use of bcmath
|
||||
functions does not help. I did not find yet a solution ...;
|
||||
- Add missing '/' at end of directory entries
|
||||
- Check is a file is encrypted or not. Returns status 'unsupported_encryption' and/or
|
||||
error code PCLZIP_ERR_UNSUPPORTED_ENCRYPTION.
|
||||
- Corrected : Bad "version need to extract" field in local file header
|
||||
- Add private method privCheckFileHeaders() in order to check local and central
|
||||
file headers. PclZip is now supporting purpose bit flag bit 3. Purpose bit flag bit 3 gives
|
||||
the ability to have a local file header without size, compressed size and crc filled.
|
||||
- Add a generic status 'error' for file status
|
||||
- Add control of compression type. PclZip only support deflate compression method.
|
||||
Before v2.2, PclZip does not check the compression method used in an archive while
|
||||
extracting. With v2.2 PclZip returns a new error status for a file using an unsupported
|
||||
compression method. New status is "unsupported_compression". New error code is
|
||||
PCLZIP_ERR_UNSUPPORTED_COMPRESSION.
|
||||
- Add optional attribute PCLZIP_OPT_STOP_ON_ERROR. This will stop the extract of files
|
||||
when errors like 'a folder with same name exists' or 'a newer file exists' or
|
||||
'a write protected file' exists, rather than set a status for the concerning file
|
||||
and resume the extract of the zip.
|
||||
- Add optional attribute PCLZIP_OPT_REPLACE_NEWER. This will force, during an extract' the
|
||||
replacement of the file, even if a newer version of the file exists.
|
||||
Note that today if a file with the same name already exists but is older it will be
|
||||
replaced by the extracted one.
|
||||
- Improve PclZipUtilOption()
|
||||
- Support of zip archive with trailing bytes. Before 2.2, PclZip checks that the central
|
||||
directory structure is the last data in the archive. Crypt encryption/decryption of
|
||||
zip archive put trailing 0 bytes after decryption. PclZip is now supporting this.
|
||||
|
||||
Version 2.1 :
|
||||
- Add the ability to abort the extraction by using a user callback function.
|
||||
The user can now return the value '2' in its callback which indicates to stop the
|
||||
extraction. For a pre call-back extract is stopped before the extration of the current
|
||||
file. For a post call back, the extraction is stopped after.
|
||||
- Add the ability to extract a file (or several files) directly in the standard output.
|
||||
This is done by the new parameter PCLZIP_OPT_EXTRACT_IN_OUTPUT with method extract().
|
||||
- Add support for parameters PCLZIP_OPT_COMMENT, PCLZIP_OPT_ADD_COMMENT,
|
||||
PCLZIP_OPT_PREPEND_COMMENT. This will create, replace, add, or prepend comments
|
||||
in the zip archive.
|
||||
- When merging two archives, the comments are not any more lost, but merged, with a
|
||||
blank space separator.
|
||||
- Corrected bug : Files are not deleted when all files are asked to be deleted.
|
||||
- Corrected bug : Folders with name '0' made PclZip to abort the create or add feature.
|
||||
|
||||
|
||||
Version 2.0 :
|
||||
***** Warning : Some new features may break the backward compatibility for your scripts.
|
||||
Please carefully read the readme file.
|
||||
- Add the ability to delete by Index, name and regular expression. This feature is
|
||||
performed by the method delete(), which uses the optional parameters
|
||||
PCLZIP_OPT_BY_INDEX, PCLZIP_OPT_BY_NAME, PCLZIP_OPT_BY_EREG or PCLZIP_OPT_BY_PREG.
|
||||
- Add the ability to extract by regular expression. To extract by regexp you must use the method
|
||||
extract(), with the option PCLZIP_OPT_BY_EREG or PCLZIP_OPT_BY_PREG
|
||||
(depending if you want to use ereg() or preg_match() syntax) followed by the
|
||||
regular expression pattern.
|
||||
- Add the ability to extract by index, directly with the extract() method. This is a
|
||||
code improvment of the extractByIndex() method.
|
||||
- Add the ability to extract by name. To extract by name you must use the method
|
||||
extract(), with the option PCLZIP_OPT_BY_NAME followed by the filename to
|
||||
extract or an array of filenames to extract. To extract all a folder, use the folder
|
||||
name rather than the filename with a '/' at the end.
|
||||
- Add the ability to add files without compression. This is done with a new attribute
|
||||
which is PCLZIP_OPT_NO_COMPRESSION.
|
||||
- Add the attribute PCLZIP_OPT_EXTRACT_AS_STRING, which allow to extract a file directly
|
||||
in a string without using any file (or temporary file).
|
||||
- Add constant PCLZIP_SEPARATOR for static configuration of filename separators in a single string.
|
||||
The default separator is now a comma (,) and not any more a blank space.
|
||||
THIS BREAK THE BACKWARD COMPATIBILITY : Please check if this may have an impact with
|
||||
your script.
|
||||
- Improve algorythm performance by removing the use of temporary files when adding or
|
||||
extracting files in an archive.
|
||||
- Add (correct) detection of empty filename zipping. This can occurs when the removed
|
||||
path is the same
|
||||
as a zipped dir. The dir is not zipped (['status'] = filtered), only its content.
|
||||
- Add better support for windows paths (thanks for help from manus@manusfreedom.com).
|
||||
- Corrected bug : When the archive file already exists with size=0, the add() method
|
||||
fails. Corrected in 2.0.
|
||||
- Remove the use of OS_WINDOWS constant. Use php_uname() function rather.
|
||||
- Control the order of index ranges in extract by index feature.
|
||||
- Change the internal management of folders (better handling of internal flag).
|
||||
|
||||
|
||||
Version 1.3 :
|
||||
- Removing the double include check. This is now done by include_once() and require_once()
|
||||
PHP directives.
|
||||
- Changing the error handling mecanism : Remove the use of an external error library.
|
||||
The former PclError...() functions are replaced by internal equivalent methods.
|
||||
By changing the environment variable PCLZIP_ERROR_EXTERNAL you can still use the former library.
|
||||
Introducing the use of constants for error codes rather than integer values. This will help
|
||||
in futur improvment.
|
||||
Introduction of error handling functions like errorCode(), errorName() and errorInfo().
|
||||
- Remove the deprecated use of calling function with arguments passed by reference.
|
||||
- Add the calling of extract(), extractByIndex(), create() and add() functions
|
||||
with variable options rather than fixed arguments.
|
||||
- Add the ability to remove all the file path while extracting or adding,
|
||||
without any need to specify the path to remove.
|
||||
This is available for extract(), extractByIndex(), create() and add() functionS by using
|
||||
the new variable options parameters :
|
||||
- PCLZIP_OPT_REMOVE_ALL_PATH : by indicating this option while calling the fct.
|
||||
- Ability to change the mode of a file after the extraction (chmod()).
|
||||
This is available for extract() and extractByIndex() functionS by using
|
||||
the new variable options parameters.
|
||||
- PCLZIP_OPT_SET_CHMOD : by setting the value of this option.
|
||||
- Ability to definition call-back options. These call-back will be called during the adding,
|
||||
or the extracting of file (extract(), extractByIndex(), create() and add() functions) :
|
||||
- PCLZIP_CB_PRE_EXTRACT : will be called before each extraction of a file. The user
|
||||
can trigerred the change the filename of the extracted file. The user can triggered the
|
||||
skip of the extraction. This is adding a 'skipped' status in the file list result value.
|
||||
- PCLZIP_CB_POST_EXTRACT : will be called after each extraction of a file.
|
||||
Nothing can be triggered from that point.
|
||||
- PCLZIP_CB_PRE_ADD : will be called before each add of a file. The user
|
||||
can trigerred the change the stored filename of the added file. The user can triggered the
|
||||
skip of the add. This is adding a 'skipped' status in the file list result value.
|
||||
- PCLZIP_CB_POST_ADD : will be called after each add of a file.
|
||||
Nothing can be triggered from that point.
|
||||
- Two status are added in the file list returned as function result : skipped & filename_too_long
|
||||
'skipped' is used when a call-back function ask for skipping the file.
|
||||
'filename_too_long' is used while adding a file with a too long filename to archive (the file is
|
||||
not added)
|
||||
- Adding the function PclZipUtilPathInclusion(), that check the inclusion of a path into
|
||||
a directory.
|
||||
- Add a check of the presence of the archive file before some actions (like list, ...)
|
||||
- Add the initialisation of field "index" in header array. This means that by
|
||||
default index will be -1 when not explicitly set by the methods.
|
||||
|
||||
Version 1.2 :
|
||||
- Adding a duplicate function.
|
||||
- Adding a merge function. The merge function is a "quick merge" function,
|
||||
it just append the content of an archive at the end of the first one. There
|
||||
is no check for duplicate files or more recent files.
|
||||
- Improve the search of the central directory end.
|
||||
|
||||
Version 1.1.2 :
|
||||
|
||||
- Changing the license of PclZip. PclZip is now released under the GNU / LGPL license
|
||||
(see License section).
|
||||
- Adding the optional support of a static temporary directory. You will need to configure
|
||||
the constant PCLZIP_TEMPORARY_DIR if you want to use this feature.
|
||||
- Improving the rename() function. In some cases rename() does not work (different
|
||||
Filesystems), so it will be replaced by a copy() + unlink() functions.
|
||||
|
||||
Version 1.1.1 :
|
||||
|
||||
- Maintenance release, no new feature.
|
||||
|
||||
Version 1.1 :
|
||||
|
||||
- New method Add() : adding files in the archive
|
||||
- New method ExtractByIndex() : partial extract of the archive, files are identified by
|
||||
their index in the archive
|
||||
- New method DeleteByIndex() : delete some files/folder entries from the archive,
|
||||
files are identified by their index in the archive.
|
||||
- Adding a test of the zlib extension presence. If not present abort the script.
|
||||
|
||||
Version 1.0.1 :
|
||||
|
||||
- No new feature
|
||||
|
||||
|
||||
3 - Corrected bugs
|
||||
==================
|
||||
|
||||
Corrected in Version 2.0 :
|
||||
- Corrected : During an extraction, if a call-back fucntion is used and try to skip
|
||||
a file, all the extraction process is stopped.
|
||||
|
||||
Corrected in Version 1.3 :
|
||||
- Corrected : Support of static synopsis for method extract() is broken.
|
||||
- Corrected : invalid size of archive content field (0xFF) should be (0xFFFF).
|
||||
- Corrected : When an extract is done with a remove_path parameter, the entry for
|
||||
the directory with exactly the same path is not skipped/filtered.
|
||||
- Corrected : extractByIndex() and deleteByIndex() were not managing index in the
|
||||
right way. For example indexes '1,3-5,11' will only extract files 1 and 11. This
|
||||
is due to a sort of the index resulting table that puts 11 before 3-5 (sort on
|
||||
string and not interger). The sort is temporarilly removed, this means that
|
||||
you must provide a sorted list of index ranges.
|
||||
|
||||
Corrected in Version 1.2 :
|
||||
|
||||
- Nothing.
|
||||
|
||||
Corrected in Version 1.1.2 :
|
||||
|
||||
- Corrected : Winzip is unable to delete or add new files in a PclZip created archives.
|
||||
|
||||
Corrected in Version 1.1.1 :
|
||||
|
||||
- Corrected : When archived file is not compressed (0% compression), the
|
||||
extract method fails.
|
||||
|
||||
Corrected in Version 1.1 :
|
||||
|
||||
- Corrected : Adding a complete tree of folder may result in a bad archive
|
||||
creation.
|
||||
|
||||
Corrected in Version 1.0.1 :
|
||||
|
||||
- Corrected : Error while compressing files greater than PCLZIP_READ_BLOCK_SIZE (default=1024).
|
||||
|
||||
|
||||
4 - Known bugs or limitations
|
||||
=============================
|
||||
|
||||
Please publish bugs reports in SourceForge :
|
||||
http://sourceforge.net/tracker/?group_id=40254&atid=427564
|
||||
|
||||
In Version 2.x :
|
||||
- PclZip does only support file uncompressed or compressed with deflate (compression method 8)
|
||||
- PclZip does not support password protected zip archive
|
||||
- Some concern were seen when changing mtime of a file while archiving.
|
||||
Seems to be linked to Daylight Saving Time (PclTest_changing_mtime).
|
||||
|
||||
In Version 1.2 :
|
||||
|
||||
- merge() methods does not check for duplicate files or last date of modifications.
|
||||
|
||||
In Version 1.1 :
|
||||
|
||||
- Limitation : Using 'extract' fields in the file header in the zip archive is not supported.
|
||||
- WinZip is unable to delete a single file in a PclZip created archive. It is also unable to
|
||||
add a file in a PclZip created archive. (Corrected in v.1.2)
|
||||
|
||||
In Version 1.0.1 :
|
||||
|
||||
- Adding a complete tree of folder may result in a bad archive
|
||||
creation. (Corrected in V.1.1).
|
||||
- Path given to methods must be in the unix format (/) and not the Windows format (\).
|
||||
Workaround : Use only / directory separators.
|
||||
- PclZip is using temporary files that are sometime the name of the file with a .tmp or .gz
|
||||
added suffix. Files with these names may already exist and may be overwritten.
|
||||
Workaround : none.
|
||||
- PclZip does not check if the zlib extension is present. If it is absent, the zip
|
||||
file is not created and the lib abort without warning.
|
||||
Workaround : enable the zlib extension on the php install
|
||||
|
||||
In Version 1.0 :
|
||||
|
||||
- Error while compressing files greater than PCLZIP_READ_BLOCK_SIZE (default=1024).
|
||||
(Corrected in v.1.0.1)
|
||||
- Limitation : Multi-disk zip archive are not supported.
|
||||
|
||||
|
||||
5 - License
|
||||
===========
|
||||
|
||||
Since version 1.1.2, PclZip Library is released under GNU/LGPL license.
|
||||
This library is free, so you can use it at no cost.
|
||||
|
||||
HOWEVER, if you release a script, an application, a library or any kind of
|
||||
code using PclZip library (or a part of it), YOU MUST :
|
||||
- Indicate in the documentation (or a readme file), that your work
|
||||
uses PclZip Library, and make a reference to the author and the web site
|
||||
http://www.phpconcept.net
|
||||
- Gives the ability to the final user to update the PclZip libary.
|
||||
|
||||
I will also appreciate that you send me a mail (vincent@phpconcept.net), just to
|
||||
be aware that someone is using PclZip.
|
||||
|
||||
For more information about GNU/LGPL license : http://www.gnu.org
|
||||
|
||||
6 - Warning
|
||||
=================
|
||||
|
||||
This library and the associated files are non commercial, non professional work.
|
||||
It should not have unexpected results. However if any damage is caused by this software
|
||||
the author can not be responsible.
|
||||
The use of this software is at the risk of the user.
|
||||
|
||||
7 - Documentation
|
||||
=================
|
||||
PclZip User Manuel is available in English on PhpConcept : http://www.phpconcept.net/pclzip/man/en/index.php
|
||||
A Russian translation was done by Feskov Kuzma : http://php.russofile.ru/ru/authors/unsort/zip/
|
||||
|
||||
8 - Author
|
||||
==========
|
||||
|
||||
This software was written by Vincent Blavet (vincent@phpconcept.net) on its leasure time.
|
||||
|
||||
9 - Contribute
|
||||
==============
|
||||
If you want to contribute to the development of PclZip, please contact vincent@phpconcept.net.
|
||||
If you can help in financing PhpConcept hosting service, please go to
|
||||
http://www.phpconcept.net/soutien.php
|
||||
@@ -276,7 +276,7 @@ class _Application extends \IPS\Node\Model
|
||||
}
|
||||
|
||||
$_extensions = array();
|
||||
|
||||
|
||||
foreach ( $application->extensions( $app, $extension, $construct, $checkAccess ) as $key => $class )
|
||||
{
|
||||
$_extensions[ $application->directory . '_' . $key ] = $class;
|
||||
@@ -404,6 +404,106 @@ class _Application extends \IPS\Node\Model
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* The available version we can upgrade to
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function availableUpgrade( $latestOnly=FALSE )
|
||||
{
|
||||
$update = array();
|
||||
|
||||
if( $this->update_version )
|
||||
{
|
||||
$versions = json_decode( $this->update_version, TRUE );
|
||||
if ( is_array( $versions ) and !isset( $versions[0] ) and isset( $versions['longversion'] ) )
|
||||
{
|
||||
$versions = array( $versions );
|
||||
}
|
||||
|
||||
$update = array();
|
||||
foreach ( $versions as $data )
|
||||
{
|
||||
if( !empty($data['longversion']) AND $data['longversion'] > $this->long_version )
|
||||
{
|
||||
if( $data['released'] AND intval($data['released']) == $data['released'] AND \strlen($data['released']) == 10 )
|
||||
{
|
||||
$data['released'] = (string) \IPS\DateTime::ts( $data['released'] )->localeDate();
|
||||
}
|
||||
|
||||
$update[] = $data;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( !empty( $update ) and $latestOnly )
|
||||
{
|
||||
$update = array_pop( $update );
|
||||
}
|
||||
|
||||
return $update;
|
||||
}
|
||||
|
||||
/**
|
||||
* MD5 check (returns path to files which do not match)
|
||||
*
|
||||
* @retrun array
|
||||
* @throws \IPS\Http\Request\Exception
|
||||
*/
|
||||
public static function md5Check()
|
||||
{
|
||||
$key = \IPS\IPS::licenseKey();
|
||||
$correctMd5s = \IPS\Http\Url::ips( 'md5/' . $key['key'] )->request()->get()->decodeJson();
|
||||
|
||||
return static::_md5sumCheckerIterator( \IPS\ROOT_PATH, $correctMd5s );
|
||||
}
|
||||
|
||||
/**
|
||||
* MD5 check directory
|
||||
*
|
||||
* @param string $directory Directory to look through
|
||||
* @return array
|
||||
*/
|
||||
public static function _md5sumCheckerIterator( $directory, $correctMd5s )
|
||||
{
|
||||
$return = array();
|
||||
foreach( new \DirectoryIterator( $directory ) as $file )
|
||||
{
|
||||
if ( mb_substr( $file, 0, 1 ) === '.' or mb_substr( $file, 0, 1 ) === '_' or $file == 'index.html' )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( $file->isDir() )
|
||||
{
|
||||
/* If this is an application directory but the application has been disabled then we shouldn't check it */
|
||||
if( mb_strpos( $file->getPathname(), \IPS\ROOT_PATH . DIRECTORY_SEPARATOR . 'applications' ) !== FALSE AND $file->getFilename() != 'applications' )
|
||||
{
|
||||
$applications = static::applications();
|
||||
|
||||
/* If the directory name is a valid application key and the application is not enabled, don't check it */
|
||||
if( isset( $applications[ $file->getFilename() ] ) AND !static::appIsEnabled( $file->getFilename() ) )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$return = array_merge( $return, static::_md5sumCheckerIterator( $file->getPathname(), $correctMd5s ) );
|
||||
}
|
||||
elseif ( mb_substr( $file, -4 ) === '.php' )
|
||||
{
|
||||
$fullPath = $file->getPathname();
|
||||
$shortPath = mb_substr( $fullPath, mb_strlen( \IPS\ROOT_PATH ) );
|
||||
|
||||
if ( $shortPath != '/init.php' and isset( $correctMd5s[ $shortPath ] ) and md5( preg_replace( '#\s#', '', utf8_decode( file_get_contents( $fullPath ) ) ) ) != $correctMd5s[ $shortPath ] ) // We strip whitespace since FTP in ASCII mode will change the whitespace characters
|
||||
{
|
||||
$return[] = $fullPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief [ActiveRecord] Database Table
|
||||
*/
|
||||
@@ -518,7 +618,7 @@ EOF;
|
||||
if( $this->modules === NULL )
|
||||
{
|
||||
$modules = \IPS\Application\Module::modules();
|
||||
$this->modules = $modules[ $this->directory ];
|
||||
$this->modules = array_key_exists( $this->directory, $modules ) ? $modules[ $this->directory ] : array();
|
||||
}
|
||||
|
||||
/* Return */
|
||||
@@ -584,8 +684,8 @@ EOF;
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
$classes[ mb_substr( $file, 0, -4 ) ] = new $classname;
|
||||
{
|
||||
$classes[ mb_substr( $file, 0, -4 ) ] = new $classname( $checkAccess === TRUE ? \IPS\Member::loggedIn() : ( $checkAccess === FALSE ? NULL : $checkAccess ) );
|
||||
}
|
||||
catch( \RuntimeException $e ){}
|
||||
}
|
||||
@@ -717,7 +817,7 @@ EOF;
|
||||
}
|
||||
|
||||
/* Default */
|
||||
if( $this->directory != 'core' and $this->enabled )
|
||||
if( $this->enabled )
|
||||
{
|
||||
$buttons['default'] = array(
|
||||
'icon' => $this->default ? 'star' : 'star-o',
|
||||
@@ -1523,6 +1623,7 @@ EOF;
|
||||
$module->position = ++$position;
|
||||
$module->area = $area;
|
||||
$module->default_controller = $data['default_controller'];
|
||||
$module->default = isset( $data['default'] ) and $data['default'];
|
||||
$module->save( TRUE );
|
||||
}
|
||||
}
|
||||
@@ -1601,6 +1702,32 @@ EOF;
|
||||
{
|
||||
\IPS\Db::i()->insert( 'core_notification_defaults', $inserts );
|
||||
}
|
||||
|
||||
/* Install Menu items @note IPS applications are handled by FrontNavigation::buildDefaultNavigation */
|
||||
if ( ! in_array( $this->directory, static::$ipsApps ) )
|
||||
{
|
||||
try
|
||||
{
|
||||
$topLevelPosition = \IPS\Db::i()->select( 'MAX(position) + 1', 'core_menu', 'parent IS NULL' )->first();
|
||||
}
|
||||
catch( \IPS\Db\Exception $ex )
|
||||
{
|
||||
$topLevelPosition = 0;
|
||||
}
|
||||
|
||||
foreach( $this->extensions( 'core', 'FrontNavigation', FALSE ) as $key => $class )
|
||||
{
|
||||
\IPS\Db::i()->insert( 'core_menu', array(
|
||||
'app' => $this->directory,
|
||||
'extension' => $key,
|
||||
'config' => json_encode( array() ),
|
||||
'position' => $topLevelPosition++,
|
||||
'parent' => NULL,
|
||||
'permissions' => '',
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1915,6 +2042,8 @@ EOF;
|
||||
'sc_title' => $data['sc_title'],
|
||||
'sc_order' => $data['sc_order']
|
||||
) );
|
||||
|
||||
$currentSettings[ $data['sc_key'] ] = $data;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1943,9 +2072,18 @@ EOF;
|
||||
|
||||
if ( $update )
|
||||
{
|
||||
$defaultCurrentSettings = $currentSettings;
|
||||
foreach( \IPS\Theme::themes() as $theme )
|
||||
{
|
||||
$currentSettings = iterator_to_array( \IPS\Db::i()->select( '*', 'core_theme_settings_fields', array( 'sc_set_id=?', $theme->id ) )->setKeyField('sc_key') );
|
||||
if ( $theme->id == \IPS\Theme::defaultTheme() )
|
||||
{
|
||||
$currentSettings = $defaultCurrentSettings;
|
||||
}
|
||||
else
|
||||
{
|
||||
$currentSettings = iterator_to_array( \IPS\Db::i()->select( '*', 'core_theme_settings_fields', array( 'sc_set_id=?', $theme->id ) )->setKeyField('sc_key') );
|
||||
}
|
||||
|
||||
$added = FALSE;
|
||||
$save = json_decode( $theme->template_settings, TRUE );
|
||||
|
||||
@@ -1972,6 +2110,41 @@ EOF;
|
||||
'sc_order' => $data['sc_order'],
|
||||
) );
|
||||
}
|
||||
else
|
||||
{
|
||||
/* Update */
|
||||
\IPS\Db::i()->update( 'core_theme_settings_fields', array(
|
||||
'sc_tab_key' => $data['sc_tab_key'],
|
||||
'sc_type' => $data['sc_type'],
|
||||
'sc_multiple' => $data['sc_multiple'],
|
||||
'sc_default' => $data['sc_default'],
|
||||
'sc_show_in_vse' => ( isset( $data['sc_show_in_vse'] ) ) ? $data['sc_show_in_vse'] : 0,
|
||||
'sc_content' => $data['sc_content'],
|
||||
'sc_title' => $data['sc_title'],
|
||||
'sc_order' => $data['sc_order']
|
||||
), array( 'sc_set_id=? AND sc_key=?', $theme->id, $data['sc_key'] ) );
|
||||
|
||||
$currentId = $currentSettings[ $data['sc_key'] ]['sc_id'];
|
||||
|
||||
try
|
||||
{
|
||||
$currentValue = \IPS\Db::i()->select( 'sv_value', 'core_theme_settings_values', array( array( 'sv_id=?', $currentId ) ) )->first();
|
||||
}
|
||||
catch( \UnderFlowException $ex )
|
||||
{
|
||||
$currentValue = $currentSettings[ $data['sc_key'] ]['sc_default'];
|
||||
}
|
||||
|
||||
/* Are we using the existing default? If so, update it */
|
||||
if ( ( $data['sc_default'] != $currentSettings[ $data['sc_key'] ]['sc_default'] ) and ( $currentValue == $currentSettings[ $data['sc_key'] ]['sc_default'] ) )
|
||||
{
|
||||
$added = TRUE;
|
||||
$save[ $data['sc_key'] ] = $data['sc_default'];
|
||||
|
||||
\IPS\Db::i()->delete('core_theme_settings_values', array('sv_id=?', $currentId ) );
|
||||
\IPS\Db::i()->insert('core_theme_settings_values', array( 'sv_id' => $currentId, 'sv_value' => (string)$data['sc_default'] ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( $added )
|
||||
@@ -2013,7 +2186,16 @@ EOF;
|
||||
{
|
||||
$i = 0;
|
||||
$inserted = 0;
|
||||
|
||||
|
||||
if ( \IPS\Dispatcher::hasInstance() AND class_exists( '\IPS\Dispatcher', FALSE ) and \IPS\Dispatcher::i()->controllerLocation === 'setup' )
|
||||
{
|
||||
$class = '\IPS\Theme';
|
||||
}
|
||||
else
|
||||
{
|
||||
$class = ( \IPS\Theme::designersModeEnabled() ) ? '\IPS\Theme\Advanced\Theme' : '\IPS\Theme';
|
||||
}
|
||||
|
||||
if( file_exists( \IPS\ROOT_PATH . "/applications/{$this->directory}/data/theme.xml" ) )
|
||||
{
|
||||
unset( \IPS\Data\Store::i()->themes );
|
||||
@@ -2070,7 +2252,7 @@ EOF;
|
||||
|
||||
try
|
||||
{
|
||||
\IPS\Theme::addTemplate( $template );
|
||||
$class::addTemplate( $template );
|
||||
}
|
||||
catch( \OverflowException $e )
|
||||
{
|
||||
@@ -2093,7 +2275,7 @@ EOF;
|
||||
|
||||
try
|
||||
{
|
||||
\IPS\Theme::addCss( $css );
|
||||
$class::addCss( $css );
|
||||
}
|
||||
catch( \OverflowException $e )
|
||||
{
|
||||
@@ -2113,7 +2295,7 @@ EOF;
|
||||
'content' => base64_decode( $xml->readString() ),
|
||||
);
|
||||
|
||||
\IPS\Theme::addResource( $resource, TRUE );
|
||||
$class::addResource( $resource, TRUE );
|
||||
}
|
||||
|
||||
if( $limit !== null AND $i === ( $limit + $offset ) )
|
||||
@@ -2371,6 +2553,27 @@ EOF;
|
||||
}
|
||||
}
|
||||
|
||||
/* If we are about to adjust the column to not allow NULL values then adjust those values first... */
|
||||
if( isset( $columnData['allow_null'] ) and $columnData['allow_null'] === FALSE )
|
||||
{
|
||||
$defaultValue = "''";
|
||||
|
||||
/* Default value */
|
||||
if( isset( $columnData['default'] ) and !in_array( \strtoupper( $columnData['type'] ), array( 'TINYTEXT', 'TEXT', 'MEDIUMTEXT', 'LONGTEXT', 'BLOB', 'MEDIUMBLOB', 'BIGBLOB', 'LONGBLOB' ) ) )
|
||||
{
|
||||
if( $columnData['type'] == 'BIT' )
|
||||
{
|
||||
$defaultValue = "{$columnData['default']}";
|
||||
}
|
||||
else
|
||||
{
|
||||
$defaultValue = in_array( $columnData['type'], array( 'TINYINT', 'SMALLINT', 'MEDIUMINT', 'INT', 'INTEGER', 'BIGINT', 'REAL', 'DOUBLE', 'FLOAT', 'DECIMAL', 'NUMERIC' ) ) ? floatval( $columnData['default'] ) : ( ! in_array( $columnData['default'], array( 'CURRENT_TIMESTAMP', 'BIT' ) ) ? '\'' . $db->escape_string( $columnData['default'] ) . '\'' : $columnData['default'] );
|
||||
}
|
||||
}
|
||||
|
||||
$changesToMake[] = array( 'table' => $tableName, 'query' => "UPDATE `{$db->prefix}{$db->escape_string( $tableName )}` SET `{$db->escape_string( $columnName )}`={$defaultValue} WHERE `{$db->escape_string( $columnName )}` IS NULL;" );
|
||||
}
|
||||
|
||||
$tableChanges[] = "CHANGE COLUMN `{$db->escape_string( $columnName )}` {$db->compileColumnDefinition( $columnData )}";
|
||||
}
|
||||
}
|
||||
@@ -2427,6 +2630,34 @@ EOF;
|
||||
}
|
||||
}
|
||||
|
||||
/* And loop any install routine for columns added to other tables */
|
||||
if ( file_exists( \IPS\ROOT_PATH . "/applications/{$this->directory}/setup/install/queries.json" ) )
|
||||
{
|
||||
foreach( json_decode( file_get_contents( \IPS\ROOT_PATH . "/applications/{$this->directory}/setup/install/queries.json" ), TRUE ) as $query )
|
||||
{
|
||||
switch ( $query['method'] )
|
||||
{
|
||||
/* Add column */
|
||||
case 'addColumn':
|
||||
$localDefinition = \IPS\Db::i()->getTableDefinition( $query['params'][0] );
|
||||
if ( !isset( $localDefinition['columns'][ $query['params'][1]['name'] ] ) )
|
||||
{
|
||||
$changesToMake[] = array( 'table' => $query['params'][0], 'query' => "ALTER TABLE `{$db->prefix}{$query['params'][0]}` ADD COLUMN {$db->compileColumnDefinition( $query['params'][1] )}" );
|
||||
}
|
||||
else
|
||||
{
|
||||
$correctDefinition = $db->compileColumnDefinition( $query['params'][1] );
|
||||
if ( $correctDefinition != $db->compileColumnDefinition( $localDefinition['columns'][ $query['params'][1]['name'] ] ) )
|
||||
{
|
||||
$tableChanges[] = array( 'table' => $query['params'][0], 'query' => "ALTER TABLE `{$db->prefix}{$query['params'][0]}` CHANGE COLUMN `{$db->escape_string( $columnName )}` {$db->compileColumnDefinition( $columnData )}" );
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Return */
|
||||
return $changesToMake;
|
||||
}
|
||||
|
||||
@@ -3220,7 +3451,10 @@ EOF;
|
||||
{
|
||||
$templatesToRecompile[ $class ] = $class;
|
||||
}
|
||||
|
||||
|
||||
/* Delete menu items */
|
||||
\IPS\core\FrontNavigation::deleteByApplication( $this );
|
||||
|
||||
/* Delete data from shared tables */
|
||||
\IPS\Content\Search\Index::i()->removeApplicationContent( $this );
|
||||
\IPS\DB::i()->delete( 'core_permission_index', array( 'app=? AND perm_type=? AND perm_type_id IN(?)', 'core', 'module', \IPS\Db::i()->select( 'sys_module_id', 'core_modules', array( 'sys_module_application=?', $this->directory ) ) ) );
|
||||
@@ -3304,7 +3538,19 @@ EOF;
|
||||
\IPS\Theme::removeTemplates( $this->directory, NULL, NULL, NULL, TRUE );
|
||||
\IPS\Theme::removeCss( $this->directory, NULL, NULL, NULL, TRUE );
|
||||
\IPS\Theme::removeResources( $this->directory, NULL, NULL, NULL, TRUE );
|
||||
|
||||
|
||||
/* Delete theme settings */
|
||||
$valueIds = iterator_to_array( \IPS\Db::i()->select( 'sc_id', 'core_theme_settings_fields', array( array( 'sc_app=?', $this->directory ) ) ) );
|
||||
|
||||
\IPS\Db::i()->delete( 'core_theme_settings_fields', array( 'sc_app=?', $this->directory ) );
|
||||
|
||||
if ( count( $valueIds ) )
|
||||
{
|
||||
\IPS\Db::i()->delete( 'core_theme_settings_values', array( 'sv_id IN(?)', implode( ',', $valueIds ) ) );
|
||||
}
|
||||
|
||||
unset( \IPS\Data\Store::i()->themes );
|
||||
|
||||
/* Delete any stored files */
|
||||
foreach( $this->extensions( 'core', 'FileStorage', TRUE ) as $extension )
|
||||
{
|
||||
@@ -3344,6 +3590,24 @@ EOF;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Revert other database changes performed by installation */
|
||||
if( file_exists( \IPS\ROOT_PATH . "/applications/{$this->directory}/setup/install/queries.json" ) )
|
||||
{
|
||||
$schema = json_decode( file_get_contents( \IPS\ROOT_PATH . "/applications/{$this->directory}/setup/install/queries.json" ), TRUE );
|
||||
|
||||
ksort($schema);
|
||||
|
||||
foreach( $schema as $instruction )
|
||||
{
|
||||
switch ( $instruction['method'] )
|
||||
{
|
||||
case 'addColumn':
|
||||
\IPS\Db::i()->dropColumn( $instruction['params'][0], $instruction['params'][1]['name'] );
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* delete widgets */
|
||||
\IPS\Db::i()->delete( 'core_widgets', array( 'app = ?', $this->directory ) );
|
||||
@@ -3433,4 +3697,17 @@ EOF;
|
||||
|
||||
return $versions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Can view page even when user is a guest when guests cannot access the site
|
||||
*
|
||||
* @param \IPS\Application\Module $module The module
|
||||
* @param string $controller The controller
|
||||
* @param string|NULL $do To "do" parameter
|
||||
* @return bool
|
||||
*/
|
||||
public function allowGuestAccess( \IPS\Application\Module $module, $controller, $do )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
@@ -27,11 +27,12 @@ class _BuilderIterator extends \RecursiveIteratorIterator
|
||||
* @brief The application
|
||||
*/
|
||||
protected $application;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @return void
|
||||
* @param \IPS\Application $application
|
||||
*/
|
||||
public function __construct( \IPS\Application $application )
|
||||
{
|
||||
|
||||
@@ -231,7 +231,7 @@ class _Module extends \IPS\Node\Model implements \IPS\Node\Permissions
|
||||
$buttons['permissions'] = array(
|
||||
'icon' => 'lock',
|
||||
'title' => 'permissions',
|
||||
'link' => \IPS\Http\Url::internal( "{$url}&do=permissions&id={$this->_id}" ) . ( $subnode ? '&subnode=1' : '' ),
|
||||
'link' => "{$url}&do=permissions&id={$this->_id}" . ( $subnode ? '&subnode=1' : '' ),
|
||||
'data' => array( 'ipsDialog' => '', 'ipsDialog-title' => \IPS\Member::loggedIn()->language()->addToStack('permissions') )
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Archive Class
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - SVN_YYYY Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Social Suite
|
||||
* @since 28 Jul 2015
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Archive Class
|
||||
*/
|
||||
abstract class _Archive
|
||||
{
|
||||
/**
|
||||
* @brief The root folder name which should be ignored (with trailing slash)
|
||||
*/
|
||||
protected $containerName = '';
|
||||
|
||||
/**
|
||||
* Create object from local file
|
||||
*
|
||||
* @param string $path Path to archive file
|
||||
* @param string $containerName The root folder name which should be ignored (with trailing slash)
|
||||
* @return \IPS\Archive
|
||||
*/
|
||||
public static function fromLocalFile( $path, $containerName = '' )
|
||||
{
|
||||
return new static;
|
||||
}
|
||||
|
||||
/**
|
||||
* Number of files
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
abstract public function numberOfFiles();
|
||||
|
||||
/**
|
||||
* Get file name
|
||||
*
|
||||
* @param int $i File number
|
||||
* @return string
|
||||
* @throws \OutOfRangeException
|
||||
*/
|
||||
abstract public function getFileName( $i );
|
||||
|
||||
/**
|
||||
* Get file contents
|
||||
*
|
||||
* @param int $i File number
|
||||
* @return string
|
||||
* @throws \OutOfRangeException
|
||||
*/
|
||||
abstract public function getFileContents( $i );
|
||||
|
||||
/**
|
||||
* Extract
|
||||
*
|
||||
* @param string $destination Destination directory
|
||||
* @param int|NULL $limit Number of files to extract
|
||||
* @param int $offset Offset
|
||||
* @retrun bool If true, all files were extracted, if false, there is more to extract
|
||||
*/
|
||||
public function extract( $destination, $limit=NULL, $offset=0 )
|
||||
{
|
||||
$done = 0;
|
||||
while ( $limit === NULL or $limit > $done ) // OutOfRangeException will break if $limit is NULL
|
||||
{
|
||||
try
|
||||
{
|
||||
$path = $this->getFileName( $offset + $done );
|
||||
if ( $path and mb_substr( $path, -1 ) !== '/' )
|
||||
{
|
||||
/* Create a directory if needed */
|
||||
$dir = dirname( $path );
|
||||
if ( !is_dir( $dir ) )
|
||||
{
|
||||
@mkdir( $dir, \IPS\FOLDER_PERMISSION_NO_WRITE );
|
||||
}
|
||||
|
||||
/* Write contents */
|
||||
$contents = $this->getFileContents( $offset + $done );
|
||||
$fh = @\fopen( $destination . '/' . $path, 'w+' );
|
||||
if ( $fh === FALSE )
|
||||
{
|
||||
throw new \IPS\Archive\Exception( '', \IPS\Archive\Exception::COULD_NOT_WRITE );
|
||||
}
|
||||
if ( @\fwrite( $fh, $contents ) === FALSE )
|
||||
{
|
||||
throw new \IPS\Archive\Exception( '', \IPS\Archive\Exception::COULD_NOT_WRITE );
|
||||
}
|
||||
@\fclose( $fh );
|
||||
}
|
||||
|
||||
$done++;
|
||||
}
|
||||
catch ( \OutOfRangeException $e )
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Archive Exception Class
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - SVN_YYYY Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Social Suite
|
||||
* @since 28 Jul 2015
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS\Archive;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Login Exception Class
|
||||
*/
|
||||
class _Exception extends \RuntimeException
|
||||
{
|
||||
const COULD_NOT_OPEN = 1;
|
||||
const COULD_NOT_WRITE = 2;
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief PclZip Zip Class
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - SVN_YYYY Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Social Suite
|
||||
* @since 28 Jul 2015
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS\Archive\Zip;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
require_once( \IPS\ROOT_PATH . '/system/3rd_party/pclzip/pclzip.lib.php' );
|
||||
|
||||
/**
|
||||
* @brief PclZip Zip Class
|
||||
*/
|
||||
class _PclZip extends \IPS\Archive\Zip
|
||||
{
|
||||
/**
|
||||
* @brief PclZip Object
|
||||
*/
|
||||
protected $zipArchive;
|
||||
|
||||
/**
|
||||
* @brief Files
|
||||
*/
|
||||
protected $files;
|
||||
|
||||
/**
|
||||
* Create object from local file
|
||||
*
|
||||
* @param string $path Path to archive file
|
||||
* @return \IPS\Archive
|
||||
*/
|
||||
public static function _fromLocalFile( $path )
|
||||
{
|
||||
$object = new static;
|
||||
$object->zipArchive = new \PclZip( $path );
|
||||
return $object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Number of files
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function numberOfFiles()
|
||||
{
|
||||
$properties = $this->zipArchive->properties();
|
||||
return $properties['nb'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get file name
|
||||
*
|
||||
* @param int $i File number
|
||||
* @return string
|
||||
* @throws \RuntimeException
|
||||
* @throws \OutOfRangeException
|
||||
*/
|
||||
public function getFileName( $i )
|
||||
{
|
||||
if ( $this->files === NULL )
|
||||
{
|
||||
$this->files = $this->zipArchive->listContent();
|
||||
}
|
||||
|
||||
/* pclZip returns 0 on an unrecoverable failure */
|
||||
if ( $this->files === 0 )
|
||||
{
|
||||
throw new \RuntimeException;
|
||||
}
|
||||
|
||||
if ( isset( $this->files[ $i ] ) )
|
||||
{
|
||||
return mb_substr( $this->files[ $i ]['filename'], mb_strlen( $this->containerName ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new \OutOfRangeException;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get file contents
|
||||
*
|
||||
* @param int $i File number
|
||||
* @return string
|
||||
* @throws \OutOfRangeException
|
||||
*/
|
||||
public function getFileContents( $i )
|
||||
{
|
||||
$content = $this->zipArchive->extractByIndex( $i, \PCLZIP_OPT_EXTRACT_AS_STRING );
|
||||
return $content[0]['content'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Zip Archive Class
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - SVN_YYYY Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Social Suite
|
||||
* @since 28 Jul 2015
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS\Archive;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Zip Archive Class
|
||||
*/
|
||||
abstract class _Zip extends \IPS\Archive
|
||||
{
|
||||
/**
|
||||
* Create object from local file
|
||||
*
|
||||
* @param string $path Path to archive file
|
||||
* @param string $containerName The root folder name which should be ignored (with trailing slash)
|
||||
* @return \IPS\Archive
|
||||
*/
|
||||
public static function fromLocalFile( $path, $containerName = '' )
|
||||
{
|
||||
if ( extension_loaded( 'Zip' ) )
|
||||
{
|
||||
$object = Zip\ZipArchive::_fromLocalFile( $path );
|
||||
}
|
||||
else
|
||||
{
|
||||
$object = Zip\PclZip::_fromLocalFile( $path );
|
||||
}
|
||||
|
||||
$object->containerName = $containerName;
|
||||
return $object;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief ZipArchive Zip Class
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - SVN_YYYY Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Social Suite
|
||||
* @since 28 Jul 2015
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS\Archive\Zip;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief ZipArchive Zip Class
|
||||
*/
|
||||
class _ZipArchive extends \IPS\Archive\Zip
|
||||
{
|
||||
/**
|
||||
* @brief ZipArchive Object
|
||||
*/
|
||||
protected $zipArchive;
|
||||
|
||||
/**
|
||||
* Create object from local file
|
||||
*
|
||||
* @param string $path Path to archive file
|
||||
* @return \IPS\Archive
|
||||
*/
|
||||
public static function _fromLocalFile( $path )
|
||||
{
|
||||
$object = new static;
|
||||
$object->zipArchive = new \ZipArchive;
|
||||
$open = $object->zipArchive->open( $path );
|
||||
if ( $open !== TRUE )
|
||||
{
|
||||
throw new \IPS\Archive\Exception( $open, \IPS\Archive\Exception::COULD_NOT_OPEN );
|
||||
}
|
||||
return $object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Number of files
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function numberOfFiles()
|
||||
{
|
||||
return $this->zipArchive->numFiles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get file name
|
||||
*
|
||||
* @param int $i File number
|
||||
* @return string
|
||||
* @throws \OutOfRangeException
|
||||
*/
|
||||
public function getFileName( $i )
|
||||
{
|
||||
$info = $this->zipArchive->statIndex( $i );
|
||||
if ( $info === FALSE )
|
||||
{
|
||||
throw new \OutOfRangeException;
|
||||
}
|
||||
|
||||
return mb_substr( $info['name'], mb_strlen( $this->containerName ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get file contents
|
||||
*
|
||||
* @param int $i File number
|
||||
* @return string
|
||||
* @throws \OutOfRangeException
|
||||
*/
|
||||
public function getFileContents( $i )
|
||||
{
|
||||
return $this->zipArchive->getFromIndex( $i );
|
||||
}
|
||||
}
|
||||
+40
-53
@@ -317,6 +317,8 @@ abstract class _Comment extends \IPS\Content
|
||||
}
|
||||
else
|
||||
{
|
||||
$item->save();
|
||||
|
||||
if ( $item->containerWrapper() AND !$item->approvedButHidden() AND $item->container()->_unapprovedComments !== NULL )
|
||||
{
|
||||
$item->container()->_unapprovedComments = $item->container()->_unapprovedComments + 1;
|
||||
@@ -503,7 +505,7 @@ abstract class _Comment extends \IPS\Content
|
||||
}
|
||||
catch ( \BadMethodCallException $e ) { }
|
||||
|
||||
return ( !$this->isFirst() and ( static::modPermission( 'hide', $member, $container ) or ( $member->member_id == $this->author()->member_id and $member->group['gbw_soft_delete_own'] ) ) );
|
||||
return ( !$this->isFirst() and ( static::modPermission( 'hide', $member, $container ) or ( $member->member_id and $member->member_id == $this->author()->member_id and $member->group['gbw_soft_delete_own'] ) ) );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -665,7 +667,7 @@ abstract class _Comment extends \IPS\Content
|
||||
*/
|
||||
public function editLine()
|
||||
{
|
||||
if ( $this instanceof \IPS\Content\EditHistory and $this->mapped('edit_time') and ( $this->mapped('edit_show') or \IPS\Member::loggedIn()->modPermission('can_view_editlog') ) )
|
||||
if ( $this instanceof \IPS\Content\EditHistory and $this->mapped('edit_time') and ( $this->mapped('edit_show') or \IPS\Member::loggedIn()->modPermission('can_view_editlog') ) and \IPS\Settings::i()->edit_log )
|
||||
{
|
||||
return \IPS\Theme::i()->getTemplate( 'global', 'core' )->commentEditLine( $this, ( isset( static::$databaseColumnMap['edit_reason'] ) and $this->mapped('edit_reason') ) );
|
||||
}
|
||||
@@ -675,7 +677,7 @@ abstract class _Comment extends \IPS\Content
|
||||
/**
|
||||
* Get edit history
|
||||
*
|
||||
* @param bool $staff Set true for moderators who have permission to view the full log which will show edits not made by the autor and private edits
|
||||
* @param bool $staff Set true for moderators who have permission to view the full log which will show edits not made by the author and private edits
|
||||
* @return \IPS\Db\Select
|
||||
*/
|
||||
public function editHistory( $staff=FALSE )
|
||||
@@ -699,17 +701,7 @@ abstract class _Comment extends \IPS\Content
|
||||
$template = static::$commentTemplate[1];
|
||||
return call_user_func_array( array( \IPS\Theme::i(), 'getTemplate' ), static::$commentTemplate[0] )->$template( $this->item(), $this );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get HTML for search result display
|
||||
*
|
||||
* @return callable
|
||||
*/
|
||||
public function searchResultHtml()
|
||||
{
|
||||
return \IPS\Theme::i()->getTemplate( 'search', 'core', 'front' )->contentComment( $this );
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Users to receieve immediate notifications
|
||||
*
|
||||
@@ -728,7 +720,7 @@ abstract class _Comment extends \IPS\Content
|
||||
$memberFollowers
|
||||
);
|
||||
|
||||
return \IPS\Db::i()->union( $unions, 'follow_added', $limit, 'follow_member_id', FALSE, \IPS\Db::SELECT_SQL_CALC_FOUND_ROWS );
|
||||
return \IPS\Db::i()->union( $unions, 'follow_added', $limit, 'follow_member_id, follow_id, follow_app, follow_area, follow_rel_id, follow_added, follow_is_anon, follow_notify_do, follow_notify_meta, follow_notify_freq, follow_notify_sent, follow_visible', FALSE, \IPS\Db::SELECT_SQL_CALC_FOUND_ROWS );
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -756,6 +748,15 @@ abstract class _Comment extends \IPS\Content
|
||||
{
|
||||
$item = $this->item();
|
||||
|
||||
/* Remove any notifications */
|
||||
$idColumn = static::$databaseColumnId;
|
||||
\IPS\Db::i()->delete( 'core_notifications', array( 'item_sub_class=? AND item_sub_id=?', (string) get_called_class(), (int) $this->$idColumn ) );
|
||||
|
||||
$item->resyncCommentCounts();
|
||||
$item->resyncLastComment();
|
||||
$item->save();
|
||||
|
||||
/* We have to do this *after* updating the last comment data for the item, because that uses the cached data from the item (i.e. topic) */
|
||||
try
|
||||
{
|
||||
if ( $item->container()->_comments !== NULL )
|
||||
@@ -765,14 +766,6 @@ abstract class _Comment extends \IPS\Content
|
||||
$item->container()->save();
|
||||
}
|
||||
} catch ( \BadMethodCallException $e ) {}
|
||||
|
||||
$item->resyncCommentCounts();
|
||||
|
||||
/* Remove any notifications */
|
||||
$idColumn = static::$databaseColumnId;
|
||||
\IPS\Db::i()->delete( 'core_notifications', array( 'item_sub_class=? AND item_sub_id=?', (string) get_called_class(), (int) $this->$idColumn ) );
|
||||
|
||||
$item->resyncLastComment();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -810,7 +803,12 @@ abstract class _Comment extends \IPS\Content
|
||||
catch( \BadMethodCallException $e ) { }
|
||||
|
||||
}
|
||||
|
||||
$item->resyncCommentCounts();
|
||||
$item->resyncLastComment();
|
||||
$item->save();
|
||||
|
||||
/* We have to do this *after* updating the last comment data for the item, because that uses the cached data from the item (i.e. topic) */
|
||||
try
|
||||
{
|
||||
if ( $item->container()->_comments !== NULL )
|
||||
@@ -820,9 +818,6 @@ abstract class _Comment extends \IPS\Content
|
||||
$item->container()->save();
|
||||
}
|
||||
} catch ( \BadMethodCallException $e ) {}
|
||||
|
||||
$item->resyncCommentCounts();
|
||||
$item->resyncLastComment();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -969,6 +964,7 @@ abstract class _Comment extends \IPS\Content
|
||||
{
|
||||
$this->item()->container()->_reviews = ( $this->item()->container()->_reviews - 1 );
|
||||
$this->item()->resyncLastReview();
|
||||
$this->item()->save();
|
||||
$this->item()->container()->setLastReview();
|
||||
}
|
||||
if ( $this->item()->container()->_unapprovedReviews !== NULL )
|
||||
@@ -987,6 +983,7 @@ abstract class _Comment extends \IPS\Content
|
||||
}
|
||||
|
||||
$this->item()->resyncLastComment();
|
||||
$this->item()->save();
|
||||
$this->item()->container()->setLastComment();
|
||||
}
|
||||
if ( $this->item()->container()->_unapprovedComments !== NULL and $this->hidden() === 1 )
|
||||
@@ -1031,6 +1028,7 @@ abstract class _Comment extends \IPS\Content
|
||||
/* Last comment */
|
||||
$this->item()->resyncLastComment();
|
||||
$this->item()->resyncLastReview();
|
||||
$this->item()->save();
|
||||
if ( $container = $this->item()->containerWrapper() )
|
||||
{
|
||||
$container->setLastComment();
|
||||
@@ -1171,6 +1169,15 @@ abstract class _Comment extends \IPS\Content
|
||||
}
|
||||
}
|
||||
|
||||
if ( $joinContainer )
|
||||
{
|
||||
$containerClass = $itemClass::$containerNodeClass;
|
||||
$joins[] = array(
|
||||
'from' => $containerClass::$databaseTable,
|
||||
'where' => array_merge( array( array( $itemClass::$databaseTable . '.' . $itemClass::$databasePrefix . $itemClass::$databaseColumnMap['container'] . '=' . $containerClass::$databaseTable . '.' . $containerClass::$databasePrefix . $containerClass::$databaseColumnId ) ), $containerWhere )
|
||||
);
|
||||
}
|
||||
|
||||
/* Build the select clause */
|
||||
if( $countOnly )
|
||||
{
|
||||
@@ -1214,14 +1221,7 @@ abstract class _Comment extends \IPS\Content
|
||||
$select = \IPS\Db::i()->select( $selectClause, static::$databaseTable, $where, $order, $limit, NULL, NULL, $queryFlags )
|
||||
->join( $itemClass::$databaseTable, array_merge( array( array( static::$databaseTable . "." . static::$databasePrefix . static::$databaseColumnMap['item'] . "=" . $itemClass::$databaseTable . "." . $itemClass::$databasePrefix . $itemClass::$databaseColumnId ) ), $itemWhere ), 'STRAIGHT_JOIN' );
|
||||
}
|
||||
|
||||
|
||||
if ( $joinContainer )
|
||||
{
|
||||
$containerClass = $itemClass::$containerNodeClass;
|
||||
$select->join( $containerClass::$databaseTable, array_merge( array( array( $itemClass::$databaseTable . '.' . $itemClass::$databasePrefix . $itemClass::$databaseColumnMap['container'] . '=' . $containerClass::$databaseTable . '.' . $containerClass::$databasePrefix . $containerClass::$databaseColumnId ) ), $containerWhere ) );
|
||||
}
|
||||
|
||||
|
||||
if ( count( $joins ) )
|
||||
{
|
||||
foreach( $joins as $join )
|
||||
@@ -1277,7 +1277,7 @@ abstract class _Comment extends \IPS\Content
|
||||
*/
|
||||
public function canShare()
|
||||
{
|
||||
return ( \IPS\Member::loggedIn()->member_id and in_array( 'IPS\Content\Shareable', class_implements( get_called_class() ) ) );
|
||||
return ( $this->canView( \IPS\Member::load( 0 ) ) and in_array( 'IPS\Content\Shareable', class_implements( get_called_class() ) ) );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1287,27 +1287,14 @@ abstract class _Comment extends \IPS\Content
|
||||
*/
|
||||
public function sharelinks()
|
||||
{
|
||||
$item = $this->item();
|
||||
if( !count( $this->sharelinks ) )
|
||||
{
|
||||
/* If the item is shareable, we need to initiate the share services */
|
||||
if ( $this instanceof Shareable )
|
||||
if ( $this instanceof Shareable and $this->canShare() )
|
||||
{
|
||||
$shareService = 'IPS\core\ShareLinks\Service';
|
||||
|
||||
foreach( $shareService::shareLinks() as $node )
|
||||
{
|
||||
if( $node->enabled and ( $node->groups === "*" or \IPS\Member::loggedIn()->inGroup( explode( ',', $node->groups ) ) ) )
|
||||
{
|
||||
if( file_exists( \IPS\ROOT_PATH . '/system/Content/ShareServices/' . ucwords( $node->key ) . '.php' ) )
|
||||
{
|
||||
$className = "IPS\\Content\\ShareServices\\" . ucwords( $node->key );
|
||||
$idColumn = static::$databaseColumnId;
|
||||
$url = $this->url( 'find' )->setQueryString( 'comment', $this->$idColumn );
|
||||
$this->sharelinks[ $node->key ] = new $className( $url, $item->mapped('title') );
|
||||
}
|
||||
}
|
||||
}
|
||||
$idColumn = static::$databaseColumnId;
|
||||
$shareUrl = $this->url( 'find' )->setQueryString( 'comment', $this->$idColumn );
|
||||
|
||||
$this->sharelinks = \IPS\core\ShareLinks\Service::getAllServices( $shareUrl, $this->item()->mapped('title') );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+206
-67
@@ -248,10 +248,22 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
*/
|
||||
public function truncated( $oneLine=FALSE )
|
||||
{
|
||||
/* Specifically remove quotes, and any scripts (which someone with HTML posting allowed may have legitimately enabled */
|
||||
$text = \IPS\Text\Parser::removeElements( $this->content(), array( 'blockquote', 'script' ) );
|
||||
|
||||
/* Convert headers and paragraphs into line breaks or just spaces */
|
||||
$text = str_replace( array( '</p>', '</h1>', '</h2>', '</h3>', '</h4>', '</h5>', '</h6>' ), ( $oneLine ? ' ' : '<br>' ), $text );
|
||||
$text = strip_tags( $text, '<br>' );
|
||||
|
||||
/* Add a space at the end of list items to prevent two list items from running into each other */
|
||||
$text = str_replace( '</li>', ' </li>', $text );
|
||||
|
||||
/* Remove all HTML apart from <br>s*/
|
||||
$text = strip_tags( $text, '<br>' );
|
||||
|
||||
/* Remove any <br>s from the start so there isn't just blank space at the top, but maintaining <br>s elsewhere */
|
||||
$text = preg_replace( '/^[(\s)(<br>)\x{00a0}]*/u', '', $text );
|
||||
|
||||
/* Return */
|
||||
return $text;
|
||||
}
|
||||
|
||||
@@ -419,7 +431,7 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
isset( $containerClass::$modPerm )
|
||||
and
|
||||
(
|
||||
$member->modPermission( $containerClass::$modPerm ) === 0
|
||||
$member->modPermission( $containerClass::$modPerm ) === -1
|
||||
or
|
||||
(
|
||||
is_array( $member->modPermission( $containerClass::$modPerm ) )
|
||||
@@ -787,13 +799,15 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
$index->save();
|
||||
|
||||
/* Create a report */
|
||||
\IPS\Db::i()->insert( 'core_rc_reports', array(
|
||||
$reportInsert = array(
|
||||
'rid' => $index->id,
|
||||
'report' => $reportContent,
|
||||
'report_by' => (int) \IPS\Member::loggedIn()->member_id,
|
||||
'date_reported' => time(),
|
||||
'ip_address' => \IPS\Request::i()->ipAddress()
|
||||
) );
|
||||
);
|
||||
|
||||
\IPS\Db::i()->insert( 'core_rc_reports', $reportInsert );
|
||||
|
||||
|
||||
/* Rebuild */
|
||||
@@ -822,7 +836,7 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
$moderators[ $mod['type'] ][] = $mod['id'];
|
||||
}
|
||||
}
|
||||
$notification = new \IPS\Notification( \IPS\Application::load('core'), 'report_center', $index, array( $index, $reportContent, $this ) );
|
||||
$notification = new \IPS\Notification( \IPS\Application::load('core'), 'report_center', $index, array( $index, $reportInsert, $this ) );
|
||||
foreach ( \IPS\Db::i()->select( '*', 'core_members', ( count( $moderators['m'] ) ? \IPS\Db::i()->in( 'member_id', $moderators['m'] ) . ' OR ' : '' ) . \IPS\Db::i()->in( 'member_group_id', $moderators['g'] ) . ' OR ' . \IPS\Db::i()->findInSet( 'mgroup_others', $moderators['g'] ) ) as $member )
|
||||
{
|
||||
$notification->recipients->attach( \IPS\Member::constructFromData( $member ) );
|
||||
@@ -871,13 +885,93 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
/**
|
||||
* Get HTML for search result display
|
||||
*
|
||||
* @return callable
|
||||
* @param array $indexData Data from the search index
|
||||
* @param array $authorData Basic data about the author. Only includes columns returned by \IPS\Member::columnsForPhoto()
|
||||
* @param array $itemData Basic data about the item. Only includes columns returned by item::basicDataColumns()
|
||||
* @param array|NULL $containerData Basic data about the author. Only includes columns returned by container::basicDataColumns()
|
||||
* @param array $reputationData Array of people who have given reputation and the reputation they gave
|
||||
* @param int|NULL $reviewRating If this is a review, the rating
|
||||
* @param bool $iPostedIn If the user has posted in the item
|
||||
* @param string $view 'expanded' or 'condensed'
|
||||
* @param bool $asItem Displaying results as items?
|
||||
* @return string
|
||||
*/
|
||||
public function activityStreamHtml()
|
||||
public static function searchResult( array $indexData, array $authorData, array $itemData, array $containerData = NULL, array $reputationData, $reviewRating, $iPostedIn, $view, $asItem )
|
||||
{
|
||||
return \IPS\Theme::i()->getTemplate( 'system', 'core', 'front' )->activityStreamResult( $this );
|
||||
/* Item details */
|
||||
$itemClass = $indexData['index_class'];
|
||||
if ( in_array( 'IPS\Content\Comment', class_parents( get_called_class() ) ) )
|
||||
{
|
||||
$itemClass = static::$itemClass;
|
||||
$unread = $itemClass::unreadFromData( NULL, $indexData['index_date_updated'], $indexData['index_date_created'], $indexData['index_item_id'], $indexData['index_container_id'], FALSE );
|
||||
}
|
||||
else
|
||||
{
|
||||
$unread = static::unreadFromData( NULL, $indexData['index_date_updated'], $indexData['index_date_created'], $indexData['index_item_id'], $indexData['index_container_id'], FALSE );
|
||||
}
|
||||
$itemUrl = $itemClass::urlFromIndexData( $indexData, $itemData );
|
||||
|
||||
/* Object URL */
|
||||
$indefiniteArticle = static::_indefiniteArticle( $containerData );
|
||||
if ( in_array( 'IPS\Content\Comment', class_parents( get_called_class() ) ) )
|
||||
{
|
||||
if ( in_array( 'IPS\Content\Review', class_parents( get_called_class() ) ) )
|
||||
{
|
||||
$objectUrl = $itemUrl->setQueryString( array( 'do' => 'findReview', 'review' => $indexData['index_object_id'] ) );
|
||||
$showRepUrl = $itemUrl->setQueryString( array( 'do' => 'showRepReview', 'review' => $indexData['index_object_id'] ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
$objectUrl = $itemUrl->setQueryString( array( 'do' => 'findComment', 'comment' => $indexData['index_object_id'] ) );
|
||||
$showRepUrl = $itemUrl->setQueryString( array( 'do' => 'showRepComment', 'comment' => $indexData['index_object_id'] ) );
|
||||
}
|
||||
|
||||
$indefiniteArticle = $itemClass::_indefiniteArticle( $containerData );
|
||||
}
|
||||
else
|
||||
{
|
||||
$objectUrl = $itemUrl;
|
||||
$showRepUrl = $itemUrl->setQueryString( 'do', 'showRep' );
|
||||
}
|
||||
$articles = array( 'indefinite' => $indefiniteArticle, 'definite' => \IPS\Member::loggedIn()->language()->addToStack( $itemClass::$title, FALSE, array( 'strtolower' => TRUE ) ) );
|
||||
|
||||
/* Container details */
|
||||
$containerUrl = NULL;
|
||||
$containerTitle = NULL;
|
||||
if ( isset( $itemClass::$containerNodeClass ) )
|
||||
{
|
||||
$containerClass = $itemClass::$containerNodeClass;
|
||||
$containerTitle = \IPS\Member::loggedIn()->language()->addToStack( $containerClass::$titleLangPrefix . $indexData['index_container_id'] );
|
||||
$containerUrl = $containerClass::urlFromIndexData( $indexData, $itemData, $containerData );
|
||||
}
|
||||
|
||||
/* Reputation */
|
||||
$repCount = array_sum( $reputationData );
|
||||
|
||||
/* Snippet */
|
||||
$snippet = static::searchResultSnippet( $indexData, $authorData, $itemData, $containerData, $reputationData, $reviewRating, $view );
|
||||
|
||||
/* Return */
|
||||
return \IPS\Theme::i()->getTemplate( 'system', 'core', 'front' )->searchResult( $indexData, $articles, $authorData, $itemData, $unread, $asItem ? $itemUrl : $objectUrl, $itemUrl, $containerUrl, $containerTitle, $repCount, $showRepUrl, $snippet, $iPostedIn, $view );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get snippet HTML for search result display
|
||||
*
|
||||
* @param array $indexData Data from the search index
|
||||
* @param array $authorData Basic data about the author. Only includes columns returned by \IPS\Member::columnsForPhoto()
|
||||
* @param array $itemData Basic data about the item. Only includes columns returned by item::basicDataColumns()
|
||||
* @param array|NULL $containerData Basic data about the author. Only includes columns returned by container::basicDataColumns()
|
||||
* @param array $reputationData Array of people who have given reputation and the reputation they gave
|
||||
* @param int|NULL $reviewRating If this is a review, the rating
|
||||
* @param string $view 'expanded' or 'condensed'
|
||||
* @return callable
|
||||
*/
|
||||
public static function searchResultSnippet( array $indexData, array $authorData, array $itemData, array $containerData = NULL, array $reputationData, $reviewRating, $view )
|
||||
{
|
||||
return $view == 'expanded' ? \IPS\Theme::i()->getTemplate( 'system', 'core', 'front' )->searchResultSnippet( $indexData ) : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the filters that are available for selecting table rows
|
||||
*
|
||||
@@ -938,59 +1032,85 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
*/
|
||||
public function canGiveReputation( $type, \IPS\Member $member = NULL )
|
||||
{
|
||||
if ( \IPS\Member::loggedIn()->member_id and \IPS\Settings::i()->reputation_enabled and !in_array( $this->author()->member_group_id, explode( ',', \IPS\Settings::i()->reputation_protected_groups ) ) and $this instanceof \IPS\Content\Reputation )
|
||||
{
|
||||
/* Can we rep ourselves? */
|
||||
$member = $member ?: \IPS\Member::loggedIn();
|
||||
if ( $this->author() == $member and !\IPS\Settings::i()->reputation_can_self_vote )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/* Are we allowed to give reputation at all? - We still need to allow users to undo rep they have given */
|
||||
$limit = ( $type === 1 ) ? $member->group['g_rep_max_positive'] : $member->group['g_rep_max_negative'];
|
||||
if ( $limit == 0 and !$this->repGiven( $member ) )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Protected Groups */
|
||||
if ( in_array( $this->author()->member_group_id, explode( ',', \IPS\Settings::i()->reputation_protected_groups ) ) )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/* Now work it out */
|
||||
switch ( \IPS\Settings::i()->reputation_point_types )
|
||||
{
|
||||
/* If we're using a positive system, we can rep positive only if we haven't repped yet, or negative only if we have */
|
||||
case 'like':
|
||||
case 'positive':
|
||||
return ( $this->repGiven( $member ) === 1 ) ? ( $type === -1 ) : ( $type === 1 );
|
||||
|
||||
/* If we're using a negative system, we can rep negative only if we haven't repped yet, or positive only if we have */
|
||||
case 'negative':
|
||||
return $this->repGiven( $member ) ? ( $type === 1 ) : ( $type === -1 );
|
||||
|
||||
/* If we're using a positive and negative system - it's more complicated... */
|
||||
case 'both':
|
||||
switch ( $this->repGiven( $member ) )
|
||||
{
|
||||
/* If we haven't repped yet, we can do either */
|
||||
case 0:
|
||||
return TRUE;
|
||||
|
||||
/* If we've given positive rep already, we can only do negative now */
|
||||
case 1:
|
||||
return ( $type === -1 );
|
||||
|
||||
/* If we've given negative rep already, we can only do positive now */
|
||||
case -1:
|
||||
return ( $type === 1 );
|
||||
}
|
||||
}
|
||||
return static::_canGiveReputation( $type, $this->author()->member_id, $this->author()->member_group_id, $this->repGiven( $member ), $member );
|
||||
}
|
||||
|
||||
/**
|
||||
* Can give reputation?
|
||||
*
|
||||
* @note This method is also ran to check if a member can "unrep"
|
||||
* @param int $type 1 for positive, -1 for negative
|
||||
* @param int $authorId The author ID
|
||||
* @param int $authorGroup The author group
|
||||
* @param int $repGiven The reputation we have already given (1 = positive, -1 = negative, 0 = no rep given)
|
||||
* @param \IPS\Member|NULL $member The member to check for (NULL for currently logged in member)
|
||||
* @return bool
|
||||
*/
|
||||
public static function _canGiveReputation( $type, $authorId, $authorGroup, $repGiven, \IPS\Member $member = NULL )
|
||||
{
|
||||
/* Reputation needs to be enabled */
|
||||
if ( !\IPS\Settings::i()->reputation_enabled or !in_array( 'IPS\Content\Reputation', class_implements( get_called_class() ) ) )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/* Guests cannot rep */
|
||||
$member = $member ?: \IPS\Member::loggedIn();
|
||||
if ( !$member->member_id )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/* Author needs to not be protected */
|
||||
if ( in_array( $authorGroup, explode( ',', \IPS\Settings::i()->reputation_protected_groups ) ) )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/* Users may not be able to rep themselves */
|
||||
if ( !\IPS\Settings::i()->reputation_can_self_vote and $authorId == $member->member_id )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/* Rep per day */
|
||||
$limit = ( $type === 1 ) ? $member->group['g_rep_max_positive'] : $member->group['g_rep_max_negative'];
|
||||
if ( $limit == 0 and !$repGiven )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/* Have we already given rep? */
|
||||
switch ( \IPS\Settings::i()->reputation_point_types )
|
||||
{
|
||||
/* If we're using a positive system, we can rep positive only if we haven't repped yet, or negative only if we have */
|
||||
case 'like':
|
||||
case 'positive':
|
||||
return ( $repGiven === 1 ) ? ( $type === -1 ) : ( $type === 1 );
|
||||
|
||||
/* If we're using a negative system, we can rep negative only if we haven't repped yet, or positive only if we have */
|
||||
case 'negative':
|
||||
return $repGiven ? ( $type === 1 ) : ( $type === -1 );
|
||||
|
||||
/* If we're using a positive and negative system - it's more complicated... */
|
||||
case 'both':
|
||||
switch ( $repGiven )
|
||||
{
|
||||
/* If we haven't repped yet, we can do either */
|
||||
case 0:
|
||||
return TRUE;
|
||||
|
||||
/* If we've given positive rep already, we can only do negative now */
|
||||
case 1:
|
||||
return ( $type === -1 );
|
||||
|
||||
/* If we've given negative rep already, we can only do positive now */
|
||||
case -1:
|
||||
return ( $type === 1 );
|
||||
}
|
||||
}
|
||||
|
||||
/* Catch all */
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
@@ -1028,13 +1148,15 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
$limit = ( $type === 1 ) ? $member->group['g_rep_max_positive'] : $member->group['g_rep_max_negative'];
|
||||
|
||||
if ( $limit !== 999 )
|
||||
{
|
||||
$count = \IPS\Db::i()->select( 'COUNT(*)', 'core_reputation_index', array( 'member_id=? AND rep_date>?', $member->member_id, \IPS\DateTime::create()->sub( new \DateInterval( 'P1D' ) )->getTimestamp() ) )->first();
|
||||
$count = \IPS\Db::i()->select( 'COUNT(*)', 'core_reputation_index', array( 'member_id=? AND rep_date>? AND rep_rating=?', $member->member_id, \IPS\DateTime::create()->sub( new \DateInterval( 'P1D' ) )->getTimestamp(), $type ) )->first();
|
||||
if ( $count >= $limit )
|
||||
{
|
||||
throw new \DomainException( \IPS\Member::loggedIn()->language()->addToStack('rep_daily_exceeded', FALSE, array( 'sprintf' => array( $limit ) ) ) );
|
||||
throw new \DomainException( \IPS\Member::loggedIn()->language()->addToStack( \IPS\Settings::i()->reputation_point_types == 'like' ? 'rep_daily_exceeded_likes' :'rep_daily_exceeded', FALSE, array( 'sprintf' => array( $limit ) ) ) );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1049,7 +1171,7 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
) );
|
||||
|
||||
/* Send Notification */
|
||||
if ( $type === 1 AND $this->author()->member_id AND $this->author() != \IPS\Member::loggedIn() )
|
||||
if ( $type === 1 AND $this->author()->member_id AND $this->author() != \IPS\Member::loggedIn() AND $this->canView( $this->author() ) )
|
||||
{
|
||||
$notification = new \IPS\Notification( \IPS\Application::load('core'), 'new_likes', $this, array( $this, \IPS\Member::loggedIn() ) );
|
||||
$notification->recipients->attach( $this->author() );
|
||||
@@ -1186,7 +1308,7 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
{
|
||||
/* 3 random names */
|
||||
$idColumn = static::$databaseColumnId;
|
||||
foreach ( \IPS\Db::i()->select( '*', 'core_reputation_index', array( 'app=? AND type=? AND type_id=? AND member_id!=?', static::$application, static::$reputationType, $this->$idColumn, ( \IPS\Member::loggedIn()->member_id ) ?: 0 ), 'RAND()', 3 ) as $rep )
|
||||
foreach ( \IPS\Db::i()->select( '*', 'core_reputation_index', array( 'app=? AND type=? AND type_id=? AND member_id!=? AND rep_rating=?', static::$application, static::$reputationType, $this->$idColumn, ( \IPS\Member::loggedIn()->member_id ) ?: 0, 1 ), 'RAND()', 3 ) as $rep )
|
||||
{
|
||||
$people[] = \IPS\Theme::i()->getTemplate( 'global', 'core' )->userLink( \IPS\Member::load( $rep['member_id'] ) );
|
||||
$reputation--;
|
||||
@@ -1267,7 +1389,7 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
const FOLLOW_PUBLIC = 1;
|
||||
const FOLLOW_ANONYMOUS = 2;
|
||||
|
||||
const NOTIFICATIONS_PER_BATCH = 100;
|
||||
const NOTIFICATIONS_PER_BATCH = 50;
|
||||
|
||||
/**
|
||||
* Send notifications
|
||||
@@ -1548,13 +1670,18 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( isset( $class::$commentClass ) )
|
||||
|
||||
if ( !( $member instanceof \IPS\Member ) )
|
||||
{
|
||||
$member = $member ? \IPS\Member::loggedIn() : NULL;
|
||||
}
|
||||
|
||||
if ( isset( $class::$commentClass ) and $class::supportsComments( $member ) )
|
||||
{
|
||||
$classes[] = $class::$commentClass;
|
||||
}
|
||||
|
||||
if ( isset( $class::$reviewClass ) )
|
||||
if ( isset( $class::$reviewClass ) and $class::supportsReviews( $member ) )
|
||||
{
|
||||
$classes[] = $class::$reviewClass;
|
||||
}
|
||||
@@ -1610,6 +1737,18 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
* @return string
|
||||
*/
|
||||
public function indefiniteArticle( \IPS\Lang $lang = NULL )
|
||||
{
|
||||
$container = ( $this instanceof \IPS\Content\Comment ) ? $this->item()->containerWrapper() : $this->containerWrapper();
|
||||
return static::_indefiniteArticle( $container ? $container->_data : array(), $lang );
|
||||
}
|
||||
|
||||
/**
|
||||
* Indefinite Article
|
||||
*
|
||||
* @param \IPS\Lang|NULL $language The language to use, or NULL for the language of the currently logged in member
|
||||
* @return string
|
||||
*/
|
||||
public static function _indefiniteArticle( array $containerData = NULL, \IPS\Lang $lang = NULL )
|
||||
{
|
||||
$lang = $lang ?: \IPS\Member::loggedIn()->language();
|
||||
return $lang->addToStack( '__indefart_' . static::$title, FALSE );
|
||||
|
||||
@@ -177,6 +177,13 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
*/
|
||||
protected function checkForNewReplies()
|
||||
{
|
||||
/* If auto-polling isn't enabled, kill the polling now */
|
||||
if ( !\IPS\Settings::i()->auto_polling_enabled )
|
||||
{
|
||||
\IPS\Output::i()->json( array( 'error' => 'auto_polling_disabled' ) );
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$class = static::$contentModel;
|
||||
@@ -206,11 +213,20 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
}
|
||||
}
|
||||
|
||||
$newComments = $item->comments( NULL, 0, 'date', 'asc', NULL, NULL, \IPS\DateTime::ts( $lastComment->$commentDateColumn ), array_merge( $where, array( "{$authorColumn} != " . \IPS\Member::loggedIn()->member_id ) ) );
|
||||
/* We will fetch up to 200 comments - anything over this is excessive.
|
||||
@see https://community.invisionpower.com/4bugtrack/active-reports/x-new-replies-doesnt-show-more-than-15-replies-r7192/ */
|
||||
$newComments = $item->comments( 200, 0, 'date', 'asc', NULL, NULL, \IPS\DateTime::ts( $lastComment->$commentDateColumn ), array_merge( $where, array( "{$authorColumn} != " . \IPS\Member::loggedIn()->member_id ) ) );
|
||||
|
||||
if ( \IPS\Request::i()->type === 'count' )
|
||||
{
|
||||
$data = array( 'count' => (int) count( $newComments ), 'totalCount' => $item->mapped('num_comments') );
|
||||
$data = array(
|
||||
'totalNewCount' => (int) count( $newComments ),
|
||||
'count' => (int) count( $newComments ), /* This is here for legacy purposes only */
|
||||
'perPage' => $class::getCommentsPerPage(),
|
||||
'totalCount' => $item->mapped('num_comments'),
|
||||
'title' => $item->mapped('title') ,
|
||||
'spillOverUrl' => $item->url( 'getNewComment' )
|
||||
);
|
||||
|
||||
if( $data['count'] === 1 ){
|
||||
$itemData = reset( $newComments );
|
||||
@@ -254,9 +270,9 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
{
|
||||
$class = static::$contentModel;
|
||||
$item = $class::loadAndCheckPerms( \IPS\Request::i()->id );
|
||||
if ( !$item->canEdit() )
|
||||
if ( !$item->canEdit() and !\IPS\Request::i()->form_submitted ) // We check if the form has been submitted to prevent the user loosing their content
|
||||
{
|
||||
throw new \DomainException;
|
||||
throw new \OutOfRangeException;
|
||||
}
|
||||
|
||||
$form = new \IPS\Helpers\Form( 'form', \IPS\Member::loggedIn()->language()->addToStack( $class::$formLangPrefix . '_save' ) ? $class::$formLangPrefix . '_save' : 'save' );
|
||||
@@ -291,39 +307,46 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
|
||||
if ( $values = $form->values() )
|
||||
{
|
||||
$item->processForm( $values );
|
||||
if ( isset( $item::$databaseColumnMap['updated'] ) )
|
||||
{
|
||||
$column = $item::$databaseColumnMap['updated'];
|
||||
$item->$column = time();
|
||||
}
|
||||
|
||||
if ( isset( $item::$databaseColumnMap['date'] ) and isset( $values[ $item::$formLangPrefix . 'date' ] ) )
|
||||
{
|
||||
$column = $item::$databaseColumnMap['date'];
|
||||
|
||||
if ( $values[ $item::$formLangPrefix . 'date' ] instanceof \IPS\DateTime )
|
||||
{
|
||||
$item->$column = $values[ $item::$formLangPrefix . 'date' ]->getTimestamp();
|
||||
}
|
||||
else
|
||||
if ( $item->canEdit() )
|
||||
{
|
||||
$item->processForm( $values );
|
||||
if ( isset( $item::$databaseColumnMap['updated'] ) )
|
||||
{
|
||||
$column = $item::$databaseColumnMap['updated'];
|
||||
$item->$column = time();
|
||||
}
|
||||
|
||||
if ( isset( $item::$databaseColumnMap['date'] ) and isset( $values[ $item::$formLangPrefix . 'date' ] ) )
|
||||
{
|
||||
$column = $item::$databaseColumnMap['date'];
|
||||
|
||||
if ( $values[ $item::$formLangPrefix . 'date' ] instanceof \IPS\DateTime )
|
||||
{
|
||||
$item->$column = $values[ $item::$formLangPrefix . 'date' ]->getTimestamp();
|
||||
}
|
||||
else
|
||||
{
|
||||
$item->$column = time();
|
||||
}
|
||||
}
|
||||
|
||||
$item->save();
|
||||
$item->processAfterEdit( $values );
|
||||
|
||||
\IPS\Output::i()->redirect( $item->url() );
|
||||
}
|
||||
else
|
||||
{
|
||||
$form->error = \IPS\Member::loggedIn()->language()->addToStack('edit_no_perm_err');
|
||||
}
|
||||
|
||||
$item->save();
|
||||
$item->processAfterEdit( $values );
|
||||
|
||||
\IPS\Output::i()->redirect( $item->url() );
|
||||
}
|
||||
|
||||
$this->_setBreadcrumbAndTitle( $item );
|
||||
\IPS\Output::i()->output = $form;
|
||||
\IPS\Output::i()->output = \IPS\Theme::i()->getTemplate( 'forms', 'core' )->editContentForm( \IPS\Member::loggedIn()->language()->addToStack( 'edit_title', FALSE, array( 'sprintf' => array( \IPS\Member::loggedIn()->language()->addToStack( $item::$title ) ) ) ), $form );
|
||||
}
|
||||
catch ( \Exception $e )
|
||||
{
|
||||
\IPS\Output::i()->error( 'node_error', '2S136/E', 404, '' );
|
||||
\IPS\Output::i()->error( 'edit_no_perm_err', '2S136/E', 404, '' );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -505,6 +528,8 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
}
|
||||
|
||||
$item->move( $values['move_to'], isset( $values['move_keep_link'] ) ? $values['move_keep_link'] : FALSE );
|
||||
\IPS\Session::i()->modLog( 'modlog__action_move', array( $item::$title => TRUE, $item->url()->__toString() => FALSE, $item->mapped('title') ?: ( method_exists( $item, 'item' ) ? $item->item()->mapped('title') : NULL ) => FALSE ), $item );
|
||||
|
||||
\IPS\Output::i()->redirect( $item->url() );
|
||||
}
|
||||
|
||||
@@ -536,11 +561,15 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
}
|
||||
$form = new \IPS\Helpers\Form( 'form', 'merge' );
|
||||
$form->class = 'ipsForm_vertical';
|
||||
$form->add( new \IPS\Helpers\Form\Url( 'merge_with', NULL, TRUE, array(), function ( $val ) use ( $class )
|
||||
$form->add( new \IPS\Helpers\Form\Url( 'merge_with', NULL, TRUE, array(), function ( $val ) use ( $class, $item )
|
||||
{
|
||||
try
|
||||
{
|
||||
$class::loadFromUrl( $val );
|
||||
$toMerge = $class::loadFromUrl( $val );
|
||||
if ( $toMerge == $item )
|
||||
{
|
||||
throw new \DomainException;
|
||||
}
|
||||
}
|
||||
catch ( \Exception $e )
|
||||
{
|
||||
@@ -618,9 +647,11 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
|
||||
if( $values = $form->values() )
|
||||
{
|
||||
$email = \IPS\Email::buildFromContent( $values['email_subject'], $values['email_content'] );
|
||||
$email->from = ( isset( $values['mail_from_email'] ) ) ? $values['mail_from_email'] : \IPS\Member::loggedIn()->email;
|
||||
$email->fromName = ( isset( $values['mail_from_name'] ) ) ? $values['mail_from_name'] : \IPS\Member::loggedIn()->name;
|
||||
$email = \IPS\Email::buildFromContent( $values['email_subject'], $values['email_content'] );
|
||||
$fromName = ( isset( $values['mail_from_name'] ) ) ? $values['mail_from_name'] : \IPS\Member::loggedIn()->name;
|
||||
|
||||
/* Set contact email as Reply-To instead of From, so that it will pass Spam/SPF checks */
|
||||
$email->headers['Reply-To'] = '=?UTF-8?B?' . base64_encode( $fromName ) . "?= <" . ( isset( $values['mail_from_email'] ) ? $values['mail_from_email'] : \IPS\Member::loggedIn()->email ) . '>';
|
||||
|
||||
$email->emailBody['html'] = $email->parseTextForEmail( $email->emailBody['html'] );
|
||||
$email->send( $values['email_email'] );
|
||||
@@ -698,6 +729,10 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
}
|
||||
|
||||
$this->_setBreadcrumbAndTitle( $item );
|
||||
|
||||
/* Even if guests can report something, we don't want the report form indexed in Google */
|
||||
\IPS\Output::i()->metaTags['robots'] = 'noindex';
|
||||
|
||||
\IPS\Output::i()->output = $form->customTemplate( array( call_user_func_array( array( \IPS\Theme::i(), 'getTemplate' ), array( 'forms', 'core' ) ), 'popupTemplate' ) );
|
||||
}
|
||||
catch ( \LogicException $e )
|
||||
@@ -751,7 +786,7 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
{
|
||||
if ( \IPS\Request::i()->mini )
|
||||
{
|
||||
\IPS\Output::i()->sendOutput( \IPS\Theme::i()->getTemplate( 'global', 'core' )->reputationMini( $item ) );
|
||||
\IPS\Output::i()->sendOutput( \IPS\Theme::i()->getTemplate( 'global', 'core' )->reputationMini( $item->reputation(), $item->canGiveReputation( 1 ), $item->canGiveReputation( -1 ), $item->url('showRep'), $item->url('rep') ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -862,6 +897,13 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
if ( array_key_exists( $item->container()->_id, $resetTimes ) )
|
||||
{
|
||||
$comment = $item->comments( 1, NULL, 'date', 'asc', NULL, NULL, \IPS\DateTime::ts( $resetTimes[ $item->container()->_id ] ) );
|
||||
|
||||
if ( $class::$firstCommentRequired and $comment->isFirst() )
|
||||
{
|
||||
/* link https://community.invisionpower.com/4bugtrack/active-reports/go-to-unread-link-also-links-to-the-fist-postcomment-even-for-totally-unread-content-r7864/ */
|
||||
\IPS\Output::i()->redirect( $item->url() );
|
||||
}
|
||||
|
||||
\IPS\Output::i()->redirect( $comment ? $comment->url() : $item->url() );
|
||||
}
|
||||
else
|
||||
@@ -1164,7 +1206,7 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
$form->add( new \IPS\Helpers\Form\Editor( 'comment_value', $comment->$valueField, TRUE, array(
|
||||
'app' => $class::$application,
|
||||
'key' => ucfirst( $class::$module ),
|
||||
'autoSaveKey' => 'editComment-' . $class::$application . '/' . $class::$module . '-' . $comment->id,
|
||||
'autoSaveKey' => 'editComment-' . $class::$application . '/' . $class::$module . '-' . $comment->$idField,
|
||||
'attachIds' => $comment->attachmentIds()
|
||||
) ) );
|
||||
|
||||
@@ -1178,14 +1220,14 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
}
|
||||
if ( \IPS\Member::loggedIn()->group['g_append_edit'] )
|
||||
{
|
||||
$form->add( new \IPS\Helpers\Form\Checkbox( 'comment_log_edit', \IPS\Member::loggedIn()->member_id == $comment->author()->member_id ) );
|
||||
$form->add( new \IPS\Helpers\Form\Checkbox( 'comment_log_edit', FALSE ) );
|
||||
}
|
||||
}
|
||||
|
||||
if ( $values = $form->values() )
|
||||
{
|
||||
/* Log History */
|
||||
if ( $comment instanceof \IPS\Content\EditHistory )
|
||||
if ( $comment instanceof \IPS\Content\EditHistory and \IPS\Settings::i()->edit_log )
|
||||
{
|
||||
$editIsPublic = \IPS\Member::loggedIn()->group['g_append_edit'] ? $values['comment_log_edit'] : TRUE;
|
||||
|
||||
@@ -1239,7 +1281,7 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
$comment->editContents( $values['comment_value'] );
|
||||
|
||||
/* Moderator log */
|
||||
\IPS\Session::i()->modLog( 'modlog__comment_edit', array( $comment->url()->__toString() => FALSE, $item::$title => FALSE, $item->url()->__toString() => FALSE, $item->mapped('title') => FALSE ), $item );
|
||||
\IPS\Session::i()->modLog( 'modlog__comment_edit', array( $comment->url()->__toString() => FALSE, $item::$title => TRUE, $item->url()->__toString() => FALSE, $item->mapped('title') => FALSE ), $item );
|
||||
|
||||
/* Display */
|
||||
if ( \IPS\Request::i()->isAjax() )
|
||||
@@ -1299,7 +1341,7 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
$comment->delete();
|
||||
|
||||
/* Log */
|
||||
\IPS\Session::i()->modLog( 'modlog__comment_delete', array( $item::$title => FALSE, $item->url()->__toString() => FALSE, $item->mapped('title') => FALSE ), $item );
|
||||
\IPS\Session::i()->modLog( 'modlog__comment_delete', array( $item::$title => TRUE, $item->url()->__toString() => FALSE, $item->mapped('title') => FALSE ), $item );
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1783,6 +1825,7 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
}
|
||||
|
||||
$item->resyncCommentCounts();
|
||||
$item->save();
|
||||
|
||||
if ( $object and \IPS\Request::i()->multimod != 'delete' )
|
||||
{
|
||||
|
||||
+389
-221
File diff suppressed because it is too large.
Load diff
@@ -193,16 +193,6 @@ abstract class _Review extends \IPS\Content\Comment
|
||||
|
||||
return $this->_url[ $_key ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get HTML for search result display
|
||||
*
|
||||
* @return callable
|
||||
*/
|
||||
public function searchResultHtml()
|
||||
{
|
||||
return \IPS\Theme::i()->getTemplate( 'search', 'core', 'front' )->contentReview( $this );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get line which says how many users found review helpful
|
||||
@@ -233,11 +223,11 @@ abstract class _Review extends \IPS\Content\Comment
|
||||
$column = $item::$databaseColumnMap['num_reviews'];
|
||||
$item->$column = $item->mapped('num_reviews') - 1;
|
||||
}
|
||||
$item->resyncLastReview();
|
||||
|
||||
|
||||
$ratingField = $item::$databaseColumnMap['rating'];
|
||||
$item->$ratingField = $item->averageReviewRating() ?: 0;
|
||||
|
||||
|
||||
$item->resyncLastReview();
|
||||
$item->save();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Content Filter for Search Queries
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - SVN_YYYY Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Social Suite
|
||||
* @since 6 Jul 2015
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS\Content\Search;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Content Filter for Search Queries
|
||||
*/
|
||||
class _ContentFilter
|
||||
{
|
||||
/**
|
||||
* @brief Item class
|
||||
*/
|
||||
public $itemClass;
|
||||
|
||||
/**
|
||||
* @brief Classes to include
|
||||
*/
|
||||
public $classes = array();
|
||||
|
||||
/**
|
||||
* @brief Container ID filter
|
||||
*/
|
||||
public $containerIdFilter = NULL;
|
||||
|
||||
/**
|
||||
* @brief Container IDs
|
||||
*/
|
||||
public $containerIds = array();
|
||||
|
||||
/**
|
||||
* @brief Item ID filter
|
||||
*/
|
||||
public $itemIdFilter = NULL;
|
||||
|
||||
/**
|
||||
* @brief Item IDs
|
||||
*/
|
||||
public $itemIds = array();
|
||||
|
||||
/**
|
||||
* @brief Minimum comments
|
||||
*/
|
||||
public $minimumComments = 0;
|
||||
|
||||
/**
|
||||
* @brief Minimum reviews
|
||||
*/
|
||||
public $minimumReviews = 0;
|
||||
|
||||
/**
|
||||
* @brief Minimum views
|
||||
*/
|
||||
public $minimumViews = 0;
|
||||
|
||||
/**
|
||||
* @brief Only include results which are the first comment on an item requiring a comment
|
||||
*/
|
||||
public $onlyFirstComment = FALSE;
|
||||
|
||||
/**
|
||||
* @brief Only include results which are the last comment on an item requiring a comment
|
||||
*/
|
||||
public $onlyLastComment = FALSE;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param string $itemClass The item class
|
||||
* @param bool $includeItems Include items in results?
|
||||
* @param bool $includeComments Include comments in results?
|
||||
* @param bool $includeReviews Include reviews in results?
|
||||
* @return \IPS\Content\Search\ContainerFilter
|
||||
*/
|
||||
public static function init( $itemClass, $includeItems=TRUE, $includeComments=TRUE, $includeReviews=TRUE )
|
||||
{
|
||||
$obj = new self;
|
||||
$obj->itemClass = $itemClass;
|
||||
|
||||
if ( $includeItems and !$itemClass::$firstCommentRequired )
|
||||
{
|
||||
$obj->classes[] = $itemClass;
|
||||
}
|
||||
|
||||
if ( $includeComments and isset( $itemClass::$commentClass ) )
|
||||
{
|
||||
$obj->classes[] = $itemClass::$commentClass;
|
||||
}
|
||||
|
||||
if ( $includeReviews and isset( $itemClass::$reviewClass ) )
|
||||
{
|
||||
$obj->classes[] = $itemClass::$reviewClass;
|
||||
}
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Only include results in containers
|
||||
*
|
||||
* @param array $ids Acceptable container IDs
|
||||
* @return \IPS\Content\Search\ContainerFilter (for daisy chaining)
|
||||
*/
|
||||
public function onlyInContainers( array $ids )
|
||||
{
|
||||
$this->containerIdFilter = TRUE;
|
||||
$this->containerIds = $ids;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Exclude results in containers
|
||||
*
|
||||
* @param array $ids Acceptable container IDs
|
||||
* @return \IPS\Content\Search\ContainerFilter (for daisy chaining)
|
||||
*/
|
||||
public function excludeInContainers( array $ids )
|
||||
{
|
||||
$this->containerIdFilter = FALSE;
|
||||
$this->containerIds = $ids;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Only include results in items
|
||||
*
|
||||
* @param array $ids Acceptable item IDs
|
||||
* @return \IPS\Content\Search\ContainerFilter (for daisy chaining)
|
||||
*/
|
||||
public function onlyInItems( array $ids )
|
||||
{
|
||||
$this->itemIdFilter = TRUE;
|
||||
$this->itemIds = $ids;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Exclude results in items
|
||||
*
|
||||
* @param array $ids Acceptable container IDs
|
||||
* @return \IPS\Content\Search\ContainerFilter (for daisy chaining)
|
||||
*/
|
||||
public function excludeInItems( array $ids )
|
||||
{
|
||||
$this->itemIdFilter = FALSE;
|
||||
$this->itemIds = $ids;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set minimum number of comments
|
||||
*
|
||||
* @param int $minimumComments The minimum number of comments
|
||||
* @return \IPS\Content\Search\ContainerFilter (for daisy chaining)
|
||||
*/
|
||||
public function minimumComments( $minimumComments )
|
||||
{
|
||||
$this->minimumComments = $minimumComments;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set minimum number of reviews
|
||||
*
|
||||
* @param int $minimumReviews The minimum number of reviews
|
||||
* @return \IPS\Content\Search\ContainerFilter (for daisy chaining)
|
||||
*/
|
||||
public function minimumReviews( $minimumReviews )
|
||||
{
|
||||
$this->minimumReviews = $minimumReviews;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set minimum number of views
|
||||
*
|
||||
* @param int $minimumViews The minimum number of views
|
||||
* @return \IPS\Content\Search\ContainerFilter (for daisy chaining)
|
||||
*/
|
||||
public function minimumViews( $minimumViews )
|
||||
{
|
||||
$this->minimumViews = $minimumViews;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Only include results which are the first comment on an item requiring a comment
|
||||
*
|
||||
* @return \IPS\Content\Search\ContainerFilter (for daisy chaining)
|
||||
*/
|
||||
public function onlyFirstComment()
|
||||
{
|
||||
$this->onlyFirstComment = TRUE;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Only include results which are the last comment on an item requiring a comment
|
||||
*
|
||||
* @return \IPS\Content\Search\ContainerFilter (for daisy chaining)
|
||||
*/
|
||||
public function onlyLastComment()
|
||||
{
|
||||
$this->onlyLastComment = TRUE;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -37,8 +37,7 @@ abstract class _Index extends \IPS\Patterns\Singleton
|
||||
{
|
||||
if( static::$instance === NULL )
|
||||
{
|
||||
$classname = '\\IPS\\Content\\Search\\' . ucfirst( \IPS\Settings::i()->search_engine ) . '\\Index';
|
||||
static::$instance = new $classname;
|
||||
static::$instance = new \IPS\Content\Search\Mysql\Index;
|
||||
}
|
||||
|
||||
return static::$instance;
|
||||
@@ -64,7 +63,7 @@ abstract class _Index extends \IPS\Patterns\Singleton
|
||||
{
|
||||
if( is_subclass_of( $class, 'IPS\Content\Searchable' ) )
|
||||
{
|
||||
\IPS\Task::queue( 'core', 'RebuildSearchIndex', array( 'class' => $class ) );
|
||||
\IPS\Task::queue( 'core', 'RebuildSearchIndex', array( 'class' => $class ), 5, TRUE );
|
||||
}
|
||||
}
|
||||
catch( \OutOfRangeException $ex ) {}
|
||||
@@ -111,10 +110,37 @@ abstract class _Index extends \IPS\Patterns\Singleton
|
||||
}
|
||||
|
||||
/* Get the last updated date */
|
||||
$dateUpdated = ( $object instanceof \IPS\Content\Item ) ? $object->mapped('last_comment') : $object->mapped('edit_time');
|
||||
if ( $isForItem )
|
||||
{
|
||||
$dateUpdated = $object->item()->mapped('last_comment');
|
||||
}
|
||||
else
|
||||
{
|
||||
$dateUpdated = ( $object instanceof \IPS\Content\Item ) ? $object->mapped('last_comment') : $object->mapped('edit_time');
|
||||
}
|
||||
|
||||
/* Take the HTML out of the content */
|
||||
$content = trim( strip_tags( preg_replace( "#<blockquote(.*?)>(.*)</blockquote>#ui", "", preg_replace( "#<script(.*?)>(.*)</script>#ui", "", ' ' . $object->mapped('content') . ' ' ) ) ) );
|
||||
/* Is the the latest content? */
|
||||
$isLastComment = 0;
|
||||
if ( $object instanceof \IPS\Content\Comment )
|
||||
{
|
||||
$item = $object->item();
|
||||
$latestThing = 0;
|
||||
foreach ( array( 'updated', 'last_comment', 'last_review' ) as $k )
|
||||
{
|
||||
if ( isset( $item::$databaseColumnMap[ $k ] ) and ( $item->mapped( $k ) < time() AND $item->mapped( $k ) > $latestThing ) )
|
||||
{
|
||||
$latestThing = $item->mapped( $k );
|
||||
}
|
||||
}
|
||||
|
||||
if ( $object->mapped('date') >= $latestThing )
|
||||
{
|
||||
$isLastComment = 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Take the HTML out of the content */
|
||||
$content = trim( strip_tags( preg_replace( "/(<br(?: \/)?>|<\/p>)/i", ' ', preg_replace( "#<blockquote(.*?)>(.*)</blockquote>#uis", "", preg_replace( "#<script(.*?)>(.*)</script>#uis", "", ' ' . $object->mapped('content') . ' ' ) ) ) ) );
|
||||
|
||||
/* Work out the hidden status */
|
||||
$hiddenStatus = $object->hidden();
|
||||
@@ -122,26 +148,47 @@ abstract class _Index extends \IPS\Patterns\Singleton
|
||||
{
|
||||
$hiddenStatus = $isForItem ? 1 : 2;
|
||||
}
|
||||
|
||||
if ( $hiddenStatus !== 0 and method_exists( $object, 'item' ) and $object->item()->isFutureDate() )
|
||||
{
|
||||
$hiddenStatus = 0;
|
||||
}
|
||||
|
||||
|
||||
/* Get the item index ID */
|
||||
$itemIndexId = NULL;
|
||||
if ( $object instanceof \IPS\Content\Comment )
|
||||
{
|
||||
$itemClass = $object::$itemClass;
|
||||
if ( $itemClass::$firstCommentRequired )
|
||||
{
|
||||
try
|
||||
{
|
||||
$itemIndexId = $this->getIndexId( $object::load( $object->item()->mapped('first_comment_id') ) );
|
||||
}
|
||||
catch ( \Exception $e ) { }
|
||||
}
|
||||
else
|
||||
{
|
||||
$itemIndexId = $this->getIndexId( $object->item() );
|
||||
}
|
||||
}
|
||||
|
||||
/* Return */
|
||||
return array(
|
||||
'index_class' => $class,
|
||||
'index_object_id' => $object->$idColumn,
|
||||
'index_item_id' => ( $object instanceof \IPS\Content\Item ) ? $object->$idColumn : $object->mapped('item'),
|
||||
'index_container_id' => ( $object instanceof \IPS\Content\Item ) ? (int) $object->mapped('container') : (int) $object->item()->mapped('container'),
|
||||
'index_container_id' => ( $object instanceof \IPS\Content\Item ) ? (int) $object->searchIndexContainer() : (int) $object->item()->mapped('container'),
|
||||
'index_title' => $title,
|
||||
'index_content' => $content,
|
||||
'index_permissions' => $object->searchIndexPermissions(),
|
||||
'index_date_created' => $object->mapped('date'),
|
||||
'index_date_updated' => $dateUpdated ?: $object->mapped('date'),
|
||||
'index_date_created' => intval( $object->mapped('date') ),
|
||||
'index_date_updated' => intval( $dateUpdated ?: $object->mapped('date') ),
|
||||
'index_author' => (int) $object->mapped('author'),
|
||||
'index_tags' => $tags,
|
||||
'index_hidden' => $hiddenStatus
|
||||
'index_hidden' => $hiddenStatus,
|
||||
'index_item_index_id' => $itemIndexId,
|
||||
'index_item_author' => intval( ( $object instanceof \IPS\Content\Item ) ? $object->mapped('author') : $object->item()->mapped('author') ),
|
||||
'index_is_last_comment' => $isLastComment
|
||||
);
|
||||
}
|
||||
|
||||
@@ -153,6 +200,14 @@ abstract class _Index extends \IPS\Patterns\Singleton
|
||||
*/
|
||||
abstract public function index( \IPS\Content\Searchable $object );
|
||||
|
||||
/**
|
||||
* Retrieve the search ID for an item
|
||||
*
|
||||
* @param \IPS\Content\Searchable $object Item to add
|
||||
* @return void
|
||||
*/
|
||||
abstract public function getIndexId( \IPS\Content\Searchable $object );
|
||||
|
||||
/**
|
||||
* Remove item
|
||||
*
|
||||
@@ -217,9 +272,21 @@ abstract class _Index extends \IPS\Patterns\Singleton
|
||||
* @param string|NULL $newPermissions New permissions (if applicable)
|
||||
* @param int|NULL $newHiddenStatus New hidden status (if applicable) special value 2 can be used to indicate hidden only by parent
|
||||
* @param int|NULL $newContainer New container ID (if applicable)
|
||||
* @param int|NULL $authorId The author ID to update, or NULL
|
||||
* @param int|NULL $newItemId The new item ID (if applicable)
|
||||
* @param int|NULL $$newItemAuthorId The new item author ID (if applicable)
|
||||
* @return void
|
||||
*/
|
||||
abstract public function massUpdate( $class, $containerId = NULL, $itemId = NULL, $newPermissions = NULL, $newHiddenStatus = NULL, $newContainer = NULL );
|
||||
abstract public function massUpdate( $class, $containerId = NULL, $itemId = NULL, $newPermissions = NULL, $newHiddenStatus = NULL, $newContainer = NULL, $authorId = NULL, $newItemId = NULL, $newItemAuthorId = NULL );
|
||||
|
||||
/**
|
||||
* Update data for the first and last comment after a merge
|
||||
* Sets index_is_last_comment on the last comment, and, if this is an item where the first comment is indexed rather than the item, sets index_title and index_tags on the first comment
|
||||
*
|
||||
* @param \IPS\Content\Item $item The item
|
||||
* @return void
|
||||
*/
|
||||
abstract public function rebuildAfterMerge( \IPS\Content\Item $item );
|
||||
|
||||
/**
|
||||
* Prune search index
|
||||
|
||||
@@ -31,20 +31,83 @@ class _Index extends \IPS\Content\Search\Index
|
||||
*/
|
||||
public function index( \IPS\Content\Searchable $object )
|
||||
{
|
||||
/* Get the index data */
|
||||
$indexData = $this->indexData( $object );
|
||||
|
||||
/* If we got the data... */
|
||||
if( $indexData )
|
||||
{
|
||||
/* If nobody has permission to access it, just remove it */
|
||||
if ( !$indexData['index_permissions'] )
|
||||
{
|
||||
$this->removeFromSearchIndex( $object );
|
||||
}
|
||||
/* Otherwise, go ahead... */
|
||||
else
|
||||
{
|
||||
\IPS\Db::i()->replace( 'core_search_index', $indexData );
|
||||
/* Index it */
|
||||
$newIndexId = \IPS\Db::i()->replace( 'core_search_index', $indexData, TRUE );
|
||||
|
||||
/* If that was successful... */
|
||||
if ( $newIndexId )
|
||||
{
|
||||
$class = get_class( $object );
|
||||
$databaseColumnId = $object::$databaseColumnId;
|
||||
|
||||
/* Set index_item_index_id on other index items */
|
||||
if ( $object instanceof \IPS\Content\Item )
|
||||
{
|
||||
$subClasses = array( $class );
|
||||
if ( isset( $class::$commentClass ) )
|
||||
{
|
||||
$subClasses[] = $class::$commentClass;
|
||||
}
|
||||
if ( isset( $class::$reviewClass ) )
|
||||
{
|
||||
$subClasses[] = $class::$reviewClass;
|
||||
}
|
||||
|
||||
\IPS\Db::i()->update( 'core_search_index', array( 'index_item_index_id' => $newIndexId ), array( array( \IPS\Db::i()->in( 'index_class', $subClasses ) ), array( 'index_item_id=?', $object->$databaseColumnId ) ) );
|
||||
}
|
||||
elseif ( $object instanceof \IPS\Content\Comment )
|
||||
{
|
||||
$itemClass = $object::$itemClass;
|
||||
if ( $itemClass::$firstCommentRequired and $object->isFirst() )
|
||||
{
|
||||
$itemColumnId = $class::$databaseColumnMap['item'];
|
||||
\IPS\Db::i()->update( 'core_search_index', array( 'index_item_index_id' => $newIndexId ), array( \IPS\Db::i()->in( 'index_class', array( $class, $class::$itemClass ) ) . ' AND index_item_id=?', $object->$itemColumnId ) );
|
||||
}
|
||||
}
|
||||
|
||||
/* And also set core_follow.follow_index_id */
|
||||
if ( $object instanceof \IPS\Content\Comment )
|
||||
{
|
||||
$class = $class::$itemClass;
|
||||
}
|
||||
\IPS\Db::i()->update( 'core_follow', array( 'follow_index_id' => $newIndexId ), array( 'follow_app=? AND follow_area=? AND follow_rel_id=?', $object::$application, mb_strtolower( mb_substr( $class, mb_strrpos( $class, '\\' ) + 1 ) ), $object->$databaseColumnId ) );
|
||||
|
||||
/* If this is the latest comment, unflag what was set before */
|
||||
if ( $indexData['index_is_last_comment'] and $indexData['index_item_id'] )
|
||||
{
|
||||
\IPS\Db::i()->update( 'core_search_index', array( 'index_is_last_comment' => 0 ), array( 'index_class=? AND index_item_id=? AND index_id<>?', $indexData['index_class'], $indexData['index_item_id'], $newIndexId ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the search ID for an item
|
||||
*
|
||||
* @param \IPS\Content\Searchable $object Item to add
|
||||
* @return void
|
||||
*/
|
||||
public function getIndexId( \IPS\Content\Searchable $object )
|
||||
{
|
||||
$databaseColumnId = $object::$databaseColumnId;
|
||||
return \IPS\Db::i()->select( 'index_id', 'core_search_index', array( 'index_class=? AND index_object_id=?', get_class( $object ),$object->$databaseColumnId ) )->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove item
|
||||
*
|
||||
@@ -98,9 +161,12 @@ class _Index extends \IPS\Content\Search\Index
|
||||
* @param string|NULL $newPermissions New permissions (if applicable)
|
||||
* @param int|NULL $newHiddenStatus New hidden status (if applicable) special value 2 can be used to indicate hidden only by parent
|
||||
* @param int|NULL $newContainer New container ID (if applicable)
|
||||
* @param int|NULL $authorId The author ID to update, or NULL
|
||||
* @param int|NULL $newItemId The new item ID (if applicable)
|
||||
* @param int|NULL $newItemAuthorId The new item author ID (if applicable)
|
||||
* @return void
|
||||
*/
|
||||
public function massUpdate( $class, $containerId = NULL, $itemId = NULL, $newPermissions = NULL, $newHiddenStatus = NULL, $newContainer = NULL )
|
||||
public function massUpdate( $class, $containerId = NULL, $itemId = NULL, $newPermissions = NULL, $newHiddenStatus = NULL, $newContainer = NULL, $authorId = NULL, $newItemId = NULL, $newItemAuthorId = NULL )
|
||||
{
|
||||
$where = array( array( 'index_class=?', $class ) );
|
||||
if ( $containerId !== NULL )
|
||||
@@ -111,12 +177,34 @@ class _Index extends \IPS\Content\Search\Index
|
||||
{
|
||||
$where[] = array( 'index_item_id=?', $itemId );
|
||||
}
|
||||
if ( $authorId !== NULL )
|
||||
{
|
||||
$where[] = array( 'index_item_author=?', $authorId );
|
||||
}
|
||||
|
||||
$update = array();
|
||||
if ( $newPermissions !== NULL )
|
||||
{
|
||||
$update['index_permissions'] = $newPermissions;
|
||||
}
|
||||
if ( $newContainer )
|
||||
{
|
||||
$update['index_container_id'] = $newContainer;
|
||||
}
|
||||
if ( $newItemId )
|
||||
{
|
||||
$update['index_item_id'] = $newItemId;
|
||||
}
|
||||
if ( $newItemAuthorId )
|
||||
{
|
||||
$update['index_item_author'] = $newItemAuthorId;
|
||||
}
|
||||
|
||||
if ( count( $update ) )
|
||||
{
|
||||
\IPS\Db::i()->update( 'core_search_index', $update, $where );
|
||||
}
|
||||
|
||||
if ( $newHiddenStatus !== NULL )
|
||||
{
|
||||
if ( $newHiddenStatus === 2 )
|
||||
@@ -128,14 +216,42 @@ class _Index extends \IPS\Content\Search\Index
|
||||
$where[] = array( 'index_hidden=2' );
|
||||
}
|
||||
|
||||
$update['index_hidden'] = $newHiddenStatus;
|
||||
\IPS\Db::i()->update( 'core_search_index', array( 'index_hidden' => $newHiddenStatus ), $where );
|
||||
}
|
||||
if ( $newContainer )
|
||||
}
|
||||
|
||||
/**
|
||||
* Update data for the first and last comment after a merge
|
||||
* Sets index_is_last_comment on the last comment, and, if this is an item where the first comment is indexed rather than the item, sets index_title and index_tags on the first comment
|
||||
*
|
||||
* @param \IPS\Content\Item $item The item
|
||||
* @return void
|
||||
*/
|
||||
public function rebuildAfterMerge( \IPS\Content\Item $item )
|
||||
{
|
||||
if ( $item::$commentClass )
|
||||
{
|
||||
$update['index_container_id'] = $newContainer;
|
||||
$firstComment = $item->comments( 1, 0, 'date', 'asc', NULL, FALSE, NULL, NULL, TRUE );
|
||||
$lastComment = $item->comments( 1, 0, 'date', 'desc', NULL, FALSE, NULL, NULL, TRUE );
|
||||
|
||||
$idColumn = $item::$databaseColumnId;
|
||||
$update = array( 'index_is_last_comment' => 0 );
|
||||
if ( $item::$firstCommentRequired )
|
||||
{
|
||||
$update['index_title'] = NULL;
|
||||
$update['index_tags'] = NULL;
|
||||
}
|
||||
\IPS\Db::i()->update( 'core_search_index', $update, array( 'index_class=? AND index_item_id=?', $item::$commentClass, $item->$idColumn ) );
|
||||
|
||||
if ( $firstComment )
|
||||
{
|
||||
$this->index( $firstComment );
|
||||
}
|
||||
if ( $lastComment )
|
||||
{
|
||||
$this->index( $lastComment );
|
||||
}
|
||||
}
|
||||
|
||||
\IPS\Db::i()->update( 'core_search_index', $update, $where );
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -24,6 +24,11 @@ if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
class _Query extends \IPS\Content\Search\Query
|
||||
{
|
||||
/**
|
||||
* @brief The SELECT clause
|
||||
*/
|
||||
protected $select = array( 'main' => 'main.*' );
|
||||
|
||||
/**
|
||||
* @brief The WHERE clause
|
||||
*/
|
||||
protected $where = array();
|
||||
@@ -49,137 +54,465 @@ class _Query extends \IPS\Content\Search\Query
|
||||
protected $joins = array();
|
||||
|
||||
/**
|
||||
* Filter by content type
|
||||
* @brief Item classes included
|
||||
*/
|
||||
protected $itemClasses = NULL;
|
||||
|
||||
/**
|
||||
* Filter by multiple content types
|
||||
*
|
||||
* @param string $class The type of content to search (including all comment/review classes)
|
||||
* @param array|NULL $containers An array of container IDs to filter by, or NULL to not filter by containers
|
||||
* @param int|NULL $minimumComments The minimum number of comments
|
||||
* @param int|NULL $minimumReviews The minimum number of reviews
|
||||
* @param int|NULL $minimumViews The minimum number of views
|
||||
* @param array $contentFilters Array of \IPS\Content\Search\ContentFilter objects
|
||||
* @param bool $type TRUE means only include results matching the filters, FALSE means exclude all results matching the filters
|
||||
* @return \IPS\Content\Search\Query (for daisy chaining)
|
||||
*/
|
||||
public function filterByContent( $class, $containers = NULL, $minimumComments = NULL, $minimumReviews = NULL, $minimumViews = NULL )
|
||||
public function filterByContent( array $contentFilters, $type = TRUE )
|
||||
{
|
||||
$classes = array( $class );
|
||||
if ( isset( $class::$commentClass ) )
|
||||
/* Init */
|
||||
$filters = array();
|
||||
$params = array();
|
||||
if ( $type )
|
||||
{
|
||||
$classes[] = $class::$commentClass;
|
||||
}
|
||||
if ( isset( $class::$reviewClass ) )
|
||||
{
|
||||
$classes[] = $class::$reviewClass;
|
||||
}
|
||||
$this->where[] = array( \IPS\Db::i()->in( 'index_class', $classes ) );
|
||||
|
||||
if ( $containers !== NULL )
|
||||
{
|
||||
$this->where[] = array( \IPS\Db::i()->in( 'index_container_id', $containers ) );
|
||||
$this->itemClasses = array();
|
||||
}
|
||||
|
||||
if ( $minimumComments !== NULL or $minimumReviews !== NULL or $minimumViews !== NULL )
|
||||
/* Loop the filters */
|
||||
foreach ( $contentFilters as $filter )
|
||||
{
|
||||
$this->joins[ $class::$databaseTable ] = array( $class::$databaseTable . '.' . $class::$databasePrefix . $class::$databaseColumnId . '=core_search_index.index_item_id' );
|
||||
$clause = array();
|
||||
if ( $type )
|
||||
{
|
||||
$this->itemClasses[] = $filter->itemClass;
|
||||
}
|
||||
|
||||
if ( $minimumComments !== NULL )
|
||||
/* Set the class */
|
||||
if ( count( $filter->classes ) > 1 )
|
||||
{
|
||||
$this->where[] = array( $class::$databasePrefix . $class::$databaseColumnMap['num_comments'] . '>=?', $minimumComments );
|
||||
$clause[] = \IPS\Db::i()->in( 'index_class', $filter->classes );
|
||||
}
|
||||
if ( $minimumReviews !== NULL )
|
||||
else
|
||||
{
|
||||
$this->where[] = array( $class::$databasePrefix . $class::$databaseColumnMap['num_reviews'] . '>=?', $minimumReviews );
|
||||
$clause[] = 'index_class=?';
|
||||
$params[] = array_pop( $filter->classes );
|
||||
}
|
||||
if ( $minimumViews !== NULL )
|
||||
|
||||
/* Set the containers */
|
||||
if ( $filter->containerIdFilter !== NULL )
|
||||
{
|
||||
$this->where[] = array( $class::$databasePrefix . $class::$databaseColumnMap['views'] . '>=?', $minimumViews );
|
||||
$clause[] = \IPS\Db::i()->in( 'index_container_id', $filter->containerIds, $filter->containerIdFilter === FALSE );
|
||||
}
|
||||
|
||||
/* Set the item IDs */
|
||||
if ( $filter->itemIdFilter !== NULL )
|
||||
{
|
||||
$clause[] = \IPS\Db::i()->in( 'index_item_id', $filter->itemIds, $filter->itemIdFilter === FALSE );
|
||||
}
|
||||
|
||||
/* Minimum comments/reviews/views? */
|
||||
if ( $filter->minimumComments or $filter->minimumReviews or $filter->minimumViews )
|
||||
{
|
||||
$class = $filter->itemClass;
|
||||
|
||||
$this->joins[] = array( 'from' => $class::$databaseTable, 'where' => $class::$databaseTable . '.' . $class::$databasePrefix . $class::$databaseColumnId . '=main.index_item_id' );
|
||||
|
||||
if ( $filter->minimumComments )
|
||||
{
|
||||
$this->select[ $class::$databaseTable . '_comments' ] = $class::$databaseTable . '.' . $class::$databasePrefix . $class::$databaseColumnMap['num_comments'];
|
||||
$clause[] = $class::$databaseTable . '.' . $class::$databasePrefix . $class::$databaseColumnMap['num_comments'] . '>=' . intval( $filter->minimumComments );
|
||||
}
|
||||
|
||||
if ( $filter->minimumReviews )
|
||||
{
|
||||
$this->select[ $class::$databaseTable . '_reviews' ] = $class::$databaseTable . '.' . $class::$databasePrefix . $class::$databaseColumnMap['num_reviews'];
|
||||
$clause[] = $class::$databaseTable . '.' . $class::$databasePrefix . $class::$databaseColumnMap['num_reviews'] . '>=' . intval( $filter->minimumReviews );
|
||||
}
|
||||
|
||||
if ( $filter->minimumViews )
|
||||
{
|
||||
$this->select[ $class::$databaseTable . '_views' ] = $class::$databaseTable . '.' . $class::$databasePrefix . $class::$databaseColumnMap['views'];
|
||||
$clause[] = $class::$databaseTable . '.' . $class::$databasePrefix . $class::$databaseColumnMap['views'] . '>=' . intval( $filter->minimumViews );
|
||||
}
|
||||
}
|
||||
|
||||
/* Only first comment? */
|
||||
if ( $filter->onlyFirstComment )
|
||||
{
|
||||
$clause[] = "index_title IS NOT NULL";
|
||||
}
|
||||
|
||||
/* Only last comment? */
|
||||
if ( $filter->onlyLastComment )
|
||||
{
|
||||
$clause[] = "index_is_last_comment=1";
|
||||
}
|
||||
|
||||
/* Put it together */
|
||||
if ( count( $clause ) > 1 )
|
||||
{
|
||||
$filters[] = '( ' . implode( ' AND ', $clause ) . ' )';
|
||||
}
|
||||
else
|
||||
{
|
||||
$filters[] = array_pop( $clause );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Put it all together */
|
||||
$this->where[] = array_merge( array( $type ? ( '( ' . implode( ' OR ', $filters ) . ' )' ) : ( '!( ' . implode( ' OR ', $filters ) . ' )' ) ), $params );
|
||||
|
||||
/* Return */
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Filter by content type to exclude
|
||||
* Filter by author
|
||||
*
|
||||
* @param array $classes The types of content to exclude (not including all comment/review classes)
|
||||
* @param bool $allowedContiners If TRUE, $class should be an array of allowed containers for that class - e.g. array( 'IPS\forums\Topic' => array( 1, 2 ) - otherwise, a simple array - e.g. array( 'IPS\forums\Topic' )
|
||||
* @param \IPS\Member|int|array $author The author, or an array of author IDs
|
||||
* @return \IPS\Content\Search\Query (for daisy chaining)
|
||||
*/
|
||||
public function filterByExcludeContent( array $classes, $allowedContiners=FALSE )
|
||||
{
|
||||
if ( $allowedContiners )
|
||||
public function filterByAuthor( $author )
|
||||
{
|
||||
if ( is_array( $author ) )
|
||||
{
|
||||
$where = array();
|
||||
$params = array();
|
||||
$excludeAll = array();
|
||||
foreach ( $classes as $class => $allowed )
|
||||
{
|
||||
if ( count( $allowed ) )
|
||||
{
|
||||
$where[] = '( index_class<>? OR ' . \IPS\Db::i()->in( 'index_container_id', $allowed ) . ' )';
|
||||
$params[] = $class;
|
||||
}
|
||||
else
|
||||
{
|
||||
$excludeAll[] = $class;
|
||||
}
|
||||
}
|
||||
|
||||
$finalWhere = array();
|
||||
if ( count( $excludeAll ) )
|
||||
{
|
||||
$finalWhere[] = \IPS\Db::i()->in( 'index_class', $excludeAll, TRUE );
|
||||
}
|
||||
if ( count( $where ) )
|
||||
{
|
||||
$finalWhere[] = implode( ' OR ', $where );
|
||||
}
|
||||
|
||||
$this->where[] = array_merge( array( implode( ' AND ', $finalWhere ) ), $params );
|
||||
$this->where[] = array( \IPS\Db::i()->in( 'index_author', $author ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->where[] = array( \IPS\Db::i()->in( 'index_class', $classes, TRUE ) );
|
||||
$this->where[] = array( 'index_author=?', $author instanceof \IPS\Member ? $author->member_id : $author );
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter by content item
|
||||
* Filter for profile
|
||||
*
|
||||
* @param \IPS\Content\Item $item The item
|
||||
* @param \IPS\Member $member The member whose profile is being viewed
|
||||
* @return \IPS\Content\Search\Query (for daisy chaining)
|
||||
*/
|
||||
public function filterByItem( \IPS\Content\Item $item )
|
||||
public function filterForProfile( \IPS\Member $member )
|
||||
{
|
||||
$classes = array();
|
||||
if ( isset( $item::$commentClass ) )
|
||||
{
|
||||
$classes[] = $item::$commentClass;
|
||||
}
|
||||
if ( isset( $item::$reviewClass ) )
|
||||
{
|
||||
$classes[] = $item::$reviewClass;
|
||||
}
|
||||
|
||||
$this->where[] = array( \IPS\Db::i()->in( 'index_class', $classes ) );
|
||||
|
||||
$idColumn = $item::$databaseColumnId;
|
||||
$this->where[] = array( 'index_item_id=?', $item->$idColumn );
|
||||
|
||||
$this->where[] = array( '( index_author=? OR ( index_class=? AND index_container_id=? ) )', $member->member_id, 'IPS\core\Statuses\Status', $member->member_id );
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter by author
|
||||
* Filter by item author
|
||||
*
|
||||
* @param \IPS\Member $author The author
|
||||
* @return \IPS\Content\Search\Query (for daisy chaining)
|
||||
*/
|
||||
public function filterByAuthor( \IPS\Member $author )
|
||||
public function filterByItemAuthor( \IPS\Member $author )
|
||||
{
|
||||
$this->where[] = array( 'index_author=?', $author->member_id );
|
||||
$this->where[] = array( 'index_item_author=?', $author->member_id );
|
||||
|
||||
return $this;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter by content the user follows
|
||||
*
|
||||
* @param bool $includeContainers Include content in containers the user follows?
|
||||
* @param bool $includeItems Include items and comments/reviews on items the user follows?
|
||||
* @param bool $includeMembers Include content posted by members the user follows?
|
||||
* @return \IPS\Content\Search\Query (for daisy chaining)
|
||||
*/
|
||||
public function filterByFollowed( $includeContainers, $includeItems, $includeMembers )
|
||||
{
|
||||
$where = array();
|
||||
$params = array();
|
||||
|
||||
/* Are we including items or containers? */
|
||||
if ( $includeContainers or $includeItems )
|
||||
{
|
||||
/* Work out what classes we need to examine */
|
||||
if ( $this->itemClasses !== NULL )
|
||||
{
|
||||
$classes = $this->itemClasses;
|
||||
}
|
||||
else
|
||||
{
|
||||
$classes = array();
|
||||
foreach ( \IPS\Application::allExtensions( 'core', 'ContentRouter', FALSE ) as $object )
|
||||
{
|
||||
$classes = array_merge( $object->classes, $classes );
|
||||
}
|
||||
}
|
||||
|
||||
/* Loop them */
|
||||
foreach ( $classes as $class )
|
||||
{
|
||||
if( is_subclass_of( $class, 'IPS\Content\Followable' ) )
|
||||
{
|
||||
/* Construct a subquery which will get the IDs of the items of this class that we follow */
|
||||
$subQueryWhere = array( array( 'follow_app=?', $class::$application ) );
|
||||
if ( $includeContainers and $includeItems )
|
||||
{
|
||||
$subQueryWhere[] = array( '( follow_area=? OR follow_area=? )', mb_strtolower( mb_substr( $class::$containerNodeClass, mb_strrpos( $class::$containerNodeClass, '\\' ) + 1 ) ), mb_strtolower( mb_substr( $class, mb_strrpos( $class, '\\' ) + 1 ) ) );
|
||||
}
|
||||
elseif ( $includeItems )
|
||||
{
|
||||
$subQueryWhere[] = array( 'follow_area=?', mb_strtolower( mb_substr( $class, mb_strrpos( $class, '\\' ) + 1 ) ) );
|
||||
}
|
||||
elseif ( $includeContainers )
|
||||
{
|
||||
$subQueryWhere[] = array( 'follow_area=?', mb_strtolower( mb_substr( $class::$containerNodeClass, mb_strrpos( $class::$containerNodeClass, '\\' ) + 1 ) ) );
|
||||
}
|
||||
$subQueryWhere[] = array( 'follow_member_id=?', $this->member->member_id );
|
||||
$subQuery = \IPS\Db::i()->select( 'follow_rel_id', 'core_follow', $subQueryWhere );
|
||||
|
||||
/* If we're only doing one class, there's no point in doing "item_class=? AND ( item_class=? AND item_id IN(?) )" - we can just do "item_class=? AND item_id IN(?)" */
|
||||
if ( count( $classes ) == 1 )
|
||||
{
|
||||
$where[] = 'index_item_id IN(?)';
|
||||
$params[] = $subQuery;
|
||||
}
|
||||
/* But otherwise we need to group by class, because item IDs are not unique */
|
||||
else
|
||||
{
|
||||
/* Work out what classes this applies to - need to specify comment and review classes */
|
||||
$_classes = array( $class );
|
||||
if ( isset( $class::$commentClass ) )
|
||||
{
|
||||
$_classes[] = $class::$commentClass;
|
||||
}
|
||||
if ( isset( $class::$reviewClass ) )
|
||||
{
|
||||
$_classes[] = $class::$reviewClass;
|
||||
}
|
||||
|
||||
/* Constuct the where */
|
||||
$where[] = '( ' . \IPS\Db::i()->in( 'index_class', $_classes ) . ' AND index_item_id IN( ? ) )';
|
||||
$params[] = $subQuery;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Are we including content posted by followed members? */
|
||||
if ( $includeMembers )
|
||||
{
|
||||
$where[] = 'index_author IN(?)';
|
||||
$params[] = \IPS\Db::i()->select( 'follow_rel_id', 'core_follow', array( 'follow_app=? AND follow_area=? AND follow_member_id=?', 'core', 'member', $this->member->member_id ) );
|
||||
}
|
||||
|
||||
/* Put it all together */
|
||||
if ( count( $where ) )
|
||||
{
|
||||
$this->where[] = array_merge( array( '( ' . implode( ' OR ', $where ) . ' )' ), $params );
|
||||
}
|
||||
|
||||
/* And return */
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter by content the user has posted in
|
||||
*
|
||||
* @return \IPS\Content\Search\Query (for daisy chaining)
|
||||
*/
|
||||
public function filterByItemsIPostedIn()
|
||||
{
|
||||
$this->joins[] = array(
|
||||
'from' => \IPS\Db::i()->select(
|
||||
'index_item_index_id',
|
||||
array('core_search_index', 'sub' ),
|
||||
array( array( 'index_author=' . intval( $this->member->member_id ) ) ),
|
||||
NULL, NULL, NULL, NULL, \IPS\Db::SELECT_DISTINCT
|
||||
),
|
||||
'where' => 'sub.index_item_index_id=main.index_item_index_id',
|
||||
'type' => 'STRAIGHT_JOIN'
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Filter by content the user has not read
|
||||
*
|
||||
* @note If applicable, it is more efficient to call filterByContent() before calling this method
|
||||
* @return \IPS\Content\Search\Query (for daisy chaining)
|
||||
*/
|
||||
public function filterByUnread()
|
||||
{
|
||||
/* Work out what classes we need to examine */
|
||||
if ( $this->itemClasses !== NULL )
|
||||
{
|
||||
$classes = $this->itemClasses;
|
||||
}
|
||||
else
|
||||
{
|
||||
$classes = array();
|
||||
foreach ( \IPS\Application::allExtensions( 'core', 'ContentRouter', FALSE ) as $object )
|
||||
{
|
||||
$classes = array_merge( $object->classes, $classes );
|
||||
}
|
||||
}
|
||||
|
||||
/* Loop them */
|
||||
$where = array();
|
||||
$params = array();
|
||||
foreach ( $classes as $class )
|
||||
{
|
||||
if( is_subclass_of( $class, 'IPS\Content\ReadMarkers' ) )
|
||||
{
|
||||
/* Get the actual clause */
|
||||
$unreadWhere = $this->_getUnreadWhere( $class );
|
||||
|
||||
/* Work out what classes this applies to - need to specify comment and review classes */
|
||||
$_classes = array( $class );
|
||||
if ( isset( $class::$commentClass ) )
|
||||
{
|
||||
$_classes[] = $class::$commentClass;
|
||||
}
|
||||
if ( isset( $class::$reviewClass ) )
|
||||
{
|
||||
$_classes[] = $class::$reviewClass;
|
||||
}
|
||||
|
||||
/* Add it to the array */
|
||||
$clause = array( \IPS\Db::i()->in( 'index_class', $_classes ) );
|
||||
foreach ( $unreadWhere as $_clause )
|
||||
{
|
||||
$clause[] = array_shift( $_clause );
|
||||
$params = array_merge( $params, $_clause );
|
||||
}
|
||||
$where[] = '( ' . implode( ' AND ', $clause ) . ' )';
|
||||
}
|
||||
}
|
||||
|
||||
/* Put it all together */
|
||||
$this->where[] = array_merge( array( '( ' . implode( ' OR ', $where ) . ' )' ), $params );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the 'unread' where SQL
|
||||
*
|
||||
* @param string $class Content class (\IPS\forums\Forum)
|
||||
* @return array
|
||||
*/
|
||||
protected function _getUnreadWhere( $class )
|
||||
{
|
||||
$classBits = explode( "\\", $class );
|
||||
$application = $classBits[1];
|
||||
$resetTimes = $this->member->markersResetTimes( NULL );
|
||||
$resetTimes = isset( $resetTimes[ $application ] ) ? $resetTimes[ $application ] : array();
|
||||
$oldestTime = time();
|
||||
$markers = array();
|
||||
$excludeIds = array();
|
||||
$where = array();
|
||||
$unreadWheres = array();
|
||||
$containerIds = array();
|
||||
$containerClass = ( $class::$containerNodeClass ) ? $class::$containerNodeClass : NULL;
|
||||
|
||||
foreach( $resetTimes as $containerId => $timestamp )
|
||||
{
|
||||
/* Pages has different classes per database, but recorded as 'cms' and the container ID in the marking tables */
|
||||
if ( $containerClass and method_exists( $containerClass, 'isValidContainerId' ) )
|
||||
{
|
||||
if ( ! $containerClass::isValidContainerId( $containerId ) )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$timestamp = $timestamp ?: $this->member->marked_site_read;
|
||||
|
||||
$containerIds[] = $containerId;
|
||||
$unreadWheres[] = '( index_container_id=' . $containerId . ' AND index_date_updated > ' . (int) $timestamp . ')';
|
||||
|
||||
$items = $this->member->markersItems( $application, \IPS\Content\Item::makeMarkerKey( $containerId ) );
|
||||
|
||||
if ( count( $items ) )
|
||||
{
|
||||
foreach( $items as $mid => $mtime )
|
||||
{
|
||||
if ( $mtime > $timestamp )
|
||||
{
|
||||
/* If an item has been moved from one container to another, the user may have a marker
|
||||
in it's old location, with the previously 'read' time. In this circumstance, we need
|
||||
to only use more recent read time, otherwise the topic may be incorrectly included
|
||||
in the results */
|
||||
if ( in_array( $mid, $markers ) )
|
||||
{
|
||||
$_key = array_search( $mid, $markers );
|
||||
$_mtime = intval( mb_substr( $_key, 0, mb_strpos( $_key, '.' ) ) );
|
||||
if ( $_mtime < $mtime )
|
||||
{
|
||||
unset( $markers[ $_key ] );
|
||||
}
|
||||
}
|
||||
|
||||
$markers[ $mtime . '.' . $mid ] = $mid;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if( count( $containerIds ) )
|
||||
{
|
||||
$unreadWheres[] = "( index_date_updated > " . intval( $this->member->marked_site_read ) . " AND ( index_container_id NOT IN(" . implode( ',', $containerIds ) . ") ) )";
|
||||
}
|
||||
else
|
||||
{
|
||||
$unreadWheres[] = "( index_date_updated > " . intval( $this->member->marked_site_read ) . ")";
|
||||
}
|
||||
|
||||
if( count( $unreadWheres ) )
|
||||
{
|
||||
$where[] = array( "(" . implode( " OR ", $unreadWheres ) . ")" );
|
||||
}
|
||||
|
||||
if ( count( $markers ) )
|
||||
{
|
||||
/* Avoid packet issues */
|
||||
krsort( $markers );
|
||||
$useIds = array_flip( array_slice( $markers, 0, 500, TRUE ) );
|
||||
$select = '';
|
||||
$from = '';
|
||||
$notIn = array();
|
||||
|
||||
/* What is the best date column? */
|
||||
$dateColumns = array();
|
||||
foreach ( array( 'updated', 'last_comment', 'last_review' ) as $k )
|
||||
{
|
||||
if ( isset( $class::$databaseColumnMap[ $k ] ) )
|
||||
{
|
||||
if ( is_array( $class::$databaseColumnMap[ $k ] ) )
|
||||
{
|
||||
foreach ( $class::$databaseColumnMap[ $k ] as $v )
|
||||
{
|
||||
$dateColumns[] = " IFNULL( " . $class::$databaseTable . '.'. $class::$databasePrefix . $v . ", 0 )";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$dateColumns[] = " IFNULL( " . $class::$databaseTable . '.'. $class::$databasePrefix . $class::$databaseColumnMap[ $k ] . ", 0 )";
|
||||
}
|
||||
}
|
||||
}
|
||||
$dateColumnExpression = count( $dateColumns ) > 1 ? ( 'GREATEST(' . implode( ',', $dateColumns ) . ')' ) : array_pop( $dateColumns );
|
||||
|
||||
foreach( \IPS\Db::i()->select( $class::$databaseTable . '.' . $class::$databasePrefix . $class::$databaseColumnId. ' as _id, ' . $dateColumnExpression . ' as _date', $class::$databaseTable, \IPS\Db::i()->in( $class::$databasePrefix . $class::$databaseColumnId, array_keys( $useIds ) ) ) as $row )
|
||||
{
|
||||
if ( isset( $useIds[ $row['_id'] ] ) )
|
||||
{
|
||||
if ( $useIds[ $row['_id'] ] >= $row['_date'] )
|
||||
{
|
||||
/* Still read */
|
||||
$notIn[] = intval( $row['_id'] );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( count( $notIn ) )
|
||||
{
|
||||
$where[] = array( "( index_item_id NOT IN (" . implode( ',', $notIn ) . ") )" );
|
||||
}
|
||||
}
|
||||
|
||||
return $where;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -272,13 +605,21 @@ class _Query extends \IPS\Content\Search\Query
|
||||
case static::ORDER_NEWEST_UPDATED:
|
||||
$this->order = 'index_date_updated DESC';
|
||||
break;
|
||||
|
||||
case static::ORDER_OLDEST_UPDATED:
|
||||
$this->order = 'index_date_updated ASC';
|
||||
break;
|
||||
|
||||
case static::ORDER_NEWEST_CREATED:
|
||||
$this->order = 'index_date_created DESC';
|
||||
break;
|
||||
|
||||
case static::ORDER_OLDEST_CREATED:
|
||||
$this->order = 'index_date_updated DESC';
|
||||
break;
|
||||
|
||||
case static::ORDER_RELEVANCY:
|
||||
$this->order = NULL;
|
||||
$this->order = 'calcscore DESC';
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -295,75 +636,86 @@ class _Query extends \IPS\Content\Search\Query
|
||||
*/
|
||||
protected function _searchWhereClause( $term = NULL, $tags = NULL, $method = 1 )
|
||||
{
|
||||
/* Do we have a term? */
|
||||
$where = array();
|
||||
if ( $term !== NULL )
|
||||
/* Boolean tags */
|
||||
if ( $tags !== NULL )
|
||||
{
|
||||
if( mb_strpos( $term, ' ' ) !== FALSE AND mb_strpos( $term, '+' ) === FALSE AND mb_strpos( $term, '-' ) === FALSE AND mb_strpos( $term, '"' ) === FALSE )
|
||||
$theTags = array();
|
||||
foreach( $tags as $tagEntry )
|
||||
{
|
||||
$terms = explode( ' ', $term );
|
||||
$newTerms = array();
|
||||
|
||||
foreach( $terms as $_term )
|
||||
foreach( explode( ' ', $tagEntry ) as $_term )
|
||||
{
|
||||
if( mb_strlen( $_term ) > 2 )
|
||||
{
|
||||
$newTerms[] = "+" . $_term;
|
||||
$theTags[] = "+" . $_term;
|
||||
}
|
||||
else
|
||||
{
|
||||
$newTerms[] = $_term;
|
||||
$theTags[] = $_term;
|
||||
}
|
||||
}
|
||||
|
||||
$term = implode( ' ', $newTerms );
|
||||
|
||||
$searchTags[] = implode( ' ', $theTags );
|
||||
}
|
||||
}
|
||||
|
||||
/* Try to guess what mode will be best for this query. If it looks like a MySQL boolean mode search, use that */
|
||||
if ( mb_substr( $term, 0, 1 ) === '+' or mb_substr( $term, -1, 1 ) === '*' or preg_match( '/^".*"$/', $term ) )
|
||||
/* Do we have a term? */
|
||||
$where = array();
|
||||
if ( $term !== NULL )
|
||||
{
|
||||
$indexes = 'index_content,index_title';
|
||||
if ( $method & static::TERM_TITLES_ONLY )
|
||||
{
|
||||
$mode = 'IN BOOLEAN MODE';
|
||||
$indexes = 'index_title';
|
||||
}
|
||||
/* Otherwise, we're just using the default mode - we tried using query expansion for short queries, but it didn't work well (lots of irrelevant results) */
|
||||
else
|
||||
|
||||
/* Default to 'AND' boolean mode search unless asked otherwise*/
|
||||
if ( ! ( $method & static::TERM_OR_MODE ) and ! static::termIsPhrase( $term ) )
|
||||
{
|
||||
$mode = '';
|
||||
$theWords = array();
|
||||
foreach( static::termAsWordsArray( $term ) as $_term )
|
||||
{
|
||||
$potentialOperator = mb_substr( $_term, 0, 1 );
|
||||
|
||||
if ( ! in_array( $potentialOperator, array( '-', '+', '~' ) ) )
|
||||
{
|
||||
if( mb_strlen( $_term ) > 2 )
|
||||
{
|
||||
$theWords[] = "+" . $_term;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$term = implode( ' ', $theWords );
|
||||
}
|
||||
|
||||
/* If we also have tags, create a combined where */
|
||||
if ( $tags !== NULL )
|
||||
{
|
||||
if ( $method === static::TERM_OR_TAGS )
|
||||
{
|
||||
$where[] = array( "( MATCH(index_content,index_title) AGAINST (? {$mode}) OR " . \IPS\Db::i()->findInSet( 'index_tags', $tags ) . ' )', $term );
|
||||
if ( $method & static::TERM_OR_TAGS )
|
||||
{
|
||||
$where[] = array( "( MATCH({$indexes}) AGAINST (? IN BOOLEAN MODE) ) OR " . \IPS\Db::i()->findInSet( 'index_tags', $tags ), $term );
|
||||
}
|
||||
else
|
||||
{
|
||||
$where[] = array( "MATCH(index_content,index_title) AGAINST (? {$mode})", $term );
|
||||
$where[] = array( \IPS\Db::i()->findInSet( 'index_tags', $tags ) );
|
||||
$where[] = array( "MATCH({$indexes}) AGAINST (? IN BOOLEAN MODE)", $term );
|
||||
$where[] = \IPS\Db::i()->findInSet( 'index_tags', $tags );
|
||||
}
|
||||
}
|
||||
/* Or just use the term */
|
||||
else
|
||||
{
|
||||
$where[] = array( "MATCH(index_content,index_title) AGAINST (? {$mode})", $term );
|
||||
$where[] = array( "MATCH({$indexes}) AGAINST (? IN BOOLEAN MODE)", $term );
|
||||
}
|
||||
}
|
||||
/* Or do we have tags? */
|
||||
elseif ( $tags !== NULL )
|
||||
{
|
||||
//$this->where[] = array( \IPS\Db::i()->findInSet( 'index_tags', $tags ) );
|
||||
|
||||
$tagWhere = array();
|
||||
$params = array();
|
||||
|
||||
foreach( $tags as $tag )
|
||||
{
|
||||
$where[] = \IPS\Db::i()->findInSet( 'index_tags', $tags );
|
||||
|
||||
if ( $this->order = 'calcscore DESC' )
|
||||
{
|
||||
array_push( $params, $tag, $tag, $tag, $tag );
|
||||
$tagWhere[] = "( index_tags=? OR index_tags LIKE CONCAT( ?, ',%' ) OR index_tags LIKE CONCAT( '%,', ? ) OR index_tags LIKE CONCAT( '%,', ?, ',%' ) )";
|
||||
$this->setOrder( static::ORDER_NEWEST_UPDATED );
|
||||
}
|
||||
|
||||
$where[] = array_merge( array( '(' . implode( ' OR ', $tagWhere ) . ')' ), $params );
|
||||
}
|
||||
|
||||
/* Only get stuff we have permission for */
|
||||
@@ -373,6 +725,12 @@ class _Query extends \IPS\Content\Search\Query
|
||||
$where[] = $this->hiddenClause;
|
||||
}
|
||||
|
||||
/* MOST PERTINENT search? */
|
||||
if ( mb_substr( $this->order, 0, 9 ) === 'calcscore' )
|
||||
{
|
||||
$this->select['main'] .= ',' . "( ( MATCH (index_title) AGAINST ('" . \IPS\Db::i()->escape_string( $term ) ."' IN BOOLEAN MODE) * 5 ) + (MATCH ({$indexes}) AGAINST ('" . \IPS\Db::i()->escape_string( $term ) ."' IN BOOLEAN MODE) ) ) / POWER( ( ( UNIX_TIMESTAMP( NOW() ) - index_date_updated ) / 3600 ) + 2, 1.5 ) AS calcscore";
|
||||
}
|
||||
|
||||
/* Return */
|
||||
return $where;
|
||||
}
|
||||
@@ -387,13 +745,14 @@ class _Query extends \IPS\Content\Search\Query
|
||||
*/
|
||||
public function search( $term = NULL, $tags = NULL, $method = 1 )
|
||||
{
|
||||
$query = \IPS\Db::i()->select( '*', 'core_search_index', array_merge( $this->where, $this->_searchWhereClause( $term, $tags, $method ) ), $this->order, array( $this->offset, $this->resultsToGet ), NULL, NULL, \IPS\Db::SELECT_SQL_CALC_FOUND_ROWS );
|
||||
foreach ( $this->joins as $table => $where )
|
||||
$where = array_merge( $this->where, $this->_searchWhereClause( $term, $tags, $method ) );
|
||||
$query = \IPS\Db::i()->select( implode( ', ', $this->select ), array( 'core_search_index', 'main' ), $where, $this->order, array( $this->offset, $this->resultsToGet ), NULL, NULL );
|
||||
foreach ( $this->joins as $data )
|
||||
{
|
||||
$query->join( $table, $where );
|
||||
$query->join( $data['from'], $data['where'], isset( $data['type'] ) ? $data['type'] : 'LEFT' );
|
||||
}
|
||||
|
||||
$count = $query->count( TRUE );
|
||||
|
||||
$count = $this->count( $term, $tags, $method );
|
||||
return new \IPS\Content\Search\Results( iterator_to_array( $query ), $count );
|
||||
}
|
||||
|
||||
@@ -407,6 +766,13 @@ class _Query extends \IPS\Content\Search\Query
|
||||
*/
|
||||
public function count( $term = NULL, $tags = NULL, $method = 1 )
|
||||
{
|
||||
return \IPS\Db::i()->select( 'COUNT(*)', 'core_search_index', array_merge( $this->where, $this->_searchWhereClause( $term, $tags, $method ) ) )->first();
|
||||
$query = \IPS\Db::i()->select( 'COUNT(*)', array( 'core_search_index', 'main' ), array_merge( $this->where, $this->_searchWhereClause( $term, $tags, $method ) ) );
|
||||
|
||||
foreach ( $this->joins as $data )
|
||||
{
|
||||
$query->join( $data['from'], $data['where'], isset( $data['type'] ) ? $data['type'] : 'LEFT' );
|
||||
}
|
||||
|
||||
return $query->first();
|
||||
}
|
||||
}
|
||||
+116
-105
@@ -25,6 +25,8 @@ abstract class _Query
|
||||
{
|
||||
const TERM_OR_TAGS = 1;
|
||||
const TERM_AND_TAGS = 2;
|
||||
const TERM_OR_MODE = 4;
|
||||
const TERM_TITLES_ONLY = 8;
|
||||
|
||||
const HIDDEN_VISIBLE = 0;
|
||||
const HIDDEN_UNAPPROVED = 1;
|
||||
@@ -34,6 +36,8 @@ abstract class _Query
|
||||
const ORDER_NEWEST_UPDATED = 1;
|
||||
const ORDER_NEWEST_CREATED = 2;
|
||||
const ORDER_RELEVANCY = 3;
|
||||
const ORDER_OLDEST_UPDATED = 4;
|
||||
const ORDER_OLDEST_CREATED = 5;
|
||||
|
||||
const SUPPORTS_JOIN_FILTERS = TRUE;
|
||||
|
||||
@@ -45,8 +49,7 @@ abstract class _Query
|
||||
*/
|
||||
public static function init( \IPS\Member $member = NULL )
|
||||
{
|
||||
$class = '\\IPS\\Content\\Search\\' . ucfirst( \IPS\Settings::i()->search_engine ) . '\\Query';
|
||||
return new $class( $member ?: \IPS\Member::loggedIn() );
|
||||
return new \IPS\Content\Search\Mysql\Query( $member ?: \IPS\Member::loggedIn() );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -58,7 +61,7 @@ abstract class _Query
|
||||
* @brief The member performing the search
|
||||
*/
|
||||
protected $member;
|
||||
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
@@ -69,129 +72,90 @@ abstract class _Query
|
||||
{
|
||||
$this->member = $member;
|
||||
|
||||
/* Exclude hidden items */
|
||||
if ( !$member->modPermission('can_view_hidden_content') )
|
||||
{
|
||||
$this->setHiddenFilter( static::HIDDEN_VISIBLE );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Exclude "first post" content items
|
||||
* Useful for cases such as activity streams where you do not want both the topic and the first post to show
|
||||
*
|
||||
* @return \IPS\Content\Search\Query (for daisy chaining)
|
||||
*/
|
||||
public function excludeFirstPostContentItems()
|
||||
{
|
||||
$classes = array();
|
||||
foreach ( \IPS\Content::routedClasses( TRUE, FALSE, TRUE ) as $class )
|
||||
{
|
||||
if ( in_array( 'IPS\Content\Searchable', class_implements( $class ) ) and $class::$firstCommentRequired )
|
||||
{
|
||||
$classes[] = $class;
|
||||
}
|
||||
}
|
||||
|
||||
if ( !empty( $classes ) )
|
||||
/* Exclude disabled applications */
|
||||
$filters = array();
|
||||
foreach ( \IPS\Application::applications() as $application )
|
||||
{
|
||||
return $this->filterByExcludeContent( $classes );
|
||||
if ( !$application->enabled )
|
||||
{
|
||||
foreach ( $application->extensions( 'core', 'ContentRouter' ) as $extension )
|
||||
{
|
||||
foreach ( $extension->classes as $class )
|
||||
{
|
||||
$filters[] = \IPS\Content\Search\ContentFilter::init( $class );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
if ( !empty( $filters ) )
|
||||
{
|
||||
return $this;
|
||||
$this->filterByContent( $filters, FALSE );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Exclude disabled applications
|
||||
* Filter by multiple content types
|
||||
*
|
||||
* @param array $contentFilters Array of \IPS\Content\Search\ContentFilter objects
|
||||
* @param bool $type TRUE means only include results matching the filters, FALSE means exclude all results matching the filters
|
||||
* @return \IPS\Content\Search\Query (for daisy chaining)
|
||||
*/
|
||||
public function excludeDisabledApps()
|
||||
{
|
||||
$includeClasses = array();
|
||||
foreach ( \IPS\Content::routedClasses( TRUE, FALSE, TRUE ) as $class )
|
||||
{
|
||||
if ( in_array( 'IPS\Content\Searchable', class_implements( $class ) ) )
|
||||
{
|
||||
$includeClasses[] = $class;
|
||||
|
||||
if ( isset( $class::$commentClass ) )
|
||||
{
|
||||
$includeClasses[] = $class::$commentClass;
|
||||
}
|
||||
if ( isset( $class::$reviewClass ) )
|
||||
{
|
||||
$includeClasses[] = $class::$reviewClass;
|
||||
}
|
||||
}
|
||||
}
|
||||
$allClasses = array();
|
||||
foreach ( \IPS\Content::routedClasses( FALSE, FALSE, TRUE ) as $class )
|
||||
{
|
||||
if ( in_array( 'IPS\Content\Searchable', class_implements( $class ) ) )
|
||||
{
|
||||
$allClasses[] = $class;
|
||||
|
||||
if ( isset( $class::$commentClass ) )
|
||||
{
|
||||
$allClasses[] = $class::$commentClass;
|
||||
}
|
||||
if ( isset( $class::$reviewClass ) )
|
||||
{
|
||||
$allClasses[] = $class::$reviewClass;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$classes = array_diff( $allClasses, $includeClasses );
|
||||
abstract public function filterByContent( array $contentFilters, $type = TRUE );
|
||||
|
||||
if ( !empty( $classes ) )
|
||||
{
|
||||
return $this->filterByExcludeContent( $classes );
|
||||
}
|
||||
else
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter by content type
|
||||
*
|
||||
* @param string $class The type of content to search (including all comment/review classes)
|
||||
* @param array|NULL $containers An array of container IDs to filter by, or NULL to not filter by containers
|
||||
* @param int|NULL $minimumComments The minimum number of comments
|
||||
* @param int|NULL $minimumReviews The minimum number of reviews
|
||||
* @param int|NULL $minimumViews The minimum number of views
|
||||
* @return \IPS\Content\Search\Query (for daisy chaining)
|
||||
*/
|
||||
abstract public function filterByContent( $class, $containers = NULL, $minimumComments = NULL, $minimumReviews = NULL, $minimumViews = NULL );
|
||||
|
||||
/**
|
||||
* Filter by content type to exclude
|
||||
*
|
||||
* @param string $classes The types of content to search (not including all comment/review classes)
|
||||
* @param bool $allowedContiners If TRUE, $class should be an array of allowed containers for that class - e.g. array( 'IPS\forums\Topic' => array( 1, 2 ) - otherwise, a simple array - e.g. array( 'IPS\forums\Topic' )
|
||||
* @return \IPS\Content\Search\Query (for daisy chaining)
|
||||
*/
|
||||
abstract public function filterByExcludeContent( array $classes, $allowedContiners=FALSE );
|
||||
|
||||
/**
|
||||
* Filter by content item
|
||||
*
|
||||
* @param \IPS\Content\Item $item The item
|
||||
* @return \IPS\Content\Search\Query (for daisy chaining)
|
||||
*/
|
||||
abstract public function filterByItem( \IPS\Content\Item $item );
|
||||
|
||||
/**
|
||||
* Filter by author
|
||||
*
|
||||
* @param \IPS\Member|int|array $author The author, or an array of author IDs
|
||||
* @return \IPS\Content\Search\Query (for daisy chaining)
|
||||
*/
|
||||
abstract public function filterByAuthor( $author );
|
||||
|
||||
/**
|
||||
* Filter for profile
|
||||
*
|
||||
* @param \IPS\Member $member The member whose profile is being viewed
|
||||
* @return \IPS\Content\Search\Query (for daisy chaining)
|
||||
*/
|
||||
abstract public function filterForProfile( \IPS\Member $member );
|
||||
|
||||
/**
|
||||
* Filter by item author
|
||||
*
|
||||
* @param \IPS\Member $author The author
|
||||
* @return \IPS\Content\Search\Query (for daisy chaining)
|
||||
*/
|
||||
abstract public function filterByAuthor( \IPS\Member $author );
|
||||
abstract public function filterByItemAuthor( \IPS\Member $author );
|
||||
|
||||
/**
|
||||
* Filter by content the user follows
|
||||
*
|
||||
* @param bool $includeContainers Include content in containers the user follows?
|
||||
* @param bool $includeItems Include items and comments/reviews on items the user follows?
|
||||
* @param bool $includeContainers Include content posted by members the user follows?
|
||||
* @return \IPS\Content\Search\Query (for daisy chaining)
|
||||
*/
|
||||
abstract public function filterByFollowed( $includeContainers, $includeItems, $includeMembers );
|
||||
|
||||
/**
|
||||
* Filter by content the user has posted in
|
||||
*
|
||||
* @return \IPS\Content\Search\Query (for daisy chaining)
|
||||
*/
|
||||
abstract public function filterByItemsIPostedIn();
|
||||
|
||||
/**
|
||||
* Filter by content the user has not read
|
||||
*
|
||||
* @note If applicable, it is more efficient to call filterByContent() before calling this method
|
||||
* @return \IPS\Content\Search\Query (for daisy chaining)
|
||||
*/
|
||||
abstract public function filterByUnread();
|
||||
|
||||
/**
|
||||
* Filter by start date
|
||||
@@ -279,4 +243,51 @@ abstract class _Query
|
||||
{
|
||||
return $this->search( $term, $tags, $method )->count( TRUE );
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this term a phrase?
|
||||
*
|
||||
* @param string $term The term to search for
|
||||
* @return boolean
|
||||
*/
|
||||
public static function termIsPhrase( $term )
|
||||
{
|
||||
return (boolean) preg_match( '#^".*"$#', $term );
|
||||
}
|
||||
|
||||
/**
|
||||
* Term as words array
|
||||
* Returns an array of words. A word is considered as either an entire phrase (eg "sticks stones") when $ignorePhrase is FALSE, or a sequence of characters delimited by a space.
|
||||
* Words smaller than 2 characters are ignored.
|
||||
*
|
||||
* @param string $term The term to search for
|
||||
* @param boolean $ignorePhrase When true, phrases are stripped of quotes and treated as normal words
|
||||
* @param int $minLength The minimum length a sequence of characters has to be before it is considered a word
|
||||
* @return array
|
||||
*/
|
||||
public static function termAsWordsArray( $term, $ignorePhrase=FALSE, $minLength=3 )
|
||||
{
|
||||
if ( static::termIsPhrase( $term ) )
|
||||
{
|
||||
if ( ! $ignorePhrase )
|
||||
{
|
||||
return array( $term );
|
||||
}
|
||||
else
|
||||
{
|
||||
$term = str_replace( '"', '', $term );
|
||||
}
|
||||
}
|
||||
|
||||
$theWords = array();
|
||||
foreach( explode( ' ', $term ) as $_term )
|
||||
{
|
||||
if ( mb_strlen( $_term ) > $minLength )
|
||||
{
|
||||
$theWords[] = $_term;
|
||||
}
|
||||
}
|
||||
|
||||
return $theWords;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Search Result from Index
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - SVN_YYYY Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Social Suite
|
||||
* @since 15 Sep 2015
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS\Content\Search\Result;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search Result
|
||||
*/
|
||||
class _Content extends \IPS\Content\Search\Result
|
||||
{
|
||||
/**
|
||||
* Index Data
|
||||
*/
|
||||
protected $indexData;
|
||||
|
||||
/**
|
||||
* Author Data
|
||||
*/
|
||||
protected $authorData;
|
||||
|
||||
/**
|
||||
* Item Data
|
||||
*/
|
||||
protected $itemData;
|
||||
|
||||
/**
|
||||
* Author Data
|
||||
*/
|
||||
protected $containerData;
|
||||
|
||||
/**
|
||||
* Review Rating
|
||||
*/
|
||||
protected $reviewRating;
|
||||
|
||||
/**
|
||||
* If the user has posted in the item
|
||||
*/
|
||||
protected $iPostedIn;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param array $indexData Data from index
|
||||
* @param array $authorData Basic data about the author. Only includes columns returned by \IPS\Member::columnsForPhoto()
|
||||
* @param array $itemData Basic data about the item. Only includes columns returned by item::basicDataColumns()
|
||||
* @param array|NULL $containerData Basic data about the author. Only includes columns returned by container::basicDataColumns()
|
||||
* @param array $reputationData Array of people who have given reputation and the reputation they gave
|
||||
* @param int|NULL $reviewRating If this is a review, the rating
|
||||
* @param bool $iPostedIn If the user has posted in the item
|
||||
* @return void
|
||||
*/
|
||||
public function __construct( array $indexData, array $authorData, array $itemData, array $containerData = NULL, array $reputationData, $reviewRating = NULL, $iPostedIn = FALSE )
|
||||
{
|
||||
$this->createdDate = \IPS\DateTime::ts( $indexData['index_date_created'] );
|
||||
$this->lastUpdatedDate = \IPS\DateTime::ts( $indexData['index_date_updated'] );
|
||||
$this->indexData = $indexData;
|
||||
$this->authorData = $authorData;
|
||||
$this->itemData = $itemData;
|
||||
$this->containerData = $containerData;
|
||||
$this->reputationData = $reputationData;
|
||||
$this->reviewRating = $reviewRating;
|
||||
$this->iPostedIn = $iPostedIn;
|
||||
}
|
||||
|
||||
/**
|
||||
* HTML
|
||||
*
|
||||
* @param string $view 'expanded' or 'condensed'
|
||||
* @param bool $asItem Displaying results as items?
|
||||
* @return string
|
||||
*/
|
||||
public function html( $view = 'expanded', $asItem = FALSE )
|
||||
{
|
||||
return call_user_func( array( $this->indexData['index_class'], 'searchResult' ), $this->indexData, $this->authorData, $this->itemData, $this->containerData, $this->reputationData, $this->reviewRating, $this->iPostedIn, $view, $asItem );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add to RSS feed
|
||||
*
|
||||
* @param \IPS\Xml\Rss $document Document to add to
|
||||
* @return string
|
||||
*/
|
||||
public function addToRssFeed( \IPS\Xml\Rss $document )
|
||||
{
|
||||
$class = $this->indexData['index_class'];
|
||||
$object = $class::load( $this->indexData['index_object_id'] );
|
||||
$document->addItem( $object->mapped('title'), $object->url(), $this->indexData['index_content'], \IPS\DateTime::ts( $this->indexData['index_date_created'] ) );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Search Result not from Index
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - SVN_YYYY Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Social Suite
|
||||
* @since 15 Sep 2015
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS\Content\Search\Result;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search Result not from Index
|
||||
*/
|
||||
class _Custom extends \IPS\Content\Search\Result
|
||||
{
|
||||
/**
|
||||
* @brief HTML
|
||||
*/
|
||||
protected $html;
|
||||
|
||||
/**
|
||||
* @brief Image
|
||||
*/
|
||||
protected $image;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param \IPS\DateTime $time The time for this result
|
||||
* @param string $html HTML to display
|
||||
* @param string|NULL $image HTML for image to display
|
||||
* @return void
|
||||
*/
|
||||
public function __construct( \IPS\DateTime $time, $html, $image = NULL )
|
||||
{
|
||||
$this->createdDate = $time;
|
||||
$this->lastUpdatedDate = $time;
|
||||
$this->html = $html;
|
||||
$this->image = $image;
|
||||
}
|
||||
|
||||
/**
|
||||
* HTML
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function html( $view = 'expanded' )
|
||||
{
|
||||
return \IPS\Theme::i()->getTemplate( 'streams', 'core' )->extraItem( $this->createdDate, $this->image, $this->html, $view );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Search Result
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - SVN_YYYY Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Social Suite
|
||||
* @since 15 Sep 2015
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS\Content\Search;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search Result
|
||||
*/
|
||||
abstract class _Result
|
||||
{
|
||||
/**
|
||||
* @brief Created Date
|
||||
*/
|
||||
public $createdDate;
|
||||
|
||||
/**
|
||||
* @brief Last Updated Date
|
||||
*/
|
||||
public $lastUpdatedDate;
|
||||
|
||||
/**
|
||||
* Separator for activity streams - "past hour", "today", etc.
|
||||
*
|
||||
* @param bool $createdDate If TRUE, uses $createdDate, otherwise uses $lastUpdatedDate
|
||||
* @return string
|
||||
*/
|
||||
public function streamSeparator( $createdDate=TRUE )
|
||||
{
|
||||
$date = $createdDate ? $this->createdDate : $this->lastUpdatedDate;
|
||||
|
||||
$now = \IPS\DateTime::ts( time() );
|
||||
$yesterday = clone $now;
|
||||
$yesterday = $yesterday->sub( new \DateInterval('P1D') );
|
||||
$diff = $date->diff( $now );
|
||||
if ( $diff->h < 1 && !$diff->d && !$diff->m )
|
||||
{
|
||||
return 'past_hour';
|
||||
}
|
||||
elseif ( $date->format('Y-m-d') == $now->format('Y-m-d') )
|
||||
{
|
||||
return 'today';
|
||||
}
|
||||
elseif ( $date->format('Y-m-d') == $yesterday->format('Y-m-d') )
|
||||
{
|
||||
return 'yesterday';
|
||||
}
|
||||
elseif ( !$diff->y and !$diff->m and $diff->d < 7 )
|
||||
{
|
||||
return 'last_week';
|
||||
}
|
||||
else
|
||||
{
|
||||
return 'earlier';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,11 +33,41 @@ class _Results extends \ArrayIterator
|
||||
*/
|
||||
protected $initated = FALSE;
|
||||
|
||||
/**
|
||||
* @brief Author data
|
||||
*/
|
||||
protected $authorData = array();
|
||||
|
||||
/**
|
||||
* @brief Item titles
|
||||
*/
|
||||
protected $itemData = array();
|
||||
|
||||
/**
|
||||
* @brief Container data
|
||||
*/
|
||||
protected $containerData = array();
|
||||
|
||||
/**
|
||||
* @brief Reputation data
|
||||
*/
|
||||
protected $reputationData = array();
|
||||
|
||||
/**
|
||||
* @brief Review Ratings
|
||||
*/
|
||||
protected $reviewRatings = array();
|
||||
|
||||
/**
|
||||
* @brief Index IDs of items I have posted in
|
||||
*/
|
||||
protected $iPostedIn = array();
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param array $results The results
|
||||
* @param int $countAllRows Count for all rows
|
||||
* @param array $results The results
|
||||
* @param int $countAllRows Count for all rows
|
||||
* @return void
|
||||
*/
|
||||
public function __construct( $results, $countAllRows )
|
||||
@@ -51,34 +81,107 @@ class _Results extends \ArrayIterator
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function init( $loadReputation=FALSE )
|
||||
public function init()
|
||||
{
|
||||
$classesToLoad = array( 'IPS\Member' => array() );
|
||||
/* Init */
|
||||
$membersToLoad = array();
|
||||
$itemsToLoad = array();
|
||||
$containersToLoad = array();
|
||||
$reputationIds = array();
|
||||
$reviewRatings = array();
|
||||
$itemIndexIds = array();
|
||||
|
||||
foreach ( $this->getArrayCopy() as $row )
|
||||
/* Loop */
|
||||
foreach ( $this->getArrayCopy() as $result )
|
||||
{
|
||||
$classesToLoad[ 'IPS\Member' ][ $row['index_author'] ] = $row['index_author'];
|
||||
|
||||
$classesToLoad[ $row['index_class'] ][ $row['index_object_id'] ] = $row['index_object_id'];
|
||||
$itemClass = NULL;
|
||||
if ( $row['index_item_id'] and in_array( 'IPS\Content\Comment', class_parents( $row['index_class'] ) ) )
|
||||
if ( $result['index_author'] )
|
||||
{
|
||||
$itemClass = $row['index_class']::$itemClass;
|
||||
$classesToLoad[ $itemClass ][ $row['index_item_id'] ] = $row['index_item_id'];
|
||||
}
|
||||
if ( $row['index_container_id'] )
|
||||
{
|
||||
$containerClass = $itemClass ? $itemClass::$containerNodeClass : $row['index_class']::$containerNodeClass;
|
||||
$classesToLoad[ $containerClass ][ $row['index_container_id'] ] = $row['index_container_id'];
|
||||
$membersToLoad[ $result['index_author'] ] = $result['index_author'];
|
||||
}
|
||||
|
||||
if ( $loadReputation and in_array( 'IPS\Content\Reputation', class_implements( $row['index_class'] ) ) )
|
||||
if ( in_array( 'IPS\Content\Comment', class_parents( $result['index_class'] ) ) )
|
||||
{
|
||||
$reputationIds[ $row['index_class'] ][ $row['index_object_id'] ] = $row['index_object_id'];
|
||||
$commentClass = $result['index_class'];
|
||||
$itemsToLoad[ $commentClass::$itemClass ][ $result['index_item_id'] ] = $result['index_item_id'];
|
||||
}
|
||||
else
|
||||
{
|
||||
$itemsToLoad[ $result['index_class'] ][ $result['index_item_id'] ] = $result['index_item_id'];
|
||||
}
|
||||
|
||||
if ( $result['index_container_id'] )
|
||||
{
|
||||
$class = $result['index_class'];
|
||||
if ( $class == 'IPS\core\Statuses\Status' )
|
||||
{
|
||||
if ( $result['index_container_id'] != $result['index_author'] )
|
||||
{
|
||||
$membersToLoad[ $result['index_container_id'] ] = $result['index_container_id'];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$itemClass = in_array( 'IPS\Content\Comment', class_parents( $class ) ) ? $class::$itemClass : $class;
|
||||
if ( isset( $itemClass::$containerNodeClass ) )
|
||||
{
|
||||
$containerClass = $itemClass::$containerNodeClass;
|
||||
if ( isset( $containerClass::$seoTitleColumn ) )
|
||||
{
|
||||
$containersToLoad[ $itemClass::$containerNodeClass ][ $result['index_container_id'] ] = $result['index_container_id'];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( in_array( 'IPS\Content\Reputation', class_implements( $result['index_class'] ) ) )
|
||||
{
|
||||
$reputationIds[ $result['index_class'] ][ $result['index_object_id'] ] = $result['index_object_id'];
|
||||
}
|
||||
|
||||
if ( in_array( 'IPS\Content\Review', class_parents( $result['index_class'] ) ) )
|
||||
{
|
||||
$reviewRatings[ $result['index_class'] ][ $result['index_object_id'] ] = $result['index_object_id'];
|
||||
}
|
||||
|
||||
if ( $result['index_item_index_id'] )
|
||||
{
|
||||
$itemIndexIds[] = $result['index_item_index_id'];
|
||||
}
|
||||
}
|
||||
|
||||
/* Load item data */
|
||||
foreach ( $itemsToLoad as $itemClass => $itemIds )
|
||||
{
|
||||
foreach ( \IPS\Db::i()->select( $itemClass::basicDataColumns(), $itemClass::$databaseTable, \IPS\Db::i()->in( $itemClass::$databasePrefix . $itemClass::$databaseColumnId, $itemIds ) )->setKeyField( $itemClass::$databasePrefix . $itemClass::$databaseColumnId ) as $itemId => $itemData )
|
||||
{
|
||||
$this->itemData[ $itemClass ][ $itemId ] = $itemData;
|
||||
|
||||
if ( isset( $itemData[ $itemClass::$databasePrefix . $itemClass::$databaseColumnMap['author'] ] ) )
|
||||
{
|
||||
$memberId = $itemData[ $itemClass::$databasePrefix . $itemClass::$databaseColumnMap['author'] ];
|
||||
$membersToLoad[ $memberId ] = $memberId;
|
||||
}
|
||||
}
|
||||
|
||||
if ( method_exists( $itemClass, 'searchResultExtraData' ) )
|
||||
{
|
||||
foreach ( $itemClass::searchResultExtraData( $this->itemData[ $itemClass ] ) as $id => $extraData )
|
||||
{
|
||||
$this->itemData[ $itemClass ][ $id ]['extra'] = $extraData;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Load author data */
|
||||
$this->authorData = iterator_to_array( \IPS\Db::i()->select( \IPS\Member::columnsForPhoto(), 'core_members', \IPS\Db::i()->in( 'member_id', $membersToLoad ) )->setKeyField( 'member_id' ) );
|
||||
|
||||
/* Load container data */
|
||||
foreach ( $containersToLoad as $containerClass => $containerIds )
|
||||
{
|
||||
$this->containerData[ $containerClass ] = iterator_to_array( \IPS\Db::i()->select( $containerClass::basicDataColumns(), $containerClass::$databaseTable, \IPS\Db::i()->in( $containerClass::$databasePrefix . $containerClass::$databaseColumnId, $containerIds ) )->setKeyField( $containerClass::$databasePrefix . $containerClass::$databaseColumnId ) );
|
||||
}
|
||||
|
||||
/* Load reputation data */
|
||||
$reputation = array();
|
||||
if ( count( $reputationIds ) )
|
||||
{
|
||||
@@ -103,37 +206,25 @@ class _Results extends \ArrayIterator
|
||||
break;
|
||||
}
|
||||
|
||||
foreach ( \IPS\Db::i()->select( '*', 'core_reputation_index', $where ) as $rep )
|
||||
foreach ( \IPS\Db::i()->select( array( 'app', 'type', 'type_id', 'member_id', 'rep_rating' ), 'core_reputation_index', $where ) as $rep )
|
||||
{
|
||||
$reputation[ $rep['app'] ][ $rep['type'] ][ $rep['type_id'] ][ $rep['member_id'] ] = $rep;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ( $classesToLoad as $class => $ids )
|
||||
{
|
||||
$ids = array_diff( $ids, $class::multitonIds() );
|
||||
$idColumn = $class::$databaseColumnId;
|
||||
|
||||
if ( !empty( $ids ) )
|
||||
{
|
||||
foreach ( \IPS\Db::i()->select( '*', $class::$databaseTable, \IPS\Db::i()->in( $class::$databasePrefix . $class::$databaseColumnId, $ids ) ) as $row )
|
||||
{
|
||||
$obj = $class::constructFromData( $row );
|
||||
if ( $loadReputation and $obj instanceof \IPS\Content\Reputation )
|
||||
{
|
||||
if ( isset( $reputation[ $class::$application ][ $class::$reputationType ][ $obj->$idColumn ] ) )
|
||||
{
|
||||
$obj->reputation = $reputation[ $class::$application ][ $class::$reputationType ][ $obj->$idColumn ];
|
||||
}
|
||||
else
|
||||
{
|
||||
$obj->reputation = array();
|
||||
}
|
||||
}
|
||||
}
|
||||
$this->reputationData[ $rep['app'] ][ $rep['type'] ][ $rep['type_id'] ][ $rep['member_id'] ] = $rep['rep_rating'];
|
||||
}
|
||||
}
|
||||
|
||||
/* Load rating reviews */
|
||||
foreach ( $reviewRatings as $reviewClass => $reviewIds )
|
||||
{
|
||||
$this->reviewRatings[ $reviewClass ] = iterator_to_array( \IPS\Db::i()->select( array( $reviewClass::$databasePrefix . $reviewClass::$databaseColumnId, $reviewClass::$databasePrefix . $reviewClass::$databaseColumnMap['rating'] ), $reviewClass::$databaseTable, \IPS\Db::i()->in( $reviewClass::$databasePrefix . $reviewClass::$databaseColumnId, $reviewIds ) )->setKeyField( $reviewClass::$databasePrefix . $reviewClass::$databaseColumnId )->setValueField( $reviewClass::$databasePrefix . $reviewClass::$databaseColumnMap['rating'] ) );
|
||||
}
|
||||
|
||||
/* Load data for the "I posted in this" stars */
|
||||
if ( count( $itemIndexIds ) and \IPS\Member::loggedIn()->member_id )
|
||||
{
|
||||
$this->iPostedIn = iterator_to_array( \IPS\Db::i()->select( 'index_item_index_id', 'core_search_index', array( array( \IPS\Db::i()->in( 'index_item_index_id', $itemIndexIds ) ), array( 'index_author=?', \IPS\Member::loggedIn()->member_id ) ), NULL, NULL, NULL, NULL, \IPS\Db::SELECT_DISTINCT ) );
|
||||
}
|
||||
|
||||
/* Set that we're initiated */
|
||||
$this->initated = TRUE;
|
||||
}
|
||||
|
||||
@@ -144,8 +235,41 @@ class _Results extends \ArrayIterator
|
||||
*/
|
||||
public function current()
|
||||
{
|
||||
$row = parent::current();
|
||||
return call_user_func( array( $row['index_class'], 'load' ), $row['index_object_id'] );
|
||||
$data = parent::current();
|
||||
|
||||
$class = $data['index_class'];
|
||||
$itemClass = in_array( 'IPS\Content\Comment', class_parents( $class ) ) ? $class::$itemClass : $class;
|
||||
$containerClass = isset( $itemClass::$containerNodeClass ) ? $itemClass::$containerNodeClass : NULL;
|
||||
|
||||
$reputationData = array();
|
||||
if ( in_array( 'IPS\Content\Reputation', class_implements( $class ) ) and isset( $this->reputationData[ $class::$application ][ $class::$reputationType ][ $data['index_object_id'] ] ) )
|
||||
{
|
||||
$reputationData = $this->reputationData[ $class::$application ][ $class::$reputationType ][ $data['index_object_id'] ];
|
||||
}
|
||||
|
||||
$itemData = $this->itemData[ $itemClass ][ $data['index_item_id'] ];
|
||||
if ( $class == 'IPS\core\Statuses\Status' and $data['index_container_id'] != $data['index_author'] )
|
||||
{
|
||||
$itemData['profile'] = $this->authorData[ $data['index_container_id'] ];
|
||||
}
|
||||
if ( isset( $itemData[ $itemClass::$databasePrefix . $itemClass::$databaseColumnMap['author'] ] ) )
|
||||
{
|
||||
$memberId = $itemData[ $itemClass::$databasePrefix . $itemClass::$databaseColumnMap['author'] ];
|
||||
if ( isset( $this->authorData[ $memberId ] ) )
|
||||
{
|
||||
$itemData['author'] = $this->authorData[ $memberId ];
|
||||
}
|
||||
}
|
||||
|
||||
return new Result\Content(
|
||||
$data,
|
||||
isset( $this->authorData[ $data['index_author'] ] ) ? $this->authorData[ $data['index_author'] ] : array( 'member_id' => 0, 'name' => \IPS\Member::loggedIn()->language()->addToStack('guest'), 'members_seo_name' => '', 'pp_main_photo' => NULL, 'pp_photo_type' => 'none' ),
|
||||
$itemData,
|
||||
( $containerClass and isset( $this->containerData[ $containerClass ] ) and isset( $this->containerData[ $containerClass ][ $data['index_container_id'] ] ) ) ? $this->containerData[ $containerClass ][ $data['index_container_id'] ] : NULL,
|
||||
$reputationData,
|
||||
isset( $this->reviewRatings[ $data['index_class'] ][ $data['index_object_id'] ] ) ? $this->reviewRatings[ $data['index_class'] ][ $data['index_object_id'] ] : NULL,
|
||||
in_array( $data['index_item_index_id'], $this->iPostedIn )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -179,4 +303,268 @@ class _Results extends \ArrayIterator
|
||||
return parent::count();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add in "extra" items
|
||||
*
|
||||
* @param array $extra Types ('register', 'follow_member', 'follow_content', 'photo', 'like', 'rep_neg')
|
||||
* @param \IPS\Member|NULL $author The author to limit extra items to
|
||||
* @param \IPS\DateTime|NULL $lastTime If provided, only items since this date is included. If NULL, it works out which to include based on what results are being shown
|
||||
* @param \IPS\DateTime|NULL $firstTime If provided, only items before this date is included. If NULL, it works out which to include based on what results are being shown
|
||||
* @return array
|
||||
* @note Each thing is limited to 10 items. Even though this may result in accuracies, the limit is necessary so that if there's more extra items that content it doesn't create lots of queries and slow loading
|
||||
*/
|
||||
public function addExtraItems( $extra, \IPS\Member $author = NULL, $lastTime=NULL, $firstTime = NULL )
|
||||
{
|
||||
/* Work out the timestamps */
|
||||
$results = iterator_to_array( $this );
|
||||
if ( $firstTime )
|
||||
{
|
||||
$firstTime = $firstTime->getTimestamp();
|
||||
}
|
||||
elseif ( isset( \IPS\Request::i()->page ) and \IPS\Request::i()->page != 1 )
|
||||
{
|
||||
foreach ( $results as $result )
|
||||
{
|
||||
$firstTime = $result->createdDate->getTimestamp();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ( $lastTime )
|
||||
{
|
||||
$lastTime = $lastTime->getTimestamp();
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach ( $results as $result )
|
||||
{
|
||||
$lastTime = $result->createdDate->getTimestamp();
|
||||
}
|
||||
}
|
||||
|
||||
/* We need at least a last time */
|
||||
if ( !$lastTime )
|
||||
{
|
||||
return $results;
|
||||
}
|
||||
|
||||
/* Get the extra items... */
|
||||
$extraItems = array();
|
||||
if ( $lastTime )
|
||||
{
|
||||
/* Users registered */
|
||||
if ( in_array( 'register', $extra ) )
|
||||
{
|
||||
$where = array( array( 'joined>?', $lastTime ), array( 'name<>?', '' ) );
|
||||
if ( $firstTime )
|
||||
{
|
||||
$where[] = array( 'joined<?', $firstTime );
|
||||
}
|
||||
if ( $author )
|
||||
{
|
||||
$where[] = array( 'core_members.member_id=?', $author->member_id );
|
||||
}
|
||||
foreach ( \IPS\Db::i()->select( 'core_members.member_id, core_members.name, core_members.members_seo_name, core_members.joined, core_validating.new_reg', 'core_members', $where, 'joined DESC', 10 )
|
||||
->join( 'core_validating', 'core_validating.member_id=core_members.member_id' ) as $member )
|
||||
{
|
||||
if ( empty( $member['new_reg'] ) )
|
||||
{
|
||||
$extraItems[] = new Result\Custom( \IPS\DateTime::ts( $member['joined'] ), \IPS\Member::loggedIn()->language()->addToStack( 'activity_member_joined', FALSE, array( 'htmlsprintf' => \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' )->userLinkFromData( $member['member_id'], $member['name'], $member['members_seo_name'] ) ) ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Users changed photo */
|
||||
if ( in_array( 'photo', $extra ) )
|
||||
{
|
||||
$where = array( array( 'photo_last_update>?', $lastTime ) );
|
||||
if ( $firstTime )
|
||||
{
|
||||
$where[] = array( 'photo_last_update<?', $firstTime );
|
||||
}
|
||||
if ( $author )
|
||||
{
|
||||
$where[] = array( 'member_id=?', $author->member_id );
|
||||
}
|
||||
foreach ( \IPS\Db::i()->select( array_merge( \IPS\Member::columnsForPhoto(), array( 'photo_last_update' ) ), 'core_members', $where, 'photo_last_update DESC', 10 ) as $member )
|
||||
{
|
||||
$extraItems[] = new Result\Custom(
|
||||
\IPS\DateTime::ts( $member['photo_last_update'] ),
|
||||
\IPS\Member::loggedIn()->language()->addToStack( 'activity_member_updated_photo', FALSE, array( 'htmlsprintf' => \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' )->userLinkFromData( $member['member_id'], $member['name'], $member['members_seo_name'] ) ) ),
|
||||
\IPS\Theme::i()->getTemplate( 'global', 'core', 'front' )->userPhotoFromData( $member['member_id'], $member['name'], $member['members_seo_name'], \IPS\Member::photoUrl( $member ), 'tiny' )
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/* Follow */
|
||||
if ( in_array( 'follow_member', $extra ) or in_array( 'follow_content', $extra ) )
|
||||
{
|
||||
$where = array( array( 'follow_is_anon=0' ), array( 'follow_added>?', $lastTime ) );
|
||||
if ( $firstTime )
|
||||
{
|
||||
$where[] = array( 'follow_added<?', $firstTime );
|
||||
}
|
||||
if ( in_array( 'follow_member', $extra ) xor in_array( 'follow_content', $extra ) )
|
||||
{
|
||||
if ( in_array( 'follow_member', $extra ) )
|
||||
{
|
||||
$where[] = array( 'follow_app=? AND follow_area=?', 'core', 'member' );
|
||||
}
|
||||
else
|
||||
{
|
||||
$where[] = array( '( follow_app!=? OR follow_area!=? )', 'core', 'member' );
|
||||
}
|
||||
}
|
||||
if ( $author )
|
||||
{
|
||||
if ( in_array( 'follow_member', $extra ) and in_array( 'follow_content', $extra ) )
|
||||
{
|
||||
$where[] = array( '( follow_member_id=? OR ( follow_app=? AND follow_area=? AND follow_rel_id=? ) )', $author->member_id, 'core', 'member', $author->member_id );
|
||||
}
|
||||
elseif ( in_array( 'follow_member', $extra ) )
|
||||
{
|
||||
$where[] = array( 'follow_rel_id=?', $author->member_id );
|
||||
}
|
||||
else
|
||||
{
|
||||
$where[] = array( 'follow_member_id=?', $author->member_id );
|
||||
}
|
||||
}
|
||||
|
||||
foreach ( \IPS\Db::i()->select( array( 'follow_app', 'follow_area', 'follow_rel_id', 'follow_member_id', 'follow_added' ), 'core_follow', $where, 'follow_added DESC', 10 ) as $follow )
|
||||
{
|
||||
if ( $follow['follow_app'] == 'core' and $follow['follow_area'] == 'member' )
|
||||
{
|
||||
$extraItems[] = new Result\Custom(
|
||||
\IPS\DateTime::ts( $follow['follow_added'] ),
|
||||
\IPS\Member::loggedIn()->language()->addToStack( 'activity_member_followed', FALSE, array( 'htmlsprintf' => array( \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' )->userLink( \IPS\Member::load( $follow['follow_member_id'] ) ), \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' )->userLink( \IPS\Member::load( $follow['follow_rel_id'] ) ) ) ) )
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
$class = 'IPS\\' . $follow['follow_app'] . '\\' . ucfirst( $follow['follow_area'] );
|
||||
try
|
||||
{
|
||||
$thingBeingFollowed = $class::loadAndCheckPerms( $follow['follow_rel_id'] );
|
||||
|
||||
$extraItems[] = new Result\Custom(
|
||||
\IPS\DateTime::ts( $follow['follow_added'] ),
|
||||
\IPS\Member::loggedIn()->language()->addToStack( 'activity_member_followed', FALSE, array( 'htmlsprintf' => array( \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' )->userLink( \IPS\Member::load( $follow['follow_member_id'] ) ), \IPS\Theme::i()->getTemplate( 'global', 'core', 'global' )->basicUrl( $thingBeingFollowed->url(), FALSE, $thingBeingFollowed instanceof \IPS\Node\Model ? $thingBeingFollowed->_title : $thingBeingFollowed->mapped('title'), FALSE ) ) ) )
|
||||
);
|
||||
}
|
||||
catch ( \OutOfRangeException $e ) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Likes */
|
||||
if ( in_array( 'like', $extra ) or in_array( 'rep_neg', $extra ) )
|
||||
{
|
||||
$where = array( array( 'rep_date>?', $lastTime ) );
|
||||
if ( $firstTime )
|
||||
{
|
||||
$where[] = array( 'rep_date<?', $firstTime );
|
||||
}
|
||||
if ( !in_array( 'rep_neg', $extra ) or ( \IPS\Settings::i()->reputation_point_types != 'both' and \IPS\Settings::i()->reputation_point_types != 'negative' ) )
|
||||
{
|
||||
$where[] = array( 'rep_rating=1' );
|
||||
}
|
||||
elseif ( !in_array( 'like', $extra ) or ( \IPS\Settings::i()->reputation_point_types == 'negative' ) )
|
||||
{
|
||||
$where[] = array( 'rep_rating=-1' );
|
||||
}
|
||||
if ( $author )
|
||||
{
|
||||
$where[] = array( '( member_id=? OR member_received=? )', $author->member_id, $author->member_id );
|
||||
}
|
||||
foreach ( \IPS\Db::i()->select( '*', 'core_reputation_index', $where, 'rep_date DESC', 10 ) as $rep )
|
||||
{
|
||||
try
|
||||
{
|
||||
$thingBeingRepped = NULL;
|
||||
foreach ( \IPS\Application::load( $rep['app'] )->extensions( 'core', 'ContentRouter', TRUE, TRUE ) as $ext )
|
||||
{
|
||||
foreach ( $ext->classes as $class )
|
||||
{
|
||||
if ( in_array( 'IPS\Content\Reputation', class_implements( $class ) ) and $class::$reputationType == $rep['type'] )
|
||||
{
|
||||
$thingBeingRepped = $class::loadAndCheckPerms( $rep['type_id'] );
|
||||
break;
|
||||
}
|
||||
if ( isset( $class::$commentClass ) )
|
||||
{
|
||||
$commentClass = $class::$commentClass;
|
||||
if ( in_array( 'IPS\Content\Reputation', class_implements( $commentClass ) ) and $commentClass::$reputationType == $rep['type'] )
|
||||
{
|
||||
$thingBeingRepped = $commentClass::loadAndCheckPerms( $rep['type_id'] );
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ( isset( $class::$reviewClass ) )
|
||||
{
|
||||
$reviewClass = $class::$reviewClass;
|
||||
if ( in_array( 'IPS\Content\Reputation', class_implements( $reviewClass ) ) and $reviewClass::$reputationType == $rep['type'] )
|
||||
{
|
||||
$thingBeingRepped = $reviewClass::loadAndCheckPerms( $rep['type_id'] );
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if ( !$thingBeingRepped )
|
||||
{
|
||||
throw new \OutOfRangeException;
|
||||
}
|
||||
|
||||
if ( \IPS\Settings::i()->reputation_point_types == 'like' )
|
||||
{
|
||||
$lang = 'activity_member_liked';
|
||||
}
|
||||
elseif ( $rep['rep_rating'] > 0 )
|
||||
{
|
||||
$lang = 'activity_rep_pos';
|
||||
}
|
||||
else
|
||||
{
|
||||
$lang = 'activity_rep_neg';
|
||||
}
|
||||
|
||||
$extraItems[] = new Result\Custom(
|
||||
\IPS\DateTime::ts( $rep['rep_date'] ),
|
||||
\IPS\Member::loggedIn()->language()->addToStack( $lang, FALSE, array( 'htmlsprintf' => array(
|
||||
\IPS\Theme::i()->getTemplate( 'global', 'core', 'front' )->userLink( \IPS\Member::load( $rep['member_id'] ) ),
|
||||
$thingBeingRepped->indefiniteArticle(),
|
||||
\IPS\Theme::i()->getTemplate( 'global', 'core', 'global' )->basicUrl( $thingBeingRepped->url(), FALSE, $thingBeingRepped instanceof \IPS\Content\Item ? $thingBeingRepped->mapped('title') : $thingBeingRepped->item()->mapped('title'), FALSE )
|
||||
) ) )
|
||||
);
|
||||
}
|
||||
catch ( \OutOfRangeException $e ) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Merge them in */
|
||||
if ( !empty( $extraItems ) )
|
||||
{
|
||||
$results = array_merge( $results, $extraItems );
|
||||
uasort( $results, function( $a, $b )
|
||||
{
|
||||
if ( $a->createdDate->getTimestamp() == $b->createdDate->getTimestamp() )
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
elseif( $a->createdDate->getTimestamp() < $b->createdDate->getTimestamp() )
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
/* And return */
|
||||
return $results;
|
||||
}
|
||||
}
|
||||
@@ -1,301 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Sphinx Search Index
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - SVN_YYYY Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Social Suite
|
||||
* @since 26 Aug 2014
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS\Content\Search\Sphinx;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sphinx Search Index
|
||||
*/
|
||||
class _Index extends \IPS\Content\Search\Index
|
||||
{
|
||||
/**
|
||||
* @brief The index table
|
||||
*/
|
||||
protected $table;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
\IPS\Db::i( 'sphinx', array(
|
||||
'sql_host' => \IPS\Settings::i()->search_sphinx_server,
|
||||
'sql_user' => NULL,
|
||||
'sql_pass' => NULL,
|
||||
'sql_database' => NULL,
|
||||
'sql_port' => (int) \IPS\Settings::i()->search_sphinx_port,
|
||||
'sql_socket' => NULL,
|
||||
'sql_utf8mb4' => \IPS\Settings::i()->sql_utf8mb4
|
||||
) );
|
||||
|
||||
$this->table = \IPS\Settings::i()->sphinx_prefix . 'ips';
|
||||
}
|
||||
|
||||
/**
|
||||
* Index an item
|
||||
*
|
||||
* @param \IPS\Content\Searchable $object Item to add
|
||||
* @return void
|
||||
*/
|
||||
public function index( \IPS\Content\Searchable $object )
|
||||
{
|
||||
$indexData = $this->indexData( $object );
|
||||
if ( $indexData )
|
||||
{
|
||||
$data = array_merge( array( 'id' => static::_getId( $object ), 'index_class_id' => static::_getClassId( get_class( $object ) ) ), $indexData );
|
||||
$data['index_permissions'] = json_encode( explode( ',', $data['index_permissions'] ) );
|
||||
|
||||
if( !$data['index_permissions'] )
|
||||
{
|
||||
$this->removeFromSearchIndex( $object );
|
||||
}
|
||||
else
|
||||
{
|
||||
$result = \IPS\Db::i('sphinx')->query( "REPLACE INTO {$this->table} ( " . implode( ', ', array_keys( $data ) ) . ' ) VALUES ( ' . implode( ', ', array_map( array( $this, '_escapeValue' ), $data ) ) . ' )' );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove item
|
||||
*
|
||||
* @param \IPS\Content\Searchable $object Item to remove
|
||||
* @return void
|
||||
*/
|
||||
public function removeFromSearchIndex( \IPS\Content\Searchable $object )
|
||||
{
|
||||
\IPS\Db::i('sphinx')->query( "DELETE FROM {$this->table} WHERE id=" . static::_getId( $object ) );
|
||||
|
||||
$class = get_class( $object );
|
||||
$idColumn = $class::$databaseColumnId;
|
||||
if ( isset( $class::$commentClass ) )
|
||||
{
|
||||
$commentClass = $class::$commentClass;
|
||||
|
||||
foreach ( \IPS\Db::i()->select( $commentClass::$databasePrefix . $commentClass::$databaseColumnId, $commentClass::$databaseTable, array( $commentClass::$databasePrefix . $commentClass::$databaseColumnMap['item'] . '=?', $object->$idColumn ) ) as $commentid )
|
||||
{
|
||||
\IPS\Db::i('sphinx')->query( "DELETE FROM {$this->table} WHERE id=" . static::_getId( $commentid, $commentClass ) );
|
||||
}
|
||||
}
|
||||
|
||||
if ( isset( $class::$reviewClass ) )
|
||||
{
|
||||
$reviewClass = $class::$reviewClass;
|
||||
|
||||
foreach ( \IPS\Db::i()->select( $reviewClass::$databasePrefix . $reviewClass::$databaseColumnId, $reviewClass::$databaseTable, array( $reviewClass::$databasePrefix . $reviewClass::$databaseColumnMap['item'] . '=?', $object->$idColumn ) ) as $reviewid )
|
||||
{
|
||||
\IPS\Db::i('sphinx')->query( "DELETE FROM {$this->table} WHERE id=" . static::_getId( $reviewid, $reviewClass ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all content for a classs
|
||||
*
|
||||
* @param string $class The class
|
||||
* @param int|NULL $containerId The container ID to update, or NULL
|
||||
* @return void
|
||||
*/
|
||||
public function removeClassFromSearchIndex( $class, $containerId=NULL )
|
||||
{
|
||||
$where = array( 'index_class_id=' . static::_getClassId( $class ) );
|
||||
if ( $containerId !== NULL )
|
||||
{
|
||||
$where[] = "index_container_id={$containerId}";
|
||||
}
|
||||
|
||||
$where = implode( ' AND ', $where );
|
||||
|
||||
\IPS\Db::i('sphinx')->query( "DELETE FROM {$this->table} WHERE {$where}" );
|
||||
}
|
||||
|
||||
/**
|
||||
* Mass Update (when permissions change, for example)
|
||||
*
|
||||
* @param string $class The class
|
||||
* @param int|NULL $containerId The container ID to update, or NULL
|
||||
* @param int|NULL $itemId The item ID to update, or NULL
|
||||
* @param string|NULL $newPermissions New permissions (if applicable)
|
||||
* @param int|NULL $newHiddenStatus New hidden status (if applicable) special value 2 can be used to indicate hidden only by parent
|
||||
* @param int|NULL $newContainer New container ID (if applicable)
|
||||
* @return void
|
||||
*/
|
||||
public function massUpdate( $class, $containerId = NULL, $itemId = NULL, $newPermissions = NULL, $newHiddenStatus = NULL, $newContainer = NULL )
|
||||
{
|
||||
$where = array( 'index_class_id=' . static::_getClassId( $class ) );
|
||||
if ( $containerId !== NULL )
|
||||
{
|
||||
$where[] = "index_container_id={$containerId}";
|
||||
}
|
||||
if ( $itemId !== NULL )
|
||||
{
|
||||
$where[] = "index_item_id={$itemId}";
|
||||
}
|
||||
|
||||
$update = array();
|
||||
if ( $newPermissions !== NULL )
|
||||
{
|
||||
$update['index_permissions'] = explode( ',', str_replace( '*', 0, $newPermissions ) );
|
||||
}
|
||||
if ( $newHiddenStatus !== NULL )
|
||||
{
|
||||
if ( $newHiddenStatus === 2 )
|
||||
{
|
||||
$where[] = 'index_hidden=0';
|
||||
}
|
||||
else
|
||||
{
|
||||
$where[] = 'index_hidden=2';
|
||||
}
|
||||
|
||||
$update['index_hidden'] = $newHiddenStatus;
|
||||
}
|
||||
if ( $newContainer )
|
||||
{
|
||||
$update['index_container_id'] = $newContainer;
|
||||
}
|
||||
|
||||
$set = array();
|
||||
foreach ( $update as $k => $v )
|
||||
{
|
||||
$set[] = "{$k}={$this->_escapeValue($v)}";
|
||||
}
|
||||
$set = implode( ', ', $set );
|
||||
|
||||
$where = implode( ' AND ', $where );
|
||||
|
||||
try
|
||||
{
|
||||
\IPS\Db::i('sphinx')->query( "UPDATE {$this->table} SET {$set} WHERE {$where}" );
|
||||
}
|
||||
catch ( \Exception $e )
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prune search index
|
||||
*
|
||||
* @param \IPS\DateTime|NULL $cutoff The date to delete index records from, or NULL to delete all
|
||||
* @return void
|
||||
*/
|
||||
public function prune( \IPS\DateTime $cutoff = NULL )
|
||||
{
|
||||
if ( $cutoff === NULL )
|
||||
{
|
||||
try
|
||||
{
|
||||
\IPS\Db::i('sphinx')->query( "TRUNCATE RTINDEX {$this->table}" );
|
||||
}
|
||||
catch( \IPS\Db\Exception $e )
|
||||
{
|
||||
/* If the index does not exist, we should not exit at this point in time */
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new \UnexpectedValueException;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an ID for an object
|
||||
*
|
||||
* @param \IPS\Content\Searchable|int $object Item to get ID for
|
||||
* @param string|NULL $class If $object is an ID rather than an object, provide the class
|
||||
* @return int
|
||||
*/
|
||||
public static function _getId( $object, $class = NULL )
|
||||
{
|
||||
if ( is_object( $object ) )
|
||||
{
|
||||
$class = get_class( $object );
|
||||
$idColumn = $class::$databaseColumnId;
|
||||
$id = $object->$idColumn;
|
||||
}
|
||||
else
|
||||
{
|
||||
$id = $object;
|
||||
}
|
||||
|
||||
return ( static::_getClassId( $class ) * 100000000 ) + $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an ID for an class
|
||||
*
|
||||
* @param string $class The class
|
||||
* @return int
|
||||
*/
|
||||
public static function _getClassId( $class )
|
||||
{
|
||||
$updateMap = FALSE;
|
||||
|
||||
/* Get the map (Older versions stored this in datastore, so we look there if we don't have it or use a blank array) */
|
||||
if ( !\IPS\Settings::i()->sphinx_id_map )
|
||||
{
|
||||
$map = isset( \IPS\Data\Store::i()->sphinxIdMap ) ? \IPS\Data\Store::i()->sphinxIdMap : array();
|
||||
$updateMap = TRUE;
|
||||
}
|
||||
else
|
||||
{
|
||||
$map = json_decode( \IPS\Settings::i()->sphinx_id_map, TRUE );
|
||||
}
|
||||
|
||||
/* Add it if we don't have it */
|
||||
if ( !in_array( $class, $map ) )
|
||||
{
|
||||
$map[] = $class;
|
||||
$updateMap = TRUE;
|
||||
}
|
||||
|
||||
/* Update the map if necessary */
|
||||
if ( $updateMap )
|
||||
{
|
||||
\IPS\Db::i()->update( 'core_sys_conf_settings', array( 'conf_value' => json_encode( $map ) ), array( 'conf_key=?', 'sphinx_id_map' ) );
|
||||
unset( \IPS\Data\Store::i()->settings );
|
||||
}
|
||||
|
||||
/* Return */
|
||||
return intval( array_search( $class, $map ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape a value for the query (SphinxQL doesn't support prepared statements)
|
||||
*
|
||||
* @param mixed $val The value
|
||||
* @return mixed
|
||||
*/
|
||||
protected function _escapeValue( $val )
|
||||
{
|
||||
if ( is_numeric( $val ) )
|
||||
{
|
||||
return $val;
|
||||
}
|
||||
elseif ( is_array( $val ) )
|
||||
{
|
||||
return '(' . implode( ',', array_map( array( $this, '_escapeValue' ), $val ) ) . ')';
|
||||
}
|
||||
else
|
||||
{
|
||||
return ( '\'' . \IPS\Db::i( 'sphinx' )->real_escape_string( $val ) . '\'' );
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,375 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Sphinx Search Query
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - SVN_YYYY Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Social Suite
|
||||
* @since 26 Aug 2014
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS\Content\Search\Sphinx;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sphinx Search Query
|
||||
*/
|
||||
class _Query extends \IPS\Content\Search\Query
|
||||
{
|
||||
const SUPPORTS_JOIN_FILTERS = FALSE;
|
||||
|
||||
/**
|
||||
* @brief The index table
|
||||
*/
|
||||
protected $table;
|
||||
|
||||
/**
|
||||
* @brief The WHERE clause
|
||||
*/
|
||||
protected $where = array();
|
||||
|
||||
/**
|
||||
* @brief The WHERE clause for hidden/unhidden
|
||||
*/
|
||||
protected $hiddenClause = NULL;
|
||||
|
||||
/**
|
||||
* @brief The ORDER BY clause
|
||||
*/
|
||||
protected $order = NULL;
|
||||
|
||||
/**
|
||||
* @brief The offset
|
||||
*/
|
||||
protected $offset = 0;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param \IPS\Member $member The member performing the search
|
||||
* @return void
|
||||
*/
|
||||
public function __construct( \IPS\Member $member )
|
||||
{
|
||||
\IPS\Db::i( 'sphinx', array(
|
||||
'sql_host' => \IPS\Settings::i()->search_sphinx_server,
|
||||
'sql_user' => NULL,
|
||||
'sql_pass' => NULL,
|
||||
'sql_database' => NULL,
|
||||
'sql_port' => (int) \IPS\Settings::i()->search_sphinx_port,
|
||||
'sql_socket' => NULL,
|
||||
'sql_utf8mb4' => \IPS\Settings::i()->sql_utf8mb4
|
||||
) );
|
||||
|
||||
$this->table = \IPS\Settings::i()->sphinx_prefix . 'ips';
|
||||
|
||||
parent::__construct( $member );
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter by content type
|
||||
*
|
||||
* @param string $class The type of content to search (including all comment/review classes)
|
||||
* @param array|NULL $containers An array of container IDs to filter by, or NULL to not filter by containers
|
||||
* @param int|NULL $minimumComments The minimum number of comments
|
||||
* @param int|NULL $minimumReviews The minimum number of reviews
|
||||
* @param int|NULL $minimumViews The minimum number of views
|
||||
* @return \IPS\Content\Search\Query (for daisy chaining)
|
||||
*/
|
||||
public function filterByContent( $class, $containers = NULL, $minimumComments = NULL, $minimumReviews = NULL, $minimumViews = NULL )
|
||||
{
|
||||
/* Classes */
|
||||
$classes = array( \IPS\Content\Search\Sphinx\Index::_getClassId( $class ) );
|
||||
if ( isset( $class::$commentClass ) )
|
||||
{
|
||||
$classes[] = \IPS\Content\Search\Sphinx\Index::_getClassId( $class::$commentClass );
|
||||
}
|
||||
if ( isset( $class::$reviewClass ) )
|
||||
{
|
||||
$classes[] = \IPS\Content\Search\Sphinx\Index::_getClassId( $class::$reviewClass );
|
||||
}
|
||||
$this->where[] = 'index_class_id IN(' . implode( ',', $classes ) . ')';
|
||||
|
||||
/* Containers */
|
||||
if ( $containers !== NULL )
|
||||
{
|
||||
$this->where[] = 'index_container_id IN(' . implode( ',', array_map( 'intval', $containers ) ) . ' )';
|
||||
}
|
||||
|
||||
/* We don't support anything else */
|
||||
if ( $minimumComments !== NULL or $minimumReviews !== NULL or $minimumViews !== NULL )
|
||||
{
|
||||
throw new \UnexpectedValueException;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter by content type to exclude
|
||||
*
|
||||
* @param array $classes The types of content to exclude (not including all comment/review classes)
|
||||
* @param bool $allowedContiners If TRUE, $class should be an array of allowed containers for that class - e.g. array( 'IPS\forums\Topic' => array( 1, 2 ) - otherwise, a simple array - e.g. array( 'IPS\forums\Topic' )
|
||||
* @return \IPS\Content\Search\Query (for daisy chaining)
|
||||
*/
|
||||
public function filterByExcludeContent( array $classes, $allowedContiners=FALSE )
|
||||
{
|
||||
if ( $allowedContiners )
|
||||
{
|
||||
$where = array();
|
||||
foreach ( $classes as $class => $allowed )
|
||||
{
|
||||
if ( count( $allowed ) )
|
||||
{
|
||||
$where[] = '( index_class<>' . \IPS\Content\Search\Sphinx\Index::_getClassId( $class ) . ' OR index_container_id IN( ' . implode( ',', $allowed ) . ' ) )';
|
||||
}
|
||||
else
|
||||
{
|
||||
$excludeAll[] = $class;
|
||||
}
|
||||
}
|
||||
|
||||
$finalWhere = array();
|
||||
if ( count( $excludeAll ) )
|
||||
{
|
||||
$finalWhere[] = 'index_class NOT IN('. implode( ',', array_map( array( 'IPS\Content\Search\Sphinx\Index', '_getClassId' ), $excludeAll ) ) . ')';
|
||||
}
|
||||
if ( count( $where ) )
|
||||
{
|
||||
$finalWhere[] = implode( ' OR ', $where );
|
||||
}
|
||||
|
||||
$this->where[] = array_merge( array( implode( ' AND ', $finalWhere ) ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->where[] = 'index_class NOT IN('. implode( ',', array_map( array( 'IPS\Content\Search\Sphinx\Index', '_getClassId' ), $classes ) ) . ')';
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter by content item
|
||||
*
|
||||
* @param \IPS\Content\Item $item The item
|
||||
* @return \IPS\Content\Search\Query (for daisy chaining)
|
||||
*/
|
||||
public function filterByItem( \IPS\Content\Item $item )
|
||||
{
|
||||
$class = get_class( $item );
|
||||
|
||||
$classes = array();
|
||||
if ( isset( $class::$commentClass ) )
|
||||
{
|
||||
$classes[] = \IPS\Content\Search\Sphinx\Index::_getClassId( $class::$commentClass );
|
||||
}
|
||||
if ( isset( $class::$reviewClass ) )
|
||||
{
|
||||
$classes[] = \IPS\Content\Search\Sphinx\Index::_getClassId( $class::$reviewClass );
|
||||
}
|
||||
$this->where[] = 'index_class_id IN(' . implode( ',', $classes ) . ')';
|
||||
|
||||
$idColumn = $item::$databaseColumnId;
|
||||
$this->where[] = "index_item_id={$item->$idColumn}";
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter by author
|
||||
*
|
||||
* @param \IPS\Member $author The author
|
||||
* @return \IPS\Content\Search\Query (for daisy chaining)
|
||||
*/
|
||||
public function filterByAuthor( \IPS\Member $author )
|
||||
{
|
||||
$this->where[] = "index_author={$author->member_id}";
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter by start date
|
||||
*
|
||||
* @param \IPS\DateTime|NULL $start The start date (only results AFTER this date will be returned)
|
||||
* @param \IPS\DateTime|NULL $end The end date (only results BEFORE this date will be returned)
|
||||
* @return \IPS\Content\Search\Query (for daisy chaining)
|
||||
*/
|
||||
public function filterByCreateDate( \IPS\DateTime $start = NULL, \IPS\DateTime $end = NULL )
|
||||
{
|
||||
if ( $start )
|
||||
{
|
||||
$this->where[] = 'index_date_created>' . $start->getTimestamp();
|
||||
}
|
||||
if ( $end )
|
||||
{
|
||||
$this->where[] = 'index_date_created<' . $end->getTimestamp();
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter by last updated date
|
||||
*
|
||||
* @param \IPS\DateTime|NULL $start The start date (only results AFTER this date will be returned)
|
||||
* @param \IPS\DateTime|NULL $end The end date (only results BEFORE this date will be returned)
|
||||
* @return \IPS\Content\Search\Query (for daisy chaining)
|
||||
*/
|
||||
public function filterByLastUpdatedDate( \IPS\DateTime $start = NULL, \IPS\DateTime $end = NULL )
|
||||
{
|
||||
if ( $start )
|
||||
{
|
||||
$this->where[] = 'index_date_updated>' . $start->getTimestamp();
|
||||
}
|
||||
if ( $end )
|
||||
{
|
||||
$this->where[] = 'index_date_updated<' . $end->getTimestamp();
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set hidden status
|
||||
*
|
||||
* @param int|array $statuses The statuses (array of HIDDEN_ constants)
|
||||
* @return \IPS\Content\Search\Query (for daisy chaining)
|
||||
*/
|
||||
public function setHiddenFilter( $statuses )
|
||||
{
|
||||
if ( is_null( $statuses ) )
|
||||
{
|
||||
$this->hiddenClause = NULL;
|
||||
}
|
||||
if ( is_array( $statuses ) )
|
||||
{
|
||||
$this->hiddenClause = 'index_hidden IN(' . implode( ',', array_map( 'intval', $statuses ) ) . ' )';
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->hiddenClause = 'index_hidden=' . intval( $statuses );
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set page
|
||||
*
|
||||
* @param int $page The page number
|
||||
* @return \IPS\Content\Search\Query (for daisy chaining)
|
||||
*/
|
||||
public function setPage( $page )
|
||||
{
|
||||
$page = $page ?: 1;
|
||||
$this->offset = ( $page - 1 ) * $this->resultsToGet;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set order
|
||||
*
|
||||
* @param int $order Order (see ORDER_ constants)
|
||||
* @return \IPS\Content\Search\Query (for daisy chaining)
|
||||
*/
|
||||
public function setOrder( $order )
|
||||
{
|
||||
switch ( $order )
|
||||
{
|
||||
case static::ORDER_NEWEST_UPDATED:
|
||||
$this->order = 'index_date_updated DESC';
|
||||
break;
|
||||
|
||||
case static::ORDER_NEWEST_CREATED:
|
||||
$this->order = 'index_date_created DESC';
|
||||
break;
|
||||
|
||||
case static::ORDER_RELEVANCY:
|
||||
$this->order = NULL;
|
||||
break;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search
|
||||
*
|
||||
* @param string|null $term The term to search for
|
||||
* @param array|null $tags The tags to search for
|
||||
* @param int $method \IPS\Content\Search\Index::i()->TERM_OR_TAGS or \IPS\Content\Search\Index::i()->TERM_AND_TAGS
|
||||
* @return \IPS\Content\Search\Results
|
||||
*/
|
||||
public function search( $term = NULL, $tags = NULL, $method = 1 )
|
||||
{
|
||||
/* Build the match term */
|
||||
if ( $term !== NULL )
|
||||
{
|
||||
$match[] = "( @(index_title,index_content) {$this->escape( $term )} )";
|
||||
}
|
||||
if ( $tags !== NULL )
|
||||
{
|
||||
$match[] = "( @index_tags " . implode( ' | ', array_map( array( $this, 'escape' ), $tags ) ) . ' )';
|
||||
}
|
||||
if ( !empty( $match ) )
|
||||
{
|
||||
array_unshift( $this->where, 'MATCH( \'' . implode( ( $method === static::TERM_OR_TAGS ? ' | ' : ' ' ), $match ) . '\' )' );
|
||||
}
|
||||
|
||||
/* Only get stuff we have permission for */
|
||||
$permissions = array( '\'' . $this->escape( '*' ) . '\'' );
|
||||
foreach ( $this->permissionArray() as $id )
|
||||
{
|
||||
$permissions[] = '\'' . $this->escape( $id ) . '\'';
|
||||
}
|
||||
$permCheck = 'IN(index_permissions,' . implode( ',', $permissions ) . ') AS perm_check';
|
||||
$this->where[] = 'perm_check=1';
|
||||
|
||||
/* Add the hidden clause */
|
||||
if ( $this->hiddenClause )
|
||||
{
|
||||
$this->where[] = $this->hiddenClause;
|
||||
}
|
||||
|
||||
/* Query */
|
||||
$query = \IPS\Db::i('sphinx')->query( "SELECT *, {$permCheck} FROM {$this->table} WHERE " . implode( ' AND ', $this->where ) . ( $this->order ? " ORDER BY {$this->order}" : '' ) . " LIMIT {$this->offset}, {$this->resultsToGet}" );
|
||||
|
||||
/* Get count */
|
||||
$meta = \IPS\Db::i('sphinx')->query('SHOW META');
|
||||
while ( $row = $meta->fetch_assoc() )
|
||||
{
|
||||
if ( $row['Variable_name'] === 'total' )
|
||||
{
|
||||
$count = $row['Value'];
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
/* Convert to array */
|
||||
$results = array();
|
||||
while ( $row = $query->fetch_assoc() )
|
||||
{
|
||||
$results[] = $row;
|
||||
}
|
||||
|
||||
/* Return */
|
||||
return new \IPS\Content\Search\Results( $results, $count );
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape string for SphinxQL query
|
||||
*
|
||||
* @param string $term Search term
|
||||
* @return string
|
||||
*/
|
||||
protected function escape( $term )
|
||||
{
|
||||
return str_replace( array( '\\', '(',')','|','-','!','@','~','"','&', '/', '^', '$', '=', "'", "\x00", "\n", "\r", "\x1a" ), array( '\\\\', '\\\(','\\\)','\\\|','\\\-','\\\!','\\\@','\\\~','\\\"', '\\\&', '\\\/', '\\\^', '\\\$', '\\\=', "\\'", "\\x00", "\\n", "\\r", "\\x1a" ), $term );
|
||||
}
|
||||
}
|
||||
Whitespace-only changes.
@@ -155,7 +155,7 @@ class _Twitter
|
||||
{
|
||||
$title .= ' ' . \IPS\Settings::i()->twitter_hashtag;
|
||||
}
|
||||
return \IPS\Theme::i()->getTemplate( 'sharelinks', 'core' )->twitter( urlencode( $this->url ), urlencode( $title ) );
|
||||
return \IPS\Theme::i()->getTemplate( 'sharelinks', 'core' )->twitter( urlencode( $this->url ), rawurlencode( $title ) );
|
||||
}
|
||||
catch ( \Exception $e )
|
||||
{
|
||||
|
||||
+76
-37
@@ -46,7 +46,7 @@ abstract class _Widget extends \IPS\Widget\PermissionCache
|
||||
|
||||
/* Block title */
|
||||
$form->add( new \IPS\Helpers\Form\Text( 'widget_feed_title', isset( $this->configuration['widget_feed_title'] ) ? $this->configuration['widget_feed_title'] : \IPS\Member::loggedIn()->language()->addToStack( $class::$title . '_pl' ) ) );
|
||||
|
||||
|
||||
/* Container */
|
||||
if ( isset( $class::$containerNodeClass ) )
|
||||
{
|
||||
@@ -65,26 +65,35 @@ abstract class _Widget extends \IPS\Widget\PermissionCache
|
||||
}
|
||||
|
||||
/* Types */
|
||||
if ( in_array( 'IPS\Content\Lockable', class_implements( $class ) ) or in_array( 'IPS\Content\Pinnable', class_implements( $class ) ) or in_array( 'IPS\Content\Featurable', class_implements( $class ) ) )
|
||||
if ( in_array( 'IPS\Content\Lockable', class_implements( $class ) ) )
|
||||
{
|
||||
$types = array();
|
||||
if ( in_array( 'IPS\Content\Lockable', class_implements( $class ) ) )
|
||||
{
|
||||
$types['open'] = 'mod_confirm_unlock';
|
||||
$types['closed'] = 'mod_confirm_lock';
|
||||
}
|
||||
if ( in_array( 'IPS\Content\Pinnable', class_implements( $class ) ) )
|
||||
{
|
||||
$types['pinned'] = 'mod_confirm_pin';
|
||||
$types['notpinned'] = 'mod_confirm_unpin';
|
||||
}
|
||||
if ( in_array( 'IPS\Content\Featurable', class_implements( $class ) ) )
|
||||
{
|
||||
$types['featured'] = 'mod_confirm_feature';
|
||||
$types['notfeatured'] = 'mod_confirm_unfeature';
|
||||
}
|
||||
|
||||
$form->add( new \IPS\Helpers\Form\CheckboxSet( 'widget_feed_status', isset( $this->configuration['widget_feed_status'] ) ? $this->configuration['widget_feed_status'] : array_keys( $types ), FALSE, array( 'options' => $types ) ) );
|
||||
$types = array(
|
||||
'any' => 'mod_confirm_either',
|
||||
'open' => 'mod_confirm_unlock',
|
||||
'closed' => 'mod_confirm_lock'
|
||||
);
|
||||
|
||||
$form->add( new \IPS\Helpers\Form\Radio( 'widget_feed_status_locked', isset( $this->configuration['widget_feed_status_locked'] ) ? $this->configuration['widget_feed_status_locked'] : 'any', FALSE, array( 'options' => $types ) ) );
|
||||
}
|
||||
if ( in_array( 'IPS\Content\Pinnable', class_implements( $class ) ) )
|
||||
{
|
||||
$types = array(
|
||||
'any' => 'mod_confirm_either',
|
||||
'pinned' => 'mod_confirm_pin',
|
||||
'notpinned' => 'mod_confirm_unpin'
|
||||
);
|
||||
|
||||
$form->add( new \IPS\Helpers\Form\Radio( 'widget_feed_status_pinned', isset( $this->configuration['widget_feed_status_pinned'] ) ? $this->configuration['widget_feed_status_pinned'] : 'any', FALSE, array( 'options' => $types ) ) );
|
||||
}
|
||||
if ( in_array( 'IPS\Content\Featurable', class_implements( $class ) ) )
|
||||
{
|
||||
$types = array(
|
||||
'any' => 'mod_confirm_either',
|
||||
'featured' => 'mod_confirm_feature',
|
||||
'notfeatured' => 'mod_confirm_unfeature'
|
||||
);
|
||||
|
||||
$form->add( new \IPS\Helpers\Form\Radio( 'widget_feed_status_featured', isset( $this->configuration['widget_feed_status_featured'] ) ? $this->configuration['widget_feed_status_featured'] : 'any', FALSE, array( 'options' => $types ) ) );
|
||||
}
|
||||
|
||||
/* Author */
|
||||
@@ -100,7 +109,7 @@ abstract class _Widget extends \IPS\Widget\PermissionCache
|
||||
}
|
||||
}
|
||||
catch( \OutOfRangeException $ex ) { }
|
||||
$form->add( new \IPS\Helpers\Form\Member( 'widget_feed_author', $author, FALSE, array( 'multiple' => true ) ) );
|
||||
$form->add( new \IPS\Helpers\Form\Member( 'widget_feed_author', $author, FALSE, array( 'multiple' => NULL ) ) );
|
||||
|
||||
/* Minimum comments/reviews */
|
||||
if ( isset( $class::$commentClass ) )
|
||||
@@ -128,6 +137,9 @@ abstract class _Widget extends \IPS\Widget\PermissionCache
|
||||
/* Number to show */
|
||||
$form->add( new \IPS\Helpers\Form\Number( 'widget_feed_show', isset( $this->configuration['widget_feed_show'] ) ? $this->configuration['widget_feed_show'] : 5, TRUE ) );
|
||||
|
||||
/* Date restrict */
|
||||
$form->add( new \IPS\Helpers\Form\Number( 'widget_feed_restrict_days', isset( $this->configuration['widget_feed_restrict_days'] ) ? $this->configuration['widget_feed_restrict_days'] : -1, FALSE, array( 'unlimited' => -1 ), NULL, NULL, \IPS\Member::loggedIn()->language()->addToStack('widget_feed_restrict_days_suffix') ) );
|
||||
|
||||
/* Sort */
|
||||
$sortOptions = array();
|
||||
foreach ( array( 'updated', 'title', 'num_comments', 'date', 'views', 'rating_average' ) as $k )
|
||||
@@ -162,6 +174,7 @@ abstract class _Widget extends \IPS\Widget\PermissionCache
|
||||
if ( is_array( $values[ 'widget_feed_container_' . $class::$title ] ) )
|
||||
{
|
||||
$values['widget_feed_container'] = array_keys( $values[ 'widget_feed_container_' . $class::$title ] );
|
||||
unset( $values[ 'widget_feed_container_' . $class::$title ] );
|
||||
}
|
||||
|
||||
if ( is_array( $values['widget_feed_author'] ) )
|
||||
@@ -195,37 +208,37 @@ abstract class _Widget extends \IPS\Widget\PermissionCache
|
||||
}
|
||||
|
||||
/* Status */
|
||||
if ( isset( $this->configuration['widget_feed_status'] ) and in_array( 'IPS\Content\Lockable', class_implements( $class ) ) )
|
||||
if ( isset( $this->configuration['widget_feed_status_locked'] ) and in_array( 'IPS\Content\Lockable', class_implements( $class ) ) )
|
||||
{
|
||||
if ( in_array( 'closed', $this->configuration['widget_feed_status'] ) AND !in_array( 'open', $this->configuration['widget_feed_status'] ) )
|
||||
if ( $this->configuration['widget_feed_status_locked'] == 'closed' )
|
||||
{
|
||||
$where[] = isset( $class::$databaseColumnMap['locked'] ) ? array( $class::$databaseTable . '.' . $class::$databasePrefix . $class::$databaseColumnMap['locked'] . '=?', 1 ) : array( $class::$databaseTable . '.' . $class::$databasePrefix . $class::$databaseColumnMap['status'] . '=?', 'closed' );
|
||||
}
|
||||
elseif ( in_array( 'open', $this->configuration['widget_feed_status'] ) AND !in_array( 'closed', $this->configuration['widget_feed_status'] ) )
|
||||
elseif ( $this->configuration['widget_feed_status_locked'] == 'open' )
|
||||
{
|
||||
$where[] = isset( $class::$databaseColumnMap['locked'] ) ? array( $class::$databaseTable . '.' . $class::$databasePrefix . $class::$databaseColumnMap['locked'] . '=?', 0 ) : array( $class::$databaseTable . '.' . $class::$databasePrefix . $class::$databaseColumnMap['status'] . '=?', 'open' );
|
||||
}
|
||||
}
|
||||
|
||||
if ( isset( $this->configuration['widget_feed_status'] ) and in_array( 'IPS\Content\Featurable', class_implements( $class ) ) )
|
||||
if ( isset( $this->configuration['widget_feed_status_featured'] ) and in_array( 'IPS\Content\Featurable', class_implements( $class ) ) )
|
||||
{
|
||||
if ( in_array( 'notfeatured', $this->configuration['widget_feed_status'] ) AND !in_array( 'featured', $this->configuration['widget_feed_status'] ) )
|
||||
if ( $this->configuration['widget_feed_status_featured'] == 'notfeatured' )
|
||||
{
|
||||
$where[] = array( $class::$databaseTable . '.' . $class::$databasePrefix . $class::$databaseColumnMap['featured'] . '=0' );
|
||||
}
|
||||
elseif ( in_array( 'featured', $this->configuration['widget_feed_status'] ) AND !in_array( 'notfeatured', $this->configuration['widget_feed_status'] ) )
|
||||
elseif ( $this->configuration['widget_feed_status_featured'] == 'featured' )
|
||||
{
|
||||
$where[] = array( $class::$databaseTable . '.' . $class::$databasePrefix . $class::$databaseColumnMap['featured'] . '=1' );
|
||||
}
|
||||
}
|
||||
|
||||
if ( isset( $this->configuration['widget_feed_status'] ) and in_array( 'IPS\Content\Pinnable', class_implements( $class ) ) )
|
||||
if ( isset( $this->configuration['widget_feed_status_pinned'] ) and in_array( 'IPS\Content\Pinnable', class_implements( $class ) ) )
|
||||
{
|
||||
if ( in_array( 'notpinned', $this->configuration['widget_feed_status'] ) AND !in_array( 'pinned', $this->configuration['widget_feed_status'] ) )
|
||||
if ( $this->configuration['widget_feed_status_pinned'] == 'notpinned' )
|
||||
{
|
||||
$where[] = array( $class::$databasePrefix . $class::$databaseColumnMap['pinned'] . '=0' );
|
||||
}
|
||||
elseif ( in_array( 'pinned', $this->configuration['widget_feed_status'] ) AND !in_array( 'notpinned', $this->configuration['widget_feed_status'] ) )
|
||||
elseif ( $this->configuration['widget_feed_status_pinned'] == 'pinned' )
|
||||
{
|
||||
$where[] = array( $class::$databaseTable . '.' . $class::$databasePrefix . $class::$databaseColumnMap['pinned'] . '=1' );
|
||||
}
|
||||
@@ -256,11 +269,11 @@ abstract class _Widget extends \IPS\Widget\PermissionCache
|
||||
{
|
||||
$where[] = array( $class::$databaseTable . '.' . $class::$databasePrefix . $class::$databaseColumnMap['rating_average'] . '>?', (int) $this->configuration['widget_feed_min_rating'] );
|
||||
}
|
||||
|
||||
/* Start date */
|
||||
if ( isset( $this->configuration['widget_feed_start_date'] ) and isset( $this->configuration['widget_feed_start_date'] ) and $this->configuration['widget_feed_start_date'] > 0 )
|
||||
|
||||
/* Limit to days */
|
||||
if ( isset( $this->configuration['widget_feed_restrict_days'] ) and $this->configuration['widget_feed_restrict_days'] > 0 )
|
||||
{
|
||||
$where[] = array( $class::$databaseTable . '.' . $class::$databasePrefix . $class::$databaseColumnMap['date'] . '>?', \IPS\DateTime::create()->sub( new \DateInterval( 'P' . $this->configuration['widget_feed_start_date'] . 'D' ) )->getTimestamp() );
|
||||
$where[] = array( $class::$databaseTable . '.' . $class::$databasePrefix . $class::$databaseColumnMap['date'] . '>?', \IPS\DateTime::create()->sub( new \DateInterval( 'P' . $this->configuration['widget_feed_restrict_days'] . 'D' ) )->getTimestamp() );
|
||||
}
|
||||
|
||||
return $where;
|
||||
@@ -274,15 +287,41 @@ abstract class _Widget extends \IPS\Widget\PermissionCache
|
||||
public function render()
|
||||
{
|
||||
$class = static::$class;
|
||||
|
||||
$joins = NULL;
|
||||
|
||||
if ( isset( $this->configuration['widget_feed_tags'] ) and is_array( $this->configuration['widget_feed_tags'] ) and count( $this->configuration['widget_feed_tags'] ) )
|
||||
{
|
||||
$tagWhere = array();
|
||||
$binds = array( $class::$application, $class::$module );
|
||||
foreach( $this->configuration['widget_feed_tags'] as $tag )
|
||||
{
|
||||
$tagWhere[] = "( ? LIKE CONCAT( tag_text, '%') )";
|
||||
$binds[] = $tag;
|
||||
}
|
||||
|
||||
$joins = array( array(
|
||||
'select' => 'tag_id',
|
||||
'from' => 'core_tags',
|
||||
'where' => array( array_merge( array( 'tag_meta_app=? AND tag_meta_area=? AND (' . implode( ' OR ', $tagWhere ) . ') and core_tags.tag_meta_id=' . $class::$databaseTable . '.' . $class::$databasePrefix . $class::$databaseColumnId ), $binds ) ),
|
||||
'type' => 'STRAIGHT_JOIN'
|
||||
) );
|
||||
}
|
||||
|
||||
$items = $class::getItemsWithPermission(
|
||||
$this->buildWhere(),
|
||||
( isset( $this->configuration['widget_feed_sort_on'] ) and isset( $this->configuration['widget_feed_sort_dir'] ) ) ? ( $class::$databasePrefix . $this->configuration['widget_feed_sort_on'] . ' ' . $this->configuration['widget_feed_sort_dir'] ) : NULL,
|
||||
isset( $this->configuration['widget_feed_show'] ) ? $this->configuration['widget_feed_show'] : 5,
|
||||
( !isset( $this->configuration['widget_feed_use_perms'] ) or $this->configuration['widget_feed_use_perms'] ) ? 'read' : NULL,
|
||||
FALSE
|
||||
FALSE,
|
||||
0,
|
||||
NULL,
|
||||
FALSE,
|
||||
FALSE,
|
||||
FALSE,
|
||||
FALSE,
|
||||
$joins
|
||||
);
|
||||
|
||||
|
||||
if ( count( $items ) )
|
||||
{
|
||||
return $this->output( $items, isset( $this->configuration['widget_feed_title'] ) ? $this->configuration['widget_feed_title'] : \IPS\Member::loggedIn()->language()->addToStack( $class::$title . '_pl' ) );
|
||||
|
||||
@@ -0,0 +1,326 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Content Item Feed Widget
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - SVN_YYYY Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Social Suite
|
||||
* @subpackage forums
|
||||
* @since 16 Oct 2014
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS\Content;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Content Comment Feed Widget
|
||||
*/
|
||||
abstract class _WidgetComment extends \IPS\Widget\PermissionCache
|
||||
{
|
||||
/**
|
||||
* Class
|
||||
*/
|
||||
protected static $class;
|
||||
|
||||
/**
|
||||
* Specify widget configuration
|
||||
*
|
||||
* @param null|\IPS\Helpers\Form $form Form object
|
||||
* @return \IPS\Helpers\Form
|
||||
*/
|
||||
public function configuration( &$form=null )
|
||||
{
|
||||
/* Init */
|
||||
$class = static::$class;
|
||||
$itemClass = $class::$itemClass;
|
||||
|
||||
if ( $form === null )
|
||||
{
|
||||
$form = new \IPS\Helpers\Form;
|
||||
}
|
||||
|
||||
/* Block title */
|
||||
$form->add( new \IPS\Helpers\Form\Text( 'widget_feed_title', isset( $this->configuration['widget_feed_title'] ) ? $this->configuration['widget_feed_title'] : \IPS\Member::loggedIn()->language()->addToStack( $class::$title . '_pl' ) ) );
|
||||
|
||||
/* Container */
|
||||
if ( isset( $itemClass::$containerNodeClass ) )
|
||||
{
|
||||
$form->add( new \IPS\Helpers\Form\Node( 'widget_feed_container_' . $itemClass::$title, isset( $this->configuration['widget_feed_container'] ) ? $this->configuration['widget_feed_container'] : 0, FALSE, array(
|
||||
'class' => $itemClass::$containerNodeClass,
|
||||
'zeroVal' => 'all',
|
||||
'permissionCheck' => 'view',
|
||||
'multiple' => true
|
||||
) ) );
|
||||
}
|
||||
|
||||
/* Use permissions? */
|
||||
if ( in_array( 'IPS\Content\Permissions', class_implements( $itemClass ) ) )
|
||||
{
|
||||
$form->add( new \IPS\Helpers\Form\YesNo( 'widget_feed_use_perms', isset( $this->configuration['widget_feed_use_perms'] ) ? $this->configuration['widget_feed_use_perms'] : TRUE, FALSE ) );
|
||||
}
|
||||
|
||||
/* Types */
|
||||
if ( in_array( 'IPS\Content\Lockable', class_implements( $itemClass ) ) )
|
||||
{
|
||||
$types = array(
|
||||
'any' => 'mod_confirm_either',
|
||||
'open' => 'mod_confirm_unlock',
|
||||
'closed' => 'mod_confirm_lock'
|
||||
);
|
||||
|
||||
$form->add( new \IPS\Helpers\Form\Radio( 'widget_feed_item_status_locked', isset( $this->configuration['widget_feed_item_status_locked'] ) ? $this->configuration['widget_feed_item_status_locked'] : 'any', FALSE, array( 'options' => $types ) ) );
|
||||
}
|
||||
if ( in_array( 'IPS\Content\Pinnable', class_implements( $itemClass ) ) )
|
||||
{
|
||||
$types = array(
|
||||
'any' => 'mod_confirm_either',
|
||||
'pinned' => 'mod_confirm_pin',
|
||||
'notpinned' => 'mod_confirm_unpin'
|
||||
);
|
||||
|
||||
$form->add( new \IPS\Helpers\Form\Radio( 'widget_feed_item_status_pinned', isset( $this->configuration['widget_feed_item_status_pinned'] ) ? $this->configuration['widget_feed_item_status_pinned'] : 'any', FALSE, array( 'options' => $types ) ) );
|
||||
}
|
||||
if ( in_array( 'IPS\Content\Featurable', class_implements( $itemClass ) ) )
|
||||
{
|
||||
$types = array(
|
||||
'any' => 'mod_confirm_either',
|
||||
'featured' => 'mod_confirm_feature',
|
||||
'notfeatured' => 'mod_confirm_unfeature'
|
||||
);
|
||||
|
||||
$form->add( new \IPS\Helpers\Form\Radio( 'widget_feed_item_status_featured', isset( $this->configuration['widget_feed_item_status_featured'] ) ? $this->configuration['widget_feed_item_status_featured'] : 'any', FALSE, array( 'options' => $types ) ) );
|
||||
}
|
||||
if ( in_array( 'IPS\Content\Hideable', class_implements( $class ) ) )
|
||||
{
|
||||
$types = array(
|
||||
'any' => 'mod_confirm_either',
|
||||
'visible' => 'mod_confirm_visible',
|
||||
'hidden' => 'mod_confirm_hidden'
|
||||
);
|
||||
|
||||
$form->add( new \IPS\Helpers\Form\Radio( 'widget_feed_comment_status_visible', isset( $this->configuration['widget_feed_comment_status_visible'] ) ? $this->configuration['widget_feed_comment_status_visible'] : 'any', FALSE, array( 'options' => $types ) ) );
|
||||
}
|
||||
|
||||
/* Author */
|
||||
$author = NULL;
|
||||
try
|
||||
{
|
||||
if ( isset( $this->configuration['widget_feed_item_author'] ) and is_array( $this->configuration['widget_feed_item_author'] ) )
|
||||
{
|
||||
foreach( $this->configuration['widget_feed_item_author'] as $id )
|
||||
{
|
||||
$author[ $id ] = \IPS\Member::load( $id );
|
||||
}
|
||||
}
|
||||
}
|
||||
catch( \OutOfRangeException $ex ) { }
|
||||
$form->add( new \IPS\Helpers\Form\Member( 'widget_feed_item_author', $author, FALSE, array( 'multiple' => true ) ) );
|
||||
|
||||
/* Minimum comments/reviews */
|
||||
if ( isset( $class::$commentClass ) )
|
||||
{
|
||||
if ( $class::$firstCommentRequired )
|
||||
{
|
||||
$form->add( new \IPS\Helpers\Form\Number( 'widget_feed_item_min_posts', isset( $this->configuration['widget_feed_item_min_posts'] ) ? $this->configuration['widget_feed_item_min_posts'] : 0, FALSE, array( 'unlimitedLang' => 'any', 'unlimited' => 0 ) ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
$form->add( new \IPS\Helpers\Form\Number( 'widget_feed_item_min_comments', isset( $this->configuration['widget_feed_item_min_comments'] ) ? $this->configuration['widget_feed_item_min_comments'] : 0, FALSE, array( 'unlimitedLang' => 'any', 'unlimited' => 0 ) ) );
|
||||
}
|
||||
}
|
||||
if ( isset( $class::$reviewClass ) )
|
||||
{
|
||||
$form->add( new \IPS\Helpers\Form\Number( 'widget_feed_item_min_reviews', isset( $this->configuration['widget_feed_item_min_reviews'] ) ? $this->configuration['widget_feed_item_min_reviews'] : 0, FALSE, array( 'unlimitedLang' => 'any', 'unlimited' => 0 ) ) );
|
||||
}
|
||||
|
||||
/* Rating */
|
||||
if ( in_array( 'IPS\Content\Ratings', class_implements( $class ) ) and isset( $class::$databaseColumnMap['rating_average'] ) )
|
||||
{
|
||||
$form->add( new \IPS\Helpers\Form\Rating( 'widget_feed_item_min_rating', isset( $this->configuration['widget_feed_item_min_rating'] ) ? $this->configuration['widget_feed_item_min_rating'] : 0, FALSE, array() ) );
|
||||
}
|
||||
|
||||
if ( isset( $class::$databaseColumnMap['date'] ) )
|
||||
{
|
||||
$form->add( new \IPS\Helpers\Form\Select( 'widget_feed_comment_date', isset( $this->configuration['widget_feed_comment_date'] ) ? $this->configuration['widget_feed_comment_date'] : '0', FALSE, array(
|
||||
'options' => array(
|
||||
0 => 'show_all',
|
||||
1 => 'today',
|
||||
5 => 'last_5_days',
|
||||
7 => 'last_7_days',
|
||||
10 => 'last_10_days',
|
||||
15 => 'last_15_days',
|
||||
20 => 'last_20_days',
|
||||
25 => 'last_25_days',
|
||||
30 => 'last_30_days',
|
||||
60 => 'last_60_days',
|
||||
90 => 'last_90_days',
|
||||
)
|
||||
) ) );
|
||||
}
|
||||
|
||||
/* Number to show */
|
||||
$form->add( new \IPS\Helpers\Form\Number( 'widget_feed_show', isset( $this->configuration['widget_feed_show'] ) ? $this->configuration['widget_feed_show'] : 5, TRUE ) );
|
||||
|
||||
/* Sort */
|
||||
$form->add( new \IPS\Helpers\Form\Select( 'widget_feed_sort_dir', isset( $this->configuration['widget_feed_sort_dir'] ) ? $this->configuration['widget_feed_sort_dir'] : 'desc', FALSE, array(
|
||||
'options' => array(
|
||||
'desc' => 'descending',
|
||||
'asc' => 'ascending'
|
||||
)
|
||||
) ) );
|
||||
|
||||
/* Fix up some language strings */
|
||||
$langs = array( 'widget_feed_item_status_locked', 'widget_feed_item_status_pinned', 'widget_feed_item_status_featured', 'widget_feed_comment_status_visible', 'widget_feed_item_author', 'widget_feed_item_min_posts', 'widget_feed_item_min_comments', 'widget_feed_item_min_reviews', 'widget_feed_item_min_rating' );
|
||||
$words = \IPS\Member::loggedIn()->language()->get( $langs );
|
||||
|
||||
foreach( $langs as $lang )
|
||||
{
|
||||
\IPS\Member::loggedIn()->language()->words[ $lang ] = sprintf( $words[ $lang ], \IPS\Member::loggedIn()->language()->addToStack( $itemClass::$title ) );
|
||||
}
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ran before saving widget configuration
|
||||
*
|
||||
* @param array $values Values from form
|
||||
* @return array
|
||||
*/
|
||||
public function preConfig( $values )
|
||||
{
|
||||
$class = static::$class;
|
||||
$itemClass = $class::$itemClass;
|
||||
|
||||
if ( is_array( $values[ 'widget_feed_container_' . $itemClass::$title ] ) )
|
||||
{
|
||||
$values['widget_feed_container'] = array_keys( $values[ 'widget_feed_container_' . $itemClass::$title ] );
|
||||
unset( $values[ 'widget_feed_container_' . $class::$title ] );
|
||||
}
|
||||
|
||||
return $values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get where clause
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function buildWhere()
|
||||
{
|
||||
$class = static::$class;
|
||||
$itemClass = $class::$itemClass;
|
||||
$where = array();
|
||||
|
||||
/* Container */
|
||||
if ( isset( $itemClass::$containerNodeClass ) and !empty( $this->configuration['widget_feed_container'] ) )
|
||||
{
|
||||
$where['item'][] = array( \IPS\Db::i()->in( $itemClass::$databasePrefix . $itemClass::$databaseColumnMap['container'], $this->configuration['widget_feed_container'] ) );
|
||||
}
|
||||
|
||||
/* Status */
|
||||
if ( isset( $this->configuration['widget_feed_item_status_locked'] ) and in_array( 'IPS\Content\Lockable', class_implements( $itemClass ) ) )
|
||||
{
|
||||
if ( $this->configuration['widget_feed_item_status_locked'] == 'closed' )
|
||||
{
|
||||
$where['item'][] = isset( $itemClass::$databaseColumnMap['locked'] ) ? array( $itemClass::$databaseTable . '.' . $itemClass::$databasePrefix . $itemClass::$databaseColumnMap['locked'] . '=?', 1 ) : array( $itemClass::$databaseTable . '.' . $itemClass::$databasePrefix . $itemClass::$databaseColumnMap['status'] . '=?', 'closed' );
|
||||
}
|
||||
elseif ( $this->configuration['widget_feed_item_status_locked'] == 'open' )
|
||||
{
|
||||
$where['item'][] = isset( $itemClass::$databaseColumnMap['locked'] ) ? array( $itemClass::$databaseTable . '.' . $itemClass::$databasePrefix . $itemClass::$databaseColumnMap['locked'] . '=?', 0 ) : array( $itemClass::$databaseTable . '.' . $itemClass::$databasePrefix . $itemClass::$databaseColumnMap['status'] . '=?', 'open' );
|
||||
}
|
||||
}
|
||||
|
||||
if ( isset( $this->configuration['widget_feed_item_status_featured'] ) and in_array( 'IPS\Content\Featurable', class_implements( $itemClass ) ) )
|
||||
{
|
||||
if ( $this->configuration['widget_feed_item_status_featured'] == 'notfeatured' )
|
||||
{
|
||||
$where['item'][] = array( $itemClass::$databaseTable . '.' . $itemClass::$databasePrefix . $itemClass::$databaseColumnMap['featured'] . '=0' );
|
||||
}
|
||||
elseif ( $this->configuration['widget_feed_item_status_featured'] == 'featured' )
|
||||
{
|
||||
$where['item'][] = array( $itemClass::$databaseTable . '.' . $itemClass::$databasePrefix . $itemClass::$databaseColumnMap['featured'] . '=1' );
|
||||
}
|
||||
}
|
||||
|
||||
if ( isset( $this->configuration['widget_feed_item_status_pinned'] ) and in_array( 'IPS\Content\Pinnable', class_implements( $itemClass ) ) )
|
||||
{
|
||||
if ( $this->configuration['widget_feed_item_status_pinned'] == 'notpinned' )
|
||||
{
|
||||
$where['item'][] = array( $itemClass::$databaseTable . '.' . $itemClass::$databasePrefix . $itemClass::$databaseColumnMap['pinned'] . '=0' );
|
||||
}
|
||||
elseif ( $this->configuration['widget_feed_item_status_pinned'] == 'pinned' )
|
||||
{
|
||||
$where['item'][] = array( $itemClass::$databaseTable . '.' . $itemClass::$databasePrefix . $itemClass::$databaseColumnMap['pinned'] . '=1' );
|
||||
}
|
||||
}
|
||||
|
||||
/* Author */
|
||||
if ( isset( $this->configuration['widget_feed_item_author'] ) and is_array( $this->configuration['widget_feed_item_author'] ) )
|
||||
{
|
||||
$where['item'][] = array( \IPS\Db::i()->in( $itemClass::$databaseTable . '.' . $itemClass::$databasePrefix . $itemClass::$databaseColumnMap['author'], $this->configuration['widget_feed_item_author'] ) );
|
||||
}
|
||||
|
||||
/* Min comments/reviews */
|
||||
if ( isset( $this->configuration['widget_feed_item_min_posts'] ) and isset( $this->configuration['widget_feed_item_min_posts'] ) and isset( $this->configuration['widget_feed_item_min_posts'] ) )
|
||||
{
|
||||
$where['item'][] = array( $itemClass::$databaseTable . '.' . $itemClass::$databasePrefix . $itemClass::$databaseColumnMap['num_comments'] . '>?', (int) $this->configuration['widget_feed_item_min_posts'] );
|
||||
}
|
||||
if ( isset( $this->configuration['widget_feed_item_min_comments'] ) and isset( $this->configuration['widget_feed_item_min_comments'] ) and $this->configuration['widget_feed_item_min_comments'] )
|
||||
{
|
||||
$where['item'][] = array( $itemClass::$databaseTable . '.' . $itemClass::$databasePrefix . $itemClass::$databaseColumnMap['num_comments'] . '>?', (int) $this->configuration['widget_feed_item_min_comments'] );
|
||||
}
|
||||
if ( isset( $this->configuration['widget_feed_item_min_reviews'] ) and isset( $this->configuration['widget_feed_item_min_reviews'] ) and $this->configuration['widget_feed_item_min_reviews'] )
|
||||
{
|
||||
$where['item'][] = array( $itemClass::$databaseTable . '.' . $itemClass::$databasePrefix . $itemClass::$databaseColumnMap['num_reviews'] . '>?', (int) $this->configuration['widget_feed_item_min_reviews'] );
|
||||
}
|
||||
|
||||
/* Rating */
|
||||
if ( isset( $this->configuration['widget_feed_item_min_rating'] ) and isset( $this->configuration['widget_feed_item_min_rating'] ) and $this->configuration['widget_feed_item_min_rating'] )
|
||||
{
|
||||
$where['item'][] = array( $itemClass::$databasePrefix . $itemClass::$databaseColumnMap['rating_average'] . '>?', (int) $this->configuration['widget_feed_item_min_rating'] );
|
||||
}
|
||||
|
||||
/* Start date */
|
||||
if ( isset( $this->configuration['widget_feed_comment_date'] ) and isset( $this->configuration['widget_feed_comment_date'] ) and $this->configuration['widget_feed_comment_date'] > 0 )
|
||||
{
|
||||
$where[] = array( $class::$databaseTable . '.' . $class::$databasePrefix . $class::$databaseColumnMap['date'] . '>?', \IPS\DateTime::create()->sub( new \DateInterval( 'P' . $this->configuration['widget_feed_comment_date'] . 'D' ) )->getTimestamp() );
|
||||
}
|
||||
|
||||
return $where;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a widget
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
$class = static::$class;
|
||||
|
||||
$items = $class::getItemsWithPermission(
|
||||
$this->buildWhere(),
|
||||
( isset( $this->configuration['widget_feed_sort_on'] ) and isset( $this->configuration['widget_feed_sort_dir'] ) ) ? ( $class::$databasePrefix . $class::$databaseColumnMap['date'] . ' ' . $this->configuration['widget_feed_sort_dir'] ) : NULL,
|
||||
isset( $this->configuration['widget_feed_show'] ) ? $this->configuration['widget_feed_show'] : 5,
|
||||
( !isset( $this->configuration['widget_feed_use_perms'] ) or $this->configuration['widget_feed_use_perms'] ) ? 'read' : NULL,
|
||||
( isset( $this->configuration['widget_feed_comment_status_visible'] ) and ( $this->configuration['widget_feed_comment_status_visible'] == 'either' or $this->configuration['widget_feed_comment_status_visible'] == 'hidden' ) ? TRUE : FALSE )
|
||||
);
|
||||
|
||||
if ( count( $items ) )
|
||||
{
|
||||
return $this->output( $items, isset( $this->configuration['widget_feed_title'] ) ? $this->configuration['widget_feed_title'] : \IPS\Member::loggedIn()->language()->addToStack( $class::$title . '_pl' ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -204,7 +204,7 @@ abstract class _CustomField extends \IPS\Node\Model
|
||||
return in_array( $newType, array( 'Checkbox', 'YesNo' ) );
|
||||
|
||||
case 'CheckboxSet':
|
||||
return in_array( $newType, array( 'CheckboxSet' ) );
|
||||
return in_array( $newType, array( 'CheckboxSet', 'Select' ) );
|
||||
|
||||
case 'Code':
|
||||
case 'Codemirror':
|
||||
@@ -221,6 +221,9 @@ abstract class _CustomField extends \IPS\Node\Model
|
||||
|
||||
case 'Email':
|
||||
return in_array( $newType, array( 'Email', 'Password', 'Text' ) );
|
||||
|
||||
case 'Item':
|
||||
return in_array( $newType, array( 'Item', 'TextArea' ) );
|
||||
|
||||
case 'Member':
|
||||
return in_array( $newType, array( 'Member', 'Text', 'TextArea', 'Editor' ) );
|
||||
@@ -585,7 +588,7 @@ abstract class _CustomField extends \IPS\Node\Model
|
||||
{
|
||||
if( $value !== NULL AND $value !== '' )
|
||||
{
|
||||
$value = explode( ',', htmlentities( $value, \IPS\HTMLENTITIES, 'UTF-8', FALSE ) );
|
||||
$value = explode( ',', htmlspecialchars( $value, \IPS\HTMLENTITIES, 'UTF-8', FALSE ) );
|
||||
}
|
||||
|
||||
$options['multiple'] = $this->multiple;
|
||||
@@ -595,7 +598,8 @@ abstract class _CustomField extends \IPS\Node\Model
|
||||
$options['options'] = json_decode( $this->content, TRUE );
|
||||
$options['options'] = ( $options['options'] ) ? array_combine( $options['options'], $options['options'] ) : array();
|
||||
|
||||
if ( !$this->required AND !$this->multiple )
|
||||
$requiredColumn = ( isset( $this::$databaseColumnMap['not_null'] ) and $this::$databaseColumnMap['not_null'] ) ? $this::$databaseColumnMap['not_null'] : NULL;
|
||||
if ( !$this->$requiredColumn AND !$this->multiple )
|
||||
{
|
||||
array_unshift( $options['options'], '' );
|
||||
}
|
||||
@@ -691,7 +695,14 @@ abstract class _CustomField extends \IPS\Node\Model
|
||||
*/
|
||||
public function claimAttachments( $id1 = NULL )
|
||||
{
|
||||
\IPS\File::claimAttachments( md5( get_class( $this ) . '-' . $this->id ), $id1, $this->id );
|
||||
$options = static::$editorOptions;
|
||||
|
||||
if ( !isset( $options['autoSaveKey'] ) )
|
||||
{
|
||||
$options['autoSaveKey'] = md5( get_class( $this ) . '-' . $this->id );
|
||||
}
|
||||
|
||||
\IPS\File::claimAttachments( $options['autoSaveKey'], $id1, $this->id );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -740,11 +751,11 @@ abstract class _CustomField extends \IPS\Node\Model
|
||||
|
||||
if ( $this->multiple )
|
||||
{
|
||||
return implode( '<br>', explode( ',', htmlentities( $value, \IPS\HTMLENTITIES, 'UTF-8', FALSE ) ) );
|
||||
return implode( '<br>', explode( ',', htmlspecialchars( $value, \IPS\HTMLENTITIES, 'UTF-8', FALSE ) ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
return htmlentities( \IPS\Lang::wordbreak( $value ), \IPS\HTMLENTITIES, 'UTF-8', FALSE );
|
||||
return htmlspecialchars( \IPS\Lang::wordbreak( $value ), \IPS\HTMLENTITIES, 'UTF-8', FALSE );
|
||||
}
|
||||
|
||||
case 'Codemirror':
|
||||
@@ -772,7 +783,7 @@ abstract class _CustomField extends \IPS\Node\Model
|
||||
return \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' )->rating( $this->options['max'], $value );
|
||||
|
||||
case 'TextArea':
|
||||
return \IPS\Lang::wordbreak( nl2br( htmlentities( $value, \IPS\HTMLENTITIES, 'UTF-8', FALSE ) ) );
|
||||
return \IPS\Lang::wordbreak( nl2br( htmlspecialchars( $value, \IPS\HTMLENTITIES, 'UTF-8', FALSE ) ) );
|
||||
|
||||
case 'Url':
|
||||
$munged = ( $value ) ? \IPS\Http\Url::external( $value )->makeSafeForAcp() : NULL;
|
||||
@@ -786,7 +797,7 @@ abstract class _CustomField extends \IPS\Node\Model
|
||||
|
||||
|
||||
default:
|
||||
return \IPS\Lang::wordbreak( htmlentities( $value, \IPS\HTMLENTITIES, 'UTF-8', FALSE ) );
|
||||
return \IPS\Lang::wordbreak( htmlspecialchars( $value, \IPS\HTMLENTITIES, 'UTF-8', FALSE ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -147,4 +147,18 @@ abstract class _Cache extends AbstractData
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear All Caches
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function clearAll()
|
||||
{
|
||||
/* cacheKeys stored md5 hashes of the correct value, and the cache
|
||||
is only used if the value it returns matches the hash, so clearing
|
||||
this out invalidates all caches, even if the caching engine
|
||||
does not allow us to actually clear them */
|
||||
\IPS\Data\Store::i()->cacheKeys = array();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -113,7 +113,7 @@ class _Apc extends \IPS\Data\Cache
|
||||
*/
|
||||
public function clearAll()
|
||||
{
|
||||
\IPS\Log::i( LOG_DEBUG )->setIpAddress( $_SERVER['REMOTE_ADDR'] )->write( "Flush all caches from APC", 'cache' );
|
||||
parent::clearAll();
|
||||
apc_clear_cache();
|
||||
}
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief eAccelerator Cache Class
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - SVN_YYYY Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Social Suite
|
||||
* @since 17 Sept 2013
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS\Data\Cache;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* eAccelerator Storage Class
|
||||
*/
|
||||
class _Eaccelerator extends \IPS\Data\Cache
|
||||
{
|
||||
/**
|
||||
* Server supports this method?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function supported()
|
||||
{
|
||||
return function_exists('eaccelerator_get');
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract Method: Get
|
||||
*
|
||||
* @param string $key Key
|
||||
* @return string Value from the _datastore
|
||||
*/
|
||||
protected function get( $key )
|
||||
{
|
||||
if( isset( $this->cache[ $key ] ) )
|
||||
{
|
||||
\IPS\Log::i( LOG_DEBUG )->setIpAddress( $_SERVER['REMOTE_ADDR'] )->write( "Get {$key} from eAccelerator (already loaded)", 'cache' );
|
||||
return $this->cache[ $key ];
|
||||
}
|
||||
|
||||
\IPS\Log::i( LOG_DEBUG )->setIpAddress( $_SERVER['REMOTE_ADDR'] )->write( "Get {$key} from eAccelerator", 'cache' );
|
||||
|
||||
$this->cache[ $key ] = eaccelerator_get( \IPS\SUITE_UNIQUE_KEY . '_' . $key );
|
||||
return $this->cache[ $key ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract Method: Set
|
||||
*
|
||||
* @param string $key Key
|
||||
* @param string $value Value
|
||||
* @return bool
|
||||
*/
|
||||
protected function set( $key, $value )
|
||||
{
|
||||
\IPS\Log::i( LOG_DEBUG )->setIpAddress( $_SERVER['REMOTE_ADDR'] )->write( "Set {$key} in eAccelerator", 'cache' );
|
||||
|
||||
eaccelerator_lock( \IPS\SUITE_UNIQUE_KEY . '_' . $key );
|
||||
$result = (bool) eaccelerator_put( \IPS\SUITE_UNIQUE_KEY . '_' . $key, $value );
|
||||
eaccelerator_unlock( \IPS\SUITE_UNIQUE_KEY . '_' . $key );
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract Method: Exists?
|
||||
*
|
||||
* @param string $key Key
|
||||
* @return bool
|
||||
*/
|
||||
protected function exists( $key )
|
||||
{
|
||||
if( isset( $this->cache[ $key ] ) )
|
||||
{
|
||||
\IPS\Log::i( LOG_DEBUG )->setIpAddress( $_SERVER['REMOTE_ADDR'] )->write( "Check exists {$key} from eAccelerator (already loaded)", 'cache' );
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
\IPS\Log::i( LOG_DEBUG )->setIpAddress( $_SERVER['REMOTE_ADDR'] )->write( "Check exists {$key} from eAccelerator", 'cache' );
|
||||
return (bool) $this->get( $key );
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract Method: Delete
|
||||
*
|
||||
* @param string $key Key
|
||||
* @return bool
|
||||
*/
|
||||
protected function delete( $key )
|
||||
{
|
||||
\IPS\Log::i( LOG_DEBUG )->setIpAddress( $_SERVER['REMOTE_ADDR'] )->write( "Delete {$key} from eAccelerator", 'cache' );
|
||||
return (bool) eaccelerator_rm( \IPS\SUITE_UNIQUE_KEY . '_' . $key );
|
||||
}
|
||||
|
||||
/**
|
||||
* Destructor
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
if( function_exists( 'eaccelerator_gc' ) )
|
||||
{
|
||||
\IPS\Log::i( LOG_DEBUG )->setIpAddress( $_SERVER['REMOTE_ADDR'] )->write( "eAccelerator GC", 'cache' );
|
||||
eaccelerator_gc();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract Method: Clear All Caches
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function clearAll()
|
||||
{
|
||||
\IPS\Log::i( LOG_DEBUG )->setIpAddress( $_SERVER['REMOTE_ADDR'] )->write( "Flush all caches from eAccelerator (not implemented)", 'cache' );
|
||||
}
|
||||
}
|
||||
@@ -171,10 +171,10 @@ class _Memcache extends \IPS\Data\Cache
|
||||
else
|
||||
{
|
||||
$value = $this->link->get( \IPS\SUITE_UNIQUE_KEY . '_' . $key );
|
||||
|
||||
|
||||
if( method_exists( $this->link, 'getResultCode' ) )
|
||||
{
|
||||
if ( $this->link->getResultCode() == \Memcached::RES_NOTFOUND )
|
||||
if ( $this->link->getResultCode() == \Memcached::RES_NOTFOUND or $value === FALSE )
|
||||
{
|
||||
$this->doesNotExist[] = $key;
|
||||
return FALSE;
|
||||
@@ -232,6 +232,7 @@ class _Memcache extends \IPS\Data\Cache
|
||||
*/
|
||||
public function clearAll()
|
||||
{
|
||||
parent::clearAll();
|
||||
$this->link->flush();
|
||||
}
|
||||
}
|
||||
@@ -144,6 +144,7 @@ class _None extends \IPS\Data\Cache
|
||||
*/
|
||||
public function clearAll()
|
||||
{
|
||||
parent::clearAll();
|
||||
try
|
||||
{
|
||||
\IPS\Db::i()->delete( 'core_cache' );
|
||||
|
||||
@@ -155,7 +155,8 @@ class _Redis extends \IPS\Data\Cache
|
||||
}
|
||||
|
||||
\IPS\Log::i( LOG_DEBUG )->setIpAddress( $_SERVER['REMOTE_ADDR'] )->write( "Check exists {$key} from Redis", 'cache' );
|
||||
return $this->link->exists( $key );
|
||||
/* We do a get instead of an exists() check because it will cause the cache value to be fetched and cached inline, saving another call to the server */
|
||||
return ( $this->get( $key ) === FALSE ) ? FALSE : TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -184,14 +185,4 @@ class _Redis extends \IPS\Data\Cache
|
||||
$this->link->close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract Method: Clear All Caches
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function clearAll()
|
||||
{
|
||||
\IPS\Log::i( LOG_DEBUG )->setIpAddress( $_SERVER['REMOTE_ADDR'] )->write( "Flush all caches from Redis (not implemented)", 'cache' );
|
||||
}
|
||||
}
|
||||
@@ -114,6 +114,7 @@ class _Test extends \IPS\Data\Cache
|
||||
*/
|
||||
public function clearAll()
|
||||
{
|
||||
parent::clearAll();
|
||||
foreach ( new \DirectoryIterator( $this->_path ) as $file )
|
||||
{
|
||||
if ( !$file->isDot() and $file != 'index.html' )
|
||||
|
||||
@@ -104,7 +104,7 @@ class _Wincache extends \IPS\Data\Cache
|
||||
*/
|
||||
public function clearAll()
|
||||
{
|
||||
\IPS\Log::i( LOG_DEBUG )->setIpAddress( $_SERVER['REMOTE_ADDR'] )->write( "Flush all caches from Wincache", 'cache' );
|
||||
parent::clearAll();
|
||||
wincache_ucache_clear();
|
||||
}
|
||||
}
|
||||
@@ -95,14 +95,4 @@ class _Xcache extends \IPS\Data\Cache
|
||||
\IPS\Log::i( LOG_DEBUG )->setIpAddress( $_SERVER['REMOTE_ADDR'] )->write( "Delete {$key} from Xcache", 'cache' );
|
||||
return (bool) xcache_unset( \IPS\SUITE_UNIQUE_KEY . '_' . $key );
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract Method: Clear All Caches
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function clearAll()
|
||||
{
|
||||
\IPS\Log::i( LOG_DEBUG )->setIpAddress( $_SERVER['REMOTE_ADDR'] )->write( "Flush all caches from Xcache (not implemented)", 'cache' );
|
||||
}
|
||||
}
|
||||
+57
-48
@@ -79,43 +79,45 @@ abstract class _Store extends AbstractData
|
||||
*/
|
||||
public function __get( $key )
|
||||
{
|
||||
if( \IPS\CACHE_METHOD !== 'None' )
|
||||
{
|
||||
if( isset( $this->cacheKeys ) )
|
||||
{
|
||||
$cacheKeys = $this->cacheKeys;
|
||||
|
||||
if( !is_array( $cacheKeys ) )
|
||||
{
|
||||
$cacheKeys = array();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$cacheKeys = array();
|
||||
}
|
||||
}
|
||||
|
||||
/* Try to get it from the cache store... */
|
||||
try
|
||||
{
|
||||
if ( \IPS\CACHE_METHOD !== 'None' and $key !== 'cacheKeys' and isset( $cacheKeys[ $key ] ) and isset( \IPS\Data\Cache::i()->$key ) )
|
||||
{
|
||||
$value = \IPS\Data\Cache::i()->$key;
|
||||
if ( $cacheKeys[ $key ] === md5( json_encode( $value ) ) )
|
||||
{
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
throw new \OutOfRangeException;
|
||||
}
|
||||
catch ( \OutOfRangeException $e )
|
||||
{
|
||||
$value = parent::__get( $key );
|
||||
|
||||
/* If caching is enabled, and this isn't the special map of hashes... */
|
||||
if ( \IPS\CACHE_METHOD !== 'None' and $key !== 'cacheKeys' )
|
||||
{
|
||||
/* And exists in the caching engine, and we know the hash of the correct value... */
|
||||
$cacheKeys = ( isset( $this->cacheKeys ) and is_array( $this->cacheKeys ) ) ? $this->cacheKeys : array();
|
||||
if ( isset( $cacheKeys[ $key ] ) and isset( \IPS\Data\Cache::i()->$key ) )
|
||||
{
|
||||
/* Get it... */
|
||||
$value = \IPS\Data\Cache::i()->$key;
|
||||
|
||||
/* But only use it if the hash matches */
|
||||
if ( $cacheKeys[ $key ] === md5( json_encode( $value ) ) )
|
||||
{
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Still here? throw an exception so we get it from the data storage engine */
|
||||
throw new \OutOfRangeException;
|
||||
}
|
||||
|
||||
/* If we couldn't get it from the cache engine, get it from the data storage engine */
|
||||
catch ( \OutOfRangeException $e )
|
||||
{
|
||||
/* Actually get it... */
|
||||
$value = parent::__get( $key );
|
||||
|
||||
/* If caching is enabled, and this isn't the special map of hashes... */
|
||||
if ( \IPS\CACHE_METHOD !== 'None' and $key !== 'cacheKeys' )
|
||||
{
|
||||
/* Set it in the caching engine... */
|
||||
\IPS\Data\Cache::i()->$key = $value;
|
||||
|
||||
/* And set the hash in the cacheKeys hash map */
|
||||
$cacheKeys = ( isset( $this->cacheKeys ) and is_array( $this->cacheKeys ) ) ? $this->cacheKeys : array();
|
||||
$cacheKeys[ $key ] = md5( json_encode( $value ) );
|
||||
$this->cacheKeys = $cacheKeys;
|
||||
}
|
||||
@@ -133,13 +135,17 @@ abstract class _Store extends AbstractData
|
||||
*/
|
||||
public function __set( $key, $value )
|
||||
{
|
||||
/* Actually set it in the data storage engine */
|
||||
parent::__set( $key, $value );
|
||||
|
||||
/* If caching is enabled, and this isn't the special map of hashes... */
|
||||
if ( \IPS\CACHE_METHOD !== 'None' and $key !== 'cacheKeys' )
|
||||
{
|
||||
/* Then also set it in the cache... */
|
||||
\IPS\Data\Cache::i()->$key = $value;
|
||||
|
||||
if ( isset( $this->cacheKeys ) )
|
||||
/* And set the hash in the cacheKeys hash map */
|
||||
if ( ( isset( $this->cacheKeys ) and is_array( $this->cacheKeys ) ) )
|
||||
{
|
||||
$cacheKeys = is_array( $this->cacheKeys ) ? $this->cacheKeys : array();
|
||||
$cacheKeys[ $key ] = md5( json_encode( $value ) );
|
||||
@@ -160,19 +166,25 @@ abstract class _Store extends AbstractData
|
||||
*/
|
||||
public function __isset( $key )
|
||||
{
|
||||
if ( \IPS\CACHE_METHOD !== 'None' and $key !== 'cacheKeys' and isset( $this->cacheKeys ) and isset( $this->cacheKeys[ $key ] ) and isset( \IPS\Data\Cache::i()->$key ) )
|
||||
/* If caching is enabled, and this isn't the special map of hashes, try to get it from the cache store... */
|
||||
if ( \IPS\CACHE_METHOD !== 'None' and $key !== 'cacheKeys' )
|
||||
{
|
||||
try
|
||||
/* Does it exist in the caching engine? */
|
||||
if ( isset( \IPS\Data\Cache::i()->$key ) )
|
||||
{
|
||||
/* Get it... */
|
||||
$value = \IPS\Data\Cache::i()->$key;
|
||||
if ( $this->cacheKeys[ $key ] === md5( json_encode( $value ) ) )
|
||||
|
||||
/* But only use it if the hash matches */
|
||||
$cacheKeys = ( isset( $this->cacheKeys ) and is_array( $this->cacheKeys ) ) ? $this->cacheKeys : array();
|
||||
if ( isset( $cacheKeys[ $key ] ) and $cacheKeys[ $key ] === md5( json_encode( $value ) ) )
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
catch ( \OutOfRangeException $e ) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* If we're still here, check the data storage engine... */
|
||||
return parent::__isset( $key );
|
||||
}
|
||||
|
||||
@@ -184,22 +196,19 @@ abstract class _Store extends AbstractData
|
||||
*/
|
||||
public function __unset( $key )
|
||||
{
|
||||
/* Unset it in the data storage engine */
|
||||
parent::__unset( $key );
|
||||
|
||||
/* If caching is enabled, and this isn't the special map of hashes, remove it from the cache store... */
|
||||
if ( \IPS\CACHE_METHOD !== 'None' and $key !== 'cacheKeys' )
|
||||
{
|
||||
/* Remove it from the cache store... */
|
||||
unset( \IPS\Data\Cache::i()->$key );
|
||||
|
||||
if ( isset( $this->cacheKeys ) )
|
||||
{
|
||||
$cacheKeys = is_array( $this->cacheKeys ) ? $this->cacheKeys : array();
|
||||
|
||||
if( isset( $cacheKeys[ $key ] ) )
|
||||
{
|
||||
unset( $cacheKeys[ $key ] );
|
||||
$this->cacheKeys = $cacheKeys;
|
||||
}
|
||||
}
|
||||
/* And from the special map of hashes */
|
||||
$cacheKeys = is_array( $this->cacheKeys ) ? $this->cacheKeys : array();
|
||||
unset( $cacheKeys[ $key ] );
|
||||
$this->cacheKeys = $cacheKeys;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -55,16 +55,25 @@ class _Database extends \IPS\Data\Store
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
if ( \IPS\Session\Front::loggedIn() )
|
||||
{
|
||||
/* When caching is enabled, we only need cacheKeys, the rest will come from the cache */
|
||||
if ( \IPS\CACHE_METHOD !== 'None' )
|
||||
{
|
||||
$this->initLoad[] = 'administrators';
|
||||
$this->initLoad[] = 'moderators';
|
||||
$this->initLoad[] = 'emoticons';
|
||||
$this->initLoad = array( 'cacheKeys' );
|
||||
}
|
||||
/* Otherwise, we also need a few more things depending on whether the user is logged in or not */
|
||||
else
|
||||
{
|
||||
$this->initLoad[] = 'loginHandlers';
|
||||
if ( \IPS\Session\Front::loggedIn() )
|
||||
{
|
||||
$this->initLoad[] = 'administrators';
|
||||
$this->initLoad[] = 'moderators';
|
||||
$this->initLoad[] = 'emoticons';
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->initLoad[] = 'loginHandlers';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@ class _FileSystem extends \IPS\Data\Store
|
||||
{
|
||||
/* It is remotely possible that the file was removed in between an exists check and a get call on a busy site,
|
||||
so we need to make sure the file still exists in order to prevent a php warning */
|
||||
clearstatcache( FALSE, $this->_path . '/' . $key . '.' . \IPS\SUITE_UNIQUE_KEY . '.php' );
|
||||
clearstatcache();
|
||||
|
||||
if ( file_exists( $this->_path . '/' . $key . '.' . \IPS\SUITE_UNIQUE_KEY . '.php' ) )
|
||||
{
|
||||
@@ -126,6 +126,14 @@ CONTENTS;
|
||||
|
||||
$result = (bool) @\file_put_contents( $this->_path . '/' . $key . '.' . \IPS\SUITE_UNIQUE_KEY . '.php', $contents, LOCK_EX );
|
||||
|
||||
/* Sometimes LOCK_EX is unavailable and throws file_put_contents(): Exclusive locks are not supported for this stream.
|
||||
While we would prefer an exclusive lock, it would be better to write the file if possible. */
|
||||
if( !$result )
|
||||
{
|
||||
@unlink( $this->_path . '/' . $key . '.' . \IPS\SUITE_UNIQUE_KEY . '.php' );
|
||||
$result = (bool) @\file_put_contents( $this->_path . '/' . $key . '.' . \IPS\SUITE_UNIQUE_KEY . '.php', $contents );
|
||||
}
|
||||
|
||||
@chmod( $this->_path . '/' . $key . '.' . \IPS\SUITE_UNIQUE_KEY . '.php', \IPS\IPS_FILE_PERMISSION );
|
||||
|
||||
static::$cache[ $key ] = $value;
|
||||
|
||||
@@ -148,20 +148,23 @@ class _DateTime extends \DateTime
|
||||
* @param bool $capialize TRUE if by itself, FALSE if in the middle of a sentence
|
||||
* @return string
|
||||
*/
|
||||
public function html( $capialize=TRUE )
|
||||
public function html( $capialize=TRUE, $short=FALSE )
|
||||
{
|
||||
$format = $capialize ? static::RELATIVE_FORMAT_NORMAL : static::RELATIVE_FORMAT_LOWER;
|
||||
$format = $short ? 1 : ( $capialize ? static::RELATIVE_FORMAT_NORMAL : static::RELATIVE_FORMAT_LOWER );
|
||||
|
||||
return "<time datetime='{$this->rfc3339()}' title='{$this}' data-short='" . trim( $this->relative(1) ) . "'>" . trim( $this->relative( $format ) ) . "</time>";
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Format the date according to the user's locale (without the time)
|
||||
*
|
||||
* @return string
|
||||
* @param \IPS\Member $member
|
||||
* @return string
|
||||
*/
|
||||
public function localeDate()
|
||||
public function localeDate( \IPS\Member $member = NULL )
|
||||
{
|
||||
return \IPS\Member::loggedIn()->language()->convertString( strftime( '%x', $this->getTimestamp() + $this->getTimezone()->getOffset( $this ) ) );
|
||||
$member = ( $member === NULL ) ? \IPS\Member::loggedIn() : $member;
|
||||
return $member->language()->convertString( strftime( '%x', $this->getTimestamp() + $this->getTimezone()->getOffset( $this ) ) );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -243,7 +246,7 @@ class _DateTime extends \DateTime
|
||||
* @return string
|
||||
*/
|
||||
public function localeTime( $seconds=TRUE, $minutes=TRUE )
|
||||
{
|
||||
{
|
||||
return \IPS\Member::loggedIn()->language()->convertString( strftime( $this->localeTimeFormat( $seconds, $minutes ), $this->getTimestamp() + $this->getTimezone()->getOffset( $this ) ) );
|
||||
}
|
||||
|
||||
|
||||
+28
-62
@@ -256,41 +256,33 @@ class _Db extends \mysqli
|
||||
/* Sort out subqueries */
|
||||
$binds = array();
|
||||
$i = 0;
|
||||
foreach ( $_binds as $bindVal )
|
||||
for ( $j = 0; $j < mb_strlen( $query ); $j++ )
|
||||
{
|
||||
$i++;
|
||||
if ( $bindVal instanceof \IPS\Db\Select )
|
||||
if ( $query[ $j ] == '?' )
|
||||
{
|
||||
$pos = 0;
|
||||
for ( $j=0; $j<$i; $j++ )
|
||||
if ( $_binds[ $i ] instanceof \IPS\Db\Select )
|
||||
{
|
||||
$pos = mb_strpos( $query, '?', $pos ) + 1;
|
||||
$query = mb_substr( $query, 0, $j ) . $_binds[ $i ]->query . mb_substr( $query, $j + 1);
|
||||
$j += mb_strlen( $_binds[ $i ]->query );
|
||||
|
||||
foreach ( $_binds[ $i ]->binds as $_bind )
|
||||
{
|
||||
$binds[] = $_bind;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$binds[] = $_binds[ $i ];
|
||||
}
|
||||
$query = mb_substr( $query, 0, $pos - 1 ) . $bindVal->query . mb_substr( $query, $pos );
|
||||
$i--;
|
||||
|
||||
foreach ( $bindVal->binds as $v )
|
||||
{
|
||||
$binds[] = $v;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$binds[] = $bindVal;
|
||||
$i++;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Loop values to bind */
|
||||
$i = 0;
|
||||
$longThreshold = 1048576;
|
||||
$sendAsLong = array();
|
||||
foreach ( $binds as $bindVal )
|
||||
{
|
||||
if( ( is_object( $bindVal ) OR is_string( $bindVal ) ) AND \strlen( (string) $bindVal ) > $longThreshold )
|
||||
{
|
||||
$sendAsLong[ $i ] = (string) $bindVal;
|
||||
}
|
||||
|
||||
$i++;
|
||||
switch ( gettype( $bindVal ) )
|
||||
{
|
||||
@@ -304,27 +296,13 @@ class _Db extends \mysqli
|
||||
break;
|
||||
|
||||
case 'string':
|
||||
if( \strlen( $bindVal ) > $longThreshold )
|
||||
{
|
||||
$bind->add( 'b', NULL );
|
||||
}
|
||||
else
|
||||
{
|
||||
$bind->add( 's', $bindVal );
|
||||
}
|
||||
$bind->add( 's', $bindVal );
|
||||
break;
|
||||
|
||||
case 'object':
|
||||
if( method_exists( $bindVal, '__toString' ) )
|
||||
{
|
||||
if( \strlen( $bindVal ) > $longThreshold )
|
||||
{
|
||||
$bind->add( 'b', NULL );
|
||||
}
|
||||
else
|
||||
{
|
||||
$bind->add( 's', (string) $bindVal );
|
||||
}
|
||||
$bind->add( 's', (string) $bindVal );
|
||||
break;
|
||||
}
|
||||
// Deliberately no break
|
||||
@@ -370,7 +348,7 @@ class _Db extends \mysqli
|
||||
$line = $b['line'];
|
||||
}
|
||||
|
||||
if( isset( $b['class'] ) and $b['class'] !== 'IPS\_Db' and $b['class'] !== 'IPS\Db\_Select' )
|
||||
if( isset( $b['class'] ) and !in_array( $b['class'], array( 'IPS\_Db', 'IPS\Db\_Select', 'IPS\Patterns\_ActiveRecord', 'IPS\Patterns\_ActiveRecordIterator' ) ) )
|
||||
{
|
||||
$comment = "{$b['class']}::{$b['function']}:{$line}";
|
||||
break;
|
||||
@@ -378,31 +356,18 @@ class _Db extends \mysqli
|
||||
}
|
||||
$_query = $query;
|
||||
$query = "/*{$comment}*/ {$query}";
|
||||
|
||||
|
||||
/* Prepare */
|
||||
$stmt = parent::prepare( $query );
|
||||
if( $stmt === FALSE )
|
||||
{
|
||||
throw new \IPS\Db\Exception( $this->error, $this->errno, NULL, $_query, $binds );
|
||||
}
|
||||
|
||||
|
||||
/* Bind values */
|
||||
if( $bind->haveBinds() === TRUE )
|
||||
{
|
||||
call_user_func_array( array( $stmt, 'bind_param' ), $bind->get() );
|
||||
|
||||
if( count( $sendAsLong ) )
|
||||
{
|
||||
foreach( $sendAsLong as $index => $data )
|
||||
{
|
||||
$chunks = str_split( $data, $longThreshold - 1 );
|
||||
|
||||
foreach( $chunks as $chunk )
|
||||
{
|
||||
$stmt->send_long_data( $index, $chunk );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Execute */
|
||||
@@ -650,15 +615,16 @@ class _Db extends \mysqli
|
||||
}
|
||||
|
||||
/**
|
||||
* Run REPLACE statament and return number of affected rows
|
||||
* Run REPLACE statament and return number of affected rows OR inserted ID
|
||||
*
|
||||
* @see <a href='http://dev.mysql.com/doc/refman/5.1/en/replace.html'>REPLACE Syntax</a>
|
||||
* @param string $table Table name
|
||||
* @param array $set Values to insert
|
||||
* @return \IPS\Db\Statement
|
||||
* @param string $table Table name
|
||||
* @param array $set Values to insert
|
||||
* @param bool $getInsertId If TRUE, returns the insert ID rather than the number of affected rows
|
||||
* @return int
|
||||
* @throws \IPS\Db\Exception
|
||||
*/
|
||||
public function replace( $table, $set )
|
||||
public function replace( $table, $set, $getInsertId=false )
|
||||
{
|
||||
/* Build */
|
||||
$query = $this->_buildInsertQuery( 'REPLACE', $table, $set );
|
||||
@@ -672,7 +638,7 @@ class _Db extends \mysqli
|
||||
return $stmt;
|
||||
}
|
||||
|
||||
return $stmt->affected_rows;
|
||||
return $getInsertId ? $stmt->insert_id : $stmt->affected_rows;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -322,7 +322,14 @@ class _Developer
|
||||
if ( $this->app->directory == 'core' )
|
||||
{
|
||||
/* Make sure we've got a skin set ID 1 */
|
||||
$skinSetOne = \IPS\Db::i()->select( '*', 'core_themes', array( 'set_id=?', 1 ) )->first();
|
||||
try
|
||||
{
|
||||
$skinSetOne = \IPS\Db::i()->select( '*', 'core_themes', array( 'set_id=?', 1 ) )->first();
|
||||
}
|
||||
catch( \Exception $e )
|
||||
{
|
||||
$skinSetOne = array();
|
||||
}
|
||||
|
||||
if ( ! isset( $skinSetOne['set_id'] ) )
|
||||
{
|
||||
@@ -343,10 +350,9 @@ class _Developer
|
||||
'set_long_version' => \IPS\Application::load( $this->app->directory )->long_version,
|
||||
) );
|
||||
|
||||
\IPS\Lang::saveCustom( 'core', "core_skin_set_title_1", "Default" );
|
||||
\IPS\Lang::saveCustom( 'core', "core_theme_set_title_1", "Default" );
|
||||
}
|
||||
|
||||
if ( $skinSetOne['set_name'] != 'Default' )
|
||||
else if ( $skinSetOne['set_name'] != 'Default' )
|
||||
{
|
||||
\IPS\Db::i()->update( 'core_themes', array( 'set_name' => 'Default' ), array( 'set_id=?', 1 ) );
|
||||
\IPS\Lang::saveCustom( 'core', "core_theme_set_title_1", "Default" );
|
||||
|
||||
@@ -141,19 +141,19 @@ class _Admin extends \IPS\Dispatcher\Standard
|
||||
|
||||
if( !\IPS\Request::i()->isAjax() )
|
||||
{
|
||||
$url = $url->setQueryString( 'ref', preg_replace( '!adsess=((\w){32}|&)!', "", $_SERVER['QUERY_STRING'] ) );
|
||||
$url = $url->setQueryString( 'ref', base64_encode( preg_replace( '!adsess=((\w){32}|&)!', "", $_SERVER['QUERY_STRING'] ) ) );
|
||||
}
|
||||
else if( isset( $_SERVER['HTTP_REFERER'] ) )
|
||||
{
|
||||
$previous = preg_replace( "/^(.+?)\/\?/", "", $_SERVER['HTTP_REFERER'] );
|
||||
$url = $url->setQueryString( 'ref', preg_replace( '!adsess=.*?(&|$)!', "", $previous ) );
|
||||
$url = $url->setQueryString( 'ref', base64_encode( preg_replace( '!adsess=.*?(&|$)!', "", $previous ) ) );
|
||||
}
|
||||
|
||||
\IPS\Output::i()->redirect( $url );
|
||||
}
|
||||
|
||||
/* Permission Check */
|
||||
if ( ( $this->module->key !== 'system' or !in_array( $this->controller, array( 'login', 'language', 'theme', 'livesearch' ) ) ) and !\IPS\Member::loggedIn()->hasAcpRestriction( $this->application, $this->module ) )
|
||||
if ( ( $this->module->key !== 'system' or !in_array( $this->controller, array( 'login', 'language', 'theme', 'livesearch', 'editor', 'ajax' ) ) ) and ( $this->module->key !== 'members' or $this->controller !== 'members' or !in_array( \IPS\Request::i()->do, array( 'adminDetails', 'adminEmail', 'adminPassword' ) ) ) and !\IPS\Member::loggedIn()->hasAcpRestriction( $this->application, $this->module ) )
|
||||
{
|
||||
\IPS\Output::i()->error( 'no_module_permission', '2S107/1', 403, '' );
|
||||
}
|
||||
@@ -230,8 +230,11 @@ class _Admin extends \IPS\Dispatcher\Standard
|
||||
|
||||
/* Display */
|
||||
if( $this->controller !== 'login' )
|
||||
{
|
||||
\IPS\Output::i()->sidebar['sidebar'] = \IPS\Theme::i()->getTemplate( 'global', 'core' )->sidebar( $menu['tabs'][ $currentTab ], $this->application->directory . '_' . $this->module->key );
|
||||
{
|
||||
if ( isset( $menu['tabs'][ $currentTab ] ) )
|
||||
{
|
||||
\IPS\Output::i()->sidebar['sidebar'] = \IPS\Theme::i()->getTemplate( 'global', 'core' )->sidebar( $menu['tabs'][ $currentTab ], $this->application->directory . '_' . $this->module->key );
|
||||
}
|
||||
\IPS\Output::i()->sidebar['appmenu'] = \IPS\Theme::i()->getTemplate( 'global', 'core' )->appmenu( $menu, $currentTab );
|
||||
\IPS\Output::i()->sidebar['mobilenav'] = \IPS\Theme::i()->getTemplate( 'global', 'core' )->mobileNavigation( $menu, $currentTab );
|
||||
}
|
||||
|
||||
+74
-17
@@ -224,14 +224,23 @@ class _Front extends \IPS\Dispatcher\Standard
|
||||
/* Do we need more info from the member or do they need to validate? */
|
||||
if( \IPS\Member::loggedIn()->member_id )
|
||||
{
|
||||
/* Need their name or email... */
|
||||
if( ( !\IPS\Member::loggedIn()->real_name or !\IPS\Member::loggedIn()->email ) and $this->controller !== 'register' )
|
||||
{
|
||||
\IPS\Output::i()->redirect( \IPS\Http\Url::internal( 'app=core&module=system&controller=register&do=complete' ) );
|
||||
}
|
||||
|
||||
/* Need them to validate... */
|
||||
elseif( \IPS\Member::loggedIn()->members_bitoptions['validating'] and $this->controller !== 'register' and $this->controller !== 'login' and $this->controller != 'redirect' and $this->controller !== 'contact' )
|
||||
{
|
||||
\IPS\Output::i()->redirect( \IPS\Http\Url::internal( 'app=core&module=system&controller=register&do=validating', 'front', 'register' ) );
|
||||
}
|
||||
|
||||
/* Need them to reconfirm terms/privacy policy */
|
||||
elseif ( ( \IPS\Member::loggedIn()->members_bitoptions['must_reaccept_privacy'] or \IPS\Member::loggedIn()->members_bitoptions['must_reaccept_terms'] ) and $this->controller !== 'register' )
|
||||
{
|
||||
\IPS\Output::i()->redirect( \IPS\Http\Url::internal( 'app=core&module=system&controller=register&do=reconfirm', 'front', 'register' ) );
|
||||
}
|
||||
}
|
||||
|
||||
/* Permission Check */
|
||||
@@ -262,6 +271,27 @@ class _Front extends \IPS\Dispatcher\Standard
|
||||
{
|
||||
\IPS\Output::i()->breadcrumb['module'] = array( \IPS\Http\Url::internal( 'app=' . $this->application->directory . '&module=' . $this->module->key . '&controller=' . $this->module->default_controller, 'front', $this->module->key ), $this->module->_title );
|
||||
}
|
||||
|
||||
/* Figure out what the global search is */
|
||||
if ( !$this->application->default )
|
||||
{
|
||||
foreach ( $this->application->extensions( 'core', 'ContentRouter' ) as $object )
|
||||
{
|
||||
if ( count( $object->classes ) === 1 )
|
||||
{
|
||||
$classes = $object->classes;
|
||||
foreach ( $classes as $class )
|
||||
{
|
||||
if ( is_subclass_of( $class, 'IPS\Content\Searchable' ) and $class::$includeInSiteSearch )
|
||||
{
|
||||
$type = mb_strtolower( str_replace( '\\', '_', mb_substr( array_pop( $classes ), 4 ) ) );
|
||||
\IPS\Output::i()->defaultSearchOption = array( $type, "{$type}_pl" );
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -272,7 +302,7 @@ class _Front extends \IPS\Dispatcher\Standard
|
||||
*/
|
||||
protected function notAllowedBannedPage()
|
||||
{
|
||||
return ( !\IPS\Member::loggedIn()->group['g_view_board'] and !( $this->application->directory == 'core' and ( ( $this->module->key == 'system' and in_array( $this->controller, array( 'login', 'register', 'lostpass', 'terms', 'ajax', 'privacy', 'editor', 'language', 'theme' ) ) ) or ( $this->module->key == 'contact' AND $this->controller == 'contact' ) ) ) );
|
||||
return !\IPS\Member::loggedIn()->group['g_view_board'] and !$this->application->allowGuestAccess( $this->module, $this->controller, \IPS\Request::i()->do );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -307,8 +337,7 @@ class _Front extends \IPS\Dispatcher\Standard
|
||||
/* Get cache */
|
||||
try
|
||||
{
|
||||
$cache = \IPS\Data\Cache::i()->getWithExpire( 'page_' . md5( $_SERVER['REQUEST_URI'] ) . "_{$language}_{$theme}", TRUE );
|
||||
|
||||
$cache = \IPS\Data\Cache::i()->getWithExpire( 'page_' . md5( ( !empty( $_SERVER['HTTP_HOST'] ) ? $_SERVER['HTTP_HOST'] : $_SERVER['SERVER_NAME'] ) . '/' . $_SERVER['REQUEST_URI'] ) . "_{$language}_{$theme}", TRUE );
|
||||
$sessionId = isset( \IPS\Request::i()->cookie['IPSSessionFront'] ) ? \IPS\Request::i()->cookie['IPSSessionFront'] : \IPS\Session::i()->id;
|
||||
|
||||
\IPS\Output::i()->sendOutput( str_replace( '{{csrfKey}}', md5( '&& 0&' . $sessionId ), $cache['output'] ), $cache['code'], $cache['contentType'], $cache['httpHeaders'], FALSE, TRUE );
|
||||
@@ -333,7 +362,16 @@ class _Front extends \IPS\Dispatcher\Standard
|
||||
/* Convert &showtopic= */
|
||||
if ( isset( \IPS\Request::i()->showtopic ) )
|
||||
{
|
||||
\IPS\Output::i()->redirect( \IPS\Http\Url::internal( 'app=forums&module=forums&controller=topic&id=' . \IPS\Request::i()->showtopic ) );
|
||||
$url = \IPS\Http\Url::internal( 'app=forums&module=forums&controller=topic&id=' . \IPS\Request::i()->showtopic );
|
||||
if ( isset( \IPS\Request::i()->p ) or isset( \IPS\Request::i()->findpost ) )
|
||||
{
|
||||
$url = $url->setQueryString( array( 'do' => 'findComment', 'comment' => \IPS\Request::i()->p ?: \IPS\Request::i()->findpost ) );
|
||||
}
|
||||
elseif ( isset( \IPS\Request::i()->page ) )
|
||||
{
|
||||
$url = $url->setQueryString( array( 'page' => \IPS\Request::i()->page ) );
|
||||
}
|
||||
\IPS\Output::i()->redirect( $url );
|
||||
}
|
||||
|
||||
/* Convert &showforum= */
|
||||
@@ -347,6 +385,18 @@ class _Front extends \IPS\Dispatcher\Standard
|
||||
{
|
||||
\IPS\Output::i()->redirect( \IPS\Http\Url::internal( 'app=core&module=members&controller=profile&id=' . \IPS\Request::i()->showuser ) );
|
||||
}
|
||||
|
||||
/* Support legacy subscriptions */
|
||||
if( isset( \IPS\Request::i()->app ) AND \IPS\Request::i()->app == 'subscriptions' )
|
||||
{
|
||||
/* Redirecting isn't necessary, we just need to route the payment to the appropriate area.
|
||||
@see \IPS\nexus\Application */
|
||||
\IPS\Request::i()->app = 'nexus';
|
||||
\IPS\Request::i()->module = 'payments';
|
||||
\IPS\Request::i()->section = 'receive'; /* It actually looks for section=receive, so make sure we set that */
|
||||
\IPS\Request::i()->controller = 'receive'; /* We set this just to be complete in case anywhere else only looks at controller */
|
||||
\IPS\Request::i()->validate = 'paypal';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -394,19 +444,26 @@ class _Front extends \IPS\Dispatcher\Standard
|
||||
|
||||
if( count( $widgets ) )
|
||||
{
|
||||
$templateLoad = array();
|
||||
foreach ( $widgets as $areaKey => $area )
|
||||
if ( \IPS\CACHE_METHOD === 'None' )
|
||||
{
|
||||
foreach ( $area as $widget )
|
||||
$templateLoad = array();
|
||||
foreach ( $widgets as $areaKey => $area )
|
||||
{
|
||||
if ( isset( $widget['app'] ) and $widget['app'] )
|
||||
foreach ( $area as $widget )
|
||||
{
|
||||
$templateLoad[] = array( $widget['app'], 'front', 'widgets' );
|
||||
$templateLoad[] = 'template_' . \IPS\Theme::i()->id . '_' . \IPS\Theme::makeBuiltTemplateLookupHash( $widget['app'], 'front', 'widgets' ) . '_widgets';
|
||||
if ( isset( $widget['app'] ) and $widget['app'] )
|
||||
{
|
||||
$templateLoad[] = array( $widget['app'], 'front', 'widgets' );
|
||||
$templateLoad[] = 'template_' . \IPS\Theme::i()->id . '_' . \IPS\Theme::makeBuiltTemplateLookupHash( $widget['app'], 'front', 'widgets' ) . '_widgets';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if( count( $templateLoad ) )
|
||||
{
|
||||
\IPS\Data\Store::i()->loadIntoMemory( $templateLoad );
|
||||
}
|
||||
}
|
||||
\IPS\Data\Store::i()->loadIntoMemory( $templateLoad );
|
||||
|
||||
$widgetObjects = array();
|
||||
$storeLoad = array();
|
||||
@@ -424,7 +481,7 @@ class _Front extends \IPS\Dispatcher\Standard
|
||||
}
|
||||
|
||||
$_widget = \IPS\Widget::load( $appOrPlugin, $widget['key'], ( ! empty($widget['unique'] ) ? $widget['unique'] : uniqid() ), ( isset( $widget['configuration'] ) ) ? $widget['configuration'] : array(), ( isset( $widget['restrict'] ) ? $widget['restrict'] : null ), ( $areaKey == 'sidebar' ) ? 'vertical' : 'horizontal' );
|
||||
if ( isset( $_widget->cacheKey ) )
|
||||
if ( \IPS\CACHE_METHOD === 'None' and isset( $_widget->cacheKey ) )
|
||||
{
|
||||
$storeLoad[] = $_widget->cacheKey;
|
||||
}
|
||||
@@ -436,7 +493,11 @@ class _Front extends \IPS\Dispatcher\Standard
|
||||
}
|
||||
}
|
||||
}
|
||||
\IPS\Data\Store::i()->loadIntoMemory( $storeLoad );
|
||||
|
||||
if( \IPS\CACHE_METHOD === 'None' and count( $storeLoad ) )
|
||||
{
|
||||
\IPS\Data\Store::i()->loadIntoMemory( $storeLoad );
|
||||
}
|
||||
|
||||
foreach ( $widgetObjects as $areaKey => $_widgets )
|
||||
{
|
||||
@@ -509,10 +570,6 @@ class _Front extends \IPS\Dispatcher\Standard
|
||||
{
|
||||
\IPS\Output::i()->cssFiles = array_merge( \IPS\Output::i()->cssFiles, \IPS\Theme::i()->css( 'core_responsive.css', 'core', 'front' ) );
|
||||
}
|
||||
if ( count( \IPS\Lang::languages() ) > 1 )
|
||||
{
|
||||
\IPS\Output::i()->cssFiles = array_merge( \IPS\Output::i()->cssFiles, \IPS\Theme::i()->css( 'flags.css', 'core', 'global' ) );
|
||||
}
|
||||
|
||||
if ( \IPS\Member::loggedIn()->members_bitoptions['bw_using_skin_gen'] AND ( isset( \IPS\Request::i()->cookie['vseThemeId'] ) AND \IPS\Request::i()->cookie['vseThemeId'] ) and \IPS\Member::loggedIn()->isAdmin() and \IPS\Member::loggedIn()->hasAcpRestriction( 'core', 'customization', 'theme_easy_editor' ) )
|
||||
{
|
||||
|
||||
@@ -196,9 +196,10 @@ class _Setup extends \IPS\Dispatcher
|
||||
TRUE
|
||||
);
|
||||
|
||||
if( !session_start() )
|
||||
if( !@session_start() )
|
||||
{
|
||||
\IPS\Output::i()->error( "We were unable to start a PHP session. You will need to contact your host to adjust your PHP configuration before you can continue.", '4S109/5', 500, '' );
|
||||
$lastError = error_get_last();
|
||||
\IPS\Output::i()->error( "We were unable to start a PHP session. You will need to contact your host to adjust your PHP configuration before you can continue. The error reported was: " . $lastError['message'], '4S109/5', 500, '' );
|
||||
}
|
||||
|
||||
if( $this->classname != 'IPS\core\modules\setup\upgrade\done' and $this->setupLocation != 'install' and $this->step > 1 AND ( !isset( $_SESSION['uniqueKey'] ) OR $_SESSION['uniqueKey'] != \IPS\Request::i()->key ) )
|
||||
|
||||
@@ -53,6 +53,11 @@ abstract class _Standard extends \IPS\Dispatcher
|
||||
\IPS\Output::i()->cssFiles = array_merge( \IPS\Output::i()->cssFiles, \IPS\Theme::i()->css( 'responsive.css', 'core', 'global' ) );
|
||||
}
|
||||
}
|
||||
|
||||
if ( count( \IPS\Lang::languages() ) > 1 )
|
||||
{
|
||||
\IPS\Output::i()->cssFiles = array_merge( \IPS\Output::i()->cssFiles, \IPS\Theme::i()->css( 'flags.css', 'core', 'global' ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -73,6 +78,7 @@ abstract class _Standard extends \IPS\Dispatcher
|
||||
\IPS\Output::i()->jsFiles = array_merge( \IPS\Output::i()->jsFiles, \IPS\Output::i()->js( 'plugins.js', 'core', 'plugins' ) );
|
||||
\IPS\Output::i()->jsVars['date_format'] = \IPS\Member::loggedIn()->language()->preferredDateFormat();
|
||||
\IPS\Output::i()->jsVars['date_first_day'] = 0;
|
||||
\IPS\Output::i()->jsVars['remote_image_proxy'] = intval( \IPS\Settings::i()->remote_image_proxy );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,16 +225,19 @@ abstract class _Standard extends \IPS\Dispatcher
|
||||
if ( !\IPS\Request::i()->isAjax() )
|
||||
{
|
||||
/* Templates */
|
||||
\IPS\Data\Store::i()->templateLoad[] = array( 'core', $this->controllerLocation, 'global' );
|
||||
\IPS\Data\Store::i()->templateLoad[] = array( 'core', 'global', 'global' );
|
||||
\IPS\Data\Store::i()->templateLoad[] = array( 'core', 'global', 'forms' );
|
||||
\IPS\Data\Store::i()->templateLoad[] = array( 'core', $this->controllerLocation, 'forms' );
|
||||
$templateLoad = array();
|
||||
foreach ( \IPS\Data\Store::i()->templateLoad as $data )
|
||||
if ( \IPS\CACHE_METHOD === 'None' )
|
||||
{
|
||||
$templateLoad[] = 'template_' . \IPS\Theme::i()->id . '_' . \IPS\Theme::makeBuiltTemplateLookupHash( $data[0], $data[1], $data[2] ) . '_' . $data[2];
|
||||
\IPS\Data\Store::i()->templateLoad[] = array( 'core', $this->controllerLocation, 'global' );
|
||||
\IPS\Data\Store::i()->templateLoad[] = array( 'core', 'global', 'global' );
|
||||
\IPS\Data\Store::i()->templateLoad[] = array( 'core', 'global', 'forms' );
|
||||
\IPS\Data\Store::i()->templateLoad[] = array( 'core', $this->controllerLocation, 'forms' );
|
||||
$templateLoad = array();
|
||||
foreach ( \IPS\Data\Store::i()->templateLoad as $data )
|
||||
{
|
||||
$templateLoad[] = 'template_' . \IPS\Theme::i()->id . '_' . \IPS\Theme::makeBuiltTemplateLookupHash( $data[0], $data[1], $data[2] ) . '_' . $data[2];
|
||||
}
|
||||
\IPS\Data\Store::i()->loadIntoMemory( $templateLoad );
|
||||
}
|
||||
\IPS\Data\Store::i()->loadIntoMemory( $templateLoad );
|
||||
|
||||
/* App JS */
|
||||
\IPS\Output::i()->jsFiles = array_merge( \IPS\Output::i()->jsFiles, \IPS\Output::i()->js( 'app.js' ) );
|
||||
|
||||
+71
-62
@@ -577,7 +577,7 @@ abstract class _Email
|
||||
* @see <a href='http://www.faqs.org/rfcs/rfc2369.html'>RFC 2369 describes the List-Unsubscribe header</a>
|
||||
*/
|
||||
public function send( $to, $cc=array(), $bcc=array() )
|
||||
{
|
||||
{
|
||||
/* Parse the recipients - do this first in order to extract the language key in case we need it */
|
||||
$this->recipients = array();
|
||||
|
||||
@@ -620,34 +620,14 @@ abstract class _Email
|
||||
}
|
||||
|
||||
/* Replace language stack keys with actual content */
|
||||
$this->language->parseOutputForDisplay( $this->emailBody['plain'] );
|
||||
$this->language->parseOutputForDisplay( $this->emailBody['html'] );
|
||||
$this->language->parseEmail( $this->emailBody['plain'] );
|
||||
$this->language->parseEmail( $this->emailBody['html'] );
|
||||
$this->language->parseOutputForDisplay( $this->subject );
|
||||
|
||||
/* Replace attachment and emoticon URLs */
|
||||
\IPS\Output::i()->parseFileObjectUrls( $this->emailBody['html'] );
|
||||
\IPS\Output::i()->parseFileObjectUrls( $this->emailBody['plain'] );
|
||||
|
||||
/* Outlook can't handle protocol-relative URLs, so we need to switch them out */
|
||||
foreach( $this->emailBody as $type => $content )
|
||||
{
|
||||
$this->emailBody[ $type ] = preg_replace_callback( "/\s+?src=(['\"])\/\/([^'\"]+?)(['\"])/ims", function( $matches ){
|
||||
$baseUrl = parse_url( \IPS\Settings::i()->base_url );
|
||||
|
||||
/* Try to preserve http vs https */
|
||||
if( isset( $baseUrl['scheme'] ) )
|
||||
{
|
||||
$url = $baseUrl['scheme'] . '://' . $matches[2];
|
||||
}
|
||||
else
|
||||
{
|
||||
$url = 'http://' . $matches[2];
|
||||
}
|
||||
|
||||
return " src={$matches[1]}{$url}{$matches[1]}";
|
||||
}, $content );
|
||||
}
|
||||
|
||||
$this->parseFileObjectUrls( $this->emailBody['html'] );
|
||||
$this->parseFileObjectUrls( $this->emailBody['plain'] );
|
||||
|
||||
/* Now build the actual email body and send */
|
||||
$boundary = "--==_mimepart_" . md5( uniqid() );
|
||||
|
||||
@@ -694,6 +674,33 @@ abstract class _Email
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse file object URLs and fix protocol relative URLs
|
||||
*
|
||||
* @param string $body Content
|
||||
* @return string
|
||||
*/
|
||||
public function parseFileObjectUrls( &$body )
|
||||
{
|
||||
\IPS\Output::i()->parseFileObjectUrls( $body );
|
||||
|
||||
$body = preg_replace_callback( "/\s+?(srcset|src)=(['\"])\/\/([^'\"]+?)(['\"])/ims", function( $matches ){
|
||||
$baseUrl = parse_url( \IPS\Settings::i()->base_url );
|
||||
|
||||
/* Try to preserve http vs https */
|
||||
if( isset( $baseUrl['scheme'] ) )
|
||||
{
|
||||
$url = $baseUrl['scheme'] . '://' . $matches[3];
|
||||
}
|
||||
else
|
||||
{
|
||||
$url = 'http://' . $matches[3];
|
||||
}
|
||||
|
||||
return " {$matches[1]}={$matches[2]}{$url}{$matches[2]}";
|
||||
}, $body );
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge and Send
|
||||
*
|
||||
@@ -711,6 +718,10 @@ abstract class _Email
|
||||
$vars = array();
|
||||
foreach ( $_vars as $k => $v )
|
||||
{
|
||||
if ( $k === 'unsubscribe_blurb' )
|
||||
{
|
||||
$this->language->parseOutputForDisplay( $v );
|
||||
}
|
||||
$vars[] = array( 'name' => $k, 'content' => $v );
|
||||
}
|
||||
|
||||
@@ -722,10 +733,10 @@ abstract class _Email
|
||||
$body = $this->body['html'];
|
||||
$subject = $this->subject;
|
||||
|
||||
/* Replace attachment and emoticon URLs */
|
||||
\IPS\Output::i()->parseFileObjectUrls( $body );
|
||||
/* Replace attachment and emoticon URLs, fix protocol relative URLs and clean up HTML */
|
||||
$this->parseFileObjectUrls( $body );
|
||||
|
||||
$this->language->parseOutputForDisplay( $body );
|
||||
$this->language->parseEmail( $body );
|
||||
$this->language->parseOutputForDisplay( $subject );
|
||||
|
||||
$response = \IPS\Email::mandrill( 'messages_send', \IPS\Settings::i()->mandrill_api_key, array(
|
||||
@@ -754,12 +765,12 @@ abstract class _Email
|
||||
$subject = $this->subject;
|
||||
|
||||
/* Replace attachment and emoticon URLs */
|
||||
\IPS\Output::i()->parseFileObjectUrls( $content['html'] );
|
||||
\IPS\Output::i()->parseFileObjectUrls( $content['plain'] );
|
||||
$this->parseFileObjectUrls( $content['html'] );
|
||||
$this->parseFileObjectUrls( $content['plain'] );
|
||||
|
||||
/* Replace language stack keys with actual content */
|
||||
$this->language->parseOutputForDisplay( $content['html'] );
|
||||
$this->language->parseOutputForDisplay( $content['plain'] );
|
||||
$this->language->parseEmail( $content['html'] );
|
||||
$this->language->parseEmail( $content['plain'] );
|
||||
$this->language->parseOutputForDisplay( $subject );
|
||||
|
||||
foreach ( $vars as $k => $v )
|
||||
@@ -993,7 +1004,7 @@ abstract class _Email
|
||||
*/
|
||||
public function parseTextForEmail( $text )
|
||||
{
|
||||
\IPS\Output::i()->parseFileObjectUrls( $text );
|
||||
$this->parseFileObjectUrls( $text );
|
||||
|
||||
try
|
||||
{
|
||||
@@ -1038,30 +1049,6 @@ abstract class _Email
|
||||
{
|
||||
$parent = $node->parentNode;
|
||||
|
||||
/* Outlook cannot handle protocol-relative URLs like //site.com/image.jpg - it hangs the client and freezes */
|
||||
if ( $node->tagName == 'img' )
|
||||
{
|
||||
if ( $node->getAttribute('src') )
|
||||
{
|
||||
if ( mb_substr( $node->getAttribute('src'), 0, 2 ) === '//' )
|
||||
{
|
||||
$baseUrl = parse_url( \IPS\Settings::i()->base_url );
|
||||
|
||||
/* Try to preserve http vs https */
|
||||
if( isset( $baseUrl['scheme'] ) )
|
||||
{
|
||||
$url = $baseUrl['scheme'] . ':' . $node->getAttribute('src');
|
||||
}
|
||||
else
|
||||
{
|
||||
$url = 'http:' . $node->getAttribute('src');
|
||||
}
|
||||
|
||||
$node->setAttribute( 'src', $url );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( $node->getAttribute('class') )
|
||||
{
|
||||
foreach ( explode( ' ', $node->getAttribute('class') ) as $class )
|
||||
@@ -1074,9 +1061,12 @@ abstract class _Email
|
||||
$cell = $this->_createContainerTable( $parent, $node );
|
||||
$cell->setAttribute( 'style', "font-family: 'Helvetica Neue', helvetica, sans-serif; line-height: 1.5; font-size: 14px; margin: 0;border: 1px solid #e0e0e0;border-left: 3px solid #adadad;position: relative;font-size: 13px;background: #fdfdfd" );
|
||||
|
||||
$citation = $this->_createContainerTable( $cell );
|
||||
$citation->setAttribute( 'style', "font-family: 'Helvetica Neue', helvetica, sans-serif; line-height: 1.5; font-size: 14px; background: #f5f5f5;padding: 8px 15px;color: #000;font-weight: bold;font-size: 13px;display: block;" );
|
||||
$citation->appendChild( new \DOMText( $node->getAttribute('data-cite') ) );
|
||||
if ( $node->getAttribute('data-cite') )
|
||||
{
|
||||
$citation = $this->_createContainerTable( $cell );
|
||||
$citation->setAttribute( 'style', "font-family: 'Helvetica Neue', helvetica, sans-serif; line-height: 1.5; font-size: 14px; background: #f5f5f5;padding: 8px 15px;color: #000;font-weight: bold;font-size: 13px;display: block;" );
|
||||
$citation->appendChild( new \DOMText( $node->getAttribute('data-cite') ) );
|
||||
}
|
||||
|
||||
$containerCell = $this->_createContainerTable( $cell );
|
||||
$containerCell->setAttribute( 'style', "font-family: 'Helvetica Neue', helvetica, sans-serif; line-height: 1.5; font-size: 14px; padding-left:15px" );
|
||||
@@ -1084,7 +1074,12 @@ abstract class _Email
|
||||
while( $node->childNodes->length )
|
||||
{
|
||||
foreach ( $node->childNodes as $child )
|
||||
{
|
||||
{
|
||||
if ( $child instanceof \DOMElement and $child->getAttribute('class') == 'ipsQuote_citation' )
|
||||
{
|
||||
$child->setAttribute( 'style', "font-family: 'Helvetica Neue', helvetica, sans-serif; line-height: 1.5; font-size: 14px; background: #f3f3f3; margin: 0px -15px; padding: 5px 15px; color: #222; font-weight: bold; font-size: 13px; display: block;" );
|
||||
}
|
||||
|
||||
$containerCell->appendChild( $child );
|
||||
}
|
||||
}
|
||||
@@ -1092,6 +1087,7 @@ abstract class _Email
|
||||
$parent->removeChild( $node );
|
||||
break;
|
||||
|
||||
|
||||
/* Make code look good */
|
||||
case 'ipsCode':
|
||||
$cell = $this->_createContainerTable( $parent, $node );
|
||||
@@ -1113,6 +1109,7 @@ abstract class _Email
|
||||
|
||||
/* Remove spoilers */
|
||||
case 'ipsStyle_spoiler':
|
||||
case 'ipsSpoiler':
|
||||
$cell = $this->_createContainerTable( $parent, $node );
|
||||
$cell->setAttribute( 'style', "font-family: 'Helvetica Neue', helvetica, sans-serif; line-height: 1.5; font-size: 14px; margin: 0;padding: 10px;background: #363636;color: #d8d8d8;" );
|
||||
$cell->appendChild( new \DOMText( $this->language->addToStack('email_spoiler_line') ) );
|
||||
@@ -1148,6 +1145,18 @@ abstract class _Email
|
||||
{
|
||||
$url = $url->stripQueryString( 'do' );
|
||||
}
|
||||
|
||||
/* Convert embedDo and embedComment if present */
|
||||
if ( isset( $url->queryString['embedDo'] ) )
|
||||
{
|
||||
$url = $url->setQueryString( 'do', $url->queryString['embedDo'] )->stripQueryString( 'embedDo' );
|
||||
}
|
||||
|
||||
if ( isset( $url->queryString['embedComment'] ) )
|
||||
{
|
||||
$url = $url->setQueryString( 'comment', $url->queryString['embedComment'] )->stripQueryString( 'embedComment' );
|
||||
}
|
||||
|
||||
|
||||
$a = new \DOMElement( 'a' );
|
||||
$parent->insertBefore( $a, $node );
|
||||
|
||||
@@ -93,7 +93,7 @@ class _Incoming
|
||||
/* Get the PEAR email library to parse the email */
|
||||
require_once \IPS\ROOT_PATH . "/system/3rd_party/PEAR/Mail/mimeDecode.php";
|
||||
$decoder = new \Mail_mimeDecode( $email );
|
||||
$this->mail = $decoder->decode( array(
|
||||
$this->mail = @$decoder->decode( array(
|
||||
'include_bodies' => TRUE,
|
||||
'decode_bodies' => TRUE,
|
||||
'decode_headers' => TRUE,
|
||||
@@ -240,10 +240,10 @@ class _Incoming
|
||||
print $this->data['quoted'];
|
||||
|
||||
print '<pre>';
|
||||
$this->data['raw'] = htmlentities($this->data['raw'], \IPS\HTMLENTITIES, 'UTF-8', FALSE);
|
||||
$this->data['rawbody'] = htmlentities($this->data['rawbody'], \IPS\HTMLENTITIES, 'UTF-8', FALSE);
|
||||
$this->data['quoted'] = htmlentities($this->data['quoted'], \IPS\HTMLENTITIES, 'UTF-8', FALSE);
|
||||
$this->data['message'] = htmlentities($this->data['message'], \IPS\HTMLENTITIES, 'UTF-8', FALSE);
|
||||
$this->data['raw'] = htmlspecialchars($this->data['raw'], \IPS\HTMLENTITIES, 'UTF-8', FALSE);
|
||||
$this->data['rawbody'] = htmlspecialchars($this->data['rawbody'], \IPS\HTMLENTITIES, 'UTF-8', FALSE);
|
||||
$this->data['quoted'] = htmlspecialchars($this->data['quoted'], \IPS\HTMLENTITIES, 'UTF-8', FALSE);
|
||||
$this->data['message'] = htmlspecialchars($this->data['message'], \IPS\HTMLENTITIES, 'UTF-8', FALSE);
|
||||
print_r( $this->data );
|
||||
//print_r( $this->mail );
|
||||
print '</pre>';
|
||||
|
||||
@@ -66,15 +66,6 @@ class _SMTP extends \IPS\Email
|
||||
*/
|
||||
public function connect()
|
||||
{
|
||||
$tls = FALSE;
|
||||
|
||||
/* Check for tls:// in the hostname */
|
||||
if ( mb_strpos( \IPS\Settings::i()->smtp_host, 'tls://' ) !== FALSE )
|
||||
{
|
||||
$tls = TRUE;
|
||||
\IPS\Settings::i()->smtp_host = mb_substr( \IPS\Settings::i()->smtp_host, 6 );
|
||||
}
|
||||
|
||||
/* Do we already have a connection? */
|
||||
if( static::$smtp !== NULL )
|
||||
{
|
||||
@@ -82,7 +73,7 @@ class _SMTP extends \IPS\Email
|
||||
}
|
||||
|
||||
/* Connect */
|
||||
static::$smtp = @fsockopen( \IPS\Settings::i()->smtp_host, \IPS\Settings::i()->smtp_port, $errno, $errstr );
|
||||
static::$smtp = @fsockopen( ( ( \IPS\Settings::i()->smtp_protocol == 'ssl' ) ? 'ssl://' : '' ) . \IPS\Settings::i()->smtp_host, \IPS\Settings::i()->smtp_port, $errno, $errstr );
|
||||
|
||||
if ( !static::$smtp )
|
||||
{
|
||||
@@ -100,28 +91,20 @@ class _SMTP extends \IPS\Email
|
||||
throw new \IPS\Email\Outgoing\Exception( \IPS\Member::loggedIn()->language()->addToStack('smtpmail_fsock_error_initial', FALSE, array( 'sprintf' => array( $responseCode ) ) ) );
|
||||
}
|
||||
|
||||
/* If we use ssl:// we need to strip that out */
|
||||
$host = \IPS\Settings::i()->smtp_host;
|
||||
|
||||
if( mb_strpos( \IPS\Settings::i()->smtp_host, '://' ) !== FALSE )
|
||||
{
|
||||
$host = mb_substr( \IPS\Settings::i()->smtp_host, mb_strpos( \IPS\Settings::i()->smtp_host, '://' ) + 3 );
|
||||
}
|
||||
|
||||
/* HELO/EHLO */
|
||||
try
|
||||
{
|
||||
$helo = 'EHLO';
|
||||
$responseCode = $this->_sendCommand( 'EHLO ' . $host, 250 );
|
||||
$responseCode = $this->_sendCommand( 'EHLO ' . \IPS\Settings::i()->smtp_host, 250 );
|
||||
}
|
||||
catch ( \IPS\Email\Outgoing\Exception $e )
|
||||
{
|
||||
$helo = 'HELO';
|
||||
$responseCode = $this->_sendCommand( 'HELO ' . $host, 250 );
|
||||
$responseCode = $this->_sendCommand( 'HELO ' . \IPS\Settings::i()->smtp_host, 250 );
|
||||
}
|
||||
|
||||
/* Is TLS being used? */
|
||||
if( $tls === TRUE )
|
||||
if( \IPS\Settings::i()->smtp_protocol == 'tls' )
|
||||
{
|
||||
$this->_sendCommand( 'STARTTLS', 220 );
|
||||
|
||||
@@ -131,7 +114,7 @@ class _SMTP extends \IPS\Email
|
||||
}
|
||||
|
||||
/* Exchange server (at least) wants EHLO resending for STARTTLS */
|
||||
$this->_sendCommand( $helo . ' ' . $host, 250 );
|
||||
$this->_sendCommand( $helo . ' ' . \IPS\Settings::i()->smtp_host, 250 );
|
||||
}
|
||||
|
||||
/* Authenticate */
|
||||
|
||||
+127
-12
@@ -23,6 +23,13 @@ if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
*/
|
||||
class _Amazon extends \IPS\File
|
||||
{
|
||||
|
||||
/**
|
||||
* An array of ( configuration_id => array( ext, etx ) ) extensions that require gzip versions storing
|
||||
* Looks up $storageExtensions of extension classes
|
||||
*/
|
||||
protected static $gzipExtensions = array();
|
||||
|
||||
/* !ACP Configuration */
|
||||
|
||||
/**
|
||||
@@ -137,7 +144,7 @@ class _Amazon extends \IPS\File
|
||||
{
|
||||
return \IPS\Member::loggedIn()->language()->addToStack( 'filehandler_display_name', FALSE, array( 'sprintf' => array( \IPS\Member::loggedIn()->language()->addToStack('filehandler__Amazon'), $settings['bucket'] ) ) );
|
||||
}
|
||||
|
||||
|
||||
/* !File Handling */
|
||||
|
||||
/**
|
||||
@@ -152,6 +159,42 @@ class _Amazon extends \IPS\File
|
||||
parent::__construct( $configuration );
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the gzip extensions specific for $this->configurationId
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getGzipExtensions()
|
||||
{
|
||||
if ( $this->storageExtension and ! array_key_exists( $this->storageExtension, static::$gzipExtensions ) )
|
||||
{
|
||||
$bits = explode( '_', $this->storageExtension );
|
||||
$class = '\IPS\\' . $bits[0] . '\extensions\core\FileStorage\\' . $bits[1];
|
||||
|
||||
if ( isset( $class::$storeGzipExtensions ) and is_array( $class::$storeGzipExtensions ) and count( $class::$storeGzipExtensions ) )
|
||||
{
|
||||
static::$gzipExtensions[ $this->storageExtension ] = $class::$storeGzipExtensions;
|
||||
}
|
||||
else
|
||||
{
|
||||
static::$gzipExtensions[ $this->storageExtension ] = array();
|
||||
}
|
||||
}
|
||||
|
||||
return $this->storageExtension ? static::$gzipExtensions[ $this->storageExtension ] : array();
|
||||
}
|
||||
|
||||
/**
|
||||
* AWS does not gzip content when serving it, so if we want gzip compressed JS and CSS, we need to store a copy ourselves.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function needsGzipVersion()
|
||||
{
|
||||
/* Use filename and not originalFilename so only true .js and .css files are checked, and not renamed uploads */
|
||||
return in_array( mb_substr( $this->filename, mb_strrpos( $this->filename, '.' ) + 1 ), $this->getGzipExtensions() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the base URL
|
||||
*
|
||||
@@ -162,6 +205,22 @@ class _Amazon extends \IPS\File
|
||||
return preg_replace( '#^http(s)?://#', '//', ( empty( $this->configuration['custom_url'] ) ) ? rtrim( static::buildBaseUrl( $this->configuration ), '/' ) : $this->configuration['custom_url'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Load File Data
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function load()
|
||||
{
|
||||
parent::load();
|
||||
|
||||
/* Change the public URL to the gzipped version if the browser supports it */
|
||||
if ( $this->needsGzipVersion() and ( isset( $_SERVER['HTTP_ACCEPT_ENCODING'] ) and \strpos( $_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip' ) !== false ) )
|
||||
{
|
||||
$this->url = new \IPS\Http\Url( (string) $this->url . '.gz' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save File
|
||||
*
|
||||
@@ -169,20 +228,25 @@ class _Amazon extends \IPS\File
|
||||
*/
|
||||
public function save()
|
||||
{
|
||||
/* @link http://docs.aws.amazon.com/AmazonS3/latest/dev/UsingMetadata.html regarding cleaning filenames */
|
||||
if ( preg_match( '#[^a-zA-Z0-9!\-_\.\*\(\)]#', $this->filename ) )
|
||||
{
|
||||
$this->filename = uniqid() . '_' . preg_replace( '#[^a-zA-Z0-9!\-_\.\*\(\)]#', '', $this->filename );
|
||||
}
|
||||
|
||||
$this->container = trim( $this->container, '/' );
|
||||
$this->url = static::buildBaseUrl( $this->configuration ) . "{$this->container}/{$this->filename}";
|
||||
$this->url = $this->baseUrl() . "/{$this->container}/{$this->filename}";
|
||||
$response = static::makeRequest( "{$this->container}/{$this->filename}", 'PUT', $this->configuration, $this->configurationId, (string) $this->contents() );
|
||||
|
||||
if ( $response->httpResponseCode != 200 )
|
||||
{
|
||||
throw new \RuntimeException('COULD_NOT_SAVE_FILE');
|
||||
}
|
||||
|
||||
/* Write the gzip version */
|
||||
if ( $this->needsGzipVersion() )
|
||||
{
|
||||
$response = static::makeRequest( "{$this->container}/{$this->filename}.gz", 'PUT', $this->configuration, $this->configurationId, gzencode( (string) $this->contents() ) );
|
||||
|
||||
if ( $response->httpResponseCode != 200 )
|
||||
{
|
||||
throw new \RuntimeException('COULD_NOT_SAVE_FILE');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -213,6 +277,12 @@ class _Amazon extends \IPS\File
|
||||
|
||||
if ( $response->httpResponseCode == 204 )
|
||||
{
|
||||
/* Got a gzip version? */
|
||||
if ( $this->needsGzipVersion() )
|
||||
{
|
||||
static::makeRequest( "{$this->container}/{$this->filename}.gz", 'DELETE', $this->configuration, $this->configurationId );
|
||||
}
|
||||
|
||||
/* Ok */
|
||||
return;
|
||||
}
|
||||
@@ -256,6 +326,25 @@ class _Amazon extends \IPS\File
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get filesize (in bytes)
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function filesize()
|
||||
{
|
||||
$this->container = trim( $this->container, '/' );
|
||||
|
||||
$response = static::makeRequest( "{$this->container}/{$this->filename}", 'HEAD', $this->configuration, $this->configurationId );
|
||||
|
||||
if ( $response->httpResponseCode != 200 OR !isset( $response->httpHeaders['Content-Length'] ) )
|
||||
{
|
||||
return parent::filesize();
|
||||
}
|
||||
|
||||
return $response->httpHeaders['Content-Length'];
|
||||
}
|
||||
|
||||
/* !Amazon Utility Methods */
|
||||
|
||||
/**
|
||||
@@ -273,19 +362,45 @@ class _Amazon extends \IPS\File
|
||||
{
|
||||
/* Build a request */
|
||||
$request = \IPS\Http\Url::external( static::buildBaseUrl( $configuration ) . $uri )->request();
|
||||
|
||||
/* When using virtual hosted–style buckets with SSL, the SSL wild card certificate only matches buckets that do not contain periods. To work around this, use HTTP or write your own certificate verification logic. @link http://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html */
|
||||
if ( \IPS\Request::i()->isSecure() and mb_strstr( $configuration['bucket'], '.' ) )
|
||||
{
|
||||
$request->sslCheck( FALSE );
|
||||
}
|
||||
|
||||
$date = date('r');
|
||||
$headers = array( 'Date' => $date, 'Content-Type' => \IPS\File::getMimeType( $uri ), 'Content-MD5' => base64_encode( md5( $content, TRUE ) ), 'x-amz-acl' => 'public-read' );
|
||||
|
||||
/* Make sure the file has the correct mime type, even if it is gzipped */
|
||||
$mimeUri = ( mb_substr( $uri, -3 ) === '.gz' ) ? mb_substr( $uri, 0, -3 ) : $uri;
|
||||
$headers = array( 'Date' => $date, 'Content-Type' => \IPS\File::getMimeType( $mimeUri ), 'Content-MD5' => base64_encode( md5( $content, TRUE ) ), 'x-amz-acl' => 'public-read' );
|
||||
|
||||
if ( $mimeUri !== $uri )
|
||||
{
|
||||
$headers['Content-Encoding'] = 'gzip';
|
||||
}
|
||||
|
||||
if( mb_strtoupper( $verb ) === 'PUT' )
|
||||
{
|
||||
$headers['Content-Length'] = \strlen( $content );
|
||||
}
|
||||
|
||||
|
||||
$cacheSeconds = 3600;
|
||||
$ext = mb_substr( $mimeUri, ( mb_strrpos( $mimeUri, '.' ) + 1 ) );
|
||||
|
||||
if ( preg_match( "/\." . preg_quote( $ext ) . "\.([a-zA-Z0-9]{32})\." . preg_quote( $ext ) . "$/", $mimeUri ) )
|
||||
{
|
||||
/* This is an obfuscated file, so it will change each time the file is updated so it is safe to cache for longer */
|
||||
$cacheSeconds = 3600 * 24 * 365;
|
||||
}
|
||||
|
||||
$headers['Cache-Control'] = 'max-age=' . $cacheSeconds;
|
||||
$headers['Expires'] = date( 'r', time() + $cacheSeconds );
|
||||
|
||||
/* We need to strip query string parameters for the signature, but not always (e.g. a subresource such as ?acl needs to be included and multi-
|
||||
object delete requests must include the query string params). Let the callee decide to do this or not. */
|
||||
if( isset( $configuration['_strip_querystring'] ) AND $configuration['_strip_querystring'] === TRUE )
|
||||
{
|
||||
$uri = preg_replace( "/^(.*?)\?.*$/", "$1", $uri );
|
||||
$uri = preg_replace( "/^(.*?)\?.*$/", "$1", $uri );
|
||||
}
|
||||
|
||||
$string = implode( "\n", array(
|
||||
|
||||
@@ -101,6 +101,24 @@ class _Database extends \IPS\File
|
||||
$this->filename = urldecode( $this->filename );
|
||||
$this->container = urldecode( $this->container );
|
||||
$this->originalFilename = urldecode( $this->originalFilename );
|
||||
|
||||
/* The URL may have been stored as //site.com/applications/core/interface/file/index.php?id=1&salt=1111 in which case the previous load()
|
||||
call wouldn't have loaded the data correctly, so we need to do it here now */
|
||||
if( isset( $this->url->queryString['id'] ) AND isset( $this->url->queryString['salt'] ) )
|
||||
{
|
||||
try
|
||||
{
|
||||
$record = \IPS\Db::i()->select( '*', 'core_files', array( 'id=? AND salt=?', $this->url->queryString['id'], $this->url->queryString['salt'] ) )->first();
|
||||
$this->contents = $record['contents'];
|
||||
$this->filename = $record['filename'];
|
||||
$this->originalFilename = $this->unObscureFilename( $this->filename );
|
||||
$this->container = $record['container'];
|
||||
}
|
||||
catch( \UnderflowException $ex )
|
||||
{
|
||||
throw new \RuntimeException( 'FILE_DOES_NOT_EXIST' );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -135,12 +153,6 @@ class _Database extends \IPS\File
|
||||
{
|
||||
$salt = md5( uniqid() );
|
||||
|
||||
/* Make sure name doesn't have anything that may break URLs */
|
||||
if ( preg_match( '#[^a-zA-Z0-9!\-_\.\*\(\)]#', $this->filename ) )
|
||||
{
|
||||
$this->filename = uniqid() . '_' . preg_replace( '#[^a-zA-Z0-9!\-_\.\*\(\)]#', '', $this->filename );
|
||||
}
|
||||
|
||||
$id = \IPS\Db::i()->insert( 'core_files', array(
|
||||
'filename' => $this->filename,
|
||||
'salt' => $salt,
|
||||
|
||||
+200
-25
@@ -179,26 +179,47 @@ abstract class _File
|
||||
{
|
||||
$class->container = $container;
|
||||
}
|
||||
|
||||
/* Make sure images don't have HTML in the comments, which can cause be an XSS in older versions of IE */
|
||||
|
||||
$class->storageExtension = $storageExtension;
|
||||
|
||||
/* Image-specific stuff */
|
||||
$ext = mb_substr( $filename, ( mb_strrpos( $filename, '.' ) + 1 ) );
|
||||
if( $isSafe === FALSE and in_array( $ext, \IPS\Image::$imageExtensions ) )
|
||||
if( in_array( $ext, \IPS\Image::$imageExtensions ) and ( !$isSafe or \IPS\Image::exifSupported() ) )
|
||||
{
|
||||
$contentToCheck = $data;
|
||||
|
||||
/* Get contents */
|
||||
if( $data === NULL AND $filePath !== NULL )
|
||||
{
|
||||
$handle = fopen( $filePath, 'r');
|
||||
$contentToCheck = fread( $handle, 2048 );
|
||||
fclose( $handle );
|
||||
$data = \file_get_contents( $filePath );
|
||||
$filePath = NULL;
|
||||
}
|
||||
|
||||
if( static::checkXssInFile( $contentToCheck ) )
|
||||
|
||||
/* Make sure images don't have HTML in the comments, which can cause be an XSS in older versions of IE */
|
||||
$image = NULL;
|
||||
if( !$isSafe and static::checkXssInFile( $data ) )
|
||||
{
|
||||
throw new \DomainException( "SECURITY_EXCEPTION_RAISED", 99 );
|
||||
/* Try to just strip the EXIF */
|
||||
$image = \IPS\Image::create( $data );
|
||||
$image->resize( 100, 100 );
|
||||
$data = (string) $image;
|
||||
|
||||
/* And if it still fails, throw an error */
|
||||
if ( static::checkXssInFile( $data ) )
|
||||
{
|
||||
throw new \DomainException( "SECURITY_EXCEPTION_RAISED", 99 );
|
||||
}
|
||||
}
|
||||
|
||||
/* Correct orientation */
|
||||
if ( \IPS\Image::exifSupported() )
|
||||
{
|
||||
$image = $image ?: \IPS\Image::create( $data );
|
||||
if ( $image->hasBeenRotated )
|
||||
{
|
||||
$data = (string) \IPS\Image::create( $data );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Set the name */
|
||||
$class->setFilename( $filename, $obscure );
|
||||
|
||||
@@ -448,6 +469,7 @@ abstract class _File
|
||||
public static function get( $storageExtension, $url )
|
||||
{
|
||||
$class = static::getClass( $storageExtension, $url );
|
||||
$class->storageExtension = $storageExtension;
|
||||
$class->url = $url;
|
||||
$class->load();
|
||||
return $class;
|
||||
@@ -464,7 +486,7 @@ abstract class _File
|
||||
{
|
||||
return static::getClass( $configurationId )->removeOrphanedFiles( $fileIndex, \IPS\Application::allExtensions( 'core', 'FileStorage', FALSE ) );
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Determine if the change in configuration warrants a move process
|
||||
*
|
||||
@@ -510,9 +532,9 @@ abstract class _File
|
||||
|
||||
/**
|
||||
* This is primarily a utility function to convert old style full URLs (http://site.com/uploads/monthly_04_2015/file.txt) into the new style (monthly_04_2015/file.txt)
|
||||
* @deprecated 4.1.0
|
||||
*
|
||||
* return string|boolean URL (string) if URL needed repairing, or FALSE if it did not.
|
||||
* @deprecated 4.1.0
|
||||
* @return string|boolean URL (string) if URL needed repairing, or FALSE if it did not.
|
||||
*/
|
||||
public static function repairUrl( $url )
|
||||
{
|
||||
@@ -550,6 +572,11 @@ abstract class _File
|
||||
*/
|
||||
public $configurationId;
|
||||
|
||||
/**
|
||||
* @brief Storage Extension (core_Theme, etc)
|
||||
*/
|
||||
public $storageExtension;
|
||||
|
||||
/**
|
||||
* @brief Original Filename
|
||||
*/
|
||||
@@ -638,6 +665,77 @@ abstract class _File
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send file with byte-offset supported. Requires a path to a locally stored file. This method can be more efficient than
|
||||
* getting the file contents and printing as the file contents do not need to be stored in memory, however files will need to
|
||||
* be written to disk and removed, and the method is only useful if there is a way to retrieve a file without storing the
|
||||
* contents in memory already.
|
||||
*
|
||||
* @param string $file Path to file
|
||||
* @param int|null $start Start point to print from (for ranges)
|
||||
* @param int|null $length Length to print to (for ranges)
|
||||
* @param int|null $throttle Throttle speed
|
||||
* @return void
|
||||
*/
|
||||
protected function sendFile( $file, $start=NULL, $length=NULL, $throttle=NULL )
|
||||
{
|
||||
/* Turn off output buffering if it is on */
|
||||
while( ob_get_level() > 0 )
|
||||
{
|
||||
ob_end_clean();
|
||||
}
|
||||
|
||||
if( $throttle === NULL AND !($start AND $length) AND function_exists('readfile') )
|
||||
{
|
||||
readfile( $file );
|
||||
}
|
||||
else
|
||||
{
|
||||
if( $fh = fopen( $file, 'rb' ) )
|
||||
{
|
||||
$read = ( $throttle !== NULL ) ? $throttle : 4096;
|
||||
|
||||
if( $start AND $length )
|
||||
{
|
||||
fseek( $fh, $start );
|
||||
|
||||
while( $length AND !feof( $fh ) )
|
||||
{
|
||||
if( $read > $length )
|
||||
{
|
||||
$read = $length;
|
||||
}
|
||||
|
||||
echo fread( $fh, $read );
|
||||
flush();
|
||||
|
||||
$length -= $read;
|
||||
|
||||
if( $throttle )
|
||||
{
|
||||
sleep( 1 );
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
while( ! feof( $fh ) )
|
||||
{
|
||||
echo fread( $fh, $read );
|
||||
flush();
|
||||
|
||||
if( $throttle )
|
||||
{
|
||||
sleep( 1 );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fclose( $fh );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Contents
|
||||
*
|
||||
@@ -691,15 +789,30 @@ abstract class _File
|
||||
public function setFilename( $filename, $obscure=TRUE )
|
||||
{
|
||||
$this->originalFilename = $filename;
|
||||
|
||||
|
||||
/* Make sure name doesn't have anything that may break URLs */
|
||||
if ( preg_match( '#[^a-zA-Z0-9!\-_\.\*\(\)\@]#', $filename ) )
|
||||
{
|
||||
$filename = uniqid() . '_' . preg_replace( '#[^a-zA-Z0-9!\-_\.\*\(\)\@]#', '', $filename );
|
||||
}
|
||||
|
||||
if ( $obscure )
|
||||
{
|
||||
$this->filename = static::obscureFilename( $filename );
|
||||
$filename = static::obscureFilename( $filename );
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->filename = $filename;
|
||||
$filename = $filename;
|
||||
}
|
||||
|
||||
/* Most operating systems allow a max filename length of 255 bytes, so we should make sure we don't go over that */
|
||||
if( \strlen( $filename ) > 200 )
|
||||
{
|
||||
/* If the filename is over 200 chars, grab the first 100 and the last 100 and concatenate with a dash - this should help ensure we retain the most useful info */
|
||||
$filename = mb_substr( $filename, 0, 100 ) . '-' . mb_substr( $filename, -100 );
|
||||
}
|
||||
|
||||
$this->filename = $filename;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -763,7 +876,6 @@ abstract class _File
|
||||
if ( static::isFullyQualifiedUrl( $url ) )
|
||||
{
|
||||
$url = mb_substr( $url, mb_strlen( $this->baseUrl() ) + 1 );
|
||||
|
||||
}
|
||||
|
||||
$exploded = explode( '/', $url );
|
||||
@@ -824,6 +936,36 @@ abstract class _File
|
||||
return $this->container . '/' . $this->filename;
|
||||
}
|
||||
|
||||
/**
|
||||
* The configuration settings have been updated
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function settingsUpdated()
|
||||
{
|
||||
$settings = json_decode( \IPS\Settings::i()->upload_settings, TRUE );
|
||||
|
||||
foreach( $settings as $method => $id )
|
||||
{
|
||||
if ( $id == $this->configurationId )
|
||||
{
|
||||
$exploded = explode( '_', str_replace( 'filestorage__', '', $method ) );
|
||||
$classname = "IPS\\{$exploded[0]}\\extensions\\core\\FileStorage\\{$exploded[1]}";
|
||||
|
||||
if ( method_exists( $classname, 'settingsUpdated' ) )
|
||||
{
|
||||
$classname::settingsUpdated( $this->configuration );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Clear guest page caches */
|
||||
\IPS\Data\Cache::i()->clearAll();
|
||||
|
||||
/* Clear datastore */
|
||||
\IPS\Data\Store::i()->clearAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the fully qualified URL
|
||||
*
|
||||
@@ -988,14 +1130,15 @@ abstract class _File
|
||||
$data['attach_is_image'] = TRUE;
|
||||
$data['attach_img_width'] = $dimensions[0];
|
||||
$data['attach_img_height'] = $dimensions[1];
|
||||
|
||||
unset( $image );
|
||||
|
||||
$data['attach_thumb_location'] = (string) $this->thumbnail( 'core_Attachment', $thumbDims[0], $thumbDims[1] );
|
||||
$data['attach_thumb_width'] = static::$thumbnailDimensions[0];
|
||||
$data['attach_thumb_height'] = static::$thumbnailDimensions[1];
|
||||
if ( $dimensions[0] > $thumbDims[0] or $dimensions[1] > $thumbDims[1] )
|
||||
{
|
||||
$data['attach_thumb_location'] = (string) $this->thumbnail( 'core_Attachment', $thumbDims[0], $thumbDims[1] );
|
||||
$data['attach_thumb_width'] = static::$thumbnailDimensions[0];
|
||||
$data['attach_thumb_height'] = static::$thumbnailDimensions[1];
|
||||
|
||||
$this->attachmentThumbnailUrl = $data['attach_thumb_location'];
|
||||
$this->attachmentThumbnailUrl = $data['attach_thumb_location'];
|
||||
}
|
||||
}
|
||||
|
||||
catch ( \InvalidArgumentException $e ) { }
|
||||
@@ -1259,6 +1402,38 @@ abstract class _File
|
||||
|
||||
return 'application/x-unknown'; // This, slightly unusual, type is needed to stop some browsers adding random extensions
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a value pulled from php.ini in bytes
|
||||
*
|
||||
* @note This function is intended to normalize values for things like post_max_size which could be -1, 0, 8383900, 8M, 1G, etc.
|
||||
* @return float
|
||||
*/
|
||||
public static function returnBytes( $size )
|
||||
{
|
||||
$size = trim( $size );
|
||||
|
||||
if( !$size OR $size == -1 )
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Get the last character, which may be 'm' or may be a number */
|
||||
$last = mb_strtolower( $size[ \strlen( $size ) - 1 ] );
|
||||
|
||||
/* Adjust value as necessary - note that we do not break intentionally */
|
||||
switch( $last )
|
||||
{
|
||||
case 'g':
|
||||
$size *= 1024;
|
||||
case 'm':
|
||||
$size *= 1024;
|
||||
case 'k':
|
||||
$size *= 1024;
|
||||
}
|
||||
|
||||
return (float) $size;
|
||||
}
|
||||
|
||||
/* !Mime-Type Map */
|
||||
|
||||
|
||||
+52
-69
@@ -61,7 +61,11 @@ class _FileSystem extends \IPS\File
|
||||
{
|
||||
$values['custom_url'] = NULL;
|
||||
}
|
||||
|
||||
|
||||
if ( !$testDir )
|
||||
{
|
||||
throw new \DomainException( \IPS\Member::loggedIn()->language()->addToStack( 'dir_not_provided', FALSE ) );
|
||||
}
|
||||
if ( !is_dir( $testDir ) )
|
||||
{
|
||||
throw new \DomainException( \IPS\Member::loggedIn()->language()->addToStack( 'dir_does_not_exist', FALSE, array( 'sprintf' => array( $testDir ) ) ) );
|
||||
@@ -229,61 +233,7 @@ class _FileSystem extends \IPS\File
|
||||
{
|
||||
$file = $this->configuration['dir'] . '/' . $this->container . '/' . $this->filename;
|
||||
|
||||
/* Turn off output buffering if it is on */
|
||||
while( ob_get_level() > 0 )
|
||||
{
|
||||
ob_end_clean();
|
||||
}
|
||||
|
||||
if( $throttle === NULL AND !($start AND $length) AND function_exists('readfile') )
|
||||
{
|
||||
readfile( $file );
|
||||
}
|
||||
else
|
||||
{
|
||||
if( $fh = fopen( $file, 'rb' ) )
|
||||
{
|
||||
$read = ( $throttle !== NULL ) ? $throttle : 4096;
|
||||
|
||||
if( $start AND $length )
|
||||
{
|
||||
fseek( $fh, $start );
|
||||
|
||||
while( $length AND !feof( $fh ) )
|
||||
{
|
||||
if( $read > $length )
|
||||
{
|
||||
$read = $length;
|
||||
}
|
||||
|
||||
echo fread( $fh, $read );
|
||||
flush();
|
||||
|
||||
$length -= $read;
|
||||
|
||||
if( $throttle )
|
||||
{
|
||||
sleep( 1 );
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
while( ! feof( $fh ) )
|
||||
{
|
||||
echo fread( $fh, $read );
|
||||
flush();
|
||||
|
||||
if( $throttle )
|
||||
{
|
||||
sleep( 1 );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fclose( $fh );
|
||||
}
|
||||
}
|
||||
$this->sendFile( $file, $start, $length, $throttle );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -318,6 +268,7 @@ class _FileSystem extends \IPS\File
|
||||
*
|
||||
* @return array
|
||||
* @throws \DomainException
|
||||
* @throws \RuntimeException
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function getImageDimensions()
|
||||
@@ -329,6 +280,11 @@ class _FileSystem extends \IPS\File
|
||||
|
||||
$file = $this->configuration['dir'] . '/' . $this->container . '/' . $this->filename;
|
||||
|
||||
if ( ! file_exists( $file ) )
|
||||
{
|
||||
throw new \RuntimeException;
|
||||
}
|
||||
|
||||
if( ( $image = getimagesize( $file ) ) === FALSE )
|
||||
{
|
||||
return parent::getImageDimensions();
|
||||
@@ -345,12 +301,6 @@ class _FileSystem extends \IPS\File
|
||||
*/
|
||||
public function save()
|
||||
{
|
||||
/* Make sure name doesn't have anything that may break URLs */
|
||||
if ( preg_match( '#[^a-zA-Z0-9!\-_\.\*\(\)]#', $this->filename ) )
|
||||
{
|
||||
$this->filename = uniqid() . '_' . preg_replace( '#[^a-zA-Z0-9!\-_\.\*\(\)]#', '', $this->filename );
|
||||
}
|
||||
|
||||
/* Make the folder */
|
||||
$folder = $this->configuration['dir'] . '/' . $this->getFolder();
|
||||
|
||||
@@ -380,6 +330,12 @@ class _FileSystem extends \IPS\File
|
||||
|
||||
/* Set the URL */
|
||||
$this->url = new \IPS\Http\Url( $this->fullyQualifiedUrl( "{$this->container}/{$this->filename}" ) );
|
||||
|
||||
/* Clear zend opcache if enabled */
|
||||
if( function_exists('opcache_reset') )
|
||||
{
|
||||
@opcache_reset();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -391,7 +347,15 @@ class _FileSystem extends \IPS\File
|
||||
{
|
||||
if( file_exists( "{$this->configuration['dir']}/{$this->container}/{$this->filename}" ) )
|
||||
{
|
||||
return @unlink( "{$this->configuration['dir']}/{$this->container}/{$this->filename}" );
|
||||
$result = @unlink( "{$this->configuration['dir']}/{$this->container}/{$this->filename}" );
|
||||
|
||||
/* Clear zend opcache if enabled */
|
||||
if( function_exists('opcache_reset') )
|
||||
{
|
||||
@opcache_reset();
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -403,25 +367,38 @@ class _FileSystem extends \IPS\File
|
||||
* Delete Container
|
||||
*
|
||||
* @param string $container Key
|
||||
* @param bool $skipClear Skip clearing the opcache, used for recursive calls since the parent call will do it
|
||||
* @return void
|
||||
*/
|
||||
public function deleteContainer( $container )
|
||||
public function deleteContainer( $container, $skipClear = FALSE )
|
||||
{
|
||||
$dir = $this->configuration['dir'] . '/' . $container;
|
||||
|
||||
if ( is_dir( $dir ) )
|
||||
{
|
||||
foreach ( new \DirectoryIterator( $dir ) as $f )
|
||||
{
|
||||
if ( !$f->isDot() )
|
||||
{
|
||||
unlink( $f->getPathname() );
|
||||
if( $f->isDir() )
|
||||
{
|
||||
$this->deleteContainer( $container . '/' . $f->getFilename(), TRUE );
|
||||
}
|
||||
else
|
||||
{
|
||||
unlink( $f->getPathname() );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$handle = opendir( $dir );
|
||||
closedir ( $handle );
|
||||
rmdir( $dir );
|
||||
}
|
||||
|
||||
/* Clear zend opcache if enabled */
|
||||
if( function_exists('opcache_reset') AND $skipClear === FALSE )
|
||||
{
|
||||
@opcache_reset();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -446,14 +423,20 @@ class _FileSystem extends \IPS\File
|
||||
}
|
||||
fclose( $file );
|
||||
@unlink( $chunk );
|
||||
|
||||
|
||||
/* Clear zend opcache if enabled */
|
||||
if( function_exists('opcache_reset') )
|
||||
{
|
||||
@opcache_reset();
|
||||
}
|
||||
|
||||
if ( $rename )
|
||||
{
|
||||
$newFileName = static::obscureFilename( $rename );
|
||||
rename( "{$this->configuration['dir']}/{$folder}/{$filename}", "{$this->configuration['dir']}/{$folder}/{$newFileName}" );
|
||||
return "{$folder}/{$newFileName}";
|
||||
}
|
||||
|
||||
|
||||
return "{$folder}/{$filename}";
|
||||
}
|
||||
|
||||
|
||||
+28
-12
@@ -149,12 +149,6 @@ class _Ftp extends \IPS\File
|
||||
*/
|
||||
public function save()
|
||||
{
|
||||
/* Make sure name doesn't have anything that may break URLs */
|
||||
if ( preg_match( '#[^a-zA-Z0-9!\-_\.\*\(\)]#', $this->filename ) )
|
||||
{
|
||||
$this->filename = uniqid() . '_' . preg_replace( '#[^a-zA-Z0-9!\-_\.\*\(\)]#', '', $this->filename );
|
||||
}
|
||||
|
||||
/* Get contents */
|
||||
$contents = $this->contents();
|
||||
|
||||
@@ -218,7 +212,7 @@ class _Ftp extends \IPS\File
|
||||
/**
|
||||
* @brief FTP Connection
|
||||
*/
|
||||
protected static $ftp = NULL;
|
||||
protected $ftp = NULL;
|
||||
|
||||
/**
|
||||
* Get FTP Connection
|
||||
@@ -228,20 +222,20 @@ class _Ftp extends \IPS\File
|
||||
*/
|
||||
protected function ftp()
|
||||
{
|
||||
if ( static::$ftp === NULL )
|
||||
if ( $this->ftp === NULL )
|
||||
{
|
||||
if ( $this->configuration['ftp_details']['protocol'] == 'sftp' )
|
||||
{
|
||||
static::$ftp = new \IPS\Ftp\Sftp( $this->configuration['ftp_details']['server'], $this->configuration['ftp_details']['un'], $this->configuration['ftp_details']['pw'], $this->configuration['ftp_details']['port'] );
|
||||
$this->ftp = new \IPS\Ftp\Sftp( $this->configuration['ftp_details']['server'], $this->configuration['ftp_details']['un'], $this->configuration['ftp_details']['pw'], $this->configuration['ftp_details']['port'] );
|
||||
}
|
||||
else
|
||||
{
|
||||
static::$ftp = new \IPS\Ftp( $this->configuration['ftp_details']['server'], $this->configuration['ftp_details']['un'], $this->configuration['ftp_details']['pw'], $this->configuration['ftp_details']['port'], ( $this->configuration['ftp_details']['protocol'] == 'ssl_ftp' ) );
|
||||
$this->ftp = new \IPS\Ftp( $this->configuration['ftp_details']['server'], $this->configuration['ftp_details']['un'], $this->configuration['ftp_details']['pw'], $this->configuration['ftp_details']['port'], ( $this->configuration['ftp_details']['protocol'] == 'ssl_ftp' ) );
|
||||
}
|
||||
|
||||
static::$ftp->chdir( $this->configuration['ftp_details']['path'] );
|
||||
$this->ftp->chdir( $this->configuration['ftp_details']['path'] );
|
||||
}
|
||||
return static::$ftp;
|
||||
return $this->ftp;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -336,6 +330,28 @@ class _Ftp extends \IPS\File
|
||||
|
||||
$size = $this->ftp()->size( $this->filename );
|
||||
|
||||
$this->ftp()->chdir( $this->configuration['ftp_details']['path'] );
|
||||
|
||||
return $size ?: parent::filesize();
|
||||
}
|
||||
|
||||
/**
|
||||
* Print the contents of the file
|
||||
*
|
||||
* @param int|null $start Start point to print from (for ranges)
|
||||
* @param int|null $length Length to print to (for ranges)
|
||||
* @param int|null $throttle Throttle speed
|
||||
* @return void
|
||||
*/
|
||||
public function printFile( $start=NULL, $length=NULL, $throttle=NULL )
|
||||
{
|
||||
/* Download the file locally */
|
||||
$file = $this->ftp()->download( $this->configuration['ftp_details']['path'] . ( $this->container ? '/' . $this->container : '' ) . '/' . $this->filename, NULL, TRUE );
|
||||
|
||||
/* Send the file */
|
||||
$this->sendFile( $file, $start, $length, $throttle );
|
||||
|
||||
/* Remove the temporary file */
|
||||
@unlink( $file );
|
||||
}
|
||||
}
|
||||
+9
-1
@@ -164,10 +164,11 @@ class _Ftp
|
||||
*
|
||||
* @param string $filename The file to download
|
||||
* @param string|null $target Location to save downloaded file or NULL to return contents
|
||||
* @param bool $returnPath Return the path to the downloaded file instead of the contents
|
||||
* @return string File contents
|
||||
* @throws \IPS\Ftp\Exception
|
||||
*/
|
||||
public function download( $filename, $target=NULL )
|
||||
public function download( $filename, $target=NULL, $returnPath=FALSE )
|
||||
{
|
||||
$temp = FALSE;
|
||||
if ( $target === NULL )
|
||||
@@ -180,6 +181,13 @@ class _Ftp
|
||||
{
|
||||
throw new \IPS\Ftp\Exception( 'COULD_NOT_DOWNLOAD' );
|
||||
}
|
||||
|
||||
/* We use this to avoid out of memory errors - just return path */
|
||||
if( $returnPath === TRUE )
|
||||
{
|
||||
return $target;
|
||||
}
|
||||
|
||||
$result = file_get_contents( $target );
|
||||
|
||||
if ( $temp )
|
||||
|
||||
+9
-1
@@ -226,10 +226,11 @@ class _Sftp extends \IPS\Ftp
|
||||
*
|
||||
* @param string $filename The file to download
|
||||
* @param string|null $target Location to save downloaded file or NULL to return contents
|
||||
* @param bool $returnPath Return the path to the downloaded file instead of the contents
|
||||
* @return string File contents
|
||||
* @throws \IPS\Ftp\Exception
|
||||
*/
|
||||
public function download( $filename, $target=NULL )
|
||||
public function download( $filename, $target=NULL, $returnPath=FALSE )
|
||||
{
|
||||
$temp = FALSE;
|
||||
if ( $target === NULL )
|
||||
@@ -242,6 +243,13 @@ class _Sftp extends \IPS\Ftp
|
||||
{
|
||||
throw new \IPS\Ftp\Exception( 'COULD_NOT_DOWNLOAD' );
|
||||
}
|
||||
|
||||
/* We use this to avoid out of memory errors - just return path */
|
||||
if( $returnPath === TRUE )
|
||||
{
|
||||
return $target;
|
||||
}
|
||||
|
||||
$result = file_get_contents( $target );
|
||||
|
||||
if ( $temp )
|
||||
|
||||
@@ -42,11 +42,12 @@ class _Google
|
||||
/**
|
||||
* Render
|
||||
*
|
||||
* @param int $width Width
|
||||
* @param int $heigth Height
|
||||
* @param int $width Width
|
||||
* @param int $height Height
|
||||
* @param float|NULL $zoom The zoom amount (a value between 0 being totally zoomed out view of the world, and 1 being as fully zoomed in as possible) or NULL to zoom automatically based on how much data is available
|
||||
* @return string
|
||||
*/
|
||||
public function render( $width, $height, $zoom=5 )
|
||||
public function render( $width, $height, $zoom=NULL )
|
||||
{
|
||||
if ( $this->geolocation->lat and $this->geolocation->long )
|
||||
{
|
||||
@@ -54,13 +55,11 @@ class _Google
|
||||
}
|
||||
else
|
||||
{
|
||||
$zoom = 1;
|
||||
$location = array();
|
||||
foreach ( array( 'country', 'region', 'city', 'addressLines' ) as $k )
|
||||
{
|
||||
if ( $this->geolocation->$k )
|
||||
{
|
||||
$zoom++;
|
||||
if ( is_array( $this->geolocation->$k ) )
|
||||
{
|
||||
foreach ( array_reverse( $this->geolocation->$k ) as $v )
|
||||
@@ -78,10 +77,10 @@ class _Google
|
||||
}
|
||||
|
||||
$linkUrl = \IPS\Http\Url::external( '//maps.google.com/' )->setQueryString( 'q', $location );
|
||||
|
||||
|
||||
$imageUrl = \IPS\Http\Url::external( '//maps.googleapis.com/maps/api/staticmap' )->setQueryString( array(
|
||||
'center' => $location,
|
||||
'zoom' => $zoom,
|
||||
'zoom' => $zoom === NULL ? NULL : ceil( $zoom * 8 ),
|
||||
'size' => "{$width}x{$height}",
|
||||
'sensor' => 'false',
|
||||
'markers' => $location
|
||||
|
||||
@@ -96,7 +96,7 @@ class _Dynamic extends \IPS\Helpers\Chart
|
||||
/**
|
||||
* @brief Available Types
|
||||
*/
|
||||
public $availableTypes = array( 'LineChart', 'ColumnChart', 'BarChart', 'PieChart' );
|
||||
public $availableTypes = array( 'LineChart', 'ColumnChart', 'BarChart', 'PieChart', 'Table' );
|
||||
|
||||
/**
|
||||
* @brief Available Filters
|
||||
@@ -162,7 +162,7 @@ class _Dynamic extends \IPS\Helpers\Chart
|
||||
$this->start = $defaultTimes['start'];
|
||||
$this->end = $defaultTimes['end'];
|
||||
$this->identifier = \substr( md5( $table . $dateField ), 0, 6 ) . $identifier;
|
||||
|
||||
|
||||
if ( isset( \IPS\Request::i()->type[ $this->identifier ] ) and in_array( \IPS\Request::i()->type[ $this->identifier ], $this->availableTypes ) )
|
||||
{
|
||||
$this->type = \IPS\Request::i()->type[ $this->identifier ];
|
||||
@@ -407,7 +407,7 @@ class _Dynamic extends \IPS\Helpers\Chart
|
||||
$key = array_pop( $keys );
|
||||
|
||||
$stmt = \IPS\Db::i()->select(
|
||||
"{$key}, {$this->groupBy}",
|
||||
"{$key}" . ( $this->groupBy ? ", {$this->groupBy}" : '' ),
|
||||
$this->table,
|
||||
$where,
|
||||
"{$this->dateField} ASC",
|
||||
@@ -556,7 +556,7 @@ class _Dynamic extends \IPS\Helpers\Chart
|
||||
{
|
||||
$this->options['domainAxis']['type'] = 'category';
|
||||
}
|
||||
|
||||
|
||||
$output = $this->render( $this->type, $this->options, $this->format );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,8 +47,9 @@ abstract class _Controller extends \IPS\Dispatcher\Controller
|
||||
}
|
||||
catch ( \Exception $e ) { }
|
||||
|
||||
$returnURL = new \IPS\Http\Url( $_SERVER['HTTP_REFERER'] );
|
||||
$this->_coverPhotoSet( new \IPS\Helpers\CoverPhoto( $values['cover_photo'], 0 ) );
|
||||
\IPS\Output::i()->redirect( $_SERVER['HTTP_REFERER'] );
|
||||
\IPS\Output::i()->redirect( $returnURL->setQueryString( array( '_position' => 1 ) ) );
|
||||
}
|
||||
|
||||
\IPS\Output::i()->output = $form->customTemplate( array( call_user_func_array( array( \IPS\Theme::i(), 'getTemplate' ), array( 'forms', 'core' ) ), 'popupTemplate' ) );
|
||||
|
||||
@@ -27,12 +27,14 @@ class _Address extends FormAbstract
|
||||
* @brief Default Options
|
||||
* @code
|
||||
$defaultOptions = array(
|
||||
'minimize' => FALSE, // Minimize the address field until the user focuses?
|
||||
'minimize' => FALSE, // Minimize the address field until the user focuses?
|
||||
'requireFullAddress'=> TRUE, // Does this have to be a full address? Default is TRUE, may set to FALSE if a more generic location is acceptable
|
||||
);
|
||||
* @endcode
|
||||
*/
|
||||
protected $defaultOptions = array(
|
||||
'minimize' => FALSE
|
||||
'minimize' => FALSE,
|
||||
'requireFullAddress' => TRUE,
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -181,7 +183,7 @@ class _Address extends FormAbstract
|
||||
}
|
||||
|
||||
/* Validate, return NULL if we have nothing */
|
||||
if ( !$fullyCompleted )
|
||||
if ( !$fullyCompleted and $this->options['requireFullAddress'] )
|
||||
{
|
||||
if ( $partiallyCompleted )
|
||||
{
|
||||
@@ -199,13 +201,25 @@ class _Address extends FormAbstract
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
/* Add in latitude and longitude if we can */
|
||||
try
|
||||
elseif( !$fullyCompleted )
|
||||
{
|
||||
$value->getLatLong();
|
||||
/* If we don't have any country, region city AND postal code, return NULL, otherwise everywhere ends up storing empty
|
||||
location arrays which makes it difficult to confirm if a location was actually set or not */
|
||||
if( !$value->country AND !$value->region AND !$value->city AND !$value->postalCode )
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
/* Add in latitude and longitude if we can */
|
||||
if ( $fullyCompleted )
|
||||
{
|
||||
try
|
||||
{
|
||||
$value->getLatLong();
|
||||
}
|
||||
catch( \BadFunctionCallException $e ){}
|
||||
}
|
||||
catch( \BadFunctionCallException $e ){}
|
||||
|
||||
/* Return */
|
||||
return $value;
|
||||
|
||||
@@ -100,6 +100,7 @@ class _Captcha extends FormAbstract
|
||||
/* If we previously did an AJAX validate which is still valid, return true */
|
||||
if ( isset( $_SESSION[ 'captcha-val-' . $this->name ] ) and $_SESSION[ 'captcha-val-' . $this->name ] > ( time() - 60 ) )
|
||||
{
|
||||
unset( $_SESSION[ 'captcha-val-' . $this->name ] );
|
||||
return TRUE;
|
||||
}
|
||||
/* Otherwise, check with service */
|
||||
|
||||
@@ -90,7 +90,7 @@ class _CheckboxSet extends Select
|
||||
*/
|
||||
public function getValue()
|
||||
{
|
||||
$value = array_keys( parent::getValue() );
|
||||
$value = is_array( parent::getValue() ) ? array_keys( parent::getValue() ) : array();
|
||||
if ( $this->options['returnLabels'] )
|
||||
{
|
||||
if ( is_array( $value ) )
|
||||
|
||||
@@ -33,6 +33,7 @@ class _Codemirror extends TextArea
|
||||
'placeholder' => 'e.g. ...', // A placeholder (NB: Will only work on compatible browsers)
|
||||
'tags' => array(), // An array of extra insertable tags in key => value pair with key being what is inserted and value serving as a description
|
||||
'mode' => 'php' // Formatting mode. Default is htmlmixed.
|
||||
'height' => 300 // Height of code mirror editor
|
||||
);
|
||||
* @endcode
|
||||
*/
|
||||
@@ -44,6 +45,7 @@ class _Codemirror extends TextArea
|
||||
'tags' => array(),
|
||||
'mode' => 'htmlmixed',
|
||||
'nullLang' => NULL,
|
||||
'height' => 300
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -74,6 +76,11 @@ class _Codemirror extends TextArea
|
||||
*/
|
||||
public function html()
|
||||
{
|
||||
return \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->codemirror( $this->name, $this->value, $this->required, $this->options['maxLength'], $this->options['disabled'], '', $this->options['placeholder'], $this->options['tags'], $this->options['mode'], $this->htmlId ? "{$this->htmlId}-input" : NULL );
|
||||
if ( $this->options['height'] )
|
||||
{
|
||||
$this->options['height'] = is_numeric( $this->options['height'] ) ? $this->options['height'] . 'px' : $this->options['height'];
|
||||
}
|
||||
|
||||
return \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->codemirror( $this->name, $this->value, $this->required, $this->options['maxLength'], $this->options['disabled'], '', $this->options['placeholder'], $this->options['tags'], $this->options['mode'], $this->htmlId ? "{$this->htmlId}-input" : NULL, $this->options['height'] );
|
||||
}
|
||||
}
|
||||
@@ -27,8 +27,8 @@ class _Date extends FormAbstract
|
||||
* @brief Default Options
|
||||
* @code
|
||||
$defaultOptions = array(
|
||||
'min' => new \IPS\DateTime('1970-01-01'), // Minimum date. NULL will mean no minimum (though note hard limits if it will be stored as a UNIX timestamp). Default is null.
|
||||
'max' => new \IPS\DateTime('2038-01-19'), // Maximum date. NULL will mean no maximum (though note hard limits if it will be stored as a UNIX timestamp). Default is null.
|
||||
'min' => new \IPS\DateTime('1970-01-01'), // Minimum date. NULL will mean 1901-12-13 20:45:52 (the minimum 32-bit timestamp). Default is null.
|
||||
'max' => new \IPS\DateTime('2038-01-19'), // Maximum date. NULL will mean 2038-01-19 03:14:07 (the maximum 32-bit timestamp). Default is null.
|
||||
'disabled' => FALSE, // Disables input. Default is FALSE.
|
||||
'time' => FALSE, // Also allow time input?
|
||||
'unlimited' => -1, // If any value other than NULL is provided, an "Unlimited" checkbox will be displayed. If checked, the value specified will be sent.
|
||||
@@ -85,9 +85,19 @@ class _Date extends FormAbstract
|
||||
$this->timeName = "{$name}_time";
|
||||
$this->unlimitedName = "{$name}_unlimited";
|
||||
}
|
||||
|
||||
|
||||
/* Call parent constructor */
|
||||
call_user_func_array( 'parent::__construct', func_get_args() );
|
||||
|
||||
/* Set min/max - hardcoded to 32bit values rather than using PHP_INT_MAX because the MySQL server may be 32-bit even if the web server is 64 */
|
||||
if ( !isset( $this->options['min'] ) or $this->options['min']->getTimestamp() < 2147483648 )
|
||||
{
|
||||
$this->options['min'] = \IPS\DateTime::ts( -2147483648 );
|
||||
}
|
||||
if ( !isset( $this->options['max'] ) or $this->options['max']->getTimestamp() > 2147483647 )
|
||||
{
|
||||
$this->options['max'] = \IPS\DateTime::ts( 2147483647 );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -267,13 +277,13 @@ class _Date extends FormAbstract
|
||||
}
|
||||
|
||||
/* Check minimum */
|
||||
if ( $this->options['min'] !== NULL and $this->options['min'] > $this->value )
|
||||
if ( $this->value and $this->options['min'] !== NULL and $this->options['min'] > $this->value )
|
||||
{
|
||||
throw new \LengthException( \IPS\Member::loggedIn()->language()->addToStack('form_date_min', FALSE, array( 'sprintf' => array( $this->options['min']->localeDate() ) ) ) );
|
||||
}
|
||||
|
||||
/* Check maximum */
|
||||
if ( $this->options['max'] !== NULL and $this->options['max'] < $this->value )
|
||||
if ( $this->value and $this->options['max'] !== NULL and $this->options['max'] < $this->value )
|
||||
{
|
||||
throw new \LengthException( \IPS\Member::loggedIn()->language()->addToStack('form_date_max', FALSE, array( 'sprintf' => array( $this->options['max']->localeDate() ) ) ) );
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ class _DateRange extends FormAbstract
|
||||
* @return void
|
||||
*/
|
||||
public function __construct( $name, $defaultValue=NULL, $required=FALSE, $options=array() )
|
||||
{
|
||||
{
|
||||
$this->start = new \IPS\Helpers\Form\Date( "{$name}[start]", isset( $defaultValue['start'] ) ? $defaultValue['start'] : NULL, FALSE, isset( $options['start'] ) ? $options['start'] : array() );
|
||||
$this->end = new \IPS\Helpers\Form\Date( "{$name}[end]", isset( $defaultValue['end'] ) ? $defaultValue['end'] : NULL, FALSE, isset( $options['end'] ) ? $options['end'] : array() );
|
||||
|
||||
|
||||
@@ -141,7 +141,6 @@ class _Editor extends FormAbstract
|
||||
\IPS\Output::i()->jsFiles = array_merge( \IPS\Output::i()->jsFiles, array( \IPS\Http\Url::internal( 'applications/core/interface/ckeditor/ckeditor/ckeditor.js', 'none', NULL, array(), \IPS\Http\Url::PROTOCOL_RELATIVE ) ) );
|
||||
}
|
||||
}
|
||||
\IPS\Output::i()->jsVars['strip_quotes'] = \IPS\Settings::i()->strip_quotes;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -269,7 +268,7 @@ class _Editor extends FormAbstract
|
||||
|
||||
/* If it was made without JS, convert linebreaks to <br>s */
|
||||
$noJsKey = $this->name . '_noscript';
|
||||
if ( isset( \IPS\Request::i()->$noJsKey ) AND !$value )
|
||||
if ( isset( \IPS\Request::i()->$noJsKey ) )
|
||||
{
|
||||
$value = nl2br( \IPS\Request::i()->$noJsKey, FALSE );
|
||||
}
|
||||
@@ -297,9 +296,9 @@ class _Editor extends FormAbstract
|
||||
if ( $this->uploader )
|
||||
{
|
||||
$inserts = array();
|
||||
foreach ( static::getAttachments( NULL, $this->postKey ) as $attachment )
|
||||
foreach ( $this->getAttachments() as $attachment )
|
||||
{
|
||||
if ( !isset( $parser ) or !in_array( $attachment['attach_id'], $parser->mappedAttachments ) )
|
||||
if ( !isset( $parser ) or !in_array( $attachment['attach_id'], $parser->mappedAttachments ) or array_key_exists( $attachment['attach_id'], $parser->existingAttachments ) )
|
||||
{
|
||||
if ( $attachment['attach_is_image'] )
|
||||
{
|
||||
@@ -309,21 +308,30 @@ class _Editor extends FormAbstract
|
||||
{
|
||||
$value .= \IPS\Theme::i()->getTemplate( 'editor', 'core', 'global' )->attachedFile( \IPS\Settings::i()->base_url . "applications/core/interface/file/attachment.php?id=" . $attachment['attach_id'], $attachment['attach_file'] );
|
||||
}
|
||||
|
||||
$inserts[] = array(
|
||||
'attachment_id' => $attachment['attach_id'],
|
||||
'location_key' => "{$this->options['app']}_{$this->options['key']}",
|
||||
'id1' => ( is_array( $this->options['attachIds'] ) and isset( $this->options['attachIds'][0] ) ) ? $this->options['attachIds'][0] : NULL,
|
||||
'id2' => ( is_array( $this->options['attachIds'] ) and isset( $this->options['attachIds'][1] ) ) ? $this->options['attachIds'][1] : NULL,
|
||||
'id3' => ( is_array( $this->options['attachIds'] ) and isset( $this->options['attachIds'][2] ) ) ? $this->options['attachIds'][2] : NULL,
|
||||
'temp' => is_string( $this->options['attachIds'] ) ? $this->options['attachIds'] : ( $this->options['attachIds'] === NULL ? md5( $this->options['autoSaveKey'] ) : $this->options['attachIds'] )
|
||||
);
|
||||
|
||||
if ( !isset( $parser ) or !in_array( $attachment['attach_id'], $parser->mappedAttachments ) )
|
||||
{
|
||||
$inserts[] = array(
|
||||
'attachment_id' => $attachment['attach_id'],
|
||||
'location_key' => "{$this->options['app']}_{$this->options['key']}",
|
||||
'id1' => ( is_array( $this->options['attachIds'] ) and isset( $this->options['attachIds'][0] ) ) ? $this->options['attachIds'][0] : NULL,
|
||||
'id2' => ( is_array( $this->options['attachIds'] ) and isset( $this->options['attachIds'][1] ) ) ? $this->options['attachIds'][1] : NULL,
|
||||
'id3' => ( is_array( $this->options['attachIds'] ) and isset( $this->options['attachIds'][2] ) ) ? $this->options['attachIds'][2] : NULL,
|
||||
'temp' => is_string( $this->options['attachIds'] ) ? $this->options['attachIds'] : ( $this->options['attachIds'] === NULL ? md5( $this->options['autoSaveKey'] ) : $this->options['attachIds'] )
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if( count( $inserts ) )
|
||||
{
|
||||
\IPS\Db::i()->insert( 'core_attachments_map', $inserts, TRUE );
|
||||
}
|
||||
|
||||
/* Clear out the post key for attachments that are claimed automatically */
|
||||
if ( $this->options['attachIds'] )
|
||||
{
|
||||
\IPS\Db::i()->update( 'core_attachments', array( 'attach_post_key' => '' ), array( 'attach_post_key=?', $this->postKey ) );
|
||||
}
|
||||
}
|
||||
|
||||
/* Remove abandoned attachments */
|
||||
@@ -416,26 +424,8 @@ class _Editor extends FormAbstract
|
||||
}
|
||||
|
||||
/* Load existing attachments */
|
||||
$existingAttachments = '';
|
||||
$counts = array();
|
||||
$where = array();
|
||||
if ( isset( $this->options['attachIds'] ) and is_array( $this->options['attachIds'] ) and count( $this->options['attachIds'] ) )
|
||||
{
|
||||
$where = array( array( 'location_key=?', "{$this->options['app']}_{$this->options['key']}" ) );
|
||||
$i = 1;
|
||||
foreach ( $this->options['attachIds'] as $id )
|
||||
{
|
||||
$where[] = array( "id{$i}=?", $id );
|
||||
$i++;
|
||||
}
|
||||
$_existingAttachments = iterator_to_array( \IPS\Db::i()->select( 'attachment_id', 'core_attachments_map', $where ) );
|
||||
if ( !empty( $_existingAttachments ) )
|
||||
{
|
||||
$existingAttachments = \IPS\Db::i()->in( 'attach_id', $_existingAttachments ) . ' OR ';
|
||||
}
|
||||
}
|
||||
$existing = array();
|
||||
foreach ( static::getAttachments( $existingAttachments, $this->postKey ) as $attachment )
|
||||
foreach ( $this->getAttachments() as $attachment )
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -519,7 +509,7 @@ class _Editor extends FormAbstract
|
||||
try
|
||||
{
|
||||
$attachment = \IPS\Db::i()->select( '*', 'core_attachments', array( 'attach_id=?', \IPS\Request::i()->deleteFile ) )->first();
|
||||
\IPS\Db::i()->delete( 'core_attachments_map', array_merge( $where, array( array( 'attachment_id=?', $attachment['attach_id'] ) ) ) );
|
||||
\IPS\Db::i()->delete( 'core_attachments_map', array( array( 'location_key=?', "{$this->options['app']}_{$this->options['key']}" ), array( 'attachment_id=?', $attachment['attach_id'] ) ) );
|
||||
$otherMaps = \IPS\Db::i()->select( 'COUNT(*)', 'core_attachments_map', array( 'attachment_id=?', $attachment['attach_id'] ) )->first();
|
||||
if ( !$otherMaps )
|
||||
{
|
||||
@@ -551,13 +541,17 @@ class _Editor extends FormAbstract
|
||||
*/
|
||||
public static function getEmoticons()
|
||||
{
|
||||
|
||||
if( !isset( \IPS\Data\Store::i()->emoticons ) )
|
||||
{
|
||||
$emoticons = iterator_to_array( \IPS\Db::i()->select( '*', 'core_emoticons', array( 'typed<>?', '' ) )->setKeyField('typed')->setValueField('image') );
|
||||
$emoticons = iterator_to_array( \IPS\Db::i()->select( '*', 'core_emoticons', array( 'typed<>?', '' ) )->setKeyField('typed') );
|
||||
foreach( $emoticons as $key => $emoticon )
|
||||
{
|
||||
$emoticons[ $key ] = (string) \IPS\File::get( "core_Emoticons", $emoticon )->url;
|
||||
$emoticons[ $key ] = array(
|
||||
'image' => (string) \IPS\File::get( "core_Emoticons", $emoticon['image'] )->url,
|
||||
'image_2x' => $emoticon['image_2x'] ? (string) \IPS\File::get( "core_Emoticons", $emoticon['image_2x'] )->url . ' 2x' : FALSE,
|
||||
'height' => $emoticon['height'] ?: FALSE,
|
||||
'width' => $emoticon['width'] ?: FALSE,
|
||||
);
|
||||
}
|
||||
|
||||
\IPS\Data\Store::i()->emoticons = $emoticons;
|
||||
@@ -565,27 +559,53 @@ class _Editor extends FormAbstract
|
||||
|
||||
return \IPS\Data\Store::i()->emoticons;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attachments
|
||||
*/
|
||||
protected $attachments;
|
||||
|
||||
/**
|
||||
* Fetch existing attachments - abstracted to a static method for caching
|
||||
* Fetch existing attachments
|
||||
*
|
||||
* @param string $existing Existing attachments limiter
|
||||
* @param string $postKey Post key
|
||||
* @return array
|
||||
*/
|
||||
public static function getAttachments( $existing, $postKey )
|
||||
protected function getAttachments()
|
||||
{
|
||||
static $attachments = array();
|
||||
|
||||
$_key = md5( $existing . $postKey );
|
||||
|
||||
if( isset( $attachments[ $_key ] ) )
|
||||
if ( $this->attachments === NULL )
|
||||
{
|
||||
return $attachments[ $_key ];
|
||||
$existingAttachments = '';
|
||||
$where = array();
|
||||
if ( isset( $this->options['attachIds'] ) and is_array( $this->options['attachIds'] ) and count( $this->options['attachIds'] ) )
|
||||
{
|
||||
$where = array( array( 'location_key=?', "{$this->options['app']}_{$this->options['key']}" ) );
|
||||
$i = 1;
|
||||
foreach ( $this->options['attachIds'] as $id )
|
||||
{
|
||||
$where[] = array( "id{$i}=?", $id );
|
||||
$i++;
|
||||
}
|
||||
$_existingAttachments = iterator_to_array( \IPS\Db::i()->select( 'attachment_id', 'core_attachments_map', $where ) );
|
||||
if ( !empty( $_existingAttachments ) )
|
||||
{
|
||||
$existingAttachments = \IPS\Db::i()->in( 'attach_id', $_existingAttachments ) . ' OR ';
|
||||
}
|
||||
}
|
||||
|
||||
$this->attachments = \IPS\Db::i()->select( '*', 'core_attachments', array( $existingAttachments . 'attach_post_key=?', $this->postKey ) );
|
||||
}
|
||||
|
||||
$attachments[ $_key ] = \IPS\Db::i()->select( '*', 'core_attachments', array( $existing . 'attach_post_key=?', $postKey ) );
|
||||
|
||||
return $attachments[ $_key ];
|
||||
|
||||
return $this->attachments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get CKEditor Version
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function ckeditorVersion()
|
||||
{
|
||||
preg_match( '/CKEditor\s+Included version: (.+?)\s/', file_get_contents( \IPS\ROOT_PATH . '/Credits.txt' ), $matches );
|
||||
return $matches[1];
|
||||
}
|
||||
}
|
||||
@@ -121,8 +121,7 @@ class _Form
|
||||
public function __construct( $id='form', $submitLang='save', $action=NULL, $attributes=array() )
|
||||
{
|
||||
$this->id = $id;
|
||||
$this->action = $action ?: \IPS\Request::i()->url();
|
||||
$this->action = $this->action->stripQueryString('csrfKey');
|
||||
$this->action = $action ?: \IPS\Request::i()->url()->stripQueryString( array( 'csrfKey', 'ajaxValidate' ) );
|
||||
|
||||
$this->attributes = $attributes;
|
||||
|
||||
@@ -136,17 +135,18 @@ class _Form
|
||||
$potentialMaxUploadValues = array();
|
||||
if( (float) ini_get('upload_max_filesize') > 0 )
|
||||
{
|
||||
$potentialMaxUploadValues[] = (float) ini_get('upload_max_filesize');
|
||||
$potentialMaxUploadValues[] = \IPS\File::returnBytes( ini_get('upload_max_filesize') );
|
||||
}
|
||||
if( (float) ini_get('post_max_size') > 0 )
|
||||
{
|
||||
$potentialMaxUploadValues[] = (float) ini_get('post_max_size');
|
||||
/* We need to reduce post_max_size lower because it includes the ENTIRE post and other data, such as the number of chunks, will also be sent with the request */
|
||||
$potentialMaxUploadValues[] = \IPS\File::returnBytes( ini_get('post_max_size') ) - 1048576;
|
||||
}
|
||||
if( (float) ini_get('memory_limit') > 0 )
|
||||
{
|
||||
$potentialMaxUploadValues[] = (float) ini_get('memory_limit');
|
||||
$potentialMaxUploadValues[] = \IPS\File::returnBytes( ini_get('memory_limit') );
|
||||
}
|
||||
$this->uploadField = min( $potentialMaxUploadValues ) * 1048576;
|
||||
$this->uploadField = min( $potentialMaxUploadValues );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -257,6 +257,7 @@ class _Form
|
||||
public function addMatrix( $name, $matrix )
|
||||
{
|
||||
//$matrix->manageable = FALSE;
|
||||
$matrix->id = $this->id;
|
||||
$this->tabClasses[ $this->currentTab ] = 'ipsMatrix';
|
||||
$this->elements[ $this->currentTab ][ $name ] = $matrix;
|
||||
}
|
||||
|
||||
@@ -274,11 +274,19 @@ abstract class _FormAbstract
|
||||
/* Some form helpers might want to allow nested and outer suffixes */
|
||||
$html = $this->html();
|
||||
|
||||
if( array_key_exists( 'endSuffix', $this->options ) )
|
||||
if( array_key_exists( 'endSuffix', $this->options ) and ! empty( $this->options['endSuffix'] ) )
|
||||
{
|
||||
$this->suffix = $this->options['endSuffix'];
|
||||
}
|
||||
|
||||
/* Some elements support an array for suffix, such as Number which supports preUnlimited and postUnlimited. We need to wipe out
|
||||
the suffix here before calling the row() template, however, which only supports a string and throws an Array to string conversion error.
|
||||
By this point, the element template has already ran and used the suffix if designed to */
|
||||
if( is_array( $this->suffix ) )
|
||||
{
|
||||
$this->suffix = '';
|
||||
}
|
||||
|
||||
return \IPS\Theme::i()->getTemplate( 'forms', 'core' )->row( $label, $html, $desc, $warning, $this->appearRequired, $this->error, $this->prefix, $this->suffix, $this->htmlId ?: ( $form ? "{$form->id}_{$this->name}" : NULL ), $this, $form );
|
||||
}
|
||||
catch ( \Exception $e )
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Item selector for Form Builder
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - SVN_YYYY Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Social Suite
|
||||
* @since 12 Apr 2013
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS\Helpers\Form;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/* @todo limit by container IDs */
|
||||
|
||||
/**
|
||||
* Content Item selector
|
||||
*/
|
||||
class _Item extends FormAbstract
|
||||
{
|
||||
/**
|
||||
* @brief Default Options
|
||||
* @code
|
||||
$defaultOptions = array(
|
||||
'class' => '\IPS\core\Foo', // The \Content\Item class
|
||||
'permissionCheck' => 'read', // If a permission key is provided, only content items that the member has that permission for will be available.
|
||||
'maxItems' => NULL, // Maximum items allowed to select, or NULL for unlimited
|
||||
'orderResults' => NULL|array // NULL or array( field, 'asc' ) where field is a mappable content item field (date, title, etc)
|
||||
'itemTemplate' => NULL // NULL or array of template group and template name array( \IPS\Theme::i()->getTemplate( 'group', 'app', 'location'), 'name' )
|
||||
'containerIds' => array // Array of container IDs to limit the search
|
||||
'minAjaxLength' => int // Number of characters to type before selector appears
|
||||
* 'where' => array // Array of additional where array( array( 'foo=?', 1 ) )
|
||||
* );
|
||||
* @encode
|
||||
*/
|
||||
protected $defaultOptions = array(
|
||||
'class' => NULL,
|
||||
'permissionCheck' => 'read',
|
||||
'maxItems' => NULL,
|
||||
'orderResults' => NULL,
|
||||
'itemTemplate' => NULL,
|
||||
'containerIds' => array(),
|
||||
'minAjaxLength' => 3,
|
||||
'where' => array()
|
||||
);
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
call_user_func_array( 'parent::__construct', func_get_args() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get HTML
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function html()
|
||||
{
|
||||
/* Display */
|
||||
$url = \IPS\Request::i()->url()->setQueryString( '_itemSelectName', $this->name );
|
||||
|
||||
$template = NULL;
|
||||
if ( $this->options['itemTemplate'] === NULL )
|
||||
{
|
||||
$template = array( \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' ), 'itemResult' );
|
||||
}
|
||||
|
||||
/* Are we getting some AJAX stuff? */
|
||||
if ( isset( \IPS\Request::i()->_itemSelectName ) and \IPS\Request::i()->_itemSelectName === $this->name )
|
||||
{
|
||||
$results = array();
|
||||
$class = $this->options['class'];
|
||||
$field = $class::$databasePrefix . $class::$databaseColumnMap['title'];
|
||||
$where = array( array( $field . " LIKE CONCAT('%', ?, '%')", \IPS\Request::i()->q ) );
|
||||
$idField = $class::$databaseColumnId;
|
||||
if ( isset( $class::$databaseColumnMap['container'] ) and is_array( $this->options['containerIds'] ) and count( $this->options['containerIds'] ) )
|
||||
{
|
||||
$where[] = array( \IPS\Db::i()->in( $class::$databasePrefix . $class::$databaseColumnMap['container'], $this->options['containerIds'] ) );
|
||||
}
|
||||
|
||||
if ( isset( $this->options['where'] ) and count( $this->options['where'] ) )
|
||||
{
|
||||
$where = array_merge( $where, $this->options['where'] );
|
||||
}
|
||||
|
||||
foreach( $class::getItemsWithPermission( $where, 'LENGTH(' . $field . ') ASC', array( 0, 20 ), $this->options['permissionCheck'] ) as $item )
|
||||
{
|
||||
$results[] = array(
|
||||
'id' => $item->$idField,
|
||||
'html' => call_user_func( $template, $item )
|
||||
);
|
||||
}
|
||||
|
||||
\IPS\Output::i()->json( $results );
|
||||
}
|
||||
|
||||
return \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->item( $this->name, $this->value, $this->options['maxItems'], $this->options['minAjaxLength'], $url, $template );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Value
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getValue()
|
||||
{
|
||||
$name = $this->name . '_values';
|
||||
if ( isset( \IPS\Request::i()->$name ) )
|
||||
{
|
||||
return explode( ',', \IPS\Request::i()->$name );
|
||||
}
|
||||
else
|
||||
{
|
||||
return array();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format Value
|
||||
*
|
||||
* @return \IPS\Node\Model|array|NULL
|
||||
*/
|
||||
public function formatValue()
|
||||
{
|
||||
$itemClass = $this->options['class'];
|
||||
$order = NULL;
|
||||
$items = array();
|
||||
|
||||
if ( ! $this->value )
|
||||
{
|
||||
$this->value = array();
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->value = ( is_string( $this->value ) ) ? explode( ',', $this->value ) : $this->value;
|
||||
}
|
||||
|
||||
if ( count( $this->value ) )
|
||||
{
|
||||
if ( is_array( $this->options['orderResults'] ) )
|
||||
{
|
||||
if ( isset( $itemClass::$databaseColumnMap[ $this->options['orderResults'][0] ] ) )
|
||||
{
|
||||
$order = $itemClass::$databaseColumnMap[ $this->options['orderResults'][0] ] . ' ' . $this->options['orderResults'][1];
|
||||
}
|
||||
}
|
||||
|
||||
$where = array( \IPS\Db::i()->in( $itemClass::$databaseColumnId, $this->value ) );
|
||||
foreach( $itemClass::getItemsWithPermission( array( $where ), $order, NULL, $this->options['permissionCheck'] ) as $item )
|
||||
{
|
||||
$items[ $item->_id ] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
$this->value = $items;
|
||||
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* String Value
|
||||
*
|
||||
* @param mixed $value The value
|
||||
* @return string
|
||||
*/
|
||||
public static function stringValue( $value )
|
||||
{
|
||||
if ( is_array( $value ) )
|
||||
{
|
||||
return implode( ',', array_keys( $value ) );
|
||||
}
|
||||
elseif ( is_object( $value ) )
|
||||
{
|
||||
return $value->_id;
|
||||
}
|
||||
return (string) $value;
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,8 @@ class _Member extends Text
|
||||
'source' => 'app=core&module=system&controller=ajax&do=findMember',
|
||||
'resultItemTemplate' => 'core.autocomplete.memberItem',
|
||||
'unique' => true,
|
||||
'minAjaxLength' => 3
|
||||
'minAjaxLength' => 3,
|
||||
'disallowedCharacters' => array()
|
||||
);
|
||||
if( isset( $args[3] ) and array_key_exists( 'multiple', $args[3] ) and $args[3]['multiple'] > 0 )
|
||||
{
|
||||
@@ -87,7 +88,9 @@ class _Member extends Text
|
||||
$value = $value->name;
|
||||
}
|
||||
|
||||
return \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->text( $this->name, 'text', $value, $this->required, $this->options['maxLength'], $this->options['size'], $this->options['disabled'], $this->options['autocomplete'], $this->options['placeholder'], NULL, NULL );
|
||||
/* 10/19/15 - adding htmlspecialchars around value if autocomplete is enabled so that html tag characters can be used (e.g. for members) */
|
||||
/* This value is decoded by the JS widget before use. See https://community.invisionpower.com/4bugtrack/active-reports/cannot-pm-certain-members-r6181/ */
|
||||
return \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->text( $this->name, 'text', ( $this->options['autocomplete'] ? htmlspecialchars( $value, ENT_QUOTES | \IPS\HTMLENTITIES, 'UTF-8', FALSE ) : $value ), $this->required, $this->options['maxLength'], $this->options['size'], $this->options['disabled'], $this->options['autocomplete'], $this->options['placeholder'], NULL, NULL );
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -37,7 +37,7 @@ class _Node extends FormAbstract
|
||||
'subnodes' => TRUE, // Controls if subnodes should be included. Defaults to TRUE
|
||||
'togglePerm' => 'edit', // If a permission key is provided, nodes that have this permission will toggle the element IDs in toggleIds on/off
|
||||
'toggleIds' => array(), // Element IDs to toggle on/off when togglePerm is reached
|
||||
'forceOwner' => \IPS\Member::load(), // If nodes are 'owned' you can force a specific owner
|
||||
'forceOwner' => \IPS\Member::load(), // If nodes are 'owned', the owner. NULL for currently logged in member, FALSE to not limit by owner
|
||||
'where' => array(), // where clause to control which results to display
|
||||
'disabledIds' => array(), // Array of disabled IDs
|
||||
'noParentNodes' => 'Custom' // If a value is provided, subnodes of this class which have no parent node will be added into a pseudo-group with the title provided. e.g. custom packages in Nexus do not belong to any package group
|
||||
@@ -154,7 +154,7 @@ class _Node extends FormAbstract
|
||||
$nodes = array();
|
||||
$children = array();
|
||||
$noParentNodes = array();
|
||||
if( isset( $nodeClass::$ownerTypes ) and $nodeClass::$ownerTypes !== NULL )
|
||||
if( isset( $nodeClass::$ownerTypes ) and $nodeClass::$ownerTypes !== NULL and $this->options['forceOwner'] !== FALSE )
|
||||
{
|
||||
$nodes = $nodeClass::loadByOwner( $this->options['forceOwner'] ?: \IPS\Member::loggedIn(), $this->options['where'] );
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ class _Number extends FormAbstract
|
||||
'step' => NULL, // Increase step for supported browsers. NULL will cause the value to be based off "decimals". Default is NULL.
|
||||
'range' => TRUE, // Use a range selection for supported browsers? Default is FALSE.
|
||||
'disabled' => FALSE, // Disables input. Default is FALSE.
|
||||
'suffixAfterNumber' => TRUE, // A suffix to go after the unlimited checkbox
|
||||
'endSuffix' => '...', // A suffix to go after the unlimited checkbox
|
||||
);
|
||||
* @endcode
|
||||
*/
|
||||
|
||||
@@ -184,12 +184,12 @@ class _Select extends FormAbstract
|
||||
{
|
||||
foreach ( $v as $x => $y )
|
||||
{
|
||||
$options[ $k ][ $x ] = htmlentities( $y, \IPS\HTMLENTITIES, 'UTF-8', FALSE );
|
||||
$options[ $k ][ $x ] = htmlspecialchars( $y, \IPS\HTMLENTITIES, 'UTF-8', FALSE );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$options[ $k ] = htmlentities( $v, \IPS\HTMLENTITIES, 'UTF-8', FALSE );
|
||||
$options[ $k ] = htmlspecialchars( $v, \IPS\HTMLENTITIES, 'UTF-8', FALSE );
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -261,6 +261,12 @@ class _Select extends FormAbstract
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/* If it is not required and there is no value (pagination from table advanced search form then pass validation otherwise it throws an error) */
|
||||
if ( $this->value === NULL and ! $this->required )
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
parent::validate();
|
||||
|
||||
@@ -294,4 +300,15 @@ class _Select extends FormAbstract
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* String Value
|
||||
*
|
||||
* @param mixed $value The value
|
||||
* @return string
|
||||
*/
|
||||
public static function stringValue( $value )
|
||||
{
|
||||
return parent::stringValue( $value );
|
||||
}
|
||||
}
|
||||
@@ -43,6 +43,7 @@ class _Text extends TextArea
|
||||
'prefix' => TRUE, // If TRUE, user will have option to specify one tag as a prefix
|
||||
'resultItemTemplate'=> 'core.foo.bar', // Can be used to specify a custom JavaScript template to use for the result
|
||||
'minAjaxLength' => 3, // Minimum length of value before sending AJAX lookup call
|
||||
'disallowedCharacters' => array() // An array of disallowed characters (default < > ' ")
|
||||
),
|
||||
'placeholder' => 'e.g. ...', // A placeholder (NB: Will only work on compatible browsers)
|
||||
'regex' => '/[A-Z]+/i', // RegEx of acceptable value
|
||||
@@ -89,7 +90,7 @@ class _Text extends TextArea
|
||||
|
||||
/* Set username regex */
|
||||
$args = func_get_args();
|
||||
if ( isset( $args[3]['accountUsername'] ) and $args[3]['accountUsername'] === TRUE )
|
||||
if ( isset( $args[3]['accountUsername'] ) and $args[3]['accountUsername'] !== FALSE )
|
||||
{
|
||||
$args[3]['maxLength'] = \IPS\Settings::i()->max_user_name_length;
|
||||
|
||||
@@ -122,6 +123,16 @@ class _Text extends TextArea
|
||||
*/
|
||||
public function html()
|
||||
{
|
||||
/* 10/19/15 - adding htmlspecialchars around value if autocomplete is enabled so that html tag characters can be used (e.g. for members) */
|
||||
/* This value is decoded by the JS widget before use. See https://community.invisionpower.com/4bugtrack/active-reports/cannot-pm-certain-members-r6181/ */
|
||||
if( $this->options['autocomplete'] and !empty( $this->value ) )
|
||||
{
|
||||
foreach( $this->value as $key => $value )
|
||||
{
|
||||
$this->value[ $key ] = htmlspecialchars( $value, ENT_QUOTES | \IPS\HTMLENTITIES, 'UTF-8', FALSE );
|
||||
}
|
||||
}
|
||||
|
||||
return \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->text( $this->name, $this->formType, $this->value, $this->required, $this->options['maxLength'], $this->options['size'], $this->options['disabled'], $this->options['autocomplete'], $this->options['placeholder'], NULL, $this->options['nullLang'], $this->htmlId );
|
||||
}
|
||||
|
||||
@@ -154,6 +165,11 @@ class _Text extends TextArea
|
||||
}
|
||||
}
|
||||
|
||||
if ( isset( $this->options['autocomplete'] ) )
|
||||
{
|
||||
$return = htmlspecialchars_decode( $return, ENT_QUOTES );
|
||||
}
|
||||
|
||||
/* Remove all invisible characters */
|
||||
$return = preg_replace( '/\p{C}+/u', '', $return );
|
||||
|
||||
|
||||
@@ -88,20 +88,21 @@ class _Upload extends FormAbstract
|
||||
|
||||
if( (float) ini_get('upload_max_filesize') > 0 )
|
||||
{
|
||||
$potentialValues[] = (float) ini_get('upload_max_filesize');
|
||||
$potentialValues[] = \IPS\File::returnBytes( ini_get('upload_max_filesize') );
|
||||
}
|
||||
|
||||
if( (float) ini_get('post_max_size') > 0 )
|
||||
{
|
||||
$potentialValues[] = (float) ini_get('post_max_size');
|
||||
$potentialValues[] = \IPS\File::returnBytes( ini_get('post_max_size') ) - 1048576;
|
||||
}
|
||||
|
||||
if( (float) ini_get('memory_limit') > 0 )
|
||||
{
|
||||
$potentialValues[] = (float) ini_get('memory_limit');
|
||||
$potentialValues[] = \IPS\File::returnBytes( ini_get('memory_limit') );
|
||||
}
|
||||
|
||||
$this->maxChunkSize = min( $potentialValues );
|
||||
/* We want the value in MB */
|
||||
$this->maxChunkSize = min( $potentialValues ) / 1048576;
|
||||
|
||||
/* Call parent constructor */
|
||||
call_user_func_array( 'parent::__construct', func_get_args() );
|
||||
@@ -388,7 +389,7 @@ class _Upload extends FormAbstract
|
||||
{
|
||||
/* Get the files we had already */
|
||||
$return = array();
|
||||
$tempFiles = iterator_to_array( \IPS\Db::i()->select( 'id,contents', 'core_files_temp', array( 'upload_key=?', md5( $this->name . session_id() ) ) )->setKeyField('id')->setValueField('contents') );
|
||||
$tempFiles = iterator_to_array( \IPS\Db::i()->select( 'id,contents,filename', 'core_files_temp', array( 'upload_key=?', md5( $this->name . session_id() ) ) )->setKeyField('id') );
|
||||
if ( $this->options['storageExtension'] )
|
||||
{
|
||||
$existingName = $this->name . '_existing';
|
||||
@@ -402,8 +403,10 @@ class _Upload extends FormAbstract
|
||||
{
|
||||
if ( $tempId and isset( $tempFiles[ $tempId ] ) )
|
||||
{
|
||||
$file = \IPS\File::get( $this->options['storageExtension'], $tempFiles[ $tempId ] );
|
||||
$file = \IPS\File::get( $this->options['storageExtension'], $tempFiles[ $tempId ]['contents'] );
|
||||
$file->tempId = $tempId;
|
||||
/* Reset the original filename to the real file name as $this->originalFilename is generated from the now AWS-safe $this->filename */
|
||||
$file->originalFilename = $tempFiles[ $tempId ]['filename'];
|
||||
$return[ $id ] = $file;
|
||||
}
|
||||
else
|
||||
@@ -429,7 +432,7 @@ class _Upload extends FormAbstract
|
||||
|
||||
try
|
||||
{
|
||||
\IPS\File::get( $this->options['storageExtension'], $tempFiles[ $tempId ] )->delete();
|
||||
\IPS\File::get( $this->options['storageExtension'], $tempFiles[ $tempId ]['contents'] )->delete();
|
||||
}
|
||||
catch ( \Exception $e ) { }
|
||||
}
|
||||
|
||||
@@ -82,7 +82,6 @@ class _Url extends Text
|
||||
$contentType = ( isset( $response->httpHeaders['Content-Type'] ) ) ? $response->httpHeaders['Content-Type'] : ( ( isset( $response->httpHeaders['content-type'] ) ) ? $response->httpHeaders['content-type'] : NULL );
|
||||
if( $contentType )
|
||||
{
|
||||
|
||||
foreach ( is_array( $this->options['allowedMimes'] ) ? $this->options['allowedMimes'] : array( $this->options['allowedMimes'] ) as $mime )
|
||||
{
|
||||
if ( preg_match( '/^' . str_replace( '~~', '.+', preg_quote( str_replace( '*', '~~', $mime ), '/' ) ) . '$/i', $contentType ) )
|
||||
@@ -116,7 +115,14 @@ class _Url extends Text
|
||||
}
|
||||
}
|
||||
|
||||
$this->value = \IPS\File::create( $this->options['file'], $filename, $response );
|
||||
try
|
||||
{
|
||||
$this->value = \IPS\File::create( $this->options['file'], $filename, $response );
|
||||
}
|
||||
catch( \InvalidArgumentException $e )
|
||||
{
|
||||
throw new \DomainException( 'form_url_error' );
|
||||
}
|
||||
}
|
||||
}
|
||||
catch ( \IPS\Http\Request\Exception $e )
|
||||
|
||||
@@ -54,7 +54,7 @@ class _Content extends Table
|
||||
public $sortOptions = array();
|
||||
|
||||
/**
|
||||
* @brief Honor the pinned flag for sorting. Default exclusions include vnc and activity streams
|
||||
* @brief Honor the pinned flag for sorting. Will be set to false in stream-like views
|
||||
*/
|
||||
protected $honorPinned = TRUE;
|
||||
|
||||
@@ -247,8 +247,8 @@ class _Content extends Table
|
||||
}
|
||||
|
||||
/* Get results */
|
||||
$it = $class::getItemsWithPermission( $where, $sortBy, array( ( $this->limit * ( $this->page - 1 ) ), $this->limit ), $this->permCheck, $this->includeHiddenContent, \IPS\Db::SELECT_SQL_CALC_FOUND_ROWS, NULL, $this->joinContainer, $this->joinComments, $this->joinReviews, FALSE, $this->joins );
|
||||
$this->count = $it->count( TRUE );
|
||||
$this->count = $class::getItemsWithPermission( $where, $sortBy, NULL, $this->permCheck, $this->includeHiddenContent, 0, NULL, $this->joinContainer, $this->joinComments, $this->joinReviews, TRUE, $this->joins );
|
||||
$it = $class::getItemsWithPermission( $where, $sortBy, array( ( $this->limit * ( $this->page - 1 ) ), $this->limit ), $this->permCheck, $this->includeHiddenContent, 0, NULL, $this->joinContainer, $this->joinComments, $this->joinReviews, FALSE, $this->joins );
|
||||
$this->pages = ceil( $this->count / $this->limit );
|
||||
$rows = iterator_to_array( $it );
|
||||
|
||||
|
||||
@@ -27,7 +27,17 @@ class _Custom extends Table
|
||||
* @brief Data
|
||||
*/
|
||||
protected $dataSource;
|
||||
|
||||
|
||||
/**
|
||||
* @brief Parse Source
|
||||
*/
|
||||
public $parseSource = TRUE;
|
||||
|
||||
/**
|
||||
* Number of results
|
||||
*/
|
||||
public $count = 0;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
@@ -50,6 +60,10 @@ class _Custom extends Table
|
||||
*/
|
||||
public function getRows( $advancedSearchValues )
|
||||
{
|
||||
/* Sort out paging */
|
||||
$this->count = count( $this->dataSource );
|
||||
$this->pages = ceil( $this->count / $this->limit );
|
||||
|
||||
/* Get them */
|
||||
$rows = array();
|
||||
foreach ( $this->dataSource as $i => $data )
|
||||
@@ -64,29 +78,13 @@ class _Custom extends Table
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Parse if necessary */
|
||||
if( isset( $this->parsers[ $k ] ) )
|
||||
{
|
||||
$v = call_user_func( $this->parsers[ $k ], $v, $data );
|
||||
}
|
||||
else
|
||||
{
|
||||
$v = htmlentities( $v, ENT_QUOTES | \IPS\HTMLENTITIES, 'UTF-8', FALSE );
|
||||
}
|
||||
|
||||
/* Add to array */
|
||||
$row[ $k ] = $v;
|
||||
}
|
||||
|
||||
/* Add in some buttons if necessary */
|
||||
if( $this->rowButtons !== NULL )
|
||||
{
|
||||
$row['_buttons'] = call_user_func( $this->rowButtons, $row, $i );
|
||||
}
|
||||
|
||||
/* Add it to the array */
|
||||
$rows[] = $row;
|
||||
$rows[ $i ] = $row;
|
||||
}
|
||||
|
||||
/* Quicksearch */
|
||||
@@ -95,12 +93,18 @@ class _Custom extends Table
|
||||
$quickSearchColumn = $this->quickSearch;
|
||||
$rows = array_filter( $rows, is_callable( $this->quickSearch ) ? $this->quickSearch : function( $row ) use ( $quickSearchColumn )
|
||||
{
|
||||
return mb_strpos( $row[ $quickSearchColumn ], \IPS\Request::i()->quicksearch ) !== FALSE;
|
||||
return mb_strpos( $row[ $quickSearchColumn ], trim( \IPS\Request::i()->quicksearch ) ) !== FALSE;
|
||||
} );
|
||||
}
|
||||
|
||||
/* Limit */
|
||||
if ( $this->limit and $this->count > $this->limit )
|
||||
{
|
||||
$rows = array_slice( $rows, ( $this->limit * ( $this->page - 1 ) ), $this->limit );
|
||||
}
|
||||
|
||||
/* Do we need to sort? */
|
||||
if( !empty( $rows ) and $this->sortBy and isset( $rows[0][ $this->sortBy ] ) )
|
||||
if( !empty( $rows ) and $this->sortBy and isset( $rows[ key( $rows ) ][ $this->sortBy ] ) )
|
||||
{
|
||||
$sortBy = $this->sortBy;
|
||||
$sortDirection = $this->sortDirection;
|
||||
@@ -116,6 +120,31 @@ class _Custom extends Table
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* Apply parsers */
|
||||
foreach( $rows as $i => $row )
|
||||
{
|
||||
foreach( $row as $k => $v )
|
||||
{
|
||||
/* Parse if necessary */
|
||||
if( isset( $this->parsers[ $k ] ) )
|
||||
{
|
||||
$v = call_user_func( $this->parsers[ $k ], $v, $this->dataSource[ $i ] );
|
||||
}
|
||||
else
|
||||
{
|
||||
$v = htmlspecialchars( $v, ENT_QUOTES | \IPS\HTMLENTITIES, 'UTF-8', FALSE );
|
||||
}
|
||||
|
||||
$rows[ $i ][ $k ] = $v;
|
||||
}
|
||||
|
||||
/* Add in some buttons if necessary */
|
||||
if( $this->rowButtons !== NULL )
|
||||
{
|
||||
$rows[ $i ]['_buttons'] = call_user_func( $this->rowButtons, $row, $i );
|
||||
}
|
||||
}
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
@@ -99,7 +99,7 @@ class _Db extends Table
|
||||
{
|
||||
if ( is_callable( $this->quickSearch ) )
|
||||
{
|
||||
$where[] = call_user_func( $this->quickSearch, \IPS\Request::i()->quicksearch );
|
||||
$where[] = call_user_func( $this->quickSearch, trim( \IPS\Request::i()->quicksearch ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -112,7 +112,7 @@ class _Db extends Table
|
||||
$_where[] = "LOWER(`{$c}`) LIKE CONCAT( '%', ?, '%' )";
|
||||
}
|
||||
|
||||
$where[] = array_merge( array( '(' . implode( ' OR ', $_where ) . ')' ), array_fill( 0, count( $_where ), mb_strtolower( \IPS\Request::i()->quicksearch ) ) );
|
||||
$where[] = array_merge( array( '(' . implode( ' OR ', $_where ) . ')' ), array_fill( 0, count( $_where ), mb_strtolower( trim( \IPS\Request::i()->quicksearch ) ) ) );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,7 +164,14 @@ class _Db extends Table
|
||||
break;
|
||||
|
||||
case SEARCH_SELECT:
|
||||
$where[] = array( "{$k}=?", $v );
|
||||
if ( isset( $options['multiple'] ) AND $options['multiple'] === TRUE )
|
||||
{
|
||||
$where[] = array( \IPS\Db::i()->in( $k, $v ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
$where[] = array( "{$k}=?", $v );
|
||||
}
|
||||
break;
|
||||
|
||||
case SEARCH_MEMBER:
|
||||
@@ -262,7 +269,7 @@ class _Db extends Table
|
||||
{
|
||||
foreach( $this->joins as $join )
|
||||
{
|
||||
$query->join( $join['from'], ( isset( $join['where'] ) ? $join['where'] : null ), 'LEFT' );
|
||||
$query->join( $join['from'], ( isset( $join['where'] ) ? $join['where'] : null ), ( isset( $join['type'] ) ) ? $join['type'] : 'LEFT' );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -337,7 +344,7 @@ class _Db extends Table
|
||||
{
|
||||
$select->setKeyField( $this->keyField );
|
||||
}
|
||||
|
||||
|
||||
foreach ( $select as $rowId => $row )
|
||||
{
|
||||
/* Add in any 'custom' fields */
|
||||
@@ -349,7 +356,7 @@ class _Db extends Table
|
||||
{
|
||||
$row[ $k ] = isset( $_row[ $k ] ) ? $_row[ $k ] : NULL;
|
||||
}
|
||||
|
||||
|
||||
if( !empty( $advancedSearchValues ) )
|
||||
{
|
||||
foreach ( $advancedSearchValues as $k => $v )
|
||||
@@ -369,7 +376,7 @@ class _Db extends Table
|
||||
}
|
||||
else
|
||||
{
|
||||
$v = htmlentities( $v, ENT_QUOTES | \IPS\HTMLENTITIES, 'UTF-8', FALSE );
|
||||
$v = htmlspecialchars( $v, ENT_QUOTES | \IPS\HTMLENTITIES, 'UTF-8', FALSE );
|
||||
}
|
||||
|
||||
/* Are we including this one? */
|
||||
|
||||
@@ -56,7 +56,7 @@ class _Tree
|
||||
/**
|
||||
* @brief Searchable?
|
||||
*/
|
||||
protected $searchable = FALSE;
|
||||
public $searchable = FALSE;
|
||||
|
||||
/**
|
||||
* @brief If true, will prevent any item from being moved out of its current parent, only allowing them to be reordered within their current parent
|
||||
@@ -67,6 +67,16 @@ class _Tree
|
||||
* @brief If true, root cannot be turned into sub-items, and other items cannot be turned into roots
|
||||
*/
|
||||
protected $protectRoots = FALSE;
|
||||
|
||||
/**
|
||||
* @brief Number of roots to show per page (NULL for unlimited). In most cases this doesn't make sense, since it makes re-ordering impossible. But for trees which are not orderable and which may contain a lot of roots, you can set this value
|
||||
*/
|
||||
public $rootsPerPage = NULL;
|
||||
|
||||
/**
|
||||
* @brief If using $rootsPerPage, a callback function that returns the total number of roots
|
||||
*/
|
||||
public $getTotalRoots = NULL;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
@@ -80,9 +90,11 @@ class _Tree
|
||||
* @param callback $searchable Show the search bar?
|
||||
* @param bool $lockParents If true, will prevent any item from being moved out of its current parent, only allowing them to be reordered within their current parent
|
||||
* @param bool $protectRoots If true, root cannot be turned into sub-items, and other items cannot be turned into roots
|
||||
* @param int|null $rootsPerPage Number of roots to show per page (NULL for unlimited). In most cases this doesn't make sense, since it makes re-ordering impossible. But for trees which are not orderable and which may contain a lot of roots, you can set this value
|
||||
* @param callback $getTotalRoots If using $rootsPerPage, a callback function that returns the total number of roots
|
||||
* @return void
|
||||
*/
|
||||
public function __construct( $url, $title, $getRoots, $getRow, $getRowParentId, $getChildren, $getRootButtons=NULL, $searchable=FALSE, $lockParents=FALSE, $protectRoots=FALSE )
|
||||
public function __construct( $url, $title, $getRoots, $getRow, $getRowParentId, $getChildren, $getRootButtons=NULL, $searchable=FALSE, $lockParents=FALSE, $protectRoots=FALSE, $rootsPerPage = NULL, $getTotalRoots = NULL )
|
||||
{
|
||||
$this->url = $url;
|
||||
$this->title = $title;
|
||||
@@ -94,6 +106,8 @@ class _Tree
|
||||
$this->searchable = $searchable;
|
||||
$this->lockParents = $lockParents;
|
||||
$this->protectRoots = $protectRoots;
|
||||
$this->rootsPerPage = $rootsPerPage;
|
||||
$this->getTotalRoots = $getTotalRoots;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -106,11 +120,12 @@ class _Tree
|
||||
try
|
||||
{
|
||||
/* Get rows */
|
||||
$page = isset( \IPS\Request::i()->page ) ? intval( \IPS\Request::i()->page ) : 1;
|
||||
$root = NULL;
|
||||
$rootParent = NULL;
|
||||
if( !\IPS\Request::i()->root )
|
||||
{
|
||||
$rows = call_user_func( $this->getRoots );
|
||||
$rows = call_user_func( $this->getRoots, $this->rootsPerPage ? array( ( $page - 1 ) * $this->rootsPerPage, $this->rootsPerPage ) : NULL );
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -124,9 +139,20 @@ class _Tree
|
||||
$root = call_user_func( $this->getRow, \IPS\Request::i()->root, TRUE );
|
||||
$rootParent = call_user_func( $this->getRowParentId, \IPS\Request::i()->root );
|
||||
}
|
||||
|
||||
/* Pagination? */
|
||||
$pagination = '';
|
||||
if ( $this->rootsPerPage )
|
||||
{
|
||||
$totalNumber = call_user_func( $this->getTotalRoots );
|
||||
if ( $totalNumber )
|
||||
{
|
||||
$pagination = \IPS\Theme::i()->getTemplate( 'global', 'core', 'global' )->pagination( $this->url, ceil( $totalNumber / $this->rootsPerPage ), $page, $this->rootsPerPage );
|
||||
}
|
||||
}
|
||||
|
||||
/* Display */
|
||||
return \IPS\Theme::i()->getTemplate( 'trees', 'core' )->template( $this->url, $this->title, $root, $rootParent, $rows, call_user_func( $this->getRootButtons ), $this->lockParents, $this->protectRoots, $this->searchable );
|
||||
return \IPS\Theme::i()->getTemplate( 'trees', 'core' )->template( $this->url, $this->title, $root, $rootParent, $rows, call_user_func( $this->getRootButtons ), $this->lockParents, $this->protectRoots, $this->searchable, $pagination );
|
||||
}
|
||||
catch ( \Exception $e )
|
||||
{
|
||||
|
||||
@@ -29,7 +29,7 @@ if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
$form = new \IPS\Helpers\Form;
|
||||
$form->add( ... );
|
||||
|
||||
if ( $values = $form->values )
|
||||
if ( $values = $form->values() )
|
||||
{
|
||||
return $values;
|
||||
}
|
||||
@@ -41,7 +41,7 @@ if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
// $data contains the form values from the previous step
|
||||
}
|
||||
),
|
||||
\IPS\Http\Url::internal( 'app=example&module=example&controller=example&do=wizard' );
|
||||
\IPS\Http\Url::internal( 'app=example&module=example&controller=example&do=wizard' )
|
||||
);
|
||||
* @endcode
|
||||
*/
|
||||
@@ -75,11 +75,12 @@ class _Wizard
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param array $steps An array of callback functions. Each function should return either a string to output or (if the step is done) an array (which can be blank) of arbitrary data to retain between steps (which will be passed to each callback function). The keys should be langauge keys for the title of the step.
|
||||
* @param \IPS\Http\Url $baseUrl The base URL (used when moving between steps)
|
||||
* @param array $steps An array of callback functions. Each function should return either a string to output or (if the step is done) an array (which can be blank) of arbitrary data to retain between steps (which will be passed to each callback function). The keys should be langauge keys for the title of the step.
|
||||
* @param \IPS\Http\Url $baseUrl The base URL (used when moving between steps)
|
||||
* @param array $initialData The initial data
|
||||
* @return void
|
||||
*/
|
||||
public function __construct( $steps, $baseUrl, $showSteps=TRUE )
|
||||
public function __construct( $steps, $baseUrl, $showSteps=TRUE, $initialData=NULL )
|
||||
{
|
||||
$this->steps = $steps;
|
||||
$this->baseUrl = $baseUrl;
|
||||
@@ -90,8 +91,16 @@ class _Wizard
|
||||
{
|
||||
unset( $_SESSION[ 'wizard-' . md5( $this->baseUrl ) . '-step' ] );
|
||||
unset( $_SESSION[ 'wizard-' . md5( $this->baseUrl ) . '-data' ] );
|
||||
|
||||
\IPS\Output::i()->redirect( $baseUrl );
|
||||
|
||||
if ( !is_null( $initialData ) )
|
||||
{
|
||||
$_SESSION[ 'wizard-' . md5( $this->baseUrl ) . '-data' ] = $initialData;
|
||||
}
|
||||
|
||||
if ( !\IPS\Request::i()->isAjax() )
|
||||
{
|
||||
\IPS\Output::i()->redirect( $baseUrl );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ class _Curl
|
||||
* @param bool|int $followRedirects Automatically follow redirects? If a number is provided, will follow up to that number of redirects
|
||||
* @return void
|
||||
*/
|
||||
public function __construct( $url, $timeout, $httpVersion, $followRedirects )
|
||||
public function __construct( $url, $timeout=5, $httpVersion=NULL, $followRedirects=TRUE )
|
||||
{
|
||||
/* Init */
|
||||
$this->url = $url;
|
||||
@@ -272,6 +272,22 @@ class _Curl
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle SSL checks
|
||||
*
|
||||
* @param boolean $value True will enable SSL checks, false will disable them
|
||||
* @return \IPS\Http\Request\Socket
|
||||
*/
|
||||
public function sslCheck( $value=TRUE )
|
||||
{
|
||||
curl_setopt_array( $this->curl, array(
|
||||
CURLOPT_SSL_VERIFYHOST => ( $value ) ? 2 : FALSE,
|
||||
CURLOPT_SSL_VERIFYPEER => (boolean) $value
|
||||
) );
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Magic Method: __call
|
||||
* Used for other HTTP methods (like PUT and DELETE)
|
||||
@@ -331,7 +347,7 @@ class _Curl
|
||||
}
|
||||
|
||||
/* If this is FTP we need to fudge the headers a little */
|
||||
if( $this->url->data['scheme'] == 'ftp' )
|
||||
if( isset( $this->url->data['scheme'] ) and $this->url->data['scheme'] == 'ftp' )
|
||||
{
|
||||
$output = "HTTP/1.1 200 OK\nFTP: True\r\n\r\n" . $output;
|
||||
}
|
||||
|
||||
@@ -91,6 +91,18 @@ class _Sockets
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle SSL checks
|
||||
*
|
||||
* @param boolean $value True will enable SSL checks, false will disable them
|
||||
* @return \IPS\Http\Request\Socket
|
||||
*/
|
||||
public function sslCheck( $value=TRUE )
|
||||
{
|
||||
/* This is here for compatibility with curl */
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Magic Method: __call
|
||||
*
|
||||
|
||||
+58
-18
@@ -247,7 +247,10 @@ class _Url
|
||||
{
|
||||
/* Always lowercase */
|
||||
$value = mb_strtolower( $value );
|
||||
|
||||
|
||||
/* Get rid of newlines/carriage returns as they're not cool in friendly URL titles */
|
||||
$value = str_replace( array( "\r\n", "\r", "\n" ), ' ', $value );
|
||||
|
||||
/* Just for readability */
|
||||
$value = str_replace( ' ', '-', $value );
|
||||
|
||||
@@ -358,7 +361,7 @@ class _Url
|
||||
$keys = array( $keys => $keys );
|
||||
}
|
||||
|
||||
$queryString = array_diff_key( $this->queryString, $keys );
|
||||
$queryString = array_diff_key( $this->queryString, array_combine( array_values( $keys ), array_values( $keys ) ) );
|
||||
}
|
||||
|
||||
return $this->reconstruct( $this->data, $queryString );
|
||||
@@ -494,7 +497,7 @@ class _Url
|
||||
/* Is it internal? We have to ignore protocol because areas like force login over https and secure checkout in Nexus will use https urls for an otherwise http site and we end up with infinite redirects */
|
||||
if ( $internal === NULL )
|
||||
{
|
||||
$this->isInternal = ( mb_substr( str_replace( 'https://', 'http://', $this->url ), 0, mb_strlen( str_replace( 'https://', 'http://', \IPS\Settings::i()->base_url ) ) ) === str_replace( 'https://', 'http://', \IPS\Settings::i()->base_url ) );
|
||||
$this->isInternal = ( mb_substr( str_replace( array( 'https://', 'http://' ), '//', $this->url ), 0, mb_strlen( str_replace( array( 'https://', 'http://' ), '//', \IPS\Settings::i()->base_url ) ) ) === str_replace( array( 'https://', 'http://' ), '//', \IPS\Settings::i()->base_url ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -642,17 +645,16 @@ class _Url
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the query/path
|
||||
* Get FURL query
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function queryPath()
|
||||
public function getFurlQuery()
|
||||
{
|
||||
/* Determine our gateway */
|
||||
$gatewayFile = explode( '/', str_replace( '\\', '/', $_SERVER['SCRIPT_FILENAME'] ) );
|
||||
$gatewayFile = array_pop( $gatewayFile );
|
||||
|
||||
/* Work out what the FURL "query" is */
|
||||
|
||||
$baseUrl = $this->utf8ParseUrl( \IPS\Settings::i()->base_url );
|
||||
|
||||
if ( \IPS\Settings::i()->htaccess_mod_rewrite )
|
||||
@@ -681,9 +683,7 @@ class _Url
|
||||
}
|
||||
}
|
||||
|
||||
$query = preg_replace( '#^(' . preg_quote( rtrim( $baseUrl['path'], '/' ), '#' ) . ')/(' . $gatewayFile . ')?(?:(?:\?/|\?))?(.+?)?$#', '$3', $query );
|
||||
|
||||
return $query;
|
||||
return preg_replace( '#^(' . preg_quote( rtrim( $baseUrl['path'], '/' ), '#' ) . ')/(' . $gatewayFile . ')?(?:(?:\?/|\?))?(.+?)?$#', '$3', $query );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -704,8 +704,8 @@ class _Url
|
||||
/* Get furl definition */
|
||||
$furlDefinition = static::furlDefinition();
|
||||
|
||||
/* What is the query */
|
||||
$query = $this->queryPath();
|
||||
/* Work out what the FURL "query" is */
|
||||
$query = $this->getFurlQuery();
|
||||
|
||||
/* Examine our URLs */
|
||||
$this->examineFurl( $furlDefinition, $query, $set, $usedTemplate );
|
||||
@@ -777,7 +777,23 @@ class _Url
|
||||
{
|
||||
try
|
||||
{
|
||||
$correctUrl = $usedTemplate['verify']::loadFromUrl( \IPS\Http\Url::internal( $usedTemplate['_rebuiltUrl'], 'front' ) )->url();
|
||||
$contentObject = $usedTemplate['verify']::loadFromUrl( \IPS\Http\Url::internal( $usedTemplate['_rebuiltUrl'], 'front' ) );
|
||||
|
||||
if( method_exists( $contentObject, 'canView' ) )
|
||||
{
|
||||
if( $contentObject->canView() )
|
||||
{
|
||||
$correctUrl = $contentObject->url();
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new \OutOfRangeException;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$correctUrl = $contentObject->url();
|
||||
}
|
||||
}
|
||||
catch ( \OutOfRangeException $e )
|
||||
{
|
||||
@@ -848,8 +864,24 @@ class _Url
|
||||
{
|
||||
parse_str( $data['real'], $paramsInCorrectUrl );
|
||||
$paramsInCorrectUrl = array_merge( array_keys( $paramsInCorrectUrl ), $params );
|
||||
|
||||
$correctUrl = $data['verify']::loadFromUrl( \IPS\Http\Url::internal( $requestedUrl->data['query'], 'front' ) )->url();
|
||||
|
||||
$contentObject = $data['verify']::loadFromUrl( \IPS\Http\Url::internal( $requestedUrl->data['query'], 'front' ) );
|
||||
|
||||
if( method_exists( $contentObject, 'canView' ) )
|
||||
{
|
||||
if( $contentObject->canView() )
|
||||
{
|
||||
$correctUrl = $contentObject->url();
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new \OutOfRangeException;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$correctUrl = $contentObject->url();
|
||||
}
|
||||
|
||||
$paramsToSet = array();
|
||||
foreach ( $requestedUrl->queryString as $k => $v )
|
||||
@@ -942,7 +974,7 @@ class _Url
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function examineFurl( &$furlDefinition, &$query, &$set, &$usedTemplate, $checkAlias=FALSE, $checkWithoutTopLevel=FALSE )
|
||||
public function examineFurl( &$furlDefinition, &$query, &$set, &$usedTemplate, $checkAlias=FALSE, $checkWithoutTopLevel=FALSE )
|
||||
{
|
||||
foreach ( $furlDefinition as $_key => $data )
|
||||
{
|
||||
@@ -1110,7 +1142,7 @@ class _Url
|
||||
}
|
||||
|
||||
/* Set a default user-agent since many services block requests without one, i.e. spotify */
|
||||
$requestObj->setHeaders( array( 'User-Agent' => 'IPS Community Suite 4.0' ) );
|
||||
$requestObj->setHeaders( array( 'User-Agent' => 'IPS Community Suite 4.1' ) );
|
||||
|
||||
return $requestObj;
|
||||
}
|
||||
@@ -1125,7 +1157,15 @@ class _Url
|
||||
public function import( $storageExtension )
|
||||
{
|
||||
$parts = $this->utf8ParseUrl( $this->url );
|
||||
return \IPS\File::create( $storageExtension, basename( $parts['path'] ), $this->request()->get() );
|
||||
$response = $this->request()->get();
|
||||
|
||||
/* We should not attempt to "import" 404, 403, 500, etc. responses */
|
||||
if( (int) $response->httpResponseCode !== 200 )
|
||||
{
|
||||
throw new \RuntimeException( "COULD_NOT_IMPORT" );
|
||||
}
|
||||
|
||||
return \IPS\File::create( $storageExtension, basename( $parts['path'] ), $response );
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+2
-2
@@ -79,13 +79,13 @@ class _GD extends \IPS\Image
|
||||
case 'gif':
|
||||
if ( $this->isAnimatedGif )
|
||||
{
|
||||
return $this->contents;
|
||||
return (string) $this->contents;
|
||||
}
|
||||
imagegif( $this->image );
|
||||
break;
|
||||
|
||||
case 'jpeg':
|
||||
$quality = \IPS\Settings::i()->image_jpg_quality ?: NULL;
|
||||
$quality = \IPS\Settings::i()->image_jpg_quality ?: 75;
|
||||
|
||||
imagejpeg( $this->image, NULL, $quality );
|
||||
break;
|
||||
|
||||
@@ -37,6 +37,11 @@ abstract class _Image
|
||||
{
|
||||
return function_exists( 'exif_read_data' );
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Has the image been automatically rotated?
|
||||
*/
|
||||
public $hasBeenRotated = FALSE;
|
||||
|
||||
/**
|
||||
* Create Object
|
||||
@@ -97,14 +102,17 @@ abstract class _Image
|
||||
{
|
||||
case 3:
|
||||
$obj->rotate( 180 );
|
||||
$obj->hasBeenRotated = TRUE;
|
||||
break;
|
||||
|
||||
case 6:
|
||||
$obj->rotate( -90 );
|
||||
$obj->hasBeenRotated = TRUE;
|
||||
break;
|
||||
|
||||
case 8:
|
||||
$obj->rotate( 90 );
|
||||
$obj->hasBeenRotated = TRUE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -114,14 +122,17 @@ abstract class _Image
|
||||
{
|
||||
case 3:
|
||||
$obj->rotate( 180 );
|
||||
$obj->hasBeenRotated = TRUE;
|
||||
break;
|
||||
|
||||
case 6:
|
||||
$obj->rotate( 90 );
|
||||
$obj->hasBeenRotated = TRUE;
|
||||
break;
|
||||
|
||||
case 8:
|
||||
$obj->rotate( -90 );
|
||||
$obj->hasBeenRotated = TRUE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
+61
-9
@@ -323,15 +323,20 @@ class _Lang extends \IPS\Node\Model
|
||||
* @param string $app Application Key
|
||||
* @param string $key Word key
|
||||
* @param string $newKey New Word Key
|
||||
* @param string $newApp New Application Key, if different
|
||||
* @return void
|
||||
*/
|
||||
public static function copyCustom( $app, $key, $newKey )
|
||||
public static function copyCustom( $app, $key, $newKey, $newApp=NULL )
|
||||
{
|
||||
$values = iterator_to_array( \IPS\Db::i()->select( 'lang_id, word_custom', 'core_sys_lang_words', array( 'word_app=? AND word_key=?', $app, $key ) )->setKeyField('lang_id')->setValueField('word_custom') );
|
||||
$values = array();
|
||||
foreach ( \IPS\Db::i()->select( 'lang_id, word_default, word_custom', 'core_sys_lang_words', array( 'word_app=? AND word_key=?', $app, $key ) )->setKeyField('lang_id') as $langId => $data )
|
||||
{
|
||||
$values[ $langId ] = $data['word_custom'] ?: $data['word_default'];
|
||||
}
|
||||
|
||||
foreach( $values as $row )
|
||||
{
|
||||
static::saveCustom( $app, $newKey, $values );
|
||||
static::saveCustom( $newApp ?: $app, $newKey, $values );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -506,6 +511,9 @@ class _Lang extends \IPS\Node\Model
|
||||
*/
|
||||
public function preferredDateFormat()
|
||||
{
|
||||
/* Make sure the locale has been set, important for things like the js date_format variable */
|
||||
$this->setLocale();
|
||||
|
||||
$date = new \IPS\DateTime('1992-03-04');
|
||||
return str_replace( array( '1992', '92', '03', '3', $date->strFormat('%B'), '04', ' 4', '4' ), array( 'yy', 'yy', 'mm', 'mm', 'mm', 'dd', 'dd', 'dd' ), $date->localeDate() );
|
||||
}
|
||||
@@ -671,7 +679,7 @@ class _Lang extends \IPS\Node\Model
|
||||
/* Get it */
|
||||
if( isset( $this->outputStack[ $key ] ) )
|
||||
{
|
||||
return htmlentities( $key, ENT_QUOTES | \IPS\HTMLENTITIES, 'UTF-8', FALSE );
|
||||
return htmlspecialchars( $key, ENT_QUOTES | \IPS\HTMLENTITIES, 'UTF-8', FALSE );
|
||||
}
|
||||
|
||||
$id = md5( 'ipslang_' . static::$outputSalt . $key . json_encode( $options ) );
|
||||
@@ -1432,6 +1440,35 @@ class _Lang extends \IPS\Node\Model
|
||||
$this->replaceWords( $output );
|
||||
}
|
||||
|
||||
/**
|
||||
* Emails require some additional work on top of replacing the language stack
|
||||
*
|
||||
* @param string $output Unparsed
|
||||
* @return void
|
||||
*/
|
||||
public function parseEmail( &$output )
|
||||
{
|
||||
$this->parseOutputForDisplay( $output );
|
||||
|
||||
$dir = ( $this->isrtl ) ? 'rtl' : 'ltr';
|
||||
|
||||
if ( mb_stristr( $output, '{dir}' ) )
|
||||
{
|
||||
$output = preg_replace( '#(<td\s+?)dir=([\'"]){dir}([\'"])#i', '\1dir=\2' . $dir . '\3', $output );
|
||||
|
||||
preg_match_all( '#<(body|html)([^>]+?)>#i', $output, $matches, PREG_SET_ORDER );
|
||||
|
||||
foreach( $matches as $match )
|
||||
{
|
||||
if ( mb_stristr( $match[2], '{dir}' ) )
|
||||
{
|
||||
$parsed = str_replace( '{dir}', $dir, $match[0] );
|
||||
$output = str_replace( $match[0], $parsed, $output );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip VLE tags, useful for AJAX responses where you can't edit the string anyways
|
||||
*
|
||||
@@ -1513,9 +1550,23 @@ class _Lang extends \IPS\Node\Model
|
||||
*/
|
||||
public static function replace( $find, $replace, $haystack )
|
||||
{
|
||||
if ( !is_array( $haystack ) )
|
||||
/* Reduce the number of str_replace with arrays */
|
||||
static $replaceTable = array();
|
||||
|
||||
if ( ! is_array( $haystack ) )
|
||||
{
|
||||
return str_replace( $find, $replace, $haystack );
|
||||
$hash = md5( json_encode($find) . json_encode($replace) . $haystack );
|
||||
|
||||
if ( isset( $replaceTable[ $hash ] ) )
|
||||
{
|
||||
return $replaceTable[ $hash ];
|
||||
}
|
||||
else
|
||||
{
|
||||
$output = str_replace( $find, $replace, $haystack );
|
||||
$replaceTable[ $hash ] = $output;
|
||||
return $output;
|
||||
}
|
||||
}
|
||||
|
||||
$replacement = array();
|
||||
@@ -1573,7 +1624,7 @@ class _Lang extends \IPS\Node\Model
|
||||
$sprintf = array_map(
|
||||
function ( $val ) use ( $replacement )
|
||||
{
|
||||
return htmlentities( trim( $val ), ENT_QUOTES | \IPS\HTMLENTITIES, 'UTF-8', false );
|
||||
return htmlspecialchars( trim( $val ), ENT_QUOTES | \IPS\HTMLENTITIES, 'UTF-8', false );
|
||||
},
|
||||
( is_array(
|
||||
$values[ 'options' ][ 'sprintf' ]
|
||||
@@ -1647,7 +1698,7 @@ class _Lang extends \IPS\Node\Model
|
||||
|
||||
if ( isset( $values[ 'options' ][ 'escape' ] ) )
|
||||
{
|
||||
$replacement = htmlentities( $replacement, ENT_QUOTES | \IPS\HTMLENTITIES, 'UTF-8', false );
|
||||
$replacement = htmlspecialchars( $replacement, ENT_QUOTES | \IPS\HTMLENTITIES, 'UTF-8', false );
|
||||
}
|
||||
|
||||
/* Parse URLs */
|
||||
@@ -1711,7 +1762,8 @@ class _Lang extends \IPS\Node\Model
|
||||
$replacements[ $key ] = $replacement;
|
||||
}
|
||||
|
||||
/* We do this 3 times in case a replacement contains another replacement, etc. */
|
||||
/* We do this 4 times in case a replacement contains another replacement, etc. */
|
||||
$output = static::replace( array_keys( $replacements ), array_values( $replacements ), $output );
|
||||
$output = static::replace( array_keys( $replacements ), array_values( $replacements ), $output );
|
||||
$output = static::replace( array_keys( $replacements ), array_values( $replacements ), $output );
|
||||
$output = static::replace( array_keys( $replacements ), array_values( $replacements ), $output );
|
||||
|
||||
@@ -418,6 +418,14 @@ class _Convert extends LoginAbstract
|
||||
*/
|
||||
protected function joomla( $member, $password )
|
||||
{
|
||||
/* Joomla 3 */
|
||||
if( preg_match( '/^\$2[ay]\$(0[4-9]|[1-2][0-9]|3[0-1])\$[a-zA-Z0-9.\/]{53}/', $member->conv_password ) )
|
||||
{
|
||||
$ph = new PasswordHash( 8, TRUE );
|
||||
return $ph->CheckPassword( $password, $member->conv_password ) ? TRUE : FALSE;
|
||||
}
|
||||
|
||||
/* Joomla 2 */
|
||||
if ( \IPS\Login::compareHashes( $member->conv_password, md5( $password . $member->misc ) ) )
|
||||
{
|
||||
return TRUE;
|
||||
@@ -1375,7 +1383,6 @@ class webWizAuth
|
||||
if ( \strlen( $strSecret ) == 0 || \strlen( $strSecret ) >= pow( 2, 61 ) )
|
||||
{
|
||||
return "0000000000000000000000000000000000000000";
|
||||
break;
|
||||
}
|
||||
|
||||
//Initial Hex words are used for encoding Digest.
|
||||
|
||||
@@ -106,6 +106,12 @@ class _Login
|
||||
static::$allHandlers = static::$handlers;
|
||||
}
|
||||
}
|
||||
|
||||
/* If we have no handlers, use the standard internal */
|
||||
if ( empty( static::$handlers ) )
|
||||
{
|
||||
static::$handlers['Internal'] = \IPS\Login\LoginAbstract::load('internal');
|
||||
}
|
||||
|
||||
return static::$handlers;
|
||||
}
|
||||
@@ -173,7 +179,7 @@ class _Login
|
||||
}
|
||||
}
|
||||
|
||||
$ref = new \IPS\Http\Url( $_SERVER['HTTP_REFERER'] );
|
||||
$ref = new \IPS\Http\Url( ( isset( $_SERVER['HTTP_REFERER'] ) ? $_SERVER['HTTP_REFERER'] : '' ) );
|
||||
}
|
||||
|
||||
return isset( $ref ) ? $ref->stripQueryString( 'csrfKey' ) : \IPS\Http\Url::internal( '' );
|
||||
@@ -328,9 +334,9 @@ class _Login
|
||||
/* Set any flags */
|
||||
foreach ( array_keys( $this->flags ) as $k )
|
||||
{
|
||||
if ( isset( $values[ $k ] ) and $values[ $k ] )
|
||||
if ( isset( $values[ $k ] ) )
|
||||
{
|
||||
$this->flags[ $k ] = TRUE;
|
||||
$this->flags[ $k ] = $values[ $k ];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loaded 100 of 1131 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user