Version 4.2.0
This commit is contained in:
1 parent
0dc2aee5ed
commit
96997ddd8e
2074 files changed
+125454
-55780
No files matched your search
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
+3
-1
@@ -7,7 +7,7 @@
|
||||
* primary concern and you are using an opcode cache. PLEASE DO NOT EDIT THIS
|
||||
* FILE, changes will be overwritten the next time the script is run.
|
||||
*
|
||||
* @version 4.8.0
|
||||
* @version 4.9.3
|
||||
*
|
||||
* @warning
|
||||
* You must *not* include any other HTML Purifier files before this file,
|
||||
@@ -137,6 +137,7 @@ require 'HTMLPurifier/AttrTransform/SafeObject.php';
|
||||
require 'HTMLPurifier/AttrTransform/SafeParam.php';
|
||||
require 'HTMLPurifier/AttrTransform/ScriptRequired.php';
|
||||
require 'HTMLPurifier/AttrTransform/TargetBlank.php';
|
||||
require 'HTMLPurifier/AttrTransform/TargetNoopener.php';
|
||||
require 'HTMLPurifier/AttrTransform/TargetNoreferrer.php';
|
||||
require 'HTMLPurifier/AttrTransform/Textarea.php';
|
||||
require 'HTMLPurifier/ChildDef/Chameleon.php';
|
||||
@@ -176,6 +177,7 @@ require 'HTMLPurifier/HTMLModule/StyleAttribute.php';
|
||||
require 'HTMLPurifier/HTMLModule/Tables.php';
|
||||
require 'HTMLPurifier/HTMLModule/Target.php';
|
||||
require 'HTMLPurifier/HTMLModule/TargetBlank.php';
|
||||
require 'HTMLPurifier/HTMLModule/TargetNoopener.php';
|
||||
require 'HTMLPurifier/HTMLModule/TargetNoreferrer.php';
|
||||
require 'HTMLPurifier/HTMLModule/Text.php';
|
||||
require 'HTMLPurifier/HTMLModule/Tidy.php';
|
||||
|
||||
+3
-3
@@ -19,7 +19,7 @@
|
||||
*/
|
||||
|
||||
/*
|
||||
HTML Purifier 4.8.0 - Standards Compliant HTML Filtering
|
||||
HTML Purifier 4.9.3 - Standards Compliant HTML Filtering
|
||||
Copyright (C) 2006-2008 Edward Z. Yang
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
@@ -58,12 +58,12 @@ class HTMLPurifier
|
||||
* Version of HTML Purifier.
|
||||
* @type string
|
||||
*/
|
||||
public $version = '4.8.0';
|
||||
public $version = '4.9.3';
|
||||
|
||||
/**
|
||||
* Constant with version of HTML Purifier.
|
||||
*/
|
||||
const VERSION = '4.8.0';
|
||||
const VERSION = '4.9.3';
|
||||
|
||||
/**
|
||||
* Global configuration object.
|
||||
|
||||
@@ -131,6 +131,7 @@ require_once $__dir . '/HTMLPurifier/AttrTransform/SafeObject.php';
|
||||
require_once $__dir . '/HTMLPurifier/AttrTransform/SafeParam.php';
|
||||
require_once $__dir . '/HTMLPurifier/AttrTransform/ScriptRequired.php';
|
||||
require_once $__dir . '/HTMLPurifier/AttrTransform/TargetBlank.php';
|
||||
require_once $__dir . '/HTMLPurifier/AttrTransform/TargetNoopener.php';
|
||||
require_once $__dir . '/HTMLPurifier/AttrTransform/TargetNoreferrer.php';
|
||||
require_once $__dir . '/HTMLPurifier/AttrTransform/Textarea.php';
|
||||
require_once $__dir . '/HTMLPurifier/ChildDef/Chameleon.php';
|
||||
@@ -170,6 +171,7 @@ require_once $__dir . '/HTMLPurifier/HTMLModule/StyleAttribute.php';
|
||||
require_once $__dir . '/HTMLPurifier/HTMLModule/Tables.php';
|
||||
require_once $__dir . '/HTMLPurifier/HTMLModule/Target.php';
|
||||
require_once $__dir . '/HTMLPurifier/HTMLModule/TargetBlank.php';
|
||||
require_once $__dir . '/HTMLPurifier/HTMLModule/TargetNoopener.php';
|
||||
require_once $__dir . '/HTMLPurifier/HTMLModule/TargetNoreferrer.php';
|
||||
require_once $__dir . '/HTMLPurifier/HTMLModule/Text.php';
|
||||
require_once $__dir . '/HTMLPurifier/HTMLModule/Tidy.php';
|
||||
|
||||
+3
-3
@@ -19,8 +19,8 @@ class HTMLPurifier_Arborize
|
||||
if ($token instanceof HTMLPurifier_Token_End) {
|
||||
$token->start = null; // [MUT]
|
||||
$r = array_pop($stack);
|
||||
assert($r->name === $token->name);
|
||||
assert(empty($token->attr));
|
||||
//assert($r->name === $token->name);
|
||||
//assert(empty($token->attr));
|
||||
$r->endCol = $token->col;
|
||||
$r->endLine = $token->line;
|
||||
$r->endArmor = $token->armor;
|
||||
@@ -32,7 +32,7 @@ class HTMLPurifier_Arborize
|
||||
$stack[] = $node;
|
||||
}
|
||||
}
|
||||
assert(count($stack) == 1);
|
||||
//assert(count($stack) == 1);
|
||||
return $stack[0];
|
||||
}
|
||||
|
||||
|
||||
+7
-1
@@ -86,7 +86,13 @@ abstract class HTMLPurifier_AttrDef
|
||||
*/
|
||||
protected function mungeRgb($string)
|
||||
{
|
||||
return preg_replace('/rgb\((\d+)\s*,\s*(\d+)\s*,\s*(\d+)\)/', 'rgb(\1,\2,\3)', $string);
|
||||
$p = '\s*(\d+(\.\d+)?([%]?))\s*';
|
||||
|
||||
if (preg_match('/(rgba|hsla)\(/', $string)) {
|
||||
return preg_replace('/(rgba|hsla)\('.$p.','.$p.','.$p.','.$p.'\)/', '\1(\2,\5,\8,\11)', $string);
|
||||
}
|
||||
|
||||
return preg_replace('/(rgb|hsl)\('.$p.','.$p.','.$p.'\)/', '\1(\2,\5,\8)', $string);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+31
-6
@@ -27,13 +27,38 @@ class HTMLPurifier_AttrDef_CSS extends HTMLPurifier_AttrDef
|
||||
$definition = $config->getCSSDefinition();
|
||||
$allow_duplicates = $config->get("CSS.AllowDuplicates");
|
||||
|
||||
// we're going to break the spec and explode by semicolons.
|
||||
// This is because semicolon rarely appears in escaped form
|
||||
// Doing this is generally flaky but fast
|
||||
// IT MIGHT APPEAR IN URIs, see HTMLPurifier_AttrDef_CSSURI
|
||||
// for details
|
||||
|
||||
$declarations = explode(';', $css);
|
||||
// According to the CSS2.1 spec, the places where a
|
||||
// non-delimiting semicolon can appear are in strings
|
||||
// escape sequences. So here is some dumb hack to
|
||||
// handle quotes.
|
||||
$len = strlen($css);
|
||||
$accum = "";
|
||||
$declarations = array();
|
||||
$quoted = false;
|
||||
for ($i = 0; $i < $len; $i++) {
|
||||
$c = strcspn($css, ";'\"", $i);
|
||||
$accum .= substr($css, $i, $c);
|
||||
$i += $c;
|
||||
if ($i == $len) break;
|
||||
$d = $css[$i];
|
||||
if ($quoted) {
|
||||
$accum .= $d;
|
||||
if ($d == $quoted) {
|
||||
$quoted = false;
|
||||
}
|
||||
} else {
|
||||
if ($d == ";") {
|
||||
$declarations[] = $accum;
|
||||
$accum = "";
|
||||
} else {
|
||||
$accum .= $d;
|
||||
$quoted = $d;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($accum != "") $declarations[] = $accum;
|
||||
|
||||
$propvalues = array();
|
||||
$new_declarations = '';
|
||||
|
||||
|
||||
@@ -6,6 +6,16 @@
|
||||
class HTMLPurifier_AttrDef_CSS_Color extends HTMLPurifier_AttrDef
|
||||
{
|
||||
|
||||
/**
|
||||
* @type HTMLPurifier_AttrDef_CSS_AlphaValue
|
||||
*/
|
||||
protected $alpha;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->alpha = new HTMLPurifier_AttrDef_CSS_AlphaValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $color
|
||||
* @param HTMLPurifier_Config $config
|
||||
@@ -29,59 +39,104 @@ class HTMLPurifier_AttrDef_CSS_Color extends HTMLPurifier_AttrDef
|
||||
return $colors[$lower];
|
||||
}
|
||||
|
||||
if (strpos($color, 'rgb(') !== false) {
|
||||
// rgb literal handling
|
||||
if (preg_match('#(rgb|rgba|hsl|hsla)\(#', $color, $matches) === 1) {
|
||||
$length = strlen($color);
|
||||
if (strpos($color, ')') !== $length - 1) {
|
||||
return false;
|
||||
}
|
||||
$triad = substr($color, 4, $length - 4 - 1);
|
||||
$parts = explode(',', $triad);
|
||||
if (count($parts) !== 3) {
|
||||
|
||||
// get used function : rgb, rgba, hsl or hsla
|
||||
$function = $matches[1];
|
||||
|
||||
$parameters_size = 3;
|
||||
$alpha_channel = false;
|
||||
if (substr($function, -1) === 'a') {
|
||||
$parameters_size = 4;
|
||||
$alpha_channel = true;
|
||||
}
|
||||
|
||||
/*
|
||||
* Allowed types for values :
|
||||
* parameter_position => [type => max_value]
|
||||
*/
|
||||
$allowed_types = array(
|
||||
1 => array('percentage' => 100, 'integer' => 255),
|
||||
2 => array('percentage' => 100, 'integer' => 255),
|
||||
3 => array('percentage' => 100, 'integer' => 255),
|
||||
);
|
||||
$allow_different_types = false;
|
||||
|
||||
if (strpos($function, 'hsl') !== false) {
|
||||
$allowed_types = array(
|
||||
1 => array('integer' => 360),
|
||||
2 => array('percentage' => 100),
|
||||
3 => array('percentage' => 100),
|
||||
);
|
||||
$allow_different_types = true;
|
||||
}
|
||||
|
||||
$values = trim(str_replace($function, '', $color), ' ()');
|
||||
|
||||
$parts = explode(',', $values);
|
||||
if (count($parts) !== $parameters_size) {
|
||||
return false;
|
||||
}
|
||||
$type = false; // to ensure that they're all the same type
|
||||
|
||||
$type = false;
|
||||
$new_parts = array();
|
||||
$i = 0;
|
||||
|
||||
foreach ($parts as $part) {
|
||||
$i++;
|
||||
$part = trim($part);
|
||||
|
||||
if ($part === '') {
|
||||
return false;
|
||||
}
|
||||
$length = strlen($part);
|
||||
if ($part[$length - 1] === '%') {
|
||||
// handle percents
|
||||
if (!$type) {
|
||||
$type = 'percentage';
|
||||
} elseif ($type !== 'percentage') {
|
||||
|
||||
// different check for alpha channel
|
||||
if ($alpha_channel === true && $i === count($parts)) {
|
||||
$result = $this->alpha->validate($part, $config, $context);
|
||||
|
||||
if ($result === false) {
|
||||
return false;
|
||||
}
|
||||
$num = (float)substr($part, 0, $length - 1);
|
||||
if ($num < 0) {
|
||||
$num = 0;
|
||||
}
|
||||
if ($num > 100) {
|
||||
$num = 100;
|
||||
}
|
||||
$new_parts[] = "$num%";
|
||||
|
||||
$new_parts[] = (string)$result;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (substr($part, -1) === '%') {
|
||||
$current_type = 'percentage';
|
||||
} else {
|
||||
// handle integers
|
||||
if (!$type) {
|
||||
$type = 'integer';
|
||||
} elseif ($type !== 'integer') {
|
||||
return false;
|
||||
}
|
||||
$num = (int)$part;
|
||||
if ($num < 0) {
|
||||
$num = 0;
|
||||
}
|
||||
if ($num > 255) {
|
||||
$num = 255;
|
||||
}
|
||||
$new_parts[] = (string)$num;
|
||||
$current_type = 'integer';
|
||||
}
|
||||
|
||||
if (!array_key_exists($current_type, $allowed_types[$i])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$type) {
|
||||
$type = $current_type;
|
||||
}
|
||||
|
||||
if ($allow_different_types === false && $type != $current_type) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$max_value = $allowed_types[$i][$current_type];
|
||||
|
||||
if ($current_type == 'integer') {
|
||||
// Return value between range 0 -> $max_value
|
||||
$new_parts[] = (int)max(min($part, $max_value), 0);
|
||||
} elseif ($current_type == 'percentage') {
|
||||
$new_parts[] = (float)max(min(rtrim($part, '%'), $max_value), 0) . '%';
|
||||
}
|
||||
}
|
||||
$new_triad = implode(',', $new_parts);
|
||||
$color = "rgb($new_triad)";
|
||||
|
||||
$new_values = implode(',', $new_parts);
|
||||
|
||||
$color = $function . '(' . $new_values . ')';
|
||||
} else {
|
||||
// hexadecimal handling
|
||||
if ($color[0] === '#') {
|
||||
@@ -100,6 +155,7 @@ class HTMLPurifier_AttrDef_CSS_Color extends HTMLPurifier_AttrDef
|
||||
}
|
||||
return $color;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// vim: et sw=4 sts=4
|
||||
@@ -97,7 +97,7 @@ class HTMLPurifier_AttrDef_URI_Host extends HTMLPurifier_AttrDef
|
||||
|
||||
// PHP 5.3 and later support this functionality natively
|
||||
if (function_exists('idn_to_ascii')) {
|
||||
return idn_to_ascii($string);
|
||||
$string = idn_to_ascii($string);
|
||||
|
||||
// If we have Net_IDNA2 support, we can support IRIs by
|
||||
// punycoding them. (This is the most portable thing to do,
|
||||
@@ -123,13 +123,14 @@ class HTMLPurifier_AttrDef_URI_Host extends HTMLPurifier_AttrDef
|
||||
}
|
||||
}
|
||||
$string = implode('.', $new_parts);
|
||||
if (preg_match("/^($domainlabel\.)*$toplabel\.?$/i", $string)) {
|
||||
return $string;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
// XXX error reporting
|
||||
}
|
||||
}
|
||||
// Try again
|
||||
if (preg_match("/^($domainlabel\.)*$toplabel\.?$/i", $string)) {
|
||||
return $string;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
// must be called POST validation
|
||||
|
||||
/**
|
||||
* Adds rel="noopener" to any links which target a different window
|
||||
* than the current one. This is used to prevent malicious websites
|
||||
* from silently replacing the original window, which could be used
|
||||
* to do phishing.
|
||||
* This transform is controlled by %HTML.TargetNoopener.
|
||||
*/
|
||||
class HTMLPurifier_AttrTransform_TargetNoopener extends HTMLPurifier_AttrTransform
|
||||
{
|
||||
/**
|
||||
* @param array $attr
|
||||
* @param HTMLPurifier_Config $config
|
||||
* @param HTMLPurifier_Context $context
|
||||
* @return array
|
||||
*/
|
||||
public function transform($attr, $config, $context)
|
||||
{
|
||||
if (isset($attr['rel'])) {
|
||||
$rels = explode(' ', $attr['rel']);
|
||||
} else {
|
||||
$rels = array();
|
||||
}
|
||||
if (isset($attr['target']) && !in_array('noopener', $rels)) {
|
||||
$rels[] = 'noopener';
|
||||
}
|
||||
if (!empty($rels) || isset($attr['rel'])) {
|
||||
$attr['rel'] = implode(' ', $rels);
|
||||
}
|
||||
|
||||
return $attr;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -225,6 +225,10 @@ class HTMLPurifier_CSSDefinition extends HTMLPurifier_Definition
|
||||
);
|
||||
$max = $config->get('CSS.MaxImgLength');
|
||||
|
||||
$this->info['min-width'] =
|
||||
$this->info['max-width'] =
|
||||
$this->info['min-height'] =
|
||||
$this->info['max-height'] =
|
||||
$this->info['width'] =
|
||||
$this->info['height'] =
|
||||
$max === null ?
|
||||
|
||||
@@ -50,7 +50,7 @@ class HTMLPurifier_ChildDef_List extends HTMLPurifier_ChildDef
|
||||
// a little sanity check to make sure it's not ALL whitespace
|
||||
$all_whitespace = true;
|
||||
|
||||
$current_li = false;
|
||||
$current_li = null;
|
||||
|
||||
foreach ($children as $node) {
|
||||
if (!empty($node->is_whitespace)) {
|
||||
@@ -71,7 +71,7 @@ class HTMLPurifier_ChildDef_List extends HTMLPurifier_ChildDef
|
||||
// to handle non-list elements; non-list elements should
|
||||
// not be appended to an existing li; only li created
|
||||
// for non-list. This distinction is not currently made.
|
||||
if ($current_li === false) {
|
||||
if ($current_li === null) {
|
||||
$current_li = new HTMLPurifier_Node_Element('li');
|
||||
$result[] = $current_li;
|
||||
}
|
||||
|
||||
@@ -203,7 +203,7 @@ class HTMLPurifier_ChildDef_Table extends HTMLPurifier_ChildDef
|
||||
$current_tr_tbody->children[] = $node;
|
||||
break;
|
||||
case '#PCDATA':
|
||||
assert($node->is_whitespace);
|
||||
//assert($node->is_whitespace);
|
||||
if ($current_tr_tbody === null) {
|
||||
$ret[] = $node;
|
||||
} else {
|
||||
|
||||
+2
-2
@@ -21,7 +21,7 @@ class HTMLPurifier_Config
|
||||
* HTML Purifier's version
|
||||
* @type string
|
||||
*/
|
||||
public $version = '4.8.0';
|
||||
public $version = '4.9.3';
|
||||
|
||||
/**
|
||||
* Whether or not to automatically finalize
|
||||
@@ -333,7 +333,7 @@ class HTMLPurifier_Config
|
||||
}
|
||||
|
||||
// Raw type might be negative when using the fully optimized form
|
||||
// of stdclass, which indicates allow_null == true
|
||||
// of stdClass, which indicates allow_null == true
|
||||
$rtype = is_int($def) ? $def : $def->type;
|
||||
if ($rtype < 0) {
|
||||
$type = -$rtype;
|
||||
|
||||
@@ -24,11 +24,11 @@ class HTMLPurifier_ConfigSchema
|
||||
*
|
||||
* array(
|
||||
* 'Namespace' => array(
|
||||
* 'Directive' => new stdclass(),
|
||||
* 'Directive' => new stdClass(),
|
||||
* )
|
||||
* )
|
||||
*
|
||||
* The stdclass may have the following properties:
|
||||
* The stdClass may have the following properties:
|
||||
*
|
||||
* - If isAlias isn't set:
|
||||
* - type: Integer type of directive, see HTMLPurifier_VarParser for definitions
|
||||
@@ -39,8 +39,8 @@ class HTMLPurifier_ConfigSchema
|
||||
* - namespace: Namespace this directive aliases to
|
||||
* - name: Directive name this directive aliases to
|
||||
*
|
||||
* In certain degenerate cases, stdclass will actually be an integer. In
|
||||
* that case, the value is equivalent to an stdclass with the type
|
||||
* In certain degenerate cases, stdClass will actually be an integer. In
|
||||
* that case, the value is equivalent to an stdClass with the type
|
||||
* property set to the integer. If the integer is negative, type is
|
||||
* equal to the absolute value of integer, and allow_null is true.
|
||||
*
|
||||
@@ -105,7 +105,7 @@ class HTMLPurifier_ConfigSchema
|
||||
*/
|
||||
public function add($key, $default, $type, $allow_null)
|
||||
{
|
||||
$obj = new stdclass();
|
||||
$obj = new stdClass();
|
||||
$obj->type = is_int($type) ? $type : HTMLPurifier_VarParser::$types[$type];
|
||||
if ($allow_null) {
|
||||
$obj->allow_null = true;
|
||||
@@ -152,14 +152,14 @@ class HTMLPurifier_ConfigSchema
|
||||
*/
|
||||
public function addAlias($key, $new_key)
|
||||
{
|
||||
$obj = new stdclass;
|
||||
$obj = new stdClass;
|
||||
$obj->key = $new_key;
|
||||
$obj->isAlias = true;
|
||||
$this->info[$key] = $obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces any stdclass that only has the type property with type integer.
|
||||
* Replaces any stdClass that only has the type property with type integer.
|
||||
*/
|
||||
public function postProcess()
|
||||
{
|
||||
|
||||
Binary file not shown.
Vendored
+16
@@ -0,0 +1,16 @@
|
||||
Core.AggressivelyRemoveScript
|
||||
TYPE: bool
|
||||
VERSION: 4.9.0
|
||||
DEFAULT: true
|
||||
--DESCRIPTION--
|
||||
<p>
|
||||
This directive enables aggressive pre-filter removal of
|
||||
script tags. This is not necessary for security,
|
||||
but it can help work around a bug in libxml where embedded
|
||||
HTML elements inside script sections cause the parser to
|
||||
choke. To revert to pre-4.9.0 behavior, set this to false.
|
||||
This directive has no effect if %Core.Trusted is true,
|
||||
%Core.RemoveScriptContents is false, or %Core.HiddenElements
|
||||
does not contain script.
|
||||
</p>
|
||||
--# vim: et sw=4 sts=4
|
||||
Vendored
+36
@@ -0,0 +1,36 @@
|
||||
Core.LegacyEntityDecoder
|
||||
TYPE: bool
|
||||
VERSION: 4.9.0
|
||||
DEFAULT: false
|
||||
--DESCRIPTION--
|
||||
<p>
|
||||
Prior to HTML Purifier 4.9.0, entities were decoded by performing
|
||||
a global search replace for all entities whose decoded versions
|
||||
did not have special meanings under HTML, and replaced them with
|
||||
their decoded versions. We would match all entities, even if they did
|
||||
not have a trailing semicolon, but only if there weren't any trailing
|
||||
alphanumeric characters.
|
||||
</p>
|
||||
<table>
|
||||
<tr><th>Original</th><th>Text</th><th>Attribute</th></tr>
|
||||
<tr><td>&yen;</td><td>¥</td><td>¥</td></tr>
|
||||
<tr><td>&yen</td><td>¥</td><td>¥</td></tr>
|
||||
<tr><td>&yena</td><td>&yena</td><td>&yena</td></tr>
|
||||
<tr><td>&yen=</td><td>¥=</td><td>¥=</td></tr>
|
||||
</table>
|
||||
<p>
|
||||
In HTML Purifier 4.9.0, we changed the behavior of entity parsing
|
||||
to match entities that had missing trailing semicolons in less
|
||||
cases, to more closely match HTML5 parsing behavior:
|
||||
</p>
|
||||
<table>
|
||||
<tr><th>Original</th><th>Text</th><th>Attribute</th></tr>
|
||||
<tr><td>&yen;</td><td>¥</td><td>¥</td></tr>
|
||||
<tr><td>&yen</td><td>¥</td><td>¥</td></tr>
|
||||
<tr><td>&yena</td><td>¥a</td><td>&yena</td></tr>
|
||||
<tr><td>&yen=</td><td>¥=</td><td>&yen=</td></tr>
|
||||
</table>
|
||||
<p>
|
||||
This flag reverts back to pre-HTML Purifier 4.9.0 behavior.
|
||||
</p>
|
||||
--# vim: et sw=4 sts=4
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
--# vim: et sw=4 sts=4
|
||||
HTML.TargetNoopener
|
||||
TYPE: bool
|
||||
VERSION: 4.8.0
|
||||
DEFAULT: TRUE
|
||||
--DESCRIPTION--
|
||||
If enabled, noopener rel attributes are added to links which have
|
||||
a target attribute associated with them. This prevents malicious
|
||||
destinations from overwriting the original window.
|
||||
--# vim: et sw=4 sts=4
|
||||
+6
-1
@@ -1,5 +1,5 @@
|
||||
URI.DefaultScheme
|
||||
TYPE: string
|
||||
TYPE: string/null
|
||||
DEFAULT: 'http'
|
||||
--DESCRIPTION--
|
||||
|
||||
@@ -7,4 +7,9 @@ DEFAULT: 'http'
|
||||
Defines through what scheme the output will be served, in order to
|
||||
select the proper object validator when no scheme information is present.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Starting with HTML Purifier 4.9.0, the default scheme can be null, in
|
||||
which case we reject all URIs which do not have explicit schemes.
|
||||
</p>
|
||||
--# vim: et sw=4 sts=4
|
||||
+11
-18
@@ -112,6 +112,7 @@ class HTMLPurifier_DefinitionCache_Serializer extends HTMLPurifier_DefinitionCac
|
||||
}
|
||||
unlink($dir . '/' . $filename);
|
||||
}
|
||||
closedir($dh);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -142,6 +143,7 @@ class HTMLPurifier_DefinitionCache_Serializer extends HTMLPurifier_DefinitionCac
|
||||
unlink($dir . '/' . $filename);
|
||||
}
|
||||
}
|
||||
closedir($dh);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -198,11 +200,8 @@ class HTMLPurifier_DefinitionCache_Serializer extends HTMLPurifier_DefinitionCac
|
||||
if ($result !== false) {
|
||||
// set permissions of the new file (no execute)
|
||||
$chmod = $config->get('Cache.SerializerPermissions');
|
||||
if ($chmod === null) {
|
||||
// don't do anything
|
||||
} else {
|
||||
$chmod = $chmod & 0666;
|
||||
chmod($file, $chmod);
|
||||
if ($chmod !== null) {
|
||||
chmod($file, $chmod & 0666);
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
@@ -217,6 +216,11 @@ class HTMLPurifier_DefinitionCache_Serializer extends HTMLPurifier_DefinitionCac
|
||||
{
|
||||
$directory = $this->generateDirectoryPath($config);
|
||||
$chmod = $config->get('Cache.SerializerPermissions');
|
||||
if ($chmod === null) {
|
||||
// TODO: This races
|
||||
if (is_dir($directory)) return true;
|
||||
return mkdir($directory);
|
||||
}
|
||||
if (!is_dir($directory)) {
|
||||
$base = $this->generateBaseDirectoryPath($config);
|
||||
if (!is_dir($base)) {
|
||||
@@ -229,25 +233,14 @@ class HTMLPurifier_DefinitionCache_Serializer extends HTMLPurifier_DefinitionCac
|
||||
} elseif (!$this->_testPermissions($base, $chmod)) {
|
||||
return false;
|
||||
}
|
||||
if ($chmod === null) {
|
||||
if (!mkdir($directory, $chmod)) {
|
||||
trigger_error(
|
||||
'Base directory ' . $base . ' does not exist,
|
||||
please create or change using %Cache.SerializerPath',
|
||||
'Could not create directory ' . $directory . '',
|
||||
E_USER_WARNING
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if ($chmod !== null) {
|
||||
mkdir($directory, $chmod);
|
||||
} else {
|
||||
mkdir($directory);
|
||||
}
|
||||
if (!$this->_testPermissions($directory, $chmod)) {
|
||||
trigger_error(
|
||||
'Base directory ' . $base . ' does not exist,
|
||||
please create or change using %Cache.SerializerPath',
|
||||
E_USER_WARNING
|
||||
);
|
||||
return false;
|
||||
}
|
||||
} elseif (!$this->_testPermissions($directory, $chmod)) {
|
||||
|
||||
+9
-3
@@ -101,6 +101,14 @@ class HTMLPurifier_Encoder
|
||||
* It will parse according to UTF-8 and return a valid UTF8 string, with
|
||||
* non-SGML codepoints excluded.
|
||||
*
|
||||
* Specifically, it will permit:
|
||||
* \x{9}\x{A}\x{D}\x{20}-\x{7E}\x{A0}-\x{D7FF}\x{E000}-\x{FFFD}\x{10000}-\x{10FFFF}
|
||||
* Source: https://www.w3.org/TR/REC-xml/#NT-Char
|
||||
* Arguably this function should be modernized to the HTML5 set
|
||||
* of allowed characters:
|
||||
* https://www.w3.org/TR/html5/syntax.html#preprocessing-the-input-stream
|
||||
* which simultaneously expand and restrict the set of allowed characters.
|
||||
*
|
||||
* @param string $str The string to clean
|
||||
* @param bool $force_php
|
||||
* @return string
|
||||
@@ -122,15 +130,12 @@ class HTMLPurifier_Encoder
|
||||
* function that needs to be able to understand UTF-8 characters.
|
||||
* As of right now, only smart lossless character encoding converters
|
||||
* would need that, and I'm probably not going to implement them.
|
||||
* Once again, PHP 6 should solve all our problems.
|
||||
*/
|
||||
public static function cleanUTF8($str, $force_php = false)
|
||||
{
|
||||
// UTF-8 validity is checked since PHP 4.3.5
|
||||
// This is an optimization: if the string is already valid UTF-8, no
|
||||
// need to do PHP stuff. 99% of the time, this will be the case.
|
||||
// The regexp matches the XML char production, as well as well as excluding
|
||||
// non-SGML codepoints U+007F to U+009F
|
||||
if (preg_match(
|
||||
'/^[\x{9}\x{A}\x{D}\x{20}-\x{7E}\x{A0}-\x{D7FF}\x{E000}-\x{FFFD}\x{10000}-\x{10FFFF}]*$/Du',
|
||||
$str
|
||||
@@ -255,6 +260,7 @@ class HTMLPurifier_Encoder
|
||||
// 7F-9F is not strictly prohibited by XML,
|
||||
// but it is non-SGML, and thus we don't allow it
|
||||
(0xA0 <= $mUcs4 && 0xD7FF >= $mUcs4) ||
|
||||
(0xE000 <= $mUcs4 && 0xFFFD >= $mUcs4) ||
|
||||
(0x10000 <= $mUcs4 && 0x10FFFF >= $mUcs4)
|
||||
)
|
||||
) {
|
||||
|
||||
+133
-1
@@ -16,6 +16,138 @@ class HTMLPurifier_EntityParser
|
||||
*/
|
||||
protected $_entity_lookup;
|
||||
|
||||
/**
|
||||
* Callback regex string for entities in text.
|
||||
* @type string
|
||||
*/
|
||||
protected $_textEntitiesRegex;
|
||||
|
||||
/**
|
||||
* Callback regex string for entities in attributes.
|
||||
* @type string
|
||||
*/
|
||||
protected $_attrEntitiesRegex;
|
||||
|
||||
/**
|
||||
* Tests if the beginning of a string is a semi-optional regex
|
||||
*/
|
||||
protected $_semiOptionalPrefixRegex;
|
||||
|
||||
public function __construct() {
|
||||
// From
|
||||
// http://stackoverflow.com/questions/15532252/why-is-reg-being-rendered-as-without-the-bounding-semicolon
|
||||
$semi_optional = "quot|QUOT|lt|LT|gt|GT|amp|AMP|AElig|Aacute|Acirc|Agrave|Aring|Atilde|Auml|COPY|Ccedil|ETH|Eacute|Ecirc|Egrave|Euml|Iacute|Icirc|Igrave|Iuml|Ntilde|Oacute|Ocirc|Ograve|Oslash|Otilde|Ouml|REG|THORN|Uacute|Ucirc|Ugrave|Uuml|Yacute|aacute|acirc|acute|aelig|agrave|aring|atilde|auml|brvbar|ccedil|cedil|cent|copy|curren|deg|divide|eacute|ecirc|egrave|eth|euml|frac12|frac14|frac34|iacute|icirc|iexcl|igrave|iquest|iuml|laquo|macr|micro|middot|nbsp|not|ntilde|oacute|ocirc|ograve|ordf|ordm|oslash|otilde|ouml|para|plusmn|pound|raquo|reg|sect|shy|sup1|sup2|sup3|szlig|thorn|times|uacute|ucirc|ugrave|uml|uuml|yacute|yen|yuml";
|
||||
|
||||
// NB: three empty captures to put the fourth match in the right
|
||||
// place
|
||||
$this->_semiOptionalPrefixRegex = "/&()()()($semi_optional)/";
|
||||
|
||||
$this->_textEntitiesRegex =
|
||||
'/&(?:'.
|
||||
// hex
|
||||
'[#]x([a-fA-F0-9]+);?|'.
|
||||
// dec
|
||||
'[#]0*(\d+);?|'.
|
||||
// string (mandatory semicolon)
|
||||
// NB: order matters: match semicolon preferentially
|
||||
'([A-Za-z_:][A-Za-z0-9.\-_:]*);|'.
|
||||
// string (optional semicolon)
|
||||
"($semi_optional)".
|
||||
')/';
|
||||
|
||||
$this->_attrEntitiesRegex =
|
||||
'/&(?:'.
|
||||
// hex
|
||||
'[#]x([a-fA-F0-9]+);?|'.
|
||||
// dec
|
||||
'[#]0*(\d+);?|'.
|
||||
// string (mandatory semicolon)
|
||||
// NB: order matters: match semicolon preferentially
|
||||
'([A-Za-z_:][A-Za-z0-9.\-_:]*);|'.
|
||||
// string (optional semicolon)
|
||||
// don't match if trailing is equals or alphanumeric (URL
|
||||
// like)
|
||||
"($semi_optional)(?![=;A-Za-z0-9])".
|
||||
')/';
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Substitute entities with the parsed equivalents. Use this on
|
||||
* textual data in an HTML document (as opposed to attributes.)
|
||||
*
|
||||
* @param string $string String to have entities parsed.
|
||||
* @return string Parsed string.
|
||||
*/
|
||||
public function substituteTextEntities($string)
|
||||
{
|
||||
return preg_replace_callback(
|
||||
$this->_textEntitiesRegex,
|
||||
array($this, 'entityCallback'),
|
||||
$string
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Substitute entities with the parsed equivalents. Use this on
|
||||
* attribute contents in documents.
|
||||
*
|
||||
* @param string $string String to have entities parsed.
|
||||
* @return string Parsed string.
|
||||
*/
|
||||
public function substituteAttrEntities($string)
|
||||
{
|
||||
return preg_replace_callback(
|
||||
$this->_attrEntitiesRegex,
|
||||
array($this, 'entityCallback'),
|
||||
$string
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback function for substituteNonSpecialEntities() that does the work.
|
||||
*
|
||||
* @param array $matches PCRE matches array, with 0 the entire match, and
|
||||
* either index 1, 2 or 3 set with a hex value, dec value,
|
||||
* or string (respectively).
|
||||
* @return string Replacement string.
|
||||
*/
|
||||
|
||||
protected function entityCallback($matches)
|
||||
{
|
||||
$entity = $matches[0];
|
||||
$hex_part = @$matches[1];
|
||||
$dec_part = @$matches[2];
|
||||
$named_part = empty($matches[3]) ? @$matches[4] : $matches[3];
|
||||
if ($hex_part !== NULL && $hex_part !== "") {
|
||||
return HTMLPurifier_Encoder::unichr(hexdec($hex_part));
|
||||
} elseif ($dec_part !== NULL && $dec_part !== "") {
|
||||
return HTMLPurifier_Encoder::unichr((int) $dec_part);
|
||||
} else {
|
||||
if (!$this->_entity_lookup) {
|
||||
$this->_entity_lookup = HTMLPurifier_EntityLookup::instance();
|
||||
}
|
||||
if (isset($this->_entity_lookup->table[$named_part])) {
|
||||
return $this->_entity_lookup->table[$named_part];
|
||||
} else {
|
||||
// exact match didn't match anything, so test if
|
||||
// any of the semicolon optional match the prefix.
|
||||
// Test that this is an EXACT match is important to
|
||||
// prevent infinite loop
|
||||
if (!empty($matches[3])) {
|
||||
return preg_replace_callback(
|
||||
$this->_semiOptionalPrefixRegex,
|
||||
array($this, 'entityCallback'),
|
||||
$entity
|
||||
);
|
||||
}
|
||||
return $entity;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// LEGACY CODE BELOW
|
||||
|
||||
/**
|
||||
* Callback regex string for parsing entities.
|
||||
* @type string
|
||||
@@ -144,7 +276,7 @@ class HTMLPurifier_EntityParser
|
||||
$entity;
|
||||
} else {
|
||||
return isset($this->_special_ent2dec[$matches[3]]) ?
|
||||
$this->_special_ent2dec[$matches[3]] :
|
||||
$this->_special_dec2str[$this->_special_ent2dec[$matches[3]]] :
|
||||
$entity;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,7 +95,10 @@ class HTMLPurifier_Filter_ExtractStyleBlocks extends HTMLPurifier_Filter
|
||||
if ($tidy !== null) {
|
||||
$this->_tidy = $tidy;
|
||||
}
|
||||
$html = preg_replace_callback('#<style(?:\s.*)?>(.+)</style>#isU', array($this, 'styleCallback'), $html);
|
||||
// NB: this must be NON-greedy because if we have
|
||||
// <style>foo</style> <style>bar</style>
|
||||
// we must not grab foo</style> <style>bar
|
||||
$html = preg_replace_callback('#<style(?:\s.*)?>(.*)<\/style>#isU', array($this, 'styleCallback'), $html);
|
||||
$style_blocks = $this->_styleMatches;
|
||||
$this->_styleMatches = array(); // reset
|
||||
$context->register('StyleBlocks', $style_blocks); // $context must not be reused
|
||||
|
||||
@@ -146,7 +146,7 @@ class HTMLPurifier_Generator
|
||||
$attr = $this->generateAttributes($token->attr, $token->name);
|
||||
if ($this->_flashCompat) {
|
||||
if ($token->name == "object") {
|
||||
$flash = new stdclass();
|
||||
$flash = new stdClass();
|
||||
$flash->attr = $token->attr;
|
||||
$flash->param = array();
|
||||
$this->_flashStack[] = $flash;
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Module adds the target-based noopener attribute transformation to a tags. It
|
||||
* is enabled by HTML.TargetNoopener
|
||||
*/
|
||||
class HTMLPurifier_HTMLModule_TargetNoopener extends HTMLPurifier_HTMLModule
|
||||
{
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
public $name = 'TargetNoopener';
|
||||
|
||||
/**
|
||||
* @param HTMLPurifier_Config $config
|
||||
*/
|
||||
public function setup($config) {
|
||||
$a = $this->addBlankElement('a');
|
||||
$a->attr_transform_post[] = new HTMLPurifier_AttrTransform_TargetNoopener();
|
||||
}
|
||||
}
|
||||
@@ -271,11 +271,14 @@ class HTMLPurifier_HTMLModuleManager
|
||||
if ($config->get('HTML.TargetBlank')) {
|
||||
$modules[] = 'TargetBlank';
|
||||
}
|
||||
// NB: HTML.TargetNoreferrer must be AFTER HTML.TargetBlank
|
||||
// NB: HTML.TargetNoreferrer and HTML.TargetNoopener must be AFTER HTML.TargetBlank
|
||||
// so that its post-attr-transform gets run afterwards.
|
||||
if ($config->get('HTML.TargetNoreferrer')) {
|
||||
$modules[] = 'TargetNoreferrer';
|
||||
}
|
||||
if ($config->get('HTML.TargetNoopener')) {
|
||||
$modules[] = 'TargetNoopener';
|
||||
}
|
||||
|
||||
// merge in custom modules
|
||||
$modules = array_merge($modules, $this->userModules);
|
||||
|
||||
+29
-9
@@ -96,7 +96,7 @@ class HTMLPurifier_Lexer
|
||||
break;
|
||||
}
|
||||
|
||||
if (class_exists('DOMDocument') &&
|
||||
if (class_exists('DOMDocument', false) &&
|
||||
method_exists('DOMDocument', 'loadHTML') &&
|
||||
!extension_loaded('domxml')
|
||||
) {
|
||||
@@ -169,21 +169,24 @@ class HTMLPurifier_Lexer
|
||||
''' => "'"
|
||||
);
|
||||
|
||||
public function parseText($string, $config) {
|
||||
return $this->parseData($string, false, $config);
|
||||
}
|
||||
|
||||
public function parseAttr($string, $config) {
|
||||
return $this->parseData($string, true, $config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses special entities into the proper characters.
|
||||
*
|
||||
* This string will translate escaped versions of the special characters
|
||||
* into the correct ones.
|
||||
*
|
||||
* @warning
|
||||
* You should be able to treat the output of this function as
|
||||
* completely parsed, but that's only because all other entities should
|
||||
* have been handled previously in substituteNonSpecialEntities()
|
||||
*
|
||||
* @param string $string String character data to be parsed.
|
||||
* @return string Parsed character data.
|
||||
*/
|
||||
public function parseData($string)
|
||||
public function parseData($string, $is_attr, $config)
|
||||
{
|
||||
// following functions require at least one character
|
||||
if ($string === '') {
|
||||
@@ -209,7 +212,15 @@ class HTMLPurifier_Lexer
|
||||
}
|
||||
|
||||
// hmm... now we have some uncommon entities. Use the callback.
|
||||
$string = $this->_entity_parser->substituteSpecialEntities($string);
|
||||
if ($config->get('Core.LegacyEntityDecoder')) {
|
||||
$string = $this->_entity_parser->substituteSpecialEntities($string);
|
||||
} else {
|
||||
if ($is_attr) {
|
||||
$string = $this->_entity_parser->substituteAttrEntities($string);
|
||||
} else {
|
||||
$string = $this->_entity_parser->substituteTextEntities($string);
|
||||
}
|
||||
}
|
||||
return $string;
|
||||
}
|
||||
|
||||
@@ -323,7 +334,9 @@ class HTMLPurifier_Lexer
|
||||
}
|
||||
|
||||
// expand entities that aren't the big five
|
||||
$html = $this->_entity_parser->substituteNonSpecialEntities($html);
|
||||
if ($config->get('Core.LegacyEntityDecoder')) {
|
||||
$html = $this->_entity_parser->substituteNonSpecialEntities($html);
|
||||
}
|
||||
|
||||
// clean into wellformed UTF-8 string for an SGML context: this has
|
||||
// to be done after entity expansion because the entities sometimes
|
||||
@@ -335,6 +348,13 @@ class HTMLPurifier_Lexer
|
||||
$html = preg_replace('#<\?.+?\?>#s', '', $html);
|
||||
}
|
||||
|
||||
$hidden_elements = $config->get('Core.HiddenElements');
|
||||
if ($config->get('Core.AggressivelyRemoveScript') &&
|
||||
!($config->get('HTML.Trusted') || !$config->get('Core.RemoveScriptContents')
|
||||
|| empty($hidden_elements["script"]))) {
|
||||
$html = preg_replace('#<script[^>]*>.*?</script>#i', '', $html);
|
||||
}
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
|
||||
+23
-11
@@ -72,12 +72,20 @@ class HTMLPurifier_Lexer_DOMLex extends HTMLPurifier_Lexer
|
||||
$doc->loadHTML($html);
|
||||
restore_error_handler();
|
||||
|
||||
$body = $doc->getElementsByTagName('html')->item(0)-> // <html>
|
||||
getElementsByTagName('body')->item(0); // <body>
|
||||
|
||||
$div = $body->getElementsByTagName('div')->item(0); // <div>
|
||||
$tokens = array();
|
||||
$this->tokenizeDOM(
|
||||
$doc->getElementsByTagName('html')->item(0)-> // <html>
|
||||
getElementsByTagName('body')->item(0), // <body>
|
||||
$tokens
|
||||
);
|
||||
$this->tokenizeDOM($div, $tokens, $config);
|
||||
// If the div has a sibling, that means we tripped across
|
||||
// a premature </div> tag. So remove the div we parsed,
|
||||
// and then tokenize the rest of body. We can't tokenize
|
||||
// the sibling directly as we'll lose the tags in that case.
|
||||
if ($div->nextSibling) {
|
||||
$body->removeChild($div);
|
||||
$this->tokenizeDOM($body, $tokens, $config);
|
||||
}
|
||||
return $tokens;
|
||||
}
|
||||
|
||||
@@ -88,7 +96,7 @@ class HTMLPurifier_Lexer_DOMLex extends HTMLPurifier_Lexer
|
||||
* @param HTMLPurifier_Token[] $tokens Array-list of already tokenized tokens.
|
||||
* @return HTMLPurifier_Token of node appended to previously passed tokens.
|
||||
*/
|
||||
protected function tokenizeDOM($node, &$tokens)
|
||||
protected function tokenizeDOM($node, &$tokens, $config)
|
||||
{
|
||||
$level = 0;
|
||||
$nodes = array($level => new HTMLPurifier_Queue(array($node)));
|
||||
@@ -97,7 +105,7 @@ class HTMLPurifier_Lexer_DOMLex extends HTMLPurifier_Lexer
|
||||
while (!$nodes[$level]->isEmpty()) {
|
||||
$node = $nodes[$level]->shift(); // FIFO
|
||||
$collect = $level > 0 ? true : false;
|
||||
$needEndingTag = $this->createStartNode($node, $tokens, $collect);
|
||||
$needEndingTag = $this->createStartNode($node, $tokens, $collect, $config);
|
||||
if ($needEndingTag) {
|
||||
$closingNodes[$level][] = $node;
|
||||
}
|
||||
@@ -127,7 +135,7 @@ class HTMLPurifier_Lexer_DOMLex extends HTMLPurifier_Lexer
|
||||
* @return bool if the token needs an endtoken
|
||||
* @todo data and tagName properties don't seem to exist in DOMNode?
|
||||
*/
|
||||
protected function createStartNode($node, &$tokens, $collect)
|
||||
protected function createStartNode($node, &$tokens, $collect, $config)
|
||||
{
|
||||
// intercept non element nodes. WE MUST catch all of them,
|
||||
// but we're not getting the character reference nodes because
|
||||
@@ -151,7 +159,7 @@ class HTMLPurifier_Lexer_DOMLex extends HTMLPurifier_Lexer
|
||||
}
|
||||
}
|
||||
}
|
||||
$tokens[] = $this->factory->createText($this->parseData($data));
|
||||
$tokens[] = $this->factory->createText($this->parseText($data, $config));
|
||||
return false;
|
||||
} elseif ($node->nodeType === XML_COMMENT_NODE) {
|
||||
// this is code is only invoked for comments in script/style in versions
|
||||
@@ -252,7 +260,7 @@ class HTMLPurifier_Lexer_DOMLex extends HTMLPurifier_Lexer
|
||||
* @param HTMLPurifier_Context $context
|
||||
* @return string
|
||||
*/
|
||||
protected function wrapHTML($html, $config, $context)
|
||||
protected function wrapHTML($html, $config, $context, $use_div = true)
|
||||
{
|
||||
$def = $config->getDefinition('HTML');
|
||||
$ret = '';
|
||||
@@ -271,7 +279,11 @@ class HTMLPurifier_Lexer_DOMLex extends HTMLPurifier_Lexer
|
||||
$ret .= '<html><head>';
|
||||
$ret .= '<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />';
|
||||
// No protection if $html contains a stray </div>!
|
||||
$ret .= '</head><body>' . $html . '</body></html>';
|
||||
$ret .= '</head><body>';
|
||||
if ($use_div) $ret .= '<div>';
|
||||
$ret .= $html;
|
||||
if ($use_div) $ret .= '</div>';
|
||||
$ret .= '</body></html>';
|
||||
return $ret;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,12 +129,12 @@ class HTMLPurifier_Lexer_DirectLex extends HTMLPurifier_Lexer
|
||||
// We are not inside tag and there still is another tag to parse
|
||||
$token = new
|
||||
HTMLPurifier_Token_Text(
|
||||
$this->parseData(
|
||||
$this->parseText(
|
||||
substr(
|
||||
$html,
|
||||
$cursor,
|
||||
$position_next_lt - $cursor
|
||||
)
|
||||
), $config
|
||||
)
|
||||
);
|
||||
if ($maintain_line_numbers) {
|
||||
@@ -154,11 +154,11 @@ class HTMLPurifier_Lexer_DirectLex extends HTMLPurifier_Lexer
|
||||
// Create Text of rest of string
|
||||
$token = new
|
||||
HTMLPurifier_Token_Text(
|
||||
$this->parseData(
|
||||
$this->parseText(
|
||||
substr(
|
||||
$html,
|
||||
$cursor
|
||||
)
|
||||
), $config
|
||||
)
|
||||
);
|
||||
if ($maintain_line_numbers) {
|
||||
@@ -324,8 +324,8 @@ class HTMLPurifier_Lexer_DirectLex extends HTMLPurifier_Lexer
|
||||
$token = new
|
||||
HTMLPurifier_Token_Text(
|
||||
'<' .
|
||||
$this->parseData(
|
||||
substr($html, $cursor)
|
||||
$this->parseText(
|
||||
substr($html, $cursor), $config
|
||||
)
|
||||
);
|
||||
if ($maintain_line_numbers) {
|
||||
@@ -429,7 +429,7 @@ class HTMLPurifier_Lexer_DirectLex extends HTMLPurifier_Lexer
|
||||
if ($value === false) {
|
||||
$value = '';
|
||||
}
|
||||
return array($key => $this->parseData($value));
|
||||
return array($key => $this->parseAttr($value, $config));
|
||||
}
|
||||
|
||||
// setup loop environment
|
||||
@@ -518,7 +518,7 @@ class HTMLPurifier_Lexer_DirectLex extends HTMLPurifier_Lexer
|
||||
if ($value === false) {
|
||||
$value = '';
|
||||
}
|
||||
$array[$key] = $this->parseData($value);
|
||||
$array[$key] = $this->parseAttr($value, $config);
|
||||
$cursor++;
|
||||
} else {
|
||||
// boolattr
|
||||
|
||||
@@ -21,7 +21,7 @@ class HTMLPurifier_Lexer_PH5P extends HTMLPurifier_Lexer_DOMLex
|
||||
public function tokenizeHTML($html, $config, $context)
|
||||
{
|
||||
$new_html = $this->normalize($html, $config, $context);
|
||||
$new_html = $this->wrapHTML($new_html, $config, $context);
|
||||
$new_html = $this->wrapHTML($new_html, $config, $context, false /* no div */);
|
||||
try {
|
||||
$parser = new HTML5($new_html);
|
||||
$doc = $parser->save();
|
||||
@@ -34,9 +34,9 @@ class HTMLPurifier_Lexer_PH5P extends HTMLPurifier_Lexer_DOMLex
|
||||
$tokens = array();
|
||||
$this->tokenizeDOM(
|
||||
$doc->getElementsByTagName('html')->item(0)-> // <html>
|
||||
getElementsByTagName('body')->item(0) // <body>
|
||||
getElementsByTagName('body')->item(0) // <body>
|
||||
,
|
||||
$tokens
|
||||
$tokens, $config
|
||||
);
|
||||
return $tokens;
|
||||
}
|
||||
@@ -1515,6 +1515,7 @@ class HTML5
|
||||
// Consume the maximum number of characters possible, with the
|
||||
// consumed characters case-sensitively matching one of the
|
||||
// identifiers in the first column of the entities table.
|
||||
|
||||
$e_name = $this->characters('0-9A-Za-z;', $this->char + 1);
|
||||
$len = strlen($e_name);
|
||||
|
||||
@@ -1547,7 +1548,7 @@ class HTML5
|
||||
|
||||
// Return a character token for the character corresponding to the
|
||||
// entity name (as given by the second column of the entities table).
|
||||
return html_entity_decode('&' . $entity . ';', ENT_QUOTES, 'UTF-8');
|
||||
return html_entity_decode('&' . rtrim($entity, ';') . ';', ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
private function emitToken($token)
|
||||
|
||||
@@ -165,7 +165,7 @@ class HTMLPurifier_Strategy_MakeWellFormed extends HTMLPurifier_Strategy
|
||||
if (empty($zipper->front)) break;
|
||||
$token = $zipper->prev($token);
|
||||
// indicate that other injectors should not process this token,
|
||||
// but we need to reprocess it
|
||||
// but we need to reprocess it. See Note [Injector skips]
|
||||
unset($token->skip[$i]);
|
||||
$token->rewind = $i;
|
||||
if ($token instanceof HTMLPurifier_Token_Start) {
|
||||
@@ -210,6 +210,7 @@ class HTMLPurifier_Strategy_MakeWellFormed extends HTMLPurifier_Strategy
|
||||
if ($token instanceof HTMLPurifier_Token_Text) {
|
||||
foreach ($this->injectors as $i => $injector) {
|
||||
if (isset($token->skip[$i])) {
|
||||
// See Note [Injector skips]
|
||||
continue;
|
||||
}
|
||||
if ($token->rewind !== null && $token->rewind !== $i) {
|
||||
@@ -367,6 +368,7 @@ class HTMLPurifier_Strategy_MakeWellFormed extends HTMLPurifier_Strategy
|
||||
if ($ok) {
|
||||
foreach ($this->injectors as $i => $injector) {
|
||||
if (isset($token->skip[$i])) {
|
||||
// See Note [Injector skips]
|
||||
continue;
|
||||
}
|
||||
if ($token->rewind !== null && $token->rewind !== $i) {
|
||||
@@ -422,6 +424,7 @@ class HTMLPurifier_Strategy_MakeWellFormed extends HTMLPurifier_Strategy
|
||||
$token->start = $current_parent;
|
||||
foreach ($this->injectors as $i => $injector) {
|
||||
if (isset($token->skip[$i])) {
|
||||
// See Note [Injector skips]
|
||||
continue;
|
||||
}
|
||||
if ($token->rewind !== null && $token->rewind !== $i) {
|
||||
@@ -534,12 +537,17 @@ class HTMLPurifier_Strategy_MakeWellFormed extends HTMLPurifier_Strategy
|
||||
*/
|
||||
protected function processToken($token, $injector = -1)
|
||||
{
|
||||
// Zend OpCache miscompiles $token = array($token), so
|
||||
// avoid this pattern. See: https://github.com/ezyang/htmlpurifier/issues/108
|
||||
|
||||
// normalize forms of token
|
||||
if (is_object($token)) {
|
||||
$token = array(1, $token);
|
||||
$tmp = $token;
|
||||
$token = array(1, $tmp);
|
||||
}
|
||||
if (is_int($token)) {
|
||||
$token = array($token);
|
||||
$tmp = $token;
|
||||
$token = array($tmp);
|
||||
}
|
||||
if ($token === false) {
|
||||
$token = array(1);
|
||||
@@ -561,7 +569,12 @@ class HTMLPurifier_Strategy_MakeWellFormed extends HTMLPurifier_Strategy
|
||||
list($old, $r) = $this->zipper->splice($this->token, $delete, $token);
|
||||
|
||||
if ($injector > -1) {
|
||||
// determine appropriate skips
|
||||
// See Note [Injector skips]
|
||||
// Determine appropriate skips. Here's what the code does:
|
||||
// *If* we deleted one or more tokens, copy the skips
|
||||
// of those tokens into the skips of the new tokens (in $token).
|
||||
// Also, mark the newly inserted tokens as having come from
|
||||
// $injector.
|
||||
$oldskip = isset($old[0]) ? $old[0]->skip : array();
|
||||
foreach ($token as $object) {
|
||||
$object->skip = $oldskip;
|
||||
@@ -597,4 +610,50 @@ class HTMLPurifier_Strategy_MakeWellFormed extends HTMLPurifier_Strategy
|
||||
}
|
||||
}
|
||||
|
||||
// Note [Injector skips]
|
||||
// ~~~~~~~~~~~~~~~~~~~~~
|
||||
// When I originally designed this class, the idea behind the 'skip'
|
||||
// property of HTMLPurifier_Token was to help avoid infinite loops
|
||||
// in injector processing. For example, suppose you wrote an injector
|
||||
// that bolded swear words. Naively, you might write it so that
|
||||
// whenever you saw ****, you replaced it with <strong>****</strong>.
|
||||
//
|
||||
// When this happens, we will reprocess all of the tokens with the
|
||||
// other injectors. Now there is an opportunity for infinite loop:
|
||||
// if we rerun the swear-word injector on these tokens, we might
|
||||
// see **** and then reprocess again to get
|
||||
// <strong><strong>****</strong></strong> ad infinitum.
|
||||
//
|
||||
// Thus, the idea of a skip is that once we process a token with
|
||||
// an injector, we mark all of those tokens as having "come from"
|
||||
// the injector, and we never run the injector again on these
|
||||
// tokens.
|
||||
//
|
||||
// There were two more complications, however:
|
||||
//
|
||||
// - With HTMLPurifier_Injector_RemoveEmpty, we noticed that if
|
||||
// you had <b><i></i></b>, after you removed the <i></i>, you
|
||||
// really would like this injector to go back and reprocess
|
||||
// the <b> tag, discovering that it is now empty and can be
|
||||
// removed. So we reintroduced the possibility of infinite looping
|
||||
// by adding a "rewind" function, which let you go back to an
|
||||
// earlier point in the token stream and reprocess it with injectors.
|
||||
// Needless to say, we need to UN-skip the token so it gets
|
||||
// reprocessed.
|
||||
//
|
||||
// - Suppose that you successfuly process a token, replace it with
|
||||
// one with your skip mark, but now another injector wants to
|
||||
// process the skipped token with another token. Should you continue
|
||||
// to skip that new token, or reprocess it? If you reprocess,
|
||||
// you can end up with an infinite loop where one injector converts
|
||||
// <a> to <b>, and then another injector converts it back. So
|
||||
// we inherit the skips, but for some reason, I thought that we
|
||||
// should inherit the skip from the first token of the token
|
||||
// that we deleted. Why? Well, it seems to work OK.
|
||||
//
|
||||
// If I were to redesign this functionality, I would absolutely not
|
||||
// go about doing it this way: the semantics are just not very well
|
||||
// defined, and in any case you probably wanted to operate on trees,
|
||||
// not token streams.
|
||||
|
||||
// vim: et sw=4 sts=4
|
||||
+1
-1
@@ -26,7 +26,7 @@ abstract class HTMLPurifier_Token
|
||||
public $armor = array();
|
||||
|
||||
/**
|
||||
* Used during MakeWellFormed.
|
||||
* Used during MakeWellFormed. See Note [Injector skips]
|
||||
* @type
|
||||
*/
|
||||
public $skip;
|
||||
|
||||
+7
-5
@@ -85,11 +85,13 @@ class HTMLPurifier_URI
|
||||
$def = $config->getDefinition('URI');
|
||||
$scheme_obj = $def->getDefaultScheme($config, $context);
|
||||
if (!$scheme_obj) {
|
||||
// something funky happened to the default scheme object
|
||||
trigger_error(
|
||||
'Default scheme object "' . $def->defaultScheme . '" was not readable',
|
||||
E_USER_WARNING
|
||||
);
|
||||
if ($def->defaultScheme !== null) {
|
||||
// something funky happened to the default scheme object
|
||||
trigger_error(
|
||||
'Default scheme object "' . $def->defaultScheme . '" was not readable',
|
||||
E_USER_WARNING
|
||||
);
|
||||
} // suppress error if it's null
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Parses a user agent string into its important parts
|
||||
*
|
||||
* @author Jesse G. Donat <donatj@gmail.com>
|
||||
* @link https://github.com/donatj/PhpUserAgent
|
||||
* @link http://donatstudios.com/PHP-Parser-HTTP_USER_AGENT
|
||||
* @param string|null $u_agent User agent string to parse or null. Uses $_SERVER['HTTP_USER_AGENT'] on NULL
|
||||
* @throws \InvalidArgumentException on not having a proper user agent to parse.
|
||||
* @return string[] an array with browser, version and platform keys
|
||||
*/
|
||||
function parse_user_agent( $u_agent = null ) {
|
||||
if( is_null($u_agent) ) {
|
||||
if( isset($_SERVER['HTTP_USER_AGENT']) ) {
|
||||
$u_agent = $_SERVER['HTTP_USER_AGENT'];
|
||||
} else {
|
||||
throw new \InvalidArgumentException('parse_user_agent requires a user agent');
|
||||
}
|
||||
}
|
||||
|
||||
$platform = null;
|
||||
$browser = null;
|
||||
$version = null;
|
||||
|
||||
$empty = array( 'platform' => $platform, 'browser' => $browser, 'version' => $version );
|
||||
|
||||
if( !$u_agent ) return $empty;
|
||||
|
||||
if( preg_match('/\((.*?)\)/im', $u_agent, $parent_matches) ) {
|
||||
preg_match_all('/(?P<platform>BB\d+;|Android|CrOS|Tizen|iPhone|iPad|iPod|Linux|Macintosh|Windows(\ Phone)?|Silk|linux-gnu|BlackBerry|PlayBook|X11|(New\ )?Nintendo\ (WiiU?|3?DS)|Xbox(\ One)?)
|
||||
(?:\ [^;]*)?
|
||||
(?:;|$)/imx', $parent_matches[1], $result, PREG_PATTERN_ORDER);
|
||||
|
||||
$priority = array( 'Xbox One', 'Xbox', 'Windows Phone', 'Tizen', 'Android', 'CrOS', 'X11' );
|
||||
|
||||
$result['platform'] = array_unique($result['platform']);
|
||||
if( count($result['platform']) > 1 ) {
|
||||
if( $keys = array_intersect($priority, $result['platform']) ) {
|
||||
$platform = reset($keys);
|
||||
} else {
|
||||
$platform = $result['platform'][0];
|
||||
}
|
||||
} elseif( isset($result['platform'][0]) ) {
|
||||
$platform = $result['platform'][0];
|
||||
}
|
||||
}
|
||||
|
||||
if( $platform == 'linux-gnu' || $platform == 'X11' ) {
|
||||
$platform = 'Linux';
|
||||
} elseif( $platform == 'CrOS' ) {
|
||||
$platform = 'Chrome OS';
|
||||
}
|
||||
|
||||
preg_match_all('%(?P<browser>Camino|Kindle(\ Fire)?|Firefox|Iceweasel|IceCat|Safari|MSIE|Trident|AppleWebKit|
|
||||
TizenBrowser|Chrome|Vivaldi|IEMobile|Opera|OPR|Silk|Midori|Edge|CriOS|UCBrowser|Puffin|SamsungBrowser|
|
||||
Baiduspider|Googlebot|YandexBot|bingbot|Lynx|Version|Wget|curl|
|
||||
Valve\ Steam\ Tenfoot|
|
||||
NintendoBrowser|PLAYSTATION\ (\d|Vita)+)
|
||||
(?:\)?;?)
|
||||
(?:(?:[:/ ])(?P<version>[0-9A-Z.]+)|/(?:[A-Z]*))%ix',
|
||||
$u_agent, $result, PREG_PATTERN_ORDER);
|
||||
|
||||
// If nothing matched, return null (to avoid undefined index errors)
|
||||
if( !isset($result['browser'][0]) || !isset($result['version'][0]) ) {
|
||||
if( preg_match('%^(?!Mozilla)(?P<browser>[A-Z0-9\-]+)(/(?P<version>[0-9A-Z.]+))?%ix', $u_agent, $result) ) {
|
||||
return array( 'platform' => $platform ?: null, 'browser' => $result['browser'], 'version' => isset($result['version']) ? $result['version'] ?: null : null );
|
||||
}
|
||||
|
||||
return $empty;
|
||||
}
|
||||
|
||||
if( preg_match('/rv:(?P<version>[0-9A-Z.]+)/si', $u_agent, $rv_result) ) {
|
||||
$rv_result = $rv_result['version'];
|
||||
}
|
||||
|
||||
$browser = $result['browser'][0];
|
||||
$version = $result['version'][0];
|
||||
|
||||
$lowerBrowser = array_map('strtolower', $result['browser']);
|
||||
|
||||
$find = function ( $search, &$key, &$value = null ) use ( $lowerBrowser ) {
|
||||
$search = (array)$search;
|
||||
|
||||
foreach( $search as $val ) {
|
||||
$xkey = array_search(strtolower($val), $lowerBrowser);
|
||||
if( $xkey !== false ) {
|
||||
$value = $val;
|
||||
$key = $xkey;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
$key = 0;
|
||||
$val = '';
|
||||
if( $browser == 'Iceweasel' || strtolower($browser) == 'icecat' ) {
|
||||
$browser = 'Firefox';
|
||||
} elseif( $find('Playstation Vita', $key) ) {
|
||||
$platform = 'PlayStation Vita';
|
||||
$browser = 'Browser';
|
||||
} elseif( $find(array( 'Kindle Fire', 'Silk' ), $key, $val) ) {
|
||||
$browser = $val == 'Silk' ? 'Silk' : 'Kindle';
|
||||
$platform = 'Kindle Fire';
|
||||
if( !($version = $result['version'][$key]) || !is_numeric($version[0]) ) {
|
||||
$version = $result['version'][array_search('Version', $result['browser'])];
|
||||
}
|
||||
} elseif( $find('NintendoBrowser', $key) || $platform == 'Nintendo 3DS' ) {
|
||||
$browser = 'NintendoBrowser';
|
||||
$version = $result['version'][$key];
|
||||
} elseif( $find('Kindle', $key, $platform) ) {
|
||||
$browser = $result['browser'][$key];
|
||||
$version = $result['version'][$key];
|
||||
} elseif( $find('OPR', $key) ) {
|
||||
$browser = 'Opera Next';
|
||||
$version = $result['version'][$key];
|
||||
} elseif( $find('Opera', $key, $browser) ) {
|
||||
$find('Version', $key);
|
||||
$version = $result['version'][$key];
|
||||
} elseif( $find('Puffin', $key, $browser) ) {
|
||||
$version = $result['version'][$key];
|
||||
if( strlen($version) > 3 ) {
|
||||
$part = substr($version, -2);
|
||||
if( ctype_upper($part) ) {
|
||||
$version = substr($version, 0, -2);
|
||||
|
||||
$flags = array( 'IP' => 'iPhone', 'IT' => 'iPad', 'AP' => 'Android', 'AT' => 'Android', 'WP' => 'Windows Phone', 'WT' => 'Windows' );
|
||||
if( isset($flags[$part]) ) {
|
||||
$platform = $flags[$part];
|
||||
}
|
||||
}
|
||||
}
|
||||
} elseif( $find(array( 'IEMobile', 'Edge', 'Midori', 'Vivaldi', 'SamsungBrowser', 'Valve Steam Tenfoot', 'Chrome' ), $key, $browser) ) {
|
||||
$version = $result['version'][$key];
|
||||
} elseif( $rv_result && $find('Trident', $key) ) {
|
||||
$browser = 'MSIE';
|
||||
$version = $rv_result;
|
||||
} elseif( $find('UCBrowser', $key) ) {
|
||||
$browser = 'UC Browser';
|
||||
$version = $result['version'][$key];
|
||||
} elseif( $find('CriOS', $key) ) {
|
||||
$browser = 'Chrome';
|
||||
$version = $result['version'][$key];
|
||||
} elseif( $browser == 'AppleWebKit' ) {
|
||||
if( $platform == 'Android' && !($key = 0) ) {
|
||||
$browser = 'Android Browser';
|
||||
} elseif( strpos($platform, 'BB') === 0 ) {
|
||||
$browser = 'BlackBerry Browser';
|
||||
$platform = 'BlackBerry';
|
||||
} elseif( $platform == 'BlackBerry' || $platform == 'PlayBook' ) {
|
||||
$browser = 'BlackBerry Browser';
|
||||
} else {
|
||||
$find('Safari', $key, $browser) || $find('TizenBrowser', $key, $browser);
|
||||
}
|
||||
|
||||
$find('Version', $key);
|
||||
$version = $result['version'][$key];
|
||||
} elseif( $pKey = preg_grep('/playstation \d/i', array_map('strtolower', $result['browser'])) ) {
|
||||
$pKey = reset($pKey);
|
||||
|
||||
$platform = 'PlayStation ' . preg_replace('/[^\d]/i', '', $pKey);
|
||||
$browser = 'NetFront';
|
||||
}
|
||||
|
||||
return array( 'platform' => $platform ?: null, 'browser' => $browser ?: null, 'version' => $version ?: null );
|
||||
}
|
||||
@@ -1,12 +1,11 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Base API Controller
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Community Suite
|
||||
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) Invision Power Services, Inc.
|
||||
* @license https://www.invisioncommunity.com/legal/standards/
|
||||
* @package Invision Community
|
||||
* @since 3 Dec 2015
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS\Api;
|
||||
@@ -215,6 +214,10 @@ abstract class _Controller
|
||||
);
|
||||
|
||||
$pieces = explode( ' ', $endpoint );
|
||||
if ( !in_array( $pieces[0], array_keys( $badgeStyles ) ) )
|
||||
{
|
||||
\IPS\Output::i()->error( \IPS\Member::loggedIn()->language()->addToStack( 'api_endpoint_phpdoc_error', FALSE, array( "sprintf" => array( $endpoint ) ) ), '3S291/4', 500 );
|
||||
}
|
||||
$pieces[0] = "<span class='ipsBadge ipsBadge_{$size} " . $badgeStyles[ $pieces[0] ] . "'>" . $pieces[0] . "</span>";
|
||||
|
||||
return implode( ' ', $pieces );
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief API Exception
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Community Suite
|
||||
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) Invision Power Services, Inc.
|
||||
* @license https://www.invisioncommunity.com/legal/standards/
|
||||
* @package Invision Community
|
||||
* @since 3 Dec 2015
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS\Api;
|
||||
|
||||
+4
-5
@@ -1,12 +1,11 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief API Key
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Community Suite
|
||||
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) Invision Power Services, Inc.
|
||||
* @license https://www.invisioncommunity.com/legal/standards/
|
||||
* @package Invision Community
|
||||
* @since 3 Dec 2015
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS\Api;
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief PaginatedAPI Response
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Community Suite
|
||||
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) Invision Power Services, Inc.
|
||||
* @license https://www.invisioncommunity.com/legal/standards/
|
||||
* @package Invision Community
|
||||
* @since 3 Dec 2015
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS\Api;
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief API Response
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Community Suite
|
||||
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) Invision Power Services, Inc.
|
||||
* @license https://www.invisioncommunity.com/legal/standards/
|
||||
* @package Invision Community
|
||||
* @since 3 Dec 2015
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS\Api;
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Application Class
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Community Suite
|
||||
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) Invision Power Services, Inc.
|
||||
* @license https://www.invisioncommunity.com/legal/standards/
|
||||
* @package Invision Community
|
||||
* @since 18 Feb 2013
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS;
|
||||
@@ -249,12 +248,12 @@ class _Application extends \IPS\Node\Model
|
||||
/**
|
||||
* Get all extensions
|
||||
*
|
||||
* @param \IPS\Application|string $app The app key of the application which owns the extension
|
||||
* @param string $extension Extension Type
|
||||
* @param \IPS\Member|bool $checkAccess Check access permission for application against supplied member (or logged in member, if TRUE) before including extension
|
||||
* @param string|NULL $firstApp If specified, the application with this key will be returned first
|
||||
* @param string|NULL $firstExtensionKey If specified, the extension with this key will be returned first
|
||||
* @param bool $construct Should an object be returned? (If false, just the classname will be returned)
|
||||
* @param \IPS\Application|string $app The app key of the application which owns the extension
|
||||
* @param string $extension Extension Type
|
||||
* @param \IPS\Member|\IPS\Member\Group|bool $checkAccess Check access permission for application against supplied member/group (or logged in member, if TRUE) before including extension
|
||||
* @param string|NULL $firstApp If specified, the application with this key will be returned first
|
||||
* @param string|NULL $firstExtensionKey If specified, the extension with this key will be returned first
|
||||
* @param bool $construct Should an object be returned? (If false, just the classname will be returned)
|
||||
* @return array
|
||||
*/
|
||||
public static function allExtensions( $app, $extension, $checkAccess=TRUE, $firstApp=NULL, $firstExtensionKey=NULL, $construct=TRUE )
|
||||
@@ -512,58 +511,35 @@ class _Application extends \IPS\Node\Model
|
||||
$correctMd5s = \IPS\Http\Url::ips( 'md5' )->request()->get()->decodeJson();
|
||||
|
||||
/* And return whichever ones don't match */
|
||||
return static::_md5sumCheckerIterator( \IPS\ROOT_PATH, $correctMd5s, 1 );
|
||||
}
|
||||
|
||||
/**
|
||||
* MD5 check directory
|
||||
*
|
||||
* @param string $directory Directory to look through
|
||||
* @param array $correctMd5s The correct md5 hashes
|
||||
* @param int $depth How deep into the folder structure we are
|
||||
* @return array
|
||||
*/
|
||||
public static function _md5sumCheckerIterator( $directory, $correctMd5s, $depth = 1 )
|
||||
{
|
||||
$return = array();
|
||||
foreach( new \DirectoryIterator( $directory ) as $file )
|
||||
foreach ( $correctMd5s as $file => $md5Hash )
|
||||
{
|
||||
if ( mb_substr( $file, 0, 1 ) === '.' or mb_substr( $file, 0, 1 ) === '_' or $file == 'index.html' )
|
||||
/* Fix the admin directory */
|
||||
$file = preg_replace( '/^\/admin\//', '/' . \IPS\CP_DIRECTORY . '/', $file );
|
||||
|
||||
/* If this is an application directory but the application doesn't exist or has been disabled then we shouldn't check it */
|
||||
preg_match( '/^\/applications\/(.+?)\//', $file, $matches );
|
||||
if ( $matches )
|
||||
{
|
||||
if( in_array( $matches[1], static::$ipsApps ) AND !static::appIsEnabled( $matches[1] ) )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
/* Ignore init.php (which always changes on build) and conf_global.dist.php (which doesn't exist after install) */
|
||||
if ( in_array( $file, array( '/init.php', '/conf_global.dist.php' ) ) )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( $file->isDir() )
|
||||
/* MD5 check */
|
||||
if ( !file_exists( \IPS\ROOT_PATH . $file ) or !\IPS\Login::compareHashes( md5( preg_replace( '#\s#', '', utf8_decode( file_get_contents( \IPS\ROOT_PATH . $file ) ) ) ), $md5Hash ) ) // We strip whitespace since FTP in ASCII mode will change the whitespace characters
|
||||
{
|
||||
/* We only want to check directories which are ours (not other random directories they have on the server) and we can ignore datastore, plugins and uploads */
|
||||
if ( $depth === 1 and !in_array( $file->getFilename(), array( \IPS\CP_DIRECTORY, 'api', 'applications', 'system' ) ) )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
/* 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' )
|
||||
{
|
||||
/* If the directory name is a valid application key and the application is not enabled, don't check it */
|
||||
if( in_array( $file->getFilename(), static::$ipsApps ) AND !static::appIsEnabled( $file->getFilename() ) )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$return = array_merge( $return, static::_md5sumCheckerIterator( $file->getPathname(), $correctMd5s, $depth + 1 ) );
|
||||
}
|
||||
elseif ( mb_substr( $file, -4 ) === '.php' )
|
||||
{
|
||||
$fullPath = $file->getPathname();
|
||||
$shortPath = str_replace( '\\', '/', mb_substr( $fullPath, mb_strlen( \IPS\ROOT_PATH ) ) );
|
||||
|
||||
if ( $shortPath != '/init.php' and isset( $correctMd5s[ $shortPath ] ) and !\IPS\Login::compareHashes( 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[] = \IPS\ROOT_PATH . $file;
|
||||
}
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
@@ -727,10 +703,10 @@ EOF;
|
||||
/**
|
||||
* Get Extensions
|
||||
*
|
||||
* @param \IPS\Application|string $app The app key of the application which owns the extension
|
||||
* @param string $extension Extension Type
|
||||
* @param bool $construct Should an object be returned? (If false, just the classname will be returned)
|
||||
* @param \IPS\Member|bool $checkAccess Check access permission for extension against supplied member (or logged in member, if TRUE)
|
||||
* @param \IPS\Application|string $app The app key of the application which owns the extension
|
||||
* @param string $extension Extension Type
|
||||
* @param bool $construct Should an object be returned? (If false, just the classname will be returned)
|
||||
* @param \IPS\Member|\IPS\Member\Group|bool $checkAccess Check access permission for extension against supplied member/group (or logged in member, if TRUE)
|
||||
* @return array
|
||||
*/
|
||||
public function extensions( $app, $extension, $construct=TRUE, $checkAccess=FALSE )
|
||||
@@ -750,6 +726,19 @@ EOF;
|
||||
if ( !$file->isDir() and !$file->isDot() and mb_substr( $file, -4 ) === '.php' AND mb_substr( $file, 0, 2 ) != '._' )
|
||||
{
|
||||
$classname = 'IPS\\' . $this->directory . '\extensions\\' . $app . '\\' . $extension . '\\' . mb_substr( $file, 0, -4 );
|
||||
|
||||
/* Check if class exists - sometimes we have to use blank files to wipe out old extensions */
|
||||
try
|
||||
{
|
||||
if( !class_exists( $classname ) )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
catch( \ErrorException $e )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( method_exists( $classname, 'generate' ) )
|
||||
{
|
||||
@@ -1510,7 +1499,7 @@ EOF;
|
||||
}
|
||||
catch( \IPS\Db\Exception $e )
|
||||
{
|
||||
\IPS\Log::log( "Error: " . $instruction['params']['method'] . ' ' . json_encode( $instruction['params']['params'] ), 'upgrade_error' );
|
||||
\IPS\Log::log( "Error (" . $e->getCode() . ") " . $e->getMessage() . ": " . $instruction['params']['method'] . ' ' . json_encode( $instruction['params']['params'] ), 'upgrade_error' );
|
||||
|
||||
/* If the issue is with a create table other than exists, we should just throw it */
|
||||
if ( $instruction['params']['method'] == 'createTable' and ! in_array( $e->getCode(), array( 1007, 1050 ) ) )
|
||||
@@ -2603,14 +2592,21 @@ EOF;
|
||||
$tableChanges = array();
|
||||
$needIgnore = false;
|
||||
$innoDbFullTextIndexes = array();
|
||||
|
||||
|
||||
/* Get our local definition of this table */
|
||||
try
|
||||
{
|
||||
$localDefinition = \IPS\Db::i()->getTableDefinition( $tableName );
|
||||
$originalDefinition = $localDefinition; #Store this before it is normalised and engine stripped
|
||||
$localDefinition = \IPS\Db::i()->normalizeDefinition( $localDefinition );
|
||||
$compareDefinition = \IPS\Db::i()->normalizeDefinition( $tableDefinition );
|
||||
$tableDefinition = \IPS\Db::i()->updateDefinitionIndexLengths( $tableDefinition );
|
||||
|
||||
/* Ensure that we use the proper engine, not whatever is in the schema.json as this will confuse index sub_part lengths */
|
||||
if ( isset( $originalDefinition['engine'] ) )
|
||||
{
|
||||
$tableDefinition['engine'] = $originalDefinition['engine'];
|
||||
}
|
||||
|
||||
if ( $compareDefinition != $localDefinition )
|
||||
{
|
||||
@@ -2787,6 +2783,17 @@ EOF;
|
||||
{
|
||||
if ( $indexName != 'PRIMARY' and !isset( $compareDefinition['indexes'][ $indexName ] ) )
|
||||
{
|
||||
/* If the index is on a column which we don't recognise (which may happen on tables which we add columns to like the ones that
|
||||
store custom fields, or very naughty third parties adding columns on tables they don't own), don't drop it */
|
||||
foreach ( $indexData['columns'] as $indexedColumn )
|
||||
{
|
||||
if ( !isset( $compareDefinition['columns'][ $indexedColumn ] ) )
|
||||
{
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
|
||||
/* Still here? Go ahead */
|
||||
$dropIndexQuery = "DROP INDEX `{$db->escape_string( $indexName )}`";
|
||||
if ( !in_array( $dropIndexQuery, $tableChanges ) ) // We skip the primary key as it can cause errors related to auto-increment
|
||||
{
|
||||
@@ -2892,24 +2899,32 @@ EOF;
|
||||
static::writeJson( $queriesJsonFile, $write );
|
||||
}
|
||||
|
||||
/* Add the actual version number in upgrade.php */
|
||||
$upgradeFile = $workingDir . "/upgrade.php";
|
||||
if ( file_exists( $upgradeFile ) )
|
||||
/* Add the actual version number in upgrade.php & options.php */
|
||||
$versionReplacement = function( $file ) use ( $human, $long )
|
||||
{
|
||||
$contents = file_get_contents( $upgradeFile );
|
||||
$contents = str_replace(
|
||||
array(
|
||||
'{version_human}',
|
||||
'upg_working'
|
||||
),
|
||||
array(
|
||||
$human,
|
||||
"upg_{$long}"
|
||||
),
|
||||
$contents
|
||||
);
|
||||
\file_put_contents( $upgradeFile, $contents );
|
||||
}
|
||||
if ( file_exists( $file ) )
|
||||
{
|
||||
$contents = file_get_contents( $file );
|
||||
$contents = str_replace(
|
||||
array(
|
||||
'{version_human}',
|
||||
'upg_working',
|
||||
'{version_long}'
|
||||
),
|
||||
array(
|
||||
$human,
|
||||
"upg_{$long}",
|
||||
$long
|
||||
),
|
||||
$contents
|
||||
);
|
||||
\file_put_contents( $file, $contents );
|
||||
}
|
||||
};
|
||||
|
||||
/* Make the replacement */
|
||||
$versionReplacement( $workingDir . "/upgrade.php" );
|
||||
$versionReplacement( $workingDir . "/options.php" );
|
||||
|
||||
/* Rename the directory */
|
||||
rename( $workingDir, $setupDir . "/upg_{$long}" );
|
||||
@@ -3013,7 +3028,7 @@ EOF;
|
||||
$xml->text( "Invision Power Services, Inc" );
|
||||
$xml->endAttribute();
|
||||
$xml->startAttribute('author_url');
|
||||
$xml->text( "http://www.invisionpower.com" );
|
||||
$xml->text( "https://www.invisioncommunity.com" );
|
||||
$xml->endAttribute();
|
||||
|
||||
/* Skin settings */
|
||||
@@ -3612,10 +3627,10 @@ EOF;
|
||||
/**
|
||||
* Can the user access this application?
|
||||
*
|
||||
* @param \IPS\Member|NULL $member Member we are checking against or NULL for currently logged on user
|
||||
* @param \IPS\Member|\IPS\Member\Group|NULL $memberOrGroup Member/group we are checking against or NULL for currently logged on user
|
||||
* @return bool
|
||||
*/
|
||||
public function canAccess( $member=NULL )
|
||||
public function canAccess( $memberOrGroup=NULL )
|
||||
{
|
||||
/* If it's not enabled, we can't */
|
||||
if( !$this->enabled )
|
||||
@@ -3636,8 +3651,15 @@ EOF;
|
||||
}
|
||||
|
||||
/* Check member */
|
||||
$member = ( $member === NULL ) ? \IPS\Member::loggedIn() : $member;
|
||||
$memberGroups = array_merge( array( $member->member_group_id ), array_filter( explode( ',', $member->mgroup_others ) ) );
|
||||
if ( $memberOrGroup instanceof \IPS\Member\Group )
|
||||
{
|
||||
$memberGroups = array( $memberOrGroup->g_id );
|
||||
}
|
||||
else
|
||||
{
|
||||
$member = ( $memberGroups === NULL ) ? \IPS\Member::loggedIn() : $memberGroups;
|
||||
$memberGroups = array_merge( array( $member->member_group_id ), array_filter( explode( ',', $member->mgroup_others ) ) );
|
||||
}
|
||||
$accessGroups = explode( ',', $this->disabled_groups );
|
||||
|
||||
/* Are we in an allowed group? */
|
||||
@@ -3672,6 +3694,18 @@ EOF;
|
||||
public function save( $skipMember=FALSE )
|
||||
{
|
||||
parent::save();
|
||||
static::postToggleEnable( $skipMember );
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup after saving
|
||||
*
|
||||
* @param bool $skipMember Skip clearing member cache clearing
|
||||
* @return void
|
||||
* @note This is abstracted so it can be called externally, i.e. by the support tool
|
||||
*/
|
||||
public static function postToggleEnable( $skipMember=FALSE )
|
||||
{
|
||||
unset( \IPS\Data\Store::i()->applications );
|
||||
unset( \IPS\Data\Store::i()->frontNavigation );
|
||||
|
||||
@@ -3724,9 +3758,15 @@ EOF;
|
||||
$templatesToRecompile[ $class ] = $class;
|
||||
}
|
||||
|
||||
/* Delete profile steps */
|
||||
\IPS\Member\ProfileStep::deleteByApplication( $this );
|
||||
|
||||
/* Delete menu items */
|
||||
\IPS\core\FrontNavigation::deleteByApplication( $this );
|
||||
|
||||
/* Delete club node maps */
|
||||
\IPS\Member\Club::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 ) ) ) );
|
||||
@@ -3742,6 +3782,7 @@ EOF;
|
||||
\IPS\Db::i()->delete( 'core_queue', array( 'app=?', $this->directory ) );
|
||||
\IPS\Db::i()->delete( 'core_follow', array( 'follow_app=?', $this->directory ) );
|
||||
\IPS\Db::i()->delete( 'core_view_updates', array( "classname LIKE CONCAT( ?, '%' )", "IPS\\\\{$this->directory}" ) );
|
||||
\IPS\Db::i()->delete( 'core_moderator_logs', array( 'appcomponent=?', $this->directory ) );
|
||||
|
||||
$classes = array();
|
||||
foreach( $this->extensions( 'core', 'ContentRouter' ) AS $contentRouter )
|
||||
@@ -3778,6 +3819,9 @@ EOF;
|
||||
}
|
||||
|
||||
\IPS\Db::i()->delete( 'core_notifications', \IPS\Db::i()->in( 'item_class', $classes ) );
|
||||
|
||||
/* Delete Deletion Log Records */
|
||||
\IPS\Db::i()->delete( 'core_deletion_log', \IPS\Db::i()->in( 'dellog_content_class', $classes ) );
|
||||
}
|
||||
|
||||
unset( \IPS\Data\Store::i()->settings );
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Application builder custom filter iterator
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Community Suite
|
||||
* @since 8 Aug 2013
|
||||
* @version SVN_VERSION_NUMBER
|
||||
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) Invision Power Services, Inc.
|
||||
* @license https://www.invisioncommunity.com/legal/standards/
|
||||
* @package Invision Community
|
||||
* @since 8 Aug 2013␍
|
||||
*/
|
||||
|
||||
namespace IPS\Application;
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Application builder custom filter iterator
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Community Suite
|
||||
* @since 8 Aug 2013
|
||||
* @version SVN_VERSION_NUMBER
|
||||
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) Invision Power Services, Inc.
|
||||
* @license https://www.invisioncommunity.com/legal/standards/
|
||||
* @package Invision Community
|
||||
* @since 8 Aug 2013␍
|
||||
*/
|
||||
|
||||
namespace IPS\Application;
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Application Developer Class
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Community Suite
|
||||
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) Invision Power Services, Inc.
|
||||
* @license https://www.invisioncommunity.com/legal/standards/
|
||||
* @package Invision Community
|
||||
* @since 17 October 2013
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS\Application;
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Module Class
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Community Suite
|
||||
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) Invision Power Services, Inc.
|
||||
* @license https://www.invisioncommunity.com/legal/standards/
|
||||
* @package Invision Community
|
||||
* @since 18 Feb 2013
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS\Application;
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Archive Class
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Community Suite
|
||||
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) Invision Power Services, Inc.
|
||||
* @license https://www.invisioncommunity.com/legal/standards/
|
||||
* @package Invision Community
|
||||
* @since 28 Jul 2015
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS;
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Archive Exception Class
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Community Suite
|
||||
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) Invision Power Services, Inc.
|
||||
* @license https://www.invisioncommunity.com/legal/standards/
|
||||
* @package Invision Community
|
||||
* @since 28 Jul 2015
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS\Archive;
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief PclZip Zip Class
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Community Suite
|
||||
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) Invision Power Services, Inc.
|
||||
* @license https://www.invisioncommunity.com/legal/standards/
|
||||
* @package Invision Community
|
||||
* @since 28 Jul 2015
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS\Archive\Zip;
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Zip Archive Class
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Community Suite
|
||||
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) Invision Power Services, Inc.
|
||||
* @license https://www.invisioncommunity.com/legal/standards/
|
||||
* @package Invision Community
|
||||
* @since 28 Jul 2015
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS\Archive;
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief ZipArchive Zip Class
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Community Suite
|
||||
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) Invision Power Services, Inc.
|
||||
* @license https://www.invisioncommunity.com/legal/standards/
|
||||
* @package Invision Community
|
||||
* @since 28 Jul 2015
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS\Archive\Zip;
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Base API endpoint for Content Comments
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Community Suite
|
||||
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) Invision Power Services, Inc.
|
||||
* @license https://www.invisioncommunity.com/legal/standards/
|
||||
* @package Invision Community
|
||||
* @since 8 Dec 2015
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS\Content\Api;
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Base API endpoint for Content Items
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Community Suite
|
||||
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) Invision Power Services, Inc.
|
||||
* @license https://www.invisioncommunity.com/legal/standards/
|
||||
* @package Invision Community
|
||||
* @since 8 Dec 2015
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS\Content\Api;
|
||||
|
||||
@@ -0,0 +1,279 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Trait for Content Containers which can be used in Clubs
|
||||
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) Invision Power Services, Inc.
|
||||
* @license https://www.invisioncommunity.com/legal/standards/
|
||||
* @package Invision Community
|
||||
* @since 17 Feb 2017
|
||||
*/
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Trait for Content Containers which can be used in Clubs
|
||||
*/
|
||||
trait ClubContainer
|
||||
{
|
||||
/**
|
||||
* Get the database column which stores the club ID
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function clubIdColumn()
|
||||
{
|
||||
return 'club_id';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get front-end language string
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function clubFrontTitle()
|
||||
{
|
||||
$itemClass = static::$contentItemClass;
|
||||
return $itemClass::$title . '_pl';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get acp language string
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function clubAcpTitle()
|
||||
{
|
||||
return static::$nodeTitle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the associated club
|
||||
*
|
||||
* @return \IPS\Member\Club|NULL
|
||||
*/
|
||||
public function club()
|
||||
{
|
||||
if ( \IPS\Settings::i()->clubs )
|
||||
{
|
||||
$clubIdColumn = $this->clubIdColumn();
|
||||
if ( $this->$clubIdColumn )
|
||||
{
|
||||
try
|
||||
{
|
||||
return \IPS\Member\Club::load( $this->$clubIdColumn );
|
||||
}
|
||||
catch ( \OutOfRangeException $e ) { }
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set form for creating a node of this type in a club
|
||||
*
|
||||
* @param \IPS\Helpers\Form $form Form object
|
||||
* @return void
|
||||
*/
|
||||
public function clubForm( \IPS\Helpers\Form $form )
|
||||
{
|
||||
$itemClass = static::$contentItemClass;
|
||||
$form->add( new \IPS\Helpers\Form\Text( 'club_node_name', $this->_id ? $this->_title : \IPS\Member::loggedIn()->language()->addToStack( static::clubFrontTitle() ), TRUE, array( 'maxLength' => 255 ) ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Save club form
|
||||
*
|
||||
* @param \IPS\Member\Club $club The club
|
||||
* @param array $values Values
|
||||
* @return void
|
||||
*/
|
||||
public function saveClubForm( \IPS\Member\Club $club, $values )
|
||||
{
|
||||
$nodeClass = get_called_class();
|
||||
$itemClass = $nodeClass::$contentItemClass;
|
||||
|
||||
$this->_saveClubForm( $club, $values );
|
||||
|
||||
$needToUpdatePermissions = FALSE;
|
||||
if ( !$this->_id )
|
||||
{
|
||||
$clubIdColumn = $this->clubIdColumn();
|
||||
$this->$clubIdColumn = $club->id;
|
||||
$this->save();
|
||||
\IPS\Db::i()->insert( 'core_clubs_node_map', array(
|
||||
'club_id' => $club->id,
|
||||
'node_class' => $nodeClass,
|
||||
'node_id' => $this->_id,
|
||||
'name' => $values['club_node_name']
|
||||
) );
|
||||
|
||||
$needToUpdatePermissions = TRUE;
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->save();
|
||||
\IPS\Db::i()->update( 'core_clubs_node_map', array( 'name' => $values['club_node_name'] ), array( 'club_id=? AND node_class=? AND node_id=?', $club->id, $nodeClass, $this->_id ) );
|
||||
}
|
||||
|
||||
\IPS\Lang::saveCustom( $itemClass::$application, static::$titleLangPrefix . $this->_id, $values['club_node_name'] );
|
||||
\IPS\Lang::saveCustom( $itemClass::$application, static::$titleLangPrefix . $this->_id . '_desc', isset( $values['club_node_description'] ) ? $values['club_node_description'] : '' );
|
||||
|
||||
if ( $needToUpdatePermissions )
|
||||
{
|
||||
$this->setPermissionsToClub( $club );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Class-specific routine when saving club form
|
||||
*
|
||||
* @param \IPS\Member\Club $club The club
|
||||
* @param array $values Values
|
||||
* @return void
|
||||
*/
|
||||
public function _saveClubForm( \IPS\Member\Club $club, $values )
|
||||
{
|
||||
// Deliberately does nothing so classes can override
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the permission index permissions to a specific club
|
||||
*
|
||||
* @param \IPS\Member\Club $club The club
|
||||
* @return void
|
||||
*/
|
||||
public function setPermissionsToClub( \IPS\Member\Club $club )
|
||||
{
|
||||
/* Delete current rows */
|
||||
\IPS\Db::i()->delete( 'core_permission_index', array( 'app=? AND perm_type=? AND perm_type_id=?', static::$permApp, static::$permType, $this->_id ) );
|
||||
|
||||
/* Build new rows */
|
||||
$insert = array( 'app' => static::$permApp, 'perm_type' => static::$permType, 'perm_type_id' => $this->_id );
|
||||
foreach ( static::$permissionMap as $k => $v )
|
||||
{
|
||||
if ( in_array( $k, array( 'view', 'read' ) ) )
|
||||
{
|
||||
switch ( $club->type )
|
||||
{
|
||||
case $club::TYPE_PUBLIC:
|
||||
case $club::TYPE_OPEN:
|
||||
$insert[ 'perm_' . $v ] = '*';
|
||||
break;
|
||||
|
||||
case $club::TYPE_CLOSED:
|
||||
case $club::TYPE_PRIVATE:
|
||||
$insert[ 'perm_' . $v ] = "cm,c{$club->id}";
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
switch ( $club->type )
|
||||
{
|
||||
case $club::TYPE_PUBLIC:
|
||||
$insert[ 'perm_' . $v ] = 'ca';
|
||||
break;
|
||||
|
||||
case $club::TYPE_OPEN:
|
||||
case $club::TYPE_CLOSED:
|
||||
case $club::TYPE_PRIVATE:
|
||||
$insert[ 'perm_' . $v ] = "cm,c{$club->id}";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Insert */
|
||||
$permId = \IPS\Db::i()->insert( 'core_permission_index', $insert );
|
||||
|
||||
/* Update tags permission cache */
|
||||
if ( isset( static::$permissionMap['read'] ) )
|
||||
{
|
||||
\IPS\Db::i()->update( 'core_tags_perms', array( 'tag_perm_text' => $insert[ 'perm_' . static::$permissionMap['read'] ] ), array( 'tag_perm_aap_lookup=?', md5( static::$permApp . ';' . static::$permType . ';' . $this->_id ) ) );
|
||||
}
|
||||
|
||||
/* Make sure this object resets the permissions internally */
|
||||
$this->_permissions = array_merge( array( 'perm_id' => $permId ), $insert );
|
||||
|
||||
/* Update search index */
|
||||
$this->updateSearchIndexPermissions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch All Root Nodes
|
||||
*
|
||||
* @param string|NULL $permissionCheck The permission key to check for or NULL to not check permissions
|
||||
* @param \IPS\Member|NULL $member The member to check permissions for or NULL for the currently logged in member
|
||||
* @param mixed $where Additional WHERE clause
|
||||
* @return array
|
||||
*/
|
||||
public static function rootsWithClubs( $permissionCheck='view', $member=NULL, $where=array() )
|
||||
{
|
||||
/* Will we need to check permissions? */
|
||||
if ( $permissionCheck !== NULL )
|
||||
{
|
||||
$member = $member ?: \IPS\Member::loggedIn();
|
||||
}
|
||||
|
||||
if( static::$databaseColumnParent !== NULL )
|
||||
{
|
||||
$where[] = array( static::$databasePrefix . static::$databaseColumnParent . '=?', static::$databaseColumnParentRootValue );
|
||||
}
|
||||
|
||||
return static::nodesWithPermission( $permissionCheck, $member, $where );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check Moderator Permission
|
||||
*
|
||||
* @param string $type 'edit', 'hide', 'unhide', 'delete', etc.
|
||||
* @param \IPS\Member|NULL $member The member to check for or NULL for the currently logged in member
|
||||
* @return bool
|
||||
*/
|
||||
public function modPermission( $type, \IPS\Member $member )
|
||||
{
|
||||
if ( \IPS\Settings::i()->clubs )
|
||||
{
|
||||
$clubIdColumn = $this->clubIdColumn();
|
||||
if ( $this->$clubIdColumn and $club = $this->club() )
|
||||
{
|
||||
if ( in_array( $club->memberStatus( $member ), array( \IPS\Member\Club::STATUS_LEADER, \IPS\Member\Club::STATUS_MODERATOR ) ) )
|
||||
{
|
||||
if ( in_array( $type, explode( ',', \IPS\Settings::i()->clubs_modperms ) ) )
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return parent::modPermission( $type, $member );
|
||||
}
|
||||
|
||||
/**
|
||||
* [Node] Get parent list
|
||||
*
|
||||
* @return \SplStack
|
||||
*/
|
||||
public function parents()
|
||||
{
|
||||
$clubIdColumn = static::clubIdColumn();
|
||||
|
||||
/* Only do this if this node is actually associated with a club */
|
||||
if ( $this->$clubIdColumn )
|
||||
{
|
||||
return new \SplStack;
|
||||
}
|
||||
else
|
||||
{
|
||||
return parent::parents();
|
||||
}
|
||||
}
|
||||
}
|
||||
+212
-45
@@ -1,12 +1,11 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Content Comment Model
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Community Suite
|
||||
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) Invision Power Services, Inc.
|
||||
* @license https://www.invisioncommunity.com/legal/standards/
|
||||
* @package Invision Community
|
||||
* @since 8 Jul 2013
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS\Content;
|
||||
@@ -168,37 +167,13 @@ abstract class _Comment extends \IPS\Content
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Check if profanity filters should mod-queue this comment */
|
||||
$obj->checkProfanityFilters( $first );
|
||||
|
||||
/* Save the comment */
|
||||
$obj->save();
|
||||
|
||||
/* Pass this through our profanity filters to see if it needs to be mod queued */
|
||||
try
|
||||
{
|
||||
$hiddenByFilter = FALSE;
|
||||
if ( !$obj->hidden() AND ( ! $item::$firstCommentRequired or ( $item::$firstCommentRequired and !$obj->isFirst() ) ) AND !$member->group['g_bypass_badwords'] )
|
||||
{
|
||||
$hiddenByFilter = \IPS\core\Profanity::hiddenByFilters( $comment );
|
||||
}
|
||||
}
|
||||
catch( \BadMethodCallException $e ) { }
|
||||
|
||||
if ( $hiddenByFilter === TRUE )
|
||||
{
|
||||
if ( isset( static::$databaseColumnMap['approved'] ) )
|
||||
{
|
||||
/* 'approved' is easy, clear and concise */
|
||||
$column = static::$databaseColumnMap['approved'];
|
||||
$obj->$column = 0;
|
||||
}
|
||||
else if ( isset( static::$databaseColumnMap['hidden'] ) )
|
||||
{
|
||||
/* 'hidden' is backwards */
|
||||
$column = static::$databaseColumnMap['hidden'];
|
||||
$obj->$column = 1;
|
||||
}
|
||||
|
||||
$obj->save();
|
||||
}
|
||||
|
||||
/* Increment post count */
|
||||
try
|
||||
{
|
||||
@@ -271,6 +246,81 @@ abstract class _Comment extends \IPS\Content
|
||||
/* Return */
|
||||
return $obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check comment against profanity filters
|
||||
*
|
||||
* @note We do not save here, as saving is done in user-land code after
|
||||
* @param bool $first Is this the first comment?
|
||||
* @param bool $edit Are we editing or merging (true) or is this a new comment (false)?
|
||||
* @return bool Whether to send unapproved notifications
|
||||
*/
|
||||
protected function checkProfanityFilters( $first=FALSE, $edit=TRUE )
|
||||
{
|
||||
/* We need our item */
|
||||
$item = $this->item();
|
||||
|
||||
/* And the author */
|
||||
$member = $this->author();
|
||||
|
||||
/* Pass this through our profanity filters to see if it needs to be mod queued */
|
||||
$sendNotifications = FALSE;
|
||||
|
||||
try
|
||||
{
|
||||
$hiddenByFilter = FALSE;
|
||||
if ( !$this->hidden() AND ( ! $item::$firstCommentRequired or ( $item::$firstCommentRequired and !$first ) ) AND !$member->group['g_bypass_badwords'] )
|
||||
{
|
||||
$hiddenByFilter = \IPS\core\Profanity::hiddenByFilters( $this->content() );
|
||||
}
|
||||
/* If the comment is hidden (probably requires moderator approval) and this is the first post in the item that requires a comment (i.e. a post in a topic), then we need to set the item as requiring approval instead. */
|
||||
elseif ( $item instanceof \IPS\Content\Hideable AND !$member->group['g_bypass_badwords'] AND ( $item::$firstCommentRequired and $first ) )
|
||||
{
|
||||
$itemHiddenByFilter = \IPS\core\Profanity::hiddenByFilters( $this->content() );
|
||||
|
||||
if ( isset( $item::$databaseColumnMap['approved'] ) and $itemHiddenByFilter )
|
||||
{
|
||||
/* 'approved' is easy, clear and concise */
|
||||
$column = $item::$databaseColumnMap['approved'];
|
||||
$item->$column = 0;
|
||||
$item->save();
|
||||
}
|
||||
elseif ( isset( $item::$databaseColumnMap['hidden'] ) and $itemHiddenByFilter )
|
||||
{
|
||||
/* 'hidden' is backwards */
|
||||
$column = $item::$databaseColumnMap['hidden'];
|
||||
$item->$column = 1;
|
||||
$item->save();
|
||||
}
|
||||
|
||||
if( $edit === TRUE )
|
||||
{
|
||||
$sendNotifications = TRUE;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch( \BadMethodCallException $e ) { }
|
||||
|
||||
if ( $hiddenByFilter === TRUE )
|
||||
{
|
||||
if ( isset( static::$databaseColumnMap['approved'] ) )
|
||||
{
|
||||
/* 'approved' is easy, clear and concise */
|
||||
$column = static::$databaseColumnMap['approved'];
|
||||
$this->$column = 0;
|
||||
}
|
||||
else if ( isset( static::$databaseColumnMap['hidden'] ) )
|
||||
{
|
||||
/* 'hidden' is backwards */
|
||||
$column = static::$databaseColumnMap['hidden'];
|
||||
$this->$column = 1;
|
||||
}
|
||||
|
||||
$sendNotifications = TRUE;
|
||||
}
|
||||
|
||||
return $sendNotifications;
|
||||
}
|
||||
|
||||
/**
|
||||
* Join profile fields when loading comments?
|
||||
@@ -378,6 +428,9 @@ abstract class _Comment extends \IPS\Content
|
||||
$item->container()->save();
|
||||
}
|
||||
}
|
||||
|
||||
/* Are we tracking keywords? */
|
||||
$this->checkKeywords( $this->content() );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -482,7 +535,7 @@ abstract class _Comment extends \IPS\Content
|
||||
}
|
||||
|
||||
/* Can the member edit their own content? */
|
||||
if ( $member->member_id == $this->author()->member_id and $member->group['g_edit_posts'] and ( !( $item instanceof \IPS\Content\Lockable ) or !$item->locked() ) )
|
||||
if ( $member->member_id == $this->author()->member_id and ( $member->group['g_edit_posts'] == '1' or in_array( get_class( $item ), explode( ',', $member->group['g_edit_posts'] ) ) ) and ( !( $item instanceof \IPS\Content\Lockable ) or !$item->locked() ) )
|
||||
{
|
||||
if ( !$member->group['g_edit_cutoff'] )
|
||||
{
|
||||
@@ -518,7 +571,7 @@ abstract class _Comment extends \IPS\Content
|
||||
}
|
||||
catch ( \BadMethodCallException $e ) { }
|
||||
|
||||
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'] ) ) );
|
||||
return ( !$this->isFirst() and ( static::modPermission( 'hide', $member, $container ) or ( $member->member_id and $member->member_id == $this->author()->member_id and ( $member->group['g_hide_own_posts'] == '1' or in_array( get_class( $this->item() ), explode( ',', $member->group['g_hide_own_posts'] ) ) ) ) ) );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -565,7 +618,7 @@ abstract class _Comment extends \IPS\Content
|
||||
}
|
||||
catch ( \BadMethodCallException $e ) { }
|
||||
|
||||
return ( !$this->isFirst() and ( static::modPermission( 'delete', $member, $container ) or ( $member->member_id and $member->member_id == $this->author()->member_id and $member->group['g_delete_own_posts'] ) ) );
|
||||
return ( !$this->isFirst() and ( static::modPermission( 'delete', $member, $container ) or ( $member->member_id and $member->member_id == $this->author()->member_id and ( $member->group['g_delete_own_posts'] == '1' or in_array( get_class( $this->item() ), explode( ',', $member->group['g_delete_own_posts'] ) ) ) ) ) );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -596,6 +649,28 @@ abstract class _Comment extends \IPS\Content
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Can report?
|
||||
*
|
||||
* @param \IPS\Member|NULL $member The member to check for (NULL for currently logged in member)
|
||||
* @return bool
|
||||
*/
|
||||
public function canReport( $member=NULL )
|
||||
{
|
||||
if ( !( $this instanceof \IPS\Content\ReportCenter ) )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
if ( $this->hidden() === 1 )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
$member = $member ?: \IPS\Member::loggedIn();
|
||||
return ( $member->group['g_can_report'] == '1' or in_array( get_class( $this->item() ), explode( ',', $member->group['g_can_report'] ) ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Search Index Permissions
|
||||
*
|
||||
@@ -624,6 +699,11 @@ abstract class _Comment extends \IPS\Content
|
||||
*/
|
||||
public function isIgnored( $member=NULL )
|
||||
{
|
||||
if ( !\IPS\Settings::i()->ignore_system_on )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
if ( $member === NULL )
|
||||
{
|
||||
$member = \IPS\Member::loggedIn();
|
||||
@@ -661,16 +741,46 @@ abstract class _Comment extends \IPS\Content
|
||||
$valueField = static::$databaseColumnMap['content'];
|
||||
$oldValue = $this->$valueField;
|
||||
$this->$valueField = $newContent;
|
||||
|
||||
$isHiddenNow = $this->hidden();
|
||||
|
||||
/* Check if profanity filters should mod-queue this comment */
|
||||
$sendNotifications = $this->checkProfanityFilters( $this->isFirst(), TRUE );
|
||||
|
||||
$this->save();
|
||||
|
||||
/* Send any new mention/quote notifications */
|
||||
$this->sendAfterEditNotifications( $oldValue );
|
||||
|
||||
/* Resync the item - necessary as profanity filters may have just hidden an otherwise approved post */
|
||||
$item = $this->item();
|
||||
if( $isHiddenNow != $this->hidden() )
|
||||
{
|
||||
$item->resyncCommentCounts();
|
||||
$item->resyncLastComment();
|
||||
$item->save();
|
||||
}
|
||||
|
||||
if ( $container = $item->containerWrapper() )
|
||||
{
|
||||
$container->resetCommentCounts();
|
||||
$container->save();
|
||||
}
|
||||
|
||||
/* Reindex */
|
||||
if ( $this instanceof \IPS\Content\Searchable )
|
||||
{
|
||||
\IPS\Content\Search\Index::i()->index( $this );
|
||||
}
|
||||
|
||||
/* Send notifications */
|
||||
if ( $sendNotifications AND !in_array( 'IPS\Content\Review', class_parents( get_called_class() ) ) )
|
||||
{
|
||||
if( $this->hidden() === 1 )
|
||||
{
|
||||
$this->sendUnapprovedNotification();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -852,10 +962,11 @@ abstract class _Comment extends \IPS\Content
|
||||
/**
|
||||
* Move Comment to another item
|
||||
*
|
||||
* @param \IPS\Content\Item $item The item to move this comment too.
|
||||
* @param \IPS\Content\Item $item The item to move this comment to
|
||||
* @param bool $skip Skip rebuilding new/old content item data (used for multimod where we can do it in one go after)
|
||||
* @return void
|
||||
*/
|
||||
public function move( \IPS\Content\Item $item )
|
||||
public function move( \IPS\Content\Item $item, $skip=FALSE )
|
||||
{
|
||||
$oldItem = $this->item();
|
||||
|
||||
@@ -872,13 +983,16 @@ abstract class _Comment extends \IPS\Content
|
||||
\IPS\Db::i()->update( 'core_notifications', array( 'item_id' => $item->$idColumn ), array( 'item_class=? and item_id=? and item_sub_class=? and item_sub_id=?', (string) get_class( $item ), $oldItem->$idColumn, (string) get_called_class(), $this->$commentIdColumn ) );
|
||||
|
||||
/* Update reputation */
|
||||
if ( $this instanceof \IPS\Content\Reputation )
|
||||
if ( \IPS\IPS::classUsesTrait( $this, 'IPS\Content\Reactable' ) )
|
||||
{
|
||||
\IPS\Db::i()->update( 'core_reputation_index', array( 'item_id' => $item->$idColumn ), array( 'class_type_id_hash=?', md5( get_class( $this ) . ':' . $oldItem->$idColumn ) ) );
|
||||
}
|
||||
|
||||
$oldItem->rebuildFirstAndLastCommentData();
|
||||
$item->rebuildFirstAndLastCommentData();
|
||||
if( $skip === FALSE )
|
||||
{
|
||||
$oldItem->rebuildFirstAndLastCommentData();
|
||||
$item->rebuildFirstAndLastCommentData();
|
||||
}
|
||||
|
||||
/* Add to search index */
|
||||
if ( $this instanceof \IPS\Content\Searchable )
|
||||
@@ -937,13 +1051,19 @@ abstract class _Comment extends \IPS\Content
|
||||
$item = NULL;
|
||||
$itemId = $this->mapped('item');
|
||||
}
|
||||
|
||||
/* Remove featured comment associations */
|
||||
if( $this->isFeatured() AND $item )
|
||||
{
|
||||
\IPS\Application::load('core')->extensions( 'core', 'MetaData' )['FeaturedComments']->unfeatureComment( $item, $this );
|
||||
}
|
||||
|
||||
/* Unclaim attachments */
|
||||
\IPS\File::unclaimAttachments( $itemClass::$application . '_' . ucfirst( $itemClass::$module ), $itemId, $this->$idColumn );
|
||||
|
||||
/* Reduce the number of comment/reviews count on the item but only if the item is unapproved or visible
|
||||
* - hidden as opposed to unapproved items do not get included in either of the unapproved_comments/num_comments columns */
|
||||
if( $this->hidden() !== -1 )
|
||||
if( $this->hidden() !== -1 AND $this->hidden() !== -2 )
|
||||
{
|
||||
$columnName = ( $this->hidden() === 1 ) ? 'unapproved_comments' : 'num_comments';
|
||||
if ( in_array( 'IPS\Content\Review', class_parents( get_called_class() ) ) )
|
||||
@@ -1005,7 +1125,7 @@ abstract class _Comment extends \IPS\Content
|
||||
{
|
||||
\IPS\Member::load( $member )->recountNotifications();
|
||||
}
|
||||
|
||||
|
||||
/* Actually delete */
|
||||
parent::delete();
|
||||
|
||||
@@ -1031,7 +1151,7 @@ abstract class _Comment extends \IPS\Content
|
||||
{
|
||||
if ( $item->container()->_comments !== NULL )
|
||||
{
|
||||
if ( !$this->hidden() )
|
||||
if ( !$this->hidden() AND $this->hidden() !== -2 )
|
||||
{
|
||||
$item->container()->_comments = ( $item->container()->_comments > 0 ) ? ( $item->container()->_comments - 1 ) : 0;
|
||||
}
|
||||
@@ -1257,6 +1377,18 @@ abstract class _Comment extends \IPS\Content
|
||||
}
|
||||
}
|
||||
|
||||
/* No matter if we can or cannot view hidden items, we do not want these to show as they are queued for deletion */
|
||||
if ( isset( static::$databaseColumnMap['hidden'] ) )
|
||||
{
|
||||
$col = static::$databaseTable . '.' . static::$databasePrefix . static::$databaseColumnMap['hidden'];
|
||||
$where[] = array( "{$col}!=-2" );
|
||||
}
|
||||
else if ( isset( static::$databaseColumnMap['approved'] ) )
|
||||
{
|
||||
$col = static::$databaseTable . '.' . static::$databasePrefix . static::$databaseColumnMap['approved'];
|
||||
$where[] = array( "{$col}!=-2" );
|
||||
}
|
||||
|
||||
if ( $joinContainer AND isset( $itemClass::$containerNodeClass ) )
|
||||
{
|
||||
$containerClass = $itemClass::$containerNodeClass;
|
||||
@@ -1368,6 +1500,30 @@ abstract class _Comment extends \IPS\Content
|
||||
return array( $this->item()->$idColumn, $this->$commentIdColumn );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the content images
|
||||
*
|
||||
* @param int|null $limit Number of attachments to fetch, or NULL for all
|
||||
*
|
||||
* @return array|NULL
|
||||
* @throws \BadMethodCallException
|
||||
*/
|
||||
public function contentImages( $limit = NULL )
|
||||
{
|
||||
$idColumn = static::$databaseColumnId;
|
||||
$item = $this->item();
|
||||
$attachments = array();
|
||||
$itemIdColumn = $item::$databaseColumnId;
|
||||
$internal = \IPS\Db::i()->select( 'attachment_id', 'core_attachments_map', array( 'location_key=? and id1=? and id2=?', $item::$application . '_' . ucfirst( $item::$module ), $item->$itemIdColumn, $this->$idColumn ) );
|
||||
|
||||
foreach( \IPS\Db::i()->select( '*', 'core_attachments', array( array( 'attach_id IN(?)', $internal ), array( 'attach_is_image=1' ) ), 'attach_id ASC', $limit ) as $row )
|
||||
{
|
||||
$attachments[] = array( 'core_Attachment' => $row['attach_location'] );
|
||||
}
|
||||
|
||||
return count( $attachments ) ? $attachments : NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Existing warning
|
||||
*/
|
||||
@@ -1414,6 +1570,17 @@ abstract class _Comment extends \IPS\Content
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is a featured comment?
|
||||
*
|
||||
* @return bool
|
||||
* @note This is a wrapper for the extension so content items can extend and apply their own logic
|
||||
*/
|
||||
public function isFeatured()
|
||||
{
|
||||
return \IPS\Application::load('core')->extensions( 'core', 'MetaData' )['FeaturedComments']->isFeatured( $this );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get output for API
|
||||
*
|
||||
|
||||
+406
-432
@@ -1,12 +1,11 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Abstract Content Model
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Community Suite
|
||||
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) Invision Power Services, Inc.
|
||||
* @license https://www.invisioncommunity.com/legal/standards/
|
||||
* @package Invision Community
|
||||
* @since 3 Oct 2013
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS;
|
||||
@@ -48,6 +47,11 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
*/
|
||||
public static $includeInSitemap = TRUE;
|
||||
|
||||
/**
|
||||
* @brief Reputation Store
|
||||
*/
|
||||
protected $reputation;
|
||||
|
||||
/**
|
||||
* Should posting this increment the poster's post count?
|
||||
*
|
||||
@@ -244,6 +248,35 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
return $this->mapped('content');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the content images
|
||||
*
|
||||
* @param int|null $limit Number of attachments to fetch, or NULL for all
|
||||
*
|
||||
* @return array|NULL
|
||||
* @throws \BadMethodCallException
|
||||
*/
|
||||
public function contentImages( $limit = NULL )
|
||||
{
|
||||
$idColumn = static::$databaseColumnId;
|
||||
$internal = NULL;
|
||||
$attachments = array();
|
||||
|
||||
if ( isset( static::$databaseColumnMap['content'] ) )
|
||||
{
|
||||
$internal = \IPS\Db::i()->select( 'attachment_id', 'core_attachments_map', array( 'location_key=? and id1=?', static::$application . '_' . ucfirst( static::$module ), $this->$idColumn ) );
|
||||
}
|
||||
|
||||
if ( $internal )
|
||||
{
|
||||
foreach( \IPS\Db::i()->select( '*', 'core_attachments', array( array( 'attach_id IN(?)', $internal ), array( 'attach_is_image=1' ) ), 'attach_id ASC', $limit ) as $row )
|
||||
{
|
||||
$attachments[] = array( 'core_Attachment' => $row['attach_location'] );
|
||||
}
|
||||
}
|
||||
|
||||
return count( $attachments ) ? $attachments : NULL; }
|
||||
|
||||
/**
|
||||
* Text for use with data-ipsTruncate
|
||||
* Returns the post with paragraphs turned into line breaks
|
||||
@@ -280,12 +313,17 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
*/
|
||||
public function delete()
|
||||
{
|
||||
if ( $this instanceof \IPS\Content\Reputation )
|
||||
$idColumn = static::$databaseColumnId;
|
||||
|
||||
if ( \IPS\IPS::classUsesTrait( $this, 'IPS\Content\Reactable' ) )
|
||||
{
|
||||
$idColumn = static::$databaseColumnId;
|
||||
\IPS\Db::i()->delete( 'core_reputation_index', array( 'app=? AND type=? AND type_id=?', static::$application, static::$reputationType, $this->$idColumn ) );
|
||||
\IPS\Db::i()->delete( 'core_reputation_index', array( 'app=? AND type=? AND type_id=?', static::$application, $this->reactionType(), $this->$idColumn ) );
|
||||
}
|
||||
|
||||
/* Remove any entries in the promotions table */
|
||||
\IPS\Db::i()->delete( 'core_social_promote', array( 'promote_class=? AND promote_class_id=?', get_class( $this ), $this->$idColumn ) );
|
||||
|
||||
\IPS\Db::i()->delete( 'core_deletion_log', array( "dellog_content_class=? AND dellog_content_id=?", get_class( $this ), $this->$idColumn ) );
|
||||
parent::delete();
|
||||
|
||||
$this->expireWidgetCaches();
|
||||
@@ -329,14 +367,15 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
* Content is hidden?
|
||||
*
|
||||
* @return int
|
||||
* @li -2 is pending deletion
|
||||
* @li -1 is hidden having been hidden by a moderator
|
||||
* @li 0 is unhidden
|
||||
* @li 1 is hidden needing approval
|
||||
* @note The actual column may also contain 2 which means the item is hidden because the parent is hidden, but it is not hidden in itself. This method will return -1 in that case.
|
||||
*
|
||||
* @note A piece of content (item and comment) can have an alias for hidden OR approved.
|
||||
* With hidden: 0=not hidden, 1=hidden (needs moderator approval), -1=hidden by moderator, 2=parent item is hidden
|
||||
* With approved: 1=not hidden, 0=hidden (needs moderator approval), -1=hidden by moderator
|
||||
* With hidden: 0=not hidden, 1=hidden (needs moderator approval), -1=hidden by moderator, 2=parent item is hidden, -2=pending deletion
|
||||
* With approved: 1=not hidden, 0=hidden (needs moderator approval), -1=hidden by moderator, -2=pending deletion
|
||||
*
|
||||
* User posting has moderator approval set: When adding an unapproved ITEM (approved=0, hidden=1) you should *not* increment container()->_comments but you should update container()->_unapprovedItems
|
||||
* User posting has moderator approval set: When adding an unapproved COMMENT (approved=0, hidden=1) you should *not* increment item()->num_comments in item or container()->_comments but you should update item()->unapproved_comments and container()->_unapprovedComments
|
||||
@@ -361,6 +400,10 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
elseif ( isset( static::$databaseColumnMap['approved'] ) )
|
||||
{
|
||||
$column = static::$databaseColumnMap['approved'];
|
||||
if ( $this->$column == -2 )
|
||||
{
|
||||
return intval( $this->$column );
|
||||
}
|
||||
return $this->$column == -1 ? intval( $this->$column ) : intval( !$this->$column );
|
||||
}
|
||||
else
|
||||
@@ -430,27 +473,7 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
/* Per-container permission */
|
||||
elseif ( $container )
|
||||
{
|
||||
$containerClass = get_class( $container );
|
||||
$title = static::$title;
|
||||
if
|
||||
(
|
||||
isset( $containerClass::$modPerm )
|
||||
and
|
||||
(
|
||||
$member->modPermission( $containerClass::$modPerm ) === -1
|
||||
or
|
||||
(
|
||||
is_array( $member->modPermission( $containerClass::$modPerm ) )
|
||||
and
|
||||
in_array( $container->_id, $member->modPermission( $containerClass::$modPerm ) )
|
||||
)
|
||||
)
|
||||
and
|
||||
$member->modPermission( "can_{$type}_{$title}" )
|
||||
)
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
return $container->modPermission( $type, $member );
|
||||
}
|
||||
|
||||
/* Still here? return false */
|
||||
@@ -463,10 +486,11 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
* @param string $action The action
|
||||
* @param \IPS\Member|NULL $member The member doing the action (NULL for currently logged in member)
|
||||
* @param string|NULL $reason Reason (for hides)
|
||||
* @param bool $immediately Delete Immediately
|
||||
* @return void
|
||||
* @throws \OutOfRangeException|\InvalidArgumentException|\RuntimeException
|
||||
*/
|
||||
public function modAction( $action, \IPS\Member $member = NULL, $reason = NULL )
|
||||
public function modAction( $action, \IPS\Member $member = NULL, $reason = NULL, $immediately = FALSE )
|
||||
{
|
||||
if( $action === 'approve' )
|
||||
{
|
||||
@@ -474,13 +498,19 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
}
|
||||
|
||||
/* Check it's a valid action */
|
||||
if ( !in_array( $action, array( 'pin', 'unpin', 'feature', 'unfeature', 'hide', 'unhide', 'move', 'lock', 'unlock', 'delete', 'publish' ) ) )
|
||||
if ( !in_array( $action, array( 'pin', 'unpin', 'feature', 'unfeature', 'hide', 'unhide', 'move', 'lock', 'unlock', 'delete', 'publish', 'restore', 'restoreAsHidden' ) ) )
|
||||
{
|
||||
throw new \InvalidArgumentException;
|
||||
}
|
||||
|
||||
/* And that we can do it */
|
||||
if ( !call_user_func( array( $this, 'can' . ucfirst( $action ) ), $member ) )
|
||||
$toCheck = $action;
|
||||
if ( $action == 'restoreAsHidden' )
|
||||
{
|
||||
$toCheck = 'restore';
|
||||
}
|
||||
|
||||
if ( !call_user_func( array( $this, 'can' . ucfirst( $toCheck ) ), $member ) )
|
||||
{
|
||||
throw new \OutOfRangeException;
|
||||
}
|
||||
@@ -541,9 +571,32 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
/* Delete is just a method */
|
||||
elseif ( $action === 'delete' )
|
||||
{
|
||||
/* If we are retaining content for a period of time, we need to just hide it instead for deleting later - this only works, though, with items that implement \IPS\Content\Hideable */
|
||||
if ( \IPS\Settings::i()->dellog_retention_period AND ( $this instanceof \IPS\Content\Hideable ) AND $immediately === FALSE )
|
||||
{
|
||||
$this->logDelete();
|
||||
return;
|
||||
}
|
||||
|
||||
$idColumn = static::$databaseColumnId;
|
||||
\IPS\Db::i()->delete( 'core_deletion_log', array( "dellog_content_class=? AND dellog_content_id=?", get_class( $this ), $this->$idColumn ) );
|
||||
$this->delete();
|
||||
return;
|
||||
}
|
||||
|
||||
/* Restore is just a method */
|
||||
elseif ( $action === 'restore' )
|
||||
{
|
||||
$this->restore();
|
||||
return;
|
||||
}
|
||||
|
||||
/* Restore As Hidden is just a method */
|
||||
elseif ( $action === 'restoreAsHidden' )
|
||||
{
|
||||
$this->restore( TRUE );
|
||||
return;
|
||||
}
|
||||
|
||||
/* Publish is just a method */
|
||||
elseif ( $action === 'publish' )
|
||||
@@ -561,6 +614,200 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log for deletion later
|
||||
*
|
||||
* \IPS\Member|NULL $member The member or NULL for currently logged in
|
||||
* @return void
|
||||
*/
|
||||
public function logDelete( $member = NULL )
|
||||
{
|
||||
$member = $member ?: \IPS\Member::loggedIn();
|
||||
|
||||
/* Log it! */
|
||||
$log = new \IPS\core\DeletionLog;
|
||||
$log->setContentAndMember( $this, $member );
|
||||
$log->save();
|
||||
|
||||
if ( isset( static::$databaseColumnMap['hidden'] ) )
|
||||
{
|
||||
$column = static::$databaseColumnMap['hidden'];
|
||||
}
|
||||
else if ( isset( static::$databaseColumnMap['approved'] ) )
|
||||
{
|
||||
$column = static::$databaseColumnMap['approved'];
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if ( $this->container() )
|
||||
{
|
||||
if ( $this->container()->_comments !== NULL )
|
||||
{
|
||||
/* Hidden content already decrements */
|
||||
if ( !$this->hidden() )
|
||||
{
|
||||
$this->container()->_comments = ( $this->container()->_comments - $this->mapped('num_comments') );
|
||||
}
|
||||
}
|
||||
|
||||
if ( $this->container()->_unapprovedComments !== NULL )
|
||||
{
|
||||
$this->container()->_unapprovedComments = ( $this->container()->_unapprovedComments - $this->mapped( 'unapproved_comments' ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
catch( \BadMethodCallException $e ) {}
|
||||
|
||||
$this->$column = -2;
|
||||
$this->save();
|
||||
|
||||
if ( $this instanceof \IPS\Content\Comment )
|
||||
{
|
||||
$item = $this->item();
|
||||
|
||||
/* Update last comment stuff */
|
||||
$item->resyncLastComment();
|
||||
|
||||
/* Update last review stuff */
|
||||
$item->resyncLastReview();
|
||||
|
||||
/* Update number of comments */
|
||||
$item->resyncCommentCounts();
|
||||
|
||||
/* Update number of reviews */
|
||||
$item->resyncReviewCounts();
|
||||
|
||||
/* Save*/
|
||||
$item->save();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if ( $this->container() )
|
||||
{
|
||||
$this->container()->setLastComment();
|
||||
$this->container()->save();
|
||||
}
|
||||
}
|
||||
catch( \BadMethodCallException $e ) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore Content
|
||||
*
|
||||
* @param bool Restore as hidden?
|
||||
* @return void
|
||||
* @throws \BadMethodCallException
|
||||
*/
|
||||
public function restore( $hidden = FALSE )
|
||||
{
|
||||
try
|
||||
{
|
||||
$idColumn = static::$databaseColumnId;
|
||||
$log = \IPS\core\DeletionLog::constructFromData( \IPS\Db::i()->select( '*', 'core_deletion_log', array( "dellog_content_class=? AND dellog_content_id", get_class( $this ), $this->$idColumn ) )->first() );
|
||||
}
|
||||
catch( \UnderflowException $e )
|
||||
{
|
||||
throw new \BadMethodCallException;
|
||||
}
|
||||
|
||||
/* Restoring as hidden? */
|
||||
if ( $hidden )
|
||||
{
|
||||
if ( isset( static::$databaseColumnMap['hidden'] ) )
|
||||
{
|
||||
$column = static::$databaseColumnMap['hidden'];
|
||||
}
|
||||
else if ( isset( static::$databaseColumnMap['approved'] ) )
|
||||
{
|
||||
$column = static::$databaseColumnMap['approved'];
|
||||
}
|
||||
|
||||
$this->$column = -1;
|
||||
}
|
||||
else
|
||||
{
|
||||
if ( isset( static::$databaseColumnMap['hidden'] ) )
|
||||
{
|
||||
$column = static::$databaseColumnMap['hidden'];
|
||||
$this->$column = 0;
|
||||
}
|
||||
else if ( isset( static::$databaseColumnMap['approved'] ) )
|
||||
{
|
||||
$column = static::$databaseColumnMap['approved'];
|
||||
$this->$column = 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Save the changes */
|
||||
$this->save();
|
||||
|
||||
/* Reindex the now hidden content */
|
||||
if ( $this instanceof \IPS\Content\Item and static::$firstCommentRequired )
|
||||
{
|
||||
\IPS\Content\Search\Index::i()->index( $this->firstComment() );
|
||||
}
|
||||
else
|
||||
{
|
||||
\IPS\Content\Search\Index::i()->index( $this );
|
||||
}
|
||||
|
||||
/* Delete the log */
|
||||
$log->delete();
|
||||
|
||||
/* Log to moderate logs */
|
||||
if ( !$hidden )
|
||||
{
|
||||
try
|
||||
{
|
||||
if ( $this->container() )
|
||||
{
|
||||
if ( $this->container()->_comments !== NULL )
|
||||
{
|
||||
if ( $this instanceof \IPS\Content\Item )
|
||||
{
|
||||
$this->container()->_comments = ( $this->container()->_comments + $this->mapped('num_comments') );
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->container()->_comments = $this->container()->_comments + 1;
|
||||
}
|
||||
}
|
||||
|
||||
if ( $this->container()->_unapprovedComments !== NULL )
|
||||
{
|
||||
if ( $this instanceof \IPS\Content\Item )
|
||||
{
|
||||
$this->container()->_unapprovedComments = ( $this->container()->_unapprovedComments + $this->mapped('unapproved_comments') );
|
||||
}
|
||||
}
|
||||
|
||||
$this->container()->setLastComment();
|
||||
$this->container()->save();
|
||||
|
||||
if ( $this instanceof \IPS\Content\Comment )
|
||||
{
|
||||
$this->item()->rebuildFirstAndLastCommentData();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch( \BadMethodCallException $e ) {}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Can restore?*
|
||||
*
|
||||
* @param \IPS\Member|NULL The member, or currently logged in member
|
||||
* @return bool
|
||||
*/
|
||||
public function canRestore( $member=NULL )
|
||||
{
|
||||
$member = $member ?: \IPS\Member::loggedIn();
|
||||
return \IPS\Member::loggedIn()->modPermission('can_restore_deleted_content');
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide
|
||||
*
|
||||
@@ -722,7 +969,43 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
return \IPS\Member::loggedIn()->language()->addToStack('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Blurb for when/why/by whom this content was deleted
|
||||
*
|
||||
* @return string
|
||||
* @throws \BadMethodCallException
|
||||
*/
|
||||
public function deletedBlurb()
|
||||
{
|
||||
if ( !( $this instanceof \IPS\Content\Hideable ) )
|
||||
{
|
||||
throw new \BadMethodCallException;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$idColumn = static::$databaseColumnId;
|
||||
$log = \IPS\core\DeletionLog::constructFromData( \IPS\Db::i()->select( '*', 'core_deletion_log', array( "dellog_content_class=? AND dellog_content_id=?", get_class( $this ), $this->$idColumn ) )->first() );
|
||||
return \IPS\Member::loggedIn()->language()->addToStack( 'deletion_blurb', FALSE, array( 'sprintf' => array( $log->_deleted_by->name, $log->deleted_date->fullYearLocaleDate(), $log->deletion_date->fullYearLocaleDate() ) ) );
|
||||
}
|
||||
catch( \UnderflowException $e )
|
||||
{
|
||||
return \IPS\Member::loggedIn()->language()->addToStack('deleted');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Can promote this comment/item?
|
||||
*
|
||||
* @param \IPS\Member|NULL $member The member to check for (NULL for currently logged in member)
|
||||
* @return boolean
|
||||
*/
|
||||
public function canPromoteToSocialMedia( $member=NULL )
|
||||
{
|
||||
return \IPS\core\Promote::canPromote( $member );
|
||||
}
|
||||
|
||||
/**
|
||||
* Can report?
|
||||
*
|
||||
@@ -741,7 +1024,7 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
}
|
||||
|
||||
/* Can the member report content? */
|
||||
if ( $member->group['gbw_no_report'] )
|
||||
if ( !$member->group['g_can_report'] )
|
||||
{
|
||||
return 'no_module_permission';
|
||||
}
|
||||
@@ -866,7 +1149,7 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
{
|
||||
$oldAuthor = $this->author();
|
||||
|
||||
foreach ( array( 'author', 'author_name' ) as $k )
|
||||
foreach ( array( 'author', 'author_name', 'edit_member_name' ) as $k )
|
||||
{
|
||||
if ( isset( static::$databaseColumnMap[ $k ] ) )
|
||||
{
|
||||
@@ -878,6 +1161,7 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
break;
|
||||
|
||||
case 'author_name':
|
||||
case 'edit_member_name':
|
||||
/* Real name will contain the custom guest name if available or '' if not */
|
||||
$this->$col = $newAuthor->member_id ? $newAuthor->name : $newAuthor->real_name;
|
||||
break;
|
||||
@@ -906,9 +1190,10 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
* @param bool $asItem Displaying results as items?
|
||||
* @param bool $canIgnoreComments Can ignore comments in the result stream? Activity stream can, but search results cannot.
|
||||
* @param array $template Optional custom template
|
||||
* @param array $reactions Reaction Data
|
||||
* @return string
|
||||
*/
|
||||
public static function searchResult( array $indexData, array $authorData, array $itemData, array $containerData = NULL, array $reputationData, $reviewRating, $iPostedIn, $view, $asItem, $canIgnoreComments=FALSE, $template=NULL )
|
||||
public static function searchResult( array $indexData, array $authorData, array $itemData, array $containerData = NULL, array $reputationData, $reviewRating, $iPostedIn, $view, $asItem, $canIgnoreComments=FALSE, $template=NULL, $reactions=array() )
|
||||
{
|
||||
/* Item details */
|
||||
$itemClass = $indexData['index_class'];
|
||||
@@ -931,12 +1216,12 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
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'] ) );
|
||||
$showRepUrl = $itemUrl->setQueryString( array( 'do' => 'showReactionsReview', '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'] ) );
|
||||
$showRepUrl = $itemUrl->setQueryString( array( 'do' => 'showReactionsComment', 'comment' => $indexData['index_object_id'] ) );
|
||||
}
|
||||
|
||||
$indefiniteArticle = $itemClass::_indefiniteArticle( $containerData );
|
||||
@@ -945,7 +1230,7 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
else
|
||||
{
|
||||
$objectUrl = $itemUrl;
|
||||
$showRepUrl = $itemUrl->setQueryString( 'do', 'showRep' );
|
||||
$showRepUrl = $itemUrl->setQueryString( 'do', 'showReactions' );
|
||||
}
|
||||
$articles = array( 'indefinite' => $indefiniteArticle, 'definite' => $definiteArticle );
|
||||
|
||||
@@ -971,7 +1256,7 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
}
|
||||
|
||||
/* Return */
|
||||
return call_user_func_array( $template, array( $indexData, $articles, $authorData, $itemData, $unread, $asItem ? $itemUrl : $objectUrl, $itemUrl, $containerUrl, $containerTitle, $repCount, $showRepUrl, $snippet, $iPostedIn, $view, $canIgnoreComments ) );
|
||||
return call_user_func_array( $template, array( $indexData, $articles, $authorData, $itemData, $unread, $asItem ? $itemUrl : $objectUrl, $itemUrl, $containerUrl, $containerTitle, $repCount, $showRepUrl, $snippet, $iPostedIn, $view, $canIgnoreComments, $reactions ) );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -991,6 +1276,11 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
return $view == 'expanded' ? \IPS\Theme::i()->getTemplate( 'system', 'core', 'front' )->searchResultSnippet( $indexData ) : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Return a classname applied to the search result block
|
||||
*/
|
||||
public static $searchResultClassName = '';
|
||||
|
||||
/**
|
||||
* Return the filters that are available for selecting table rows
|
||||
*
|
||||
@@ -1039,397 +1329,6 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
|
||||
}
|
||||
|
||||
/* !Reputation */
|
||||
|
||||
/**
|
||||
* 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 \IPS\Member|NULL $member The member to check for (NULL for currently logged in member)
|
||||
* @return bool
|
||||
*/
|
||||
public function canGiveReputation( $type, \IPS\Member $member = NULL )
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Give reputation
|
||||
*
|
||||
* @param int $type 1 for positive, -1 for negative
|
||||
* @param \IPS\Member|NULL $member The member to check for (NULL for currently logged in member)
|
||||
* @return void
|
||||
* @throws \DomainException|\BadMethodCallException
|
||||
*/
|
||||
public function giveReputation( $type, \IPS\Member $member=NULL )
|
||||
{
|
||||
if ( !( $this instanceof \IPS\Content\Reputation ) )
|
||||
{
|
||||
throw new \BadMethodCallException;
|
||||
}
|
||||
|
||||
$member = $member ?: \IPS\Member::loggedIn();
|
||||
|
||||
if ( !$this->canGiveReputation( $type, $member ) )
|
||||
{
|
||||
throw new \DomainException( 'cannot_rep_user' );
|
||||
}
|
||||
|
||||
$idColumn = static::$databaseColumnId;
|
||||
\IPS\Db::i()->delete( 'core_reputation_index', array( 'app=? AND type=? AND member_id=? AND type_id=?', static::$application, static::$reputationType, $member->member_id, $this->$idColumn ) );
|
||||
|
||||
if ( $this->repGiven( $member ) )
|
||||
{
|
||||
if ( $this->reputation !== NULL )
|
||||
{
|
||||
unset( $this->reputation[ $member->member_id ] );
|
||||
}
|
||||
}
|
||||
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>? 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( \IPS\Settings::i()->reputation_point_types == 'like' ? 'rep_daily_exceeded_likes' :'rep_daily_exceeded', FALSE, array( 'sprintf' => array( $limit ) ) ) );
|
||||
}
|
||||
}
|
||||
|
||||
\IPS\Db::i()->insert( 'core_reputation_index', array(
|
||||
'member_id' => $member->member_id,
|
||||
'app' => static::$application,
|
||||
'type' => static::$reputationType,
|
||||
'type_id' => $this->$idColumn,
|
||||
'rep_date' => time(),
|
||||
'rep_rating' => $type,
|
||||
'member_received' => (int) $this->author()->member_id,
|
||||
'class_type_id_hash' => md5( get_class( $this ) . ':' . $this->$idColumn ),
|
||||
'rep_class' => get_class( $this ),
|
||||
'item_id' => (int) $this->mapped('item')
|
||||
) );
|
||||
|
||||
/* Send Notification */
|
||||
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() );
|
||||
$notification->send();
|
||||
}
|
||||
|
||||
if ( $this->reputation !== NULL )
|
||||
{
|
||||
$this->reputation[ $member->member_id ] = $type;
|
||||
}
|
||||
}
|
||||
|
||||
if( $this->author()->member_id )
|
||||
{
|
||||
$this->author()->pp_reputation_points += $type;
|
||||
$this->author()->save();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Reputation Cache
|
||||
*/
|
||||
public $reputation = NULL;
|
||||
|
||||
/**
|
||||
* Get reputation count
|
||||
*
|
||||
* @return int
|
||||
* @throws \BadMethodCallException
|
||||
*/
|
||||
public function reputation()
|
||||
{
|
||||
if ( !( $this instanceof \IPS\Content\Reputation ) )
|
||||
{
|
||||
throw new \BadMethodCallException;
|
||||
}
|
||||
|
||||
if ( $this->reputation === NULL )
|
||||
{
|
||||
$this->reputation = iterator_to_array( \IPS\Db::i()->select( 'member_id, rep_rating', 'core_reputation_index', $this->getReputationWhereClause() )->setKeyField( 'member_id' )->setValueField( 'rep_rating' ) );
|
||||
return array_sum( $this->reputation );
|
||||
}
|
||||
else
|
||||
{
|
||||
$repCount = 0;
|
||||
foreach( $this->reputation as $member => $value )
|
||||
{
|
||||
if( is_int( $value ) )
|
||||
{
|
||||
$repCount += $value;
|
||||
}
|
||||
else
|
||||
{
|
||||
$repCount += $value['rep_rating'];
|
||||
}
|
||||
|
||||
}
|
||||
return $repCount;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get reputation where clause
|
||||
*
|
||||
* @return array
|
||||
* @throws \BadMethodCallException
|
||||
*/
|
||||
public function getReputationWhereClause()
|
||||
{
|
||||
if ( !( $this instanceof \IPS\Content\Reputation ) )
|
||||
{
|
||||
throw new \BadMethodCallException;
|
||||
}
|
||||
|
||||
$idColumn = static::$databaseColumnId;
|
||||
$where = array( array( 'app=? AND type=? AND type_id=?', static::$application, static::$reputationType, $this->$idColumn ) );
|
||||
|
||||
switch( \IPS\Settings::i()->reputation_point_types )
|
||||
{
|
||||
case 'positive':
|
||||
case 'like':
|
||||
$where[] = array( 'rep_rating=?', "1" );
|
||||
break;
|
||||
case 'negative':
|
||||
$where[] = array( 'rep_rating=?', "-1" );
|
||||
break;
|
||||
}
|
||||
|
||||
return $where;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Cached like blurb
|
||||
*/
|
||||
public $likeBlurb = NULL;
|
||||
|
||||
/**
|
||||
* Get "like" blurb (You and X others like this)
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function likeBlurb()
|
||||
{
|
||||
if( $this->likeBlurb === NULL )
|
||||
{
|
||||
/* Did anyone like it? */
|
||||
$numberOfLikes = $this->reputation(); # int
|
||||
if ( $numberOfLikes )
|
||||
{
|
||||
/* Is it just us? */
|
||||
$userLiked = ( $this->repGiven() === 1 );
|
||||
if ( $userLiked and $numberOfLikes < 2 )
|
||||
{
|
||||
$this->likeBlurb = \IPS\Member::loggedIn()->language()->addToStack('like_blurb_just_you');
|
||||
}
|
||||
|
||||
/* Nope, we need to display a number... */
|
||||
else
|
||||
{
|
||||
$peopleToDisplayInMainView = array();
|
||||
$andXOthers = $numberOfLikes;
|
||||
|
||||
/* If the user liked, we always show "You" first */
|
||||
if ( $userLiked )
|
||||
{
|
||||
$peopleToDisplayInMainView[] = \IPS\Member::loggedIn()->language()->addToStack('like_blurb_you_and_others');
|
||||
$andXOthers--;
|
||||
}
|
||||
|
||||
/* If we have permission to see who the others are, we need to do that... */
|
||||
if ( \IPS\Member::loggedIn()->group['gbw_view_reps'] )
|
||||
{
|
||||
$peopleToDisplayInSecondaryView = array();
|
||||
|
||||
/* Some random names */
|
||||
$idColumn = static::$databaseColumnId;
|
||||
$i = 0;
|
||||
$peopleToDisplayInSecondaryView = array();
|
||||
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()', $userLiked ? 17 : 18 ) as $rep )
|
||||
{
|
||||
if ( $i < ( $userLiked ? 2 : 3 ) )
|
||||
{
|
||||
$peopleToDisplayInMainView[] = \IPS\Theme::i()->getTemplate( 'global', 'core' )->userLink( \IPS\Member::load( $rep['member_id'] ) );
|
||||
$andXOthers--;
|
||||
}
|
||||
else
|
||||
{
|
||||
$peopleToDisplayInSecondaryView[] = htmlspecialchars( \IPS\Member::load( $rep['member_id'] )->name, ENT_QUOTES | \IPS\HTMLENTITIES, 'UTF-8', FALSE );
|
||||
}
|
||||
$i++;
|
||||
}
|
||||
|
||||
/* If there's people to display in the secondary view, add that */
|
||||
if ( $peopleToDisplayInSecondaryView )
|
||||
{
|
||||
if ( count( $peopleToDisplayInSecondaryView ) < $andXOthers )
|
||||
{
|
||||
$peopleToDisplayInSecondaryView[] = \IPS\Member::loggedIn()->language()->addToStack( 'like_blurb_others_secondary', FALSE, array( 'pluralize' => array( $andXOthers - count( $peopleToDisplayInSecondaryView ) ) ) );
|
||||
}
|
||||
$peopleToDisplayInMainView[] = \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' )->reputationOthers( $this->url( 'showRep' ), \IPS\Member::loggedIn()->language()->addToStack( 'like_blurb_others', FALSE, array( 'pluralize' => array( $andXOthers ) ) ), json_encode( $peopleToDisplayInSecondaryView ) );
|
||||
}
|
||||
}
|
||||
|
||||
/* Otherwise the "and X others" will just be static */
|
||||
else
|
||||
{
|
||||
$peopleToDisplayInMainView[] = \IPS\Member::loggedIn()->language()->addToStack( empty( $peopleToDisplayInMainView ) ? 'like_blurb_generic' : 'like_blurb_others', FALSE, array( 'pluralize' => array( $andXOthers ) ) );
|
||||
}
|
||||
|
||||
/* Put it all together */
|
||||
$this->likeBlurb = \IPS\Member::loggedIn()->language()->addToStack( 'like_blurb', FALSE, array( 'pluralize' => array( $numberOfLikes ), 'htmlsprintf' => array( \IPS\Member::loggedIn()->language()->formatList( $peopleToDisplayInMainView ) ) ) );
|
||||
}
|
||||
|
||||
}
|
||||
/* Nobody liked it - show nothing */
|
||||
else
|
||||
{
|
||||
$this->likeBlurb = '';
|
||||
}
|
||||
}
|
||||
|
||||
return $this->likeBlurb;
|
||||
}
|
||||
|
||||
/**
|
||||
* Has reputation been given by a particular member?
|
||||
*
|
||||
* @param \IPS\Member|NULL $member The member to check for (NULL for currently logged in member)
|
||||
* @return int 1 = Positive rep given. -1 = Negative rep given. 0 = No rep given
|
||||
* @throws \BadMethodCallException
|
||||
*/
|
||||
public function repGiven( \IPS\Member $member = NULL )
|
||||
{
|
||||
if ( !( $this instanceof \IPS\Content\Reputation ) )
|
||||
{
|
||||
throw new \BadMethodCallException;
|
||||
}
|
||||
|
||||
if ( $this->reputation === NULL )
|
||||
{
|
||||
$this->reputation();
|
||||
}
|
||||
|
||||
$member = $member ?: \IPS\Member::loggedIn();
|
||||
return array_key_exists( $member->member_id, $this->reputation ) ? intval( $this->reputation[ $member->member_id ] ) : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get table showing who has given reputation
|
||||
*
|
||||
* @return \IPS\Helpers\Table\Db
|
||||
* @throws \DomainException
|
||||
* @throws \BadMethodCallException
|
||||
*/
|
||||
public function reputationTable()
|
||||
{
|
||||
if ( !( $this instanceof \IPS\Content\Reputation ) )
|
||||
{
|
||||
throw new \BadMethodCallException;
|
||||
}
|
||||
|
||||
if ( !\IPS\Member::loggedIn()->group['gbw_view_reps'] )
|
||||
{
|
||||
throw new \DomainException;
|
||||
}
|
||||
|
||||
$idColumn = static::$databaseColumnId;
|
||||
|
||||
$table = new \IPS\Helpers\Table\Db( 'core_reputation_index', $this->url('showRep'), $this->getReputationWhereClause() );
|
||||
$table->sortBy = 'rep_date';
|
||||
$table->sortDirection = 'desc';
|
||||
$table->tableTemplate = array( \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' ), 'reputationLogTable' );
|
||||
$table->rowsTemplate = array( \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' ), 'reputationLog' );
|
||||
|
||||
return $table;
|
||||
}
|
||||
|
||||
/* !Follow */
|
||||
|
||||
const FOLLOW_PUBLIC = 1;
|
||||
@@ -1874,4 +1773,79 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
{
|
||||
return (string) \IPS\Theme::i()->logo_sharer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log keyword usage, if any
|
||||
*
|
||||
* @param string $content Content/text of submission
|
||||
* @param string|NULL $title Title of submission
|
||||
* @return void
|
||||
*/
|
||||
public function checkKeywords( $content, $title=NULL )
|
||||
{
|
||||
/* Do we have any keywords to track? */
|
||||
if( !\IPS\Settings::i()->stats_keywords )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
/* We need to know the ID */
|
||||
$idColumn = static::$databaseColumnId;
|
||||
|
||||
/* If this is a content item and first comment is required, skip checking the comment */
|
||||
if ( $this instanceof \IPS\Content\Comment )
|
||||
{
|
||||
$itemClass = static::$itemClass;
|
||||
|
||||
if( $itemClass::$firstCommentRequired === TRUE )
|
||||
{
|
||||
/* During initial post, at this point the firstCommentIdColumn value won't be set, so we check for that or explicitly if this is the first post */
|
||||
if( !$this->item()->mapped('first_comment_id') OR $this->$idColumn == $this->item()->mapped('first_comment_id') )
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$words = preg_split("/[\s]+/", trim( strip_tags( preg_replace( "/<br( \/)?>/", "\n", $content ) ) ), NULL, PREG_SPLIT_NO_EMPTY );
|
||||
|
||||
if( $title !== NULL )
|
||||
{
|
||||
$titleWords = explode( ' ', $title );
|
||||
$words = array_merge( $words, $titleWords );
|
||||
}
|
||||
|
||||
$words = array_unique( $words );
|
||||
|
||||
$keywords = json_decode( \IPS\Settings::i()->stats_keywords, true );
|
||||
|
||||
$extraData = json_encode( array( 'class' => get_class( $this ), 'id' => $this->$idColumn ) );
|
||||
|
||||
foreach( $keywords as $keyword )
|
||||
{
|
||||
if( in_array( $keyword, $words ) )
|
||||
{
|
||||
\IPS\Db::i()->insert( 'core_statistics', array( 'time' => time(), 'type' => 'keyword', 'value_4' => $keyword, 'extra_data' => $extraData ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* !Deprecated */
|
||||
/* The methods below are deprecated and will be removed in Invision Community 4.3 */
|
||||
|
||||
/**
|
||||
* Get reputation count
|
||||
*
|
||||
* @return int
|
||||
* @throws \BadMethodCallException
|
||||
*/
|
||||
public function reputation()
|
||||
{
|
||||
if ( !\IPS\IPS::classUsesTrait( $this, 'IPS\Content\Reactable' ) )
|
||||
{
|
||||
throw new \BadMethodCallException;
|
||||
}
|
||||
|
||||
return $this->reactionCount();
|
||||
}
|
||||
}
|
||||
+725
-105
File diff suppressed because it is too large.
Load diff
@@ -1,12 +1,11 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Edit History Interface for Content Comments
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Community Suite
|
||||
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) Invision Power Services, Inc.
|
||||
* @license https://www.invisioncommunity.com/legal/standards/
|
||||
* @package Invision Community
|
||||
* @since 14 Oct 2013
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS\Content;
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Embeddable Interface for Content Models/Comments
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Community Suite
|
||||
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) Invision Power Services, Inc.
|
||||
* @license https://www.invisioncommunity.com/legal/standards/
|
||||
* @package Invision Community
|
||||
* @since 22 Apr 2014
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS\Content;
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Content Extension Generator
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Community Suite
|
||||
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) Invision Power Services, Inc.
|
||||
* @license https://www.invisioncommunity.com/legal/standards/
|
||||
* @package Invision Community
|
||||
* @since 26 Dec 2013
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS\Content;
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Featurable Interface for Content Models/Comments
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Community Suite
|
||||
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) Invision Power Services, Inc.
|
||||
* @license https://www.invisioncommunity.com/legal/standards/
|
||||
* @package Invision Community
|
||||
* @since 12 Nov 2013
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS\Content;
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Followable Interface for Content Models/Comments
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Community Suite
|
||||
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) Invision Power Services, Inc.
|
||||
* @license https://www.invisioncommunity.com/legal/standards/
|
||||
* @package Invision Community
|
||||
* @since 02 Dec 2013
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS\Content;
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Polls Interface for Content Models
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Community Suite
|
||||
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) Invision Power Services, Inc.
|
||||
* @license https://www.invisioncommunity.com/legal/standards/
|
||||
* @package Invision Community
|
||||
* @since 10 Jan 2013
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS\Content;
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Hidebale Interface for Content Models/Comments
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Community Suite
|
||||
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) Invision Power Services, Inc.
|
||||
* @license https://www.invisioncommunity.com/legal/standards/
|
||||
* @package Invision Community
|
||||
* @since 6 Nov 2013
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS\Content;
|
||||
|
||||
+670
-167
File diff suppressed because it is too large.
Load diff
@@ -1,12 +1,11 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Lockable Interface for Content Models/Comments
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Community Suite
|
||||
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) Invision Power Services, Inc.
|
||||
* @license https://www.invisioncommunity.com/legal/standards/
|
||||
* @package Invision Community
|
||||
* @since 12 Nov 2013
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS\Content;
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief MetaData Interface for Content Models/Comments
|
||||
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) Invision Power Services, Inc.
|
||||
* @license https://www.invisioncommunity.com/legal/standards/
|
||||
* @package Invision Community
|
||||
* @since 12 Nov 2013
|
||||
*/
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* MetaData Interface for Content Models/Comments
|
||||
*
|
||||
* @note Content classes will gain special functionality by implementing this interface
|
||||
*/
|
||||
interface MetaData
|
||||
{
|
||||
}
|
||||
@@ -1,12 +1,11 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Moderator Permissions Interface for Content Models
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Community Suite
|
||||
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) Invision Power Services, Inc.
|
||||
* @license https://www.invisioncommunity.com/legal/standards/
|
||||
* @package Invision Community
|
||||
* @since 1 Oct 2013
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS\Content;
|
||||
@@ -74,6 +73,21 @@ class _ModeratorPermissions
|
||||
$this->actions[] = 'reply_to_locked';
|
||||
}
|
||||
$this->actions[] = 'delete';
|
||||
if ( in_array( 'IPS\Content\MetaData', class_implements( $class ) ) AND $class::supportedMetaDataTypes() )
|
||||
{
|
||||
if ( in_array( 'core_FeaturedComments', $class::supportedMetaDataTypes() ) )
|
||||
{
|
||||
$this->actions[] = 'feature_comments';
|
||||
$this->actions[] = 'unfeature_comments';
|
||||
}
|
||||
|
||||
if ( in_array( 'core_ContentMessages', $class::supportedMetaDataTypes() ) )
|
||||
{
|
||||
$this->actions[] = 'add_item_message';
|
||||
$this->actions[] = 'edit_item_message';
|
||||
$this->actions[] = 'delete_item_message';
|
||||
}
|
||||
}
|
||||
|
||||
if ( isset( $class::$commentClass ) )
|
||||
{
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Permissions Interface for Content Models
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Community Suite
|
||||
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) Invision Power Services, Inc.
|
||||
* @license https://www.invisioncommunity.com/legal/standards/
|
||||
* @package Invision Community
|
||||
* @since 8 Jul 2013
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS\Content;
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Pinnable Interface for Content Models/Comments
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Community Suite
|
||||
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) Invision Power Services, Inc.
|
||||
* @license https://www.invisioncommunity.com/legal/standards/
|
||||
* @package Invision Community
|
||||
* @since 12 Nov 2013
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS\Content;
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Polls Interface for Content Models
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Community Suite
|
||||
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) Invision Power Services, Inc.
|
||||
* @license https://www.invisioncommunity.com/legal/standards/
|
||||
* @package Invision Community
|
||||
* @since 10 Jan 2013
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS\Content;
|
||||
|
||||
@@ -0,0 +1,335 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Facebook Promotion
|
||||
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) Invision Power Services, Inc.
|
||||
* @license https://www.invisioncommunity.com/legal/standards/
|
||||
* @package Invision Community
|
||||
* @since 10 Feb 2017
|
||||
*/
|
||||
|
||||
namespace IPS\Content\Promote;
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Facebook Promotion
|
||||
*/
|
||||
class _Facebook extends PromoteAbstract
|
||||
{
|
||||
/**
|
||||
* @brief Icon
|
||||
*/
|
||||
public static $icon = 'facebook';
|
||||
|
||||
/**
|
||||
* @brief Default settings
|
||||
*/
|
||||
public $defaultSettings = array(
|
||||
'token' => NULL,
|
||||
'owner' => NULL,
|
||||
'page_name' => NULL,
|
||||
'permissions' => NULL,
|
||||
'members' => NULL,
|
||||
'page' => NULL,
|
||||
'tags' => NULL,
|
||||
'image' => NULL,
|
||||
'last_sync' => 0
|
||||
);
|
||||
|
||||
/**
|
||||
* Is connected?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function connected()
|
||||
{
|
||||
return ( $this->member->fb_uid and $this->member->fb_token );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get image
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getPhoto()
|
||||
{
|
||||
if ( ! $this->settings['image'] or $this->settings['last_sync'] < time() - 86400 )
|
||||
{
|
||||
/* Fetch again */
|
||||
$response = \IPS\Http\Url::external( "https://graph.facebook.com/" . $this->settings['page'] . "/picture?type=large" )->request()->get();
|
||||
|
||||
$extension = str_replace( 'image/', '', $response->httpHeaders['Content-Type'] );
|
||||
$newFile = \IPS\File::create( 'core_Promote', 'facebook_' . $this->settings['page'] . '.' . $extension, (string) $response, NULL, FALSE, NULL, FALSE );
|
||||
|
||||
$this->saveSettings( array( 'image' => (string) $newFile->url, 'last_sync' => time() ) );
|
||||
}
|
||||
|
||||
return $this->settings['image'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
return $this->settings['page_name'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check publish permissions
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function canPostToPage()
|
||||
{
|
||||
try
|
||||
{
|
||||
$response = \IPS\Http\Url::external( "https://graph.facebook.com/{$this->member->fb_uid}/permissions" )->setQueryString( 'access_token', $this->member->fb_token )->request()->get()->decodeJson();
|
||||
|
||||
if ( isset( $response['error'] ) )
|
||||
{
|
||||
return $response['error']['message'];
|
||||
}
|
||||
|
||||
if ( !empty( $response['data'] ) )
|
||||
{
|
||||
$got = 0;
|
||||
foreach( $response['data'] as $permission )
|
||||
{
|
||||
if ( in_array( $permission['permission'], array( 'manage_pages', 'publish_pages' ) ) and $permission['status'] == 'granted' )
|
||||
{
|
||||
$got++;
|
||||
}
|
||||
}
|
||||
|
||||
return $got === 2;
|
||||
}
|
||||
}
|
||||
catch ( \IPS\Http\Request\Exception $e )
|
||||
{
|
||||
\IPS\Log::log( $e, 'facebook' );
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get pages this user manages
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getPages()
|
||||
{
|
||||
$pages = array();
|
||||
try
|
||||
{
|
||||
$response = \IPS\Http\Url::external( "https://graph.facebook.com/{$this->member->fb_uid}/accounts" )->setQueryString( 'access_token', $this->member->fb_token )->request()->get()->decodeJson();
|
||||
|
||||
if ( !empty( $response['data'] ) )
|
||||
{
|
||||
foreach( $response['data'] as $page )
|
||||
{
|
||||
$pages[ $page['id'] ] = array( $page['name'], $page['access_token'] );
|
||||
}
|
||||
}
|
||||
}
|
||||
catch ( \IPS\Http\Request\Exception $e )
|
||||
{
|
||||
\IPS\Log::log( $e, 'facebook' );
|
||||
}
|
||||
|
||||
return $pages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get form elements for this share service
|
||||
*
|
||||
* @param string $text Text for the text entry
|
||||
* @param string $link Short or full link (short when available)
|
||||
* @param string $content Additional text content (usually a comment, or the item content)
|
||||
*
|
||||
* @return array of form elements
|
||||
*/
|
||||
public function form( $text, $link=null, $content=null )
|
||||
{
|
||||
|
||||
|
||||
if ( $this->promote and $this->promote->id )
|
||||
{
|
||||
$_text = $text;
|
||||
}
|
||||
else
|
||||
{
|
||||
$_text = $text;
|
||||
|
||||
if ( $content )
|
||||
{
|
||||
$_text .= "\n" . $content;
|
||||
}
|
||||
|
||||
if ( $link )
|
||||
{
|
||||
$_text .= "\n" . $link;
|
||||
}
|
||||
|
||||
if ( count( $this->settings['tags'] ) )
|
||||
{
|
||||
$_text .= "\n" . '#' . implode( ' #', $this->settings['tags'] );
|
||||
}
|
||||
}
|
||||
|
||||
return array( new \IPS\Helpers\Form\TextArea( 'promote_social_content_facebook', $_text, FALSE, array( 'maxLength' => 2000, 'rows' => 10 ) ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Post to Facebook
|
||||
*
|
||||
* @param \IPS\core\Promote $promote Promote Object
|
||||
* @return void
|
||||
*/
|
||||
public function post( $promote )
|
||||
{
|
||||
$photos = $promote->imageObjects();
|
||||
|
||||
/* Get the last 20 items to see if we have a duplicate */
|
||||
$items = \IPS\Http\Url::external( "https://graph.facebook.com/" . $this->settings['page'] . "/feed" )->setQueryString( array( 'limit' => 20, 'access_token' => $this->settings['token'] ) )->request()->get()->decodeJson();
|
||||
|
||||
if ( isset( $items['data'] ) )
|
||||
{
|
||||
foreach( $items['data'] as $item )
|
||||
{
|
||||
$time = new \IPS\DateTime( $item['created_time'] );
|
||||
$now = new \IPS\DateTime();
|
||||
|
||||
/* Only look back past the last 30 mins */
|
||||
if ( intval( $now->diff( $time )->format('%i') ) > 30 )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( preg_replace( '#\s{1,}#', " ", $promote->text['facebook'] ) == preg_replace( '#\s{1,}#', " ", $item['message'] ) )
|
||||
{
|
||||
/* Duplicate */
|
||||
return $item['id'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! $photos or count( $photos ) === 1 )
|
||||
{
|
||||
/* Simple message */
|
||||
try
|
||||
{
|
||||
if ( $photos )
|
||||
{
|
||||
$thePhoto = array_pop( $photos );
|
||||
$this->response = \IPS\Http\Url::external( "https://graph.facebook.com/" . $this->settings['page'] . "/photos" )->request()->post( array(
|
||||
'message' => $promote->text['facebook'],
|
||||
'url' => (string) $this->returnUrlWithProtocol( $thePhoto->url ),
|
||||
'access_token' => $this->settings['token'],
|
||||
'link' => $promote->short_link
|
||||
) )->decodeJson();
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->response = \IPS\Http\Url::external( "https://graph.facebook.com/" . $this->settings['page'] . "/feed" )->request()->post( array(
|
||||
'message' => $promote->text['facebook'],
|
||||
'access_token' => $this->settings['token'],
|
||||
'link' => $promote->short_link
|
||||
) )->decodeJson();
|
||||
}
|
||||
}
|
||||
catch( \Exception $e )
|
||||
{
|
||||
\IPS\Log::log( $e, 'facebook' );
|
||||
|
||||
throw new \InvalidArgumentException( \IPS\Member::loggedIn()->language()->addToStack('facebook_publish_exception') );
|
||||
}
|
||||
|
||||
if ( isset( $this->response['id'] ) )
|
||||
{
|
||||
return $this->response['id'];
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new \InvalidArgumentException( \IPS\Member::loggedIn()->language()->addToStack('facebook_publish_exception') );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
/* We have multiple photos so we first need to create an album, and then upload into that */
|
||||
try
|
||||
{
|
||||
$this->response = \IPS\Http\Url::external( "https://graph.facebook.com/" . $this->settings['page'] . "/albums" )->request()->post( array(
|
||||
'name' => $promote->objectTitle,
|
||||
'access_token' => $this->settings['token'],
|
||||
) )->decodeJson();
|
||||
|
||||
|
||||
if ( ! isset( $this->response['id'] ) )
|
||||
{
|
||||
throw new \InvalidArgumentException('Could not create album');
|
||||
}
|
||||
|
||||
$newAlbumId = $this->response['id'];
|
||||
|
||||
foreach( $photos as $photo )
|
||||
{
|
||||
$this->response = \IPS\Http\Url::external( "https://graph.facebook.com/" . $newAlbumId . "/photos" )->request()->post( array(
|
||||
'message' => $promote->text['facebook'],
|
||||
'url' => (string) $this->returnUrlWithProtocol( $photo->url ),
|
||||
'access_token' => $this->settings['token'],
|
||||
) )->decodeJson();
|
||||
}
|
||||
}
|
||||
catch( \Exception $e )
|
||||
{
|
||||
\IPS\Log::log( $e, 'facebook' );
|
||||
|
||||
throw new \InvalidArgumentException( \IPS\Member::loggedIn()->language()->addToStack('facebook_publish_exception') );
|
||||
}
|
||||
|
||||
return $this->response['id'];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the published URL
|
||||
*
|
||||
* @param array $data Data returned from a successful POST
|
||||
* @return \IPS\Http\Url
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function getUrl( $data )
|
||||
{
|
||||
if ( $data and preg_match( '#^[0-9_]*$#', $data ) )
|
||||
{
|
||||
return \IPS\Http\Url::external( 'https://facebook.com/' . $data );
|
||||
}
|
||||
|
||||
throw new \InvalidArgumentException();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the URL is not protocol relative
|
||||
*
|
||||
* @return \IPS\Http\Url
|
||||
*/
|
||||
protected function returnUrlWithProtocol( \IPS\Http\Url $url )
|
||||
{
|
||||
if ( ! $url->data['scheme'] )
|
||||
{
|
||||
$url = $url->setScheme( ( \substr( \IPS\Settings::i()->base_url, 0, 5 ) == 'https' ) ? 'https' : 'http' );
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Internal Promotion
|
||||
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) Invision Power Services, Inc.
|
||||
* @license https://www.invisioncommunity.com/legal/standards/
|
||||
* @package Invision Community
|
||||
* @since 10 FEB 2017
|
||||
*/
|
||||
|
||||
namespace IPS\Content\Promote;
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal Promotion
|
||||
*/
|
||||
class _Internal extends PromoteAbstract
|
||||
{
|
||||
/**
|
||||
* @brief Icon
|
||||
*/
|
||||
public static $icon = 'plus';
|
||||
|
||||
/**
|
||||
* @brief Default settings
|
||||
*/
|
||||
public $defaultSettings = array();
|
||||
|
||||
/**
|
||||
* Get image
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getPhoto()
|
||||
{
|
||||
return \IPS\Theme::i()->settings['logo_sharer'] ? \IPS\Theme::i()->settings['logo_sharer'] : \IPS\Theme::i()->settings['logo_front'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
return \IPS\Settings::i()->board_name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get form elements for this share service
|
||||
*
|
||||
* @param string $text Text for the text entry
|
||||
* @param string $link Short or full link (short when available)
|
||||
* @param string $content Additional text content (usually a comment, or the item content)
|
||||
*
|
||||
* @return array of form elements
|
||||
*/
|
||||
public function form( $text, $link=null, $content=null )
|
||||
{
|
||||
return array( new \IPS\Helpers\Form\TextArea( 'promote_social_content_internal', $content ?: $text, FALSE, array( 'maxLength' => 3000, 'rows' => 6 ) ) );
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Post to internal
|
||||
*
|
||||
* @param \IPS\Content\Promote $promote Promote Object
|
||||
* @return void
|
||||
*/
|
||||
public function post( $promote )
|
||||
{
|
||||
return time();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the published URL
|
||||
*
|
||||
* @param array $data Data returned from a successful POST
|
||||
* @return \IPS\Http\Url
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function getUrl( $data )
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Promote Abstract Class
|
||||
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) Invision Power Services, Inc.
|
||||
* @license https://www.invisioncommunity.com/legal/standards/
|
||||
* @package Invision Community
|
||||
* @since 13 Jun 2013
|
||||
*/
|
||||
|
||||
namespace IPS\Content\Promote;
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Promote Handler
|
||||
*/
|
||||
abstract class _PromoteAbstract extends \IPS\Patterns\ActiveRecord
|
||||
{
|
||||
/**
|
||||
* @brief [ActiveRecord] Multiton Store
|
||||
*/
|
||||
protected static $multitons;
|
||||
|
||||
/**
|
||||
* @brief [ActiveRecord] Database Table
|
||||
*/
|
||||
public static $databaseTable = 'core_social_promote_sharers';
|
||||
|
||||
/**
|
||||
* @brief [ActiveRecord] Database Prefix
|
||||
*/
|
||||
public static $databasePrefix = 'sharer_';
|
||||
|
||||
/**
|
||||
* @brief [ActiveRecord] ID Database Column
|
||||
*/
|
||||
public static $databaseColumnId = 'key';
|
||||
|
||||
/**
|
||||
* @brief [Node] Order Database Column
|
||||
*/
|
||||
public static $databaseColumnOrder = 'key';
|
||||
|
||||
/**
|
||||
* @brief [Node] Automatically set position for new nodes
|
||||
*/
|
||||
public static $automaticPositionDetermination = FALSE;
|
||||
|
||||
/**
|
||||
* @brief Default settings
|
||||
*/
|
||||
public $defaultSettings = array();
|
||||
|
||||
/**
|
||||
* @brief The last response sent back
|
||||
*/
|
||||
public $response = NULL;
|
||||
|
||||
/**
|
||||
* @brief \IPS\core\Promote object
|
||||
*/
|
||||
public $promote = NULL;
|
||||
|
||||
/**
|
||||
* Construct ActiveRecord from database row
|
||||
*
|
||||
* @param array $data Row from database table
|
||||
* @param bool $updateMultitonStoreIfExists Replace current object in multiton store if it already exists there?
|
||||
* @return static
|
||||
*/
|
||||
public static function constructFromData( $data, $updateMultitonStoreIfExists = TRUE )
|
||||
{
|
||||
/* Initiate an object */
|
||||
$classname = '\\IPS\Content\\Promote\\' . \ucfirst( $data['sharer_key'] );
|
||||
|
||||
if ( !class_exists( $classname ) )
|
||||
{
|
||||
throw new \RuntimeException;
|
||||
}
|
||||
|
||||
$obj = new $classname;
|
||||
$obj->_new = FALSE;
|
||||
|
||||
/* Import data */
|
||||
$databasePrefixLength = \strlen( static::$databasePrefix );
|
||||
foreach ( $data as $k => $v )
|
||||
{
|
||||
if( static::$databasePrefix )
|
||||
{
|
||||
$k = \substr( $k, $databasePrefixLength );
|
||||
}
|
||||
|
||||
$obj->$k = $v;
|
||||
}
|
||||
$obj->changed = array();
|
||||
|
||||
/* Return */
|
||||
return $obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Member
|
||||
*/
|
||||
protected $member;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* \IPS\Member $member The member
|
||||
* @returns $this for daisy chaining
|
||||
*/
|
||||
public function setMember( \IPS\Member $member )
|
||||
{
|
||||
$this->member = $member;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the lowercase key
|
||||
*
|
||||
* @returns string
|
||||
*/
|
||||
public function get__key()
|
||||
{
|
||||
return mb_strtolower( $this->key );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get settings
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_settings()
|
||||
{
|
||||
return array_merge( $this->defaultSettings, json_decode( $this->_data['settings'], TRUE ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Can a member promote using this service?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function canPromote()
|
||||
{
|
||||
return $this->enabled and \IPS\core\Promote::canPromote( $this->member );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get image
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
abstract public function getPhoto();
|
||||
|
||||
/**
|
||||
* Get name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
abstract public function getName();
|
||||
|
||||
/**
|
||||
* Return the published URL
|
||||
*
|
||||
* @param array $data Data returned from a successful POST
|
||||
* @return \IPS\Http\Url
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
abstract function getUrl( $data );
|
||||
|
||||
/**
|
||||
* Bulk save settings
|
||||
*
|
||||
* @param array $settings Array of (k => v) settings
|
||||
* @return void
|
||||
*/
|
||||
public function saveSettings( $settings )
|
||||
{
|
||||
$this->settings = json_encode( array_merge( $this->settings, $settings ) );
|
||||
$this->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* [ActiveRecord] Save Changed Columns
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function save()
|
||||
{
|
||||
parent::save();
|
||||
unset( \IPS\Data\Store::i()->promoters );
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Twitter Promotion
|
||||
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) Invision Power Services, Inc.
|
||||
* @license https://www.invisioncommunity.com/legal/standards/
|
||||
* @package Invision Community
|
||||
* @since 10 FEB 2017
|
||||
*/
|
||||
|
||||
namespace IPS\Content\Promote;
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Twitter Promotion
|
||||
*/
|
||||
class _Twitter extends PromoteAbstract
|
||||
{
|
||||
/**
|
||||
* @brief Icon
|
||||
*/
|
||||
public static $icon = 'twitter';
|
||||
|
||||
/**
|
||||
* @brief Default settings
|
||||
*/
|
||||
public $defaultSettings = array(
|
||||
'id' => NULL,
|
||||
'owner' => NULL,
|
||||
'secret' => NULL,
|
||||
'token' => NULL,
|
||||
'name' => NULL,
|
||||
'permissions' => NULL,
|
||||
'members' => NULL,
|
||||
'tags' => NULL,
|
||||
'tags_method' => 'fill',
|
||||
'image' => NULL,
|
||||
'last_sync' => 0
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Twitter log in object
|
||||
*/
|
||||
protected static $twitter = NULL;
|
||||
|
||||
/**
|
||||
* Twitter object
|
||||
*
|
||||
* @return Object
|
||||
*/
|
||||
protected function twitter()
|
||||
{
|
||||
if ( static::$twitter === NULL )
|
||||
{
|
||||
static::$twitter = \IPS\Login::getHandler('Twitter');
|
||||
}
|
||||
|
||||
return static::$twitter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get image
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getPhoto()
|
||||
{
|
||||
if ( ! $this->settings['image'] or $this->settings['last_sync'] < time() - 86400 )
|
||||
{
|
||||
$user = $this->twitter()->sendRequest( 'get', 'https://api.twitter.com/1.1/account/verify_credentials.json', array(), $this->settings['token'], $this->settings['secret'] )->decodeJson();
|
||||
|
||||
/* Fetch again */
|
||||
$response = \IPS\Http\Url::external( $user['profile_image_url'] )->request()->get();
|
||||
|
||||
$extension = str_replace( 'image/', '', $response->httpHeaders['Content-Type'] );
|
||||
$newFile = \IPS\File::create( 'core_Promote', 'twitter_' . $this->settings['id'] . '.' . $extension, (string) $response, NULL, FALSE, NULL, FALSE );
|
||||
|
||||
$this->saveSettings( array( 'image' => (string) $newFile->url, 'last_sync' => time() ) );
|
||||
}
|
||||
|
||||
return $this->settings['image'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
return $this->settings['name'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check publish permissions
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function canPostToPage()
|
||||
{
|
||||
try
|
||||
{
|
||||
$this->twitter()->sendRequest( 'get', 'https://api.twitter.com/1.1/account/verify_credentials.json', array(), $this->settings['token'], $this->settings['secret'] )->decodeJson();
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
catch ( \IPS\Http\Request\Exception $e ) { }
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get form elements for this share service
|
||||
*
|
||||
* @param string $text Text for the text entry
|
||||
* @param string $link Short or full link (short when available)
|
||||
* @param string $content Additional text content (usually a comment, or the item content)
|
||||
*
|
||||
* @return array of form elements
|
||||
*/
|
||||
public function form( $text, $link=null, $content=null )
|
||||
{
|
||||
$textToActuallyUse = $text;
|
||||
|
||||
if ( $link )
|
||||
{
|
||||
$textToActuallyUse .= ' ' . $link;
|
||||
}
|
||||
|
||||
if ( mb_strlen( $textToActuallyUse ) < 140 )
|
||||
{
|
||||
/* Got any tags to add? */
|
||||
if ( count( $this->settings['tags'] ) )
|
||||
{
|
||||
if ( $this->settings['tags_method'] === 'trim' )
|
||||
{
|
||||
$urlLength = mb_strlen( $link ) + 2; // spaces either side
|
||||
$left = 140 - $urlLength;
|
||||
$tagLength = 0;
|
||||
$tagString = '';
|
||||
$useText = $text;
|
||||
foreach( $this->settings['tags'] as $tag )
|
||||
{
|
||||
if ( $left > 0 )
|
||||
{
|
||||
$tagString .= ' #' . $tag;
|
||||
$left -= mb_strlen( $tagString );
|
||||
}
|
||||
}
|
||||
|
||||
if ( $left > 20 )
|
||||
{
|
||||
$useText = mb_substr( $text, 0, $left );
|
||||
}
|
||||
|
||||
$textToActuallyUse = $useText . ' ' . $link . ' ' . $tagString;
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach( $this->settings['tags'] as $tag )
|
||||
{
|
||||
$considerAddingThis = ' #' . $tag;
|
||||
|
||||
if ( mb_strlen( $textToActuallyUse . $considerAddingThis ) < 140 )
|
||||
{
|
||||
$textToActuallyUse .= $considerAddingThis;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$return = array();
|
||||
|
||||
if ( $this->promote and $this->promote->id )
|
||||
{
|
||||
$textToActuallyUse = $text;
|
||||
$return[] = \IPS\Theme::i()->getTemplate( 'promote' )->promoteDialogTwitterDuplicate();
|
||||
}
|
||||
|
||||
$return[] = new \IPS\Helpers\Form\TextArea( 'promote_social_content_twitter', $textToActuallyUse, FALSE, array( 'maxLength' => 300, 'rows' => 3 ) );
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Post to Twitter
|
||||
*
|
||||
* @param \IPS\Content\Promote $promote Promote Object
|
||||
* @return void
|
||||
*/
|
||||
public function post( $promote )
|
||||
{
|
||||
$photos = $promote->imageObjects();
|
||||
$mediaIds = array();
|
||||
|
||||
if ( count( $photos ) )
|
||||
{
|
||||
try
|
||||
{
|
||||
$done = 0;
|
||||
foreach( $photos as $photo )
|
||||
{
|
||||
/* Twitter can only have a max of 4 images */
|
||||
if ( $done < 4 )
|
||||
{
|
||||
$this->response = $this->twitter()->sendRequest( 'post', 'https://upload.twitter.com/1.1/media/upload.json', array( 'media' => $photo->contents() ), $this->settings['token'], $this->settings['secret'] )->decodeJson();
|
||||
|
||||
if ( isset( $this->response['media_id_string'] ) )
|
||||
{
|
||||
$mediaIds[] = $this->response['media_id_string'];
|
||||
}
|
||||
else
|
||||
{
|
||||
\IPS\Log::log( $this->response, 'twitter' );
|
||||
}
|
||||
|
||||
$done++;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch( \Exception $e )
|
||||
{
|
||||
\IPS\Log::log( $e->getMessage(), 'twitter' );
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$send = array( 'status' => $promote->text['twitter'] );
|
||||
|
||||
if ( count( $mediaIds ) )
|
||||
{
|
||||
$send['media_ids'] = implode( ',', $mediaIds );
|
||||
}
|
||||
|
||||
$this->response = $this->twitter()->sendRequest( 'post', "https://api.twitter.com/1.1/statuses/update.json", $send, $this->settings['token'], $this->settings['secret'] )->decodeJson();
|
||||
|
||||
if ( isset( $this->response['id_str'] ) )
|
||||
{
|
||||
return $this->response['id_str'];
|
||||
}
|
||||
else
|
||||
{
|
||||
/* Check for non critical errors we don't need to flag as complete failures */
|
||||
if ( isset( $this->response['errors'] ) )
|
||||
{
|
||||
foreach( $this->response['errors'] as $error )
|
||||
{
|
||||
/* Status is a duplicate */
|
||||
if ( $error['code'] == 187 )
|
||||
{
|
||||
/* Fetch the original string if possible */
|
||||
foreach( $promote->responses('twitter') as $response )
|
||||
{
|
||||
if ( isset( $response['id_str'] ) )
|
||||
{
|
||||
return $response['id_str'];
|
||||
}
|
||||
}
|
||||
|
||||
/* No? Ok */
|
||||
return 'Duplicate status';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new \InvalidArgumentException( \IPS\Member::loggedIn()->language()->addToStack('twitter_publish_exception') );
|
||||
}
|
||||
}
|
||||
catch ( \Exception $e )
|
||||
{
|
||||
\IPS\Log::log( $e->getMessage(), 'twitter' );
|
||||
|
||||
throw new \InvalidArgumentException( \IPS\Member::loggedIn()->language()->addToStack('twitter_publish_exception') );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the published URL
|
||||
*
|
||||
* @param array $data Data returned from a successful POST
|
||||
* @return \IPS\Http\Url
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function getUrl( $data )
|
||||
{
|
||||
if ( $data and preg_match( '#^[0-9_]*$#', $data ) )
|
||||
{
|
||||
return \IPS\Http\Url::external( 'https://twitter.com/' . $this->settings['id'] . '/status/' . $data );
|
||||
}
|
||||
|
||||
throw new \InvalidArgumentException();
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,11 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Rating Interface for Content Models/Comments
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Community Suite
|
||||
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) Invision Power Services, Inc.
|
||||
* @license https://www.invisioncommunity.com/legal/standards/
|
||||
* @package Invision Community
|
||||
* @since 15 Jan 2013
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS\Content;
|
||||
|
||||
@@ -0,0 +1,575 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Reaction Trait
|
||||
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) Invision Power Services, Inc.
|
||||
* @license https://www.invisioncommunity.com/legal/standards/
|
||||
* @package Invision Community
|
||||
* @since 10 Nov 2016
|
||||
*/
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reaction Trait
|
||||
*/
|
||||
trait Reactable
|
||||
{
|
||||
/**
|
||||
* Reaction type
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function reactionType()
|
||||
{
|
||||
throw new \BadMethodCallException;
|
||||
}
|
||||
|
||||
/**
|
||||
* React
|
||||
*
|
||||
* @param \IPS\core\Reaction THe reaction
|
||||
* @param \IPS\Member The member reacting, or NULL
|
||||
* @return void
|
||||
* @throws \DomainException
|
||||
*/
|
||||
public function react( \IPS\Content\Reaction $reaction, \IPS\Member $member = NULL )
|
||||
{
|
||||
/* Did we pass a member? */
|
||||
$member = $member ?: \IPS\Member::loggedIn();
|
||||
|
||||
/* Figure out the owner of this - if it is content, it will be the author. If it is a node, then it will be the person who created it */
|
||||
if ( $this instanceof \IPS\Content )
|
||||
{
|
||||
$owner = $this->author();
|
||||
}
|
||||
else if ( $this instanceof \IPS\Node\Model )
|
||||
{
|
||||
$owner = $this->owner();
|
||||
}
|
||||
|
||||
/* Can we react? */
|
||||
if ( $this->canReact( $member ) === FALSE )
|
||||
{
|
||||
throw new \DomainException( 'cannot_react' );
|
||||
}
|
||||
|
||||
/* Have we hit our limit? Also, why 999 for unlimited? */
|
||||
if ( $member->group['g_rep_max_positive'] !== -1 )
|
||||
{
|
||||
$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();
|
||||
if ( $count >= $member->group['g_rep_max_positive'] )
|
||||
{
|
||||
throw new \DomainException( \IPS\Member::loggedIn()->language()->addToStack( 'react_daily_exceeded', FALSE, array( 'sprintf' => array( $member->group['g_rep_max_positive'] ) ) ) );
|
||||
}
|
||||
}
|
||||
|
||||
/* Figure out our app - we do it this way as content items and nodes will always have a lowercase namespace for the app, so if the match below fails, then 'core' can be assumed */
|
||||
$app = explode( '\\', get_class( $this ) );
|
||||
if ( \strtolower( $app[1] ) === $app[1] )
|
||||
{
|
||||
$app = $app[1];
|
||||
}
|
||||
else
|
||||
{
|
||||
$app = 'core';
|
||||
}
|
||||
|
||||
/* If this is a comment, we need the parent items ID */
|
||||
$itemId = 0;
|
||||
if ( $this instanceof \IPS\Content\Comment )
|
||||
{
|
||||
$item = $this->item();
|
||||
$itemIdColumn = $item::$databaseColumnId;
|
||||
$itemId = $item->$itemIdColumn;
|
||||
}
|
||||
|
||||
/* Have we already reacted? */
|
||||
if ( $this->reacted( $member ) )
|
||||
{
|
||||
$this->removeReaction( $member );
|
||||
}
|
||||
|
||||
/* Actually insert it */
|
||||
$idColumn = static::$databaseColumnId;
|
||||
\IPS\Db::i()->insert( 'core_reputation_index', array(
|
||||
'member_id' => $member->member_id,
|
||||
'app' => $app,
|
||||
'type' => static::reactionType(),
|
||||
'type_id' => $this->$idColumn,
|
||||
'rep_date' => \IPS\DateTime::create()->getTimestamp(),
|
||||
'rep_rating' => $reaction->value,
|
||||
'member_received' => $owner->member_id,
|
||||
'rep_class' => get_class( $this ),
|
||||
'class_type_id_hash' => md5( get_class( $this ) . ':' . $this->$idColumn ),
|
||||
'item_id' => $itemId,
|
||||
'reaction' => $reaction->id
|
||||
) );
|
||||
|
||||
/* Send the notification */
|
||||
if ( $this->author()->member_id AND $this->author() != \IPS\Member::loggedIn() AND $this->canView( $owner ) )
|
||||
{
|
||||
$notification = new \IPS\Notification( \IPS\Application::load('core'), 'new_likes', $this, array( $this, $member ) );
|
||||
$notification->recipients->attach( $owner );
|
||||
$notification->send();
|
||||
}
|
||||
|
||||
if ( $owner->member_id )
|
||||
{
|
||||
$owner->pp_reputation_points += $reaction->value;
|
||||
$owner->save();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove Reaction
|
||||
*
|
||||
* @param \IPS\Member|NULL The member, or NULL
|
||||
* @return void
|
||||
*/
|
||||
public function removeReaction( \IPS\Member $member = NULL )
|
||||
{
|
||||
$member = $member ?: \IPS\Member::loggedIn();
|
||||
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
$idColumn = static::$databaseColumnId;
|
||||
$rep = \IPS\Db::i()->select( '*', 'core_reputation_index', array( "member_id=? AND rep_class=? AND type=? AND type_id=?", $member->member_id, get_class( $this ), static::reactionType(), $this->$idColumn ) )->first();
|
||||
}
|
||||
catch( \UnderflowException $e )
|
||||
{
|
||||
throw new \OutOfRangeException;
|
||||
}
|
||||
|
||||
$memberReceived = \IPS\Member::load( $rep['member_received'] );
|
||||
$reaction = \IPS\Content\Reaction::load( $rep['reaction'] );
|
||||
}
|
||||
catch( \OutOfRangeException $e )
|
||||
{
|
||||
throw new \DomainException;
|
||||
}
|
||||
|
||||
if ( $memberReceived->member_id )
|
||||
{
|
||||
$memberReceived->pp_reputation_points = $memberReceived->pp_reputation_points - $reaction->value;
|
||||
$memberReceived->save();
|
||||
}
|
||||
|
||||
\IPS\Db::i()->delete( 'core_reputation_index', array( "id=?", $rep['id'] ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Can React
|
||||
*
|
||||
* @param \IPS\Member|NULL The member, or NULL for currently logged in
|
||||
* @return bool
|
||||
*/
|
||||
public function canReact( \IPS\Member $member = NULL )
|
||||
{
|
||||
$member = $member ?: \IPS\Member::loggedIn();
|
||||
|
||||
if ( $this instanceof \IPS\Content )
|
||||
{
|
||||
$owner = $this->author();
|
||||
}
|
||||
else if ( $this instanceof \IPS\Node\Model )
|
||||
{
|
||||
$owner = $this->owner();
|
||||
}
|
||||
|
||||
/* Only members can react */
|
||||
if ( !$member->member_id )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
if ( !$owner->member_id )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/* Protected Groups */
|
||||
if ( $owner->inGroup( explode( ',', \IPS\Settings::i()->reputation_protected_groups ) ) )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/* Reactions per day */
|
||||
if ( $member->group['g_rep_max_positive'] == 0 )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/* React to own content */
|
||||
if ( !\IPS\Settings::i()->reputation_can_self_vote AND $this->author()->member_id == $member->member_id )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/* Still here? All good. */
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Reactions Cache
|
||||
*/
|
||||
protected $_reactions = NULL;
|
||||
|
||||
/**
|
||||
* Reactions
|
||||
*
|
||||
* @param array|NULL $mixInData If the data is already know, it can be passed here to be manually set
|
||||
* @return array
|
||||
*/
|
||||
public function reactions()
|
||||
{
|
||||
if ( $this->_reactionCount === NULL )
|
||||
{
|
||||
$this->_reactionCount = 0;
|
||||
}
|
||||
|
||||
if ( $this->_reactions === NULL )
|
||||
{
|
||||
$idColumn = static::$databaseColumnId;
|
||||
$this->_reactions = array();
|
||||
|
||||
if ( is_array( $this->reputation ) )
|
||||
{
|
||||
foreach( $this->reputation AS $memberId => $reactionId )
|
||||
{
|
||||
try
|
||||
{
|
||||
$this->_reactionCount += \IPS\Content\Reaction::load( $reactionId )->value;
|
||||
$this->_reactions[ $memberId ][] = $reactionId;
|
||||
}
|
||||
catch( \OutOfRangeException $e ) { }
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach( \IPS\Db::i()->select( '*', 'core_reputation_index', $this->getReactionWhereClause() )->join( 'core_reactions', 'reaction=reaction_id' ) AS $reaction )
|
||||
{
|
||||
$this->_reactions[ $reaction['member_id'] ][] = $reaction['reaction'];
|
||||
$this->_reactionCount += $reaction['rep_rating'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->_reactions;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Reaction Count
|
||||
*/
|
||||
protected $_reactionCount = NULL;
|
||||
|
||||
/**
|
||||
* Reaction Count
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function reactionCount()
|
||||
{
|
||||
$this->reactions();
|
||||
return $this->_reactionCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reaction Where Clause
|
||||
*
|
||||
* @param \IPS\Content\Reaction|array|int|NULL This can be any one of the following: An \IPS\Content\Reaction object, an array of \IPS\Content\Reaction objects, and integer, or an array of integers, or NULL
|
||||
* @return array
|
||||
*/
|
||||
public function getReactionWhereClause( $reactions = NULL )
|
||||
{
|
||||
$idColumn = static::$databaseColumnId;
|
||||
$where = array( array( 'rep_class=? AND type=? AND type_id=? AND reaction_enabled=?', get_class( $this ), static::reactionType(), $this->$idColumn, 1 ) );
|
||||
|
||||
if ( $reactions !== NULL )
|
||||
{
|
||||
if ( !is_array( $reactions ) )
|
||||
{
|
||||
$reactions = array( $reactions );
|
||||
}
|
||||
|
||||
$in = array();
|
||||
foreach( $reactions AS $reaction )
|
||||
{
|
||||
if ( $reaction instanceof \IPS\Content\Reaction )
|
||||
{
|
||||
$in[] = $reaction->id;
|
||||
}
|
||||
else
|
||||
{
|
||||
$in[] = $reaction;
|
||||
}
|
||||
}
|
||||
|
||||
if ( count( $in ) )
|
||||
{
|
||||
$where[] = array( \IPS\Db::i()->in( 'reaction', $in ) );
|
||||
}
|
||||
}
|
||||
|
||||
return $where;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reaction Table
|
||||
*
|
||||
* @return \IPS\Helpers\Table\Db
|
||||
*/
|
||||
public function reactionTable( $reaction=NULL )
|
||||
{
|
||||
if ( !\IPS\Member::loggedIn()->group['gbw_view_reps'] )
|
||||
{
|
||||
throw new \DomainException;
|
||||
}
|
||||
|
||||
$idColumn = static::$databaseColumnId;
|
||||
|
||||
$table = new \IPS\Helpers\Table\Db( 'core_reputation_index', $this->url('showReactions'), $this->getReactionWhereClause( $reaction ) );
|
||||
$table->sortBy = 'rep_date';
|
||||
$table->sortDirection = 'desc';
|
||||
$table->tableTemplate = array( \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' ), 'reactionLogTable' );
|
||||
$table->rowsTemplate = array( \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' ), 'reactionLog' );
|
||||
$table->joins = array( array( 'from' => 'core_reactions', 'where' => 'reaction=reaction_id' ) );
|
||||
|
||||
$table->rowButtons = function( $row )
|
||||
{
|
||||
return array(
|
||||
'delete' => array(
|
||||
'icon' => 'times-circle',
|
||||
'title' => 'delete',
|
||||
'link' => $this->url( 'unreact' )->csrf()->setQueryString( array( 'member' => $row['member_id'] ) ),
|
||||
'data' => array( 'confirm' => TRUE )
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
return $table;
|
||||
}
|
||||
|
||||
/**
|
||||
* Has reacted?
|
||||
*
|
||||
* @param \IPS\Member|NULL The member, or NULL for currently logged in
|
||||
* @return \IPS\Content\Reaction|FALSE
|
||||
*/
|
||||
public function reacted( \IPS\Member $member = NULL )
|
||||
{
|
||||
$member = $member ?: \IPS\Member::loggedIn();
|
||||
|
||||
$idColumn = static::$databaseColumnId;
|
||||
try
|
||||
{
|
||||
if ( is_array( $this->reputation ) )
|
||||
{
|
||||
if ( isset( $this->reputation[ $member->member_id ] ) )
|
||||
{
|
||||
return \IPS\Content\Reaction::load( $this->reputation[ $member->member_id ] );
|
||||
}
|
||||
else
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return \IPS\Content\Reaction::load( \IPS\Db::i()->select( 'reaction', 'core_reputation_index', array( "member_id=? AND rep_class=? AND type=? AND type_id=?", $member->member_id, get_class( $this ), static::reactionType(), $this->$idColumn ) )->first() );
|
||||
}
|
||||
}
|
||||
catch( \UnderflowException $e )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Cached React Blurb
|
||||
*/
|
||||
public $reactBlurb = NULL;
|
||||
|
||||
/**
|
||||
* React Blurb
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function reactBlurb()
|
||||
{
|
||||
if ( $this->reactBlurb === NULL )
|
||||
{
|
||||
$this->reactBlurb = array();
|
||||
|
||||
if ( count( $this->reactions() ) )
|
||||
{
|
||||
$idColumn = static::$databaseColumnId;
|
||||
/* Figure out our app - we do it this way as content items and nodes will always have a lowercase namespace for the app, so if the match below fails, then 'core' can be assumed */
|
||||
$app = explode( '\\', get_class( $this ) );
|
||||
if ( \strtolower( $app[1] ) === $app[1] )
|
||||
{
|
||||
$app = $app[1];
|
||||
}
|
||||
else
|
||||
{
|
||||
$app = 'core';
|
||||
}
|
||||
foreach( \IPS\Db::i()->select( 'reaction', 'core_reputation_index', array( "app=? AND rep_class=? AND type=? AND type_id=?", $app, get_class( $this ), static::reactionType(), $this->$idColumn ) ) AS $rep )
|
||||
{
|
||||
if ( !isset( $this->reactBlurb[ $rep ] ) )
|
||||
{
|
||||
$this->reactBlurb[ $rep ] = 0;
|
||||
}
|
||||
|
||||
$this->reactBlurb[ $rep ]++;
|
||||
}
|
||||
|
||||
/* Error suppressor for https://bugs.php.net/bug.php?id=50688 */
|
||||
@uksort( $this->reactBlurb, function( $a, $b ) {
|
||||
try
|
||||
{
|
||||
$a = \IPS\Content\Reaction::load( $a );
|
||||
$b = \IPS\Content\Reaction::load( $b );
|
||||
}
|
||||
/* One of the reactions does not exist */
|
||||
catch( \OutOfRangeException $e )
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
if ( $a->position < $b->position )
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
elseif ( $a->position == $b->position )
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
} );
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->reactBlurb = array();
|
||||
}
|
||||
}
|
||||
return $this->reactBlurb;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Cached like blurb
|
||||
*/
|
||||
public $likeBlurb = NULL;
|
||||
|
||||
/**
|
||||
* Who Reacted
|
||||
*
|
||||
* @param bool|NULL Use like text instead? NULL to automatically determine
|
||||
* @return string
|
||||
*/
|
||||
public function whoReacted( $isLike = NULL )
|
||||
{
|
||||
if ( $isLike === NULL )
|
||||
{
|
||||
$isLike = \IPS\Content\Reaction::isLikeMode();
|
||||
}
|
||||
|
||||
if( $this->likeBlurb === NULL )
|
||||
{
|
||||
$langPrefix = 'react_';
|
||||
if ( $isLike )
|
||||
{
|
||||
$langPrefix = 'like_';
|
||||
}
|
||||
|
||||
/* Did anyone like it? */
|
||||
$numberOfLikes = $this->reactionCount(); # int
|
||||
if ( $numberOfLikes )
|
||||
{
|
||||
/* Is it just us? */
|
||||
$userLiked = ( $this->reacted() );
|
||||
if ( $userLiked and $numberOfLikes < 2 )
|
||||
{
|
||||
$this->likeBlurb = \IPS\Member::loggedIn()->language()->addToStack("{$langPrefix}blurb_just_you");
|
||||
}
|
||||
|
||||
/* Nope, we need to display a number... */
|
||||
else
|
||||
{
|
||||
$peopleToDisplayInMainView = array();
|
||||
$andXOthers = $numberOfLikes;
|
||||
|
||||
/* If the user liked, we always show "You" first */
|
||||
if ( $userLiked )
|
||||
{
|
||||
$peopleToDisplayInMainView[] = \IPS\Member::loggedIn()->language()->addToStack("{$langPrefix}blurb_you_and_others");
|
||||
$andXOthers--;
|
||||
}
|
||||
|
||||
$peopleToDisplayInSecondaryView = array();
|
||||
|
||||
/* Some random names */
|
||||
$idColumn = static::$databaseColumnId;
|
||||
$i = 0;
|
||||
$peopleToDisplayInSecondaryView = array();
|
||||
/* Figure out our app - we do it this way as content items and nodes will always have a lowercase namespace for the app, so if the match below fails, then 'core' can be assumed */
|
||||
$app = explode( '\\', get_class( $this ) );
|
||||
if ( \strtolower( $app[1] ) === $app[1] )
|
||||
{
|
||||
$app = $app[1];
|
||||
}
|
||||
else
|
||||
{
|
||||
$app = 'core';
|
||||
}
|
||||
foreach ( \IPS\Db::i()->select( '*', 'core_reputation_index', array( 'app=? AND type=? AND type_id=? AND member_id!=?', $app, static::reactionType(), $this->$idColumn, ( \IPS\Member::loggedIn()->member_id ) ?: 0 ), 'RAND()', $userLiked ? 17 : 18 ) as $rep )
|
||||
{
|
||||
if ( $i < ( $userLiked ? 2 : 3 ) )
|
||||
{
|
||||
$peopleToDisplayInMainView[] = htmlspecialchars( \IPS\Member::load( $rep['member_id'] )->name, ENT_QUOTES | \IPS\HTMLENTITIES, 'UTF-8', FALSE );
|
||||
$andXOthers--;
|
||||
}
|
||||
else
|
||||
{
|
||||
$peopleToDisplayInSecondaryView[] = htmlspecialchars( \IPS\Member::load( $rep['member_id'] )->name, ENT_QUOTES | \IPS\HTMLENTITIES, 'UTF-8', FALSE );
|
||||
}
|
||||
$i++;
|
||||
}
|
||||
|
||||
/* If there's people to display in the secondary view, add that */
|
||||
if ( $peopleToDisplayInSecondaryView )
|
||||
{
|
||||
if ( count( $peopleToDisplayInSecondaryView ) < $andXOthers )
|
||||
{
|
||||
$peopleToDisplayInSecondaryView[] = \IPS\Member::loggedIn()->language()->addToStack( "{$langPrefix}blurb_others_secondary", FALSE, array( 'pluralize' => array( $andXOthers - count( $peopleToDisplayInSecondaryView ) ) ) );
|
||||
}
|
||||
$peopleToDisplayInMainView[] = \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' )->reputationOthers( $this->url( 'showReactions' ), \IPS\Member::loggedIn()->language()->addToStack( "{$langPrefix}blurb_others", FALSE, array( 'pluralize' => array( $andXOthers ) ) ), json_encode( $peopleToDisplayInSecondaryView ) );
|
||||
}
|
||||
|
||||
/* Put it all together */
|
||||
$this->likeBlurb = \IPS\Member::loggedIn()->language()->addToStack( "{$langPrefix}blurb", FALSE, array( 'pluralize' => array( $numberOfLikes ), 'htmlsprintf' => array( \IPS\Member::loggedIn()->language()->formatList( $peopleToDisplayInMainView ) ) ) );
|
||||
}
|
||||
|
||||
}
|
||||
/* Nobody liked it - show nothing */
|
||||
else
|
||||
{
|
||||
$this->likeBlurb = '';
|
||||
}
|
||||
}
|
||||
|
||||
return $this->likeBlurb;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Reaction Model
|
||||
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) Invision Power Services, Inc.
|
||||
* @license https://www.invisioncommunity.com/legal/standards/
|
||||
* @package Invision Community
|
||||
* @since 10 Nov 2016
|
||||
*/
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reaction Model
|
||||
*/
|
||||
class _Reaction extends \IPS\Node\Model
|
||||
{
|
||||
/**
|
||||
* @brief Database Table
|
||||
*/
|
||||
public static $databaseTable = 'core_reactions';
|
||||
|
||||
/**
|
||||
* @brief Database Prefix
|
||||
*/
|
||||
public static $databasePrefix = 'reaction_';
|
||||
|
||||
/**
|
||||
* @brief Multiton Store
|
||||
*/
|
||||
protected static $multitons;
|
||||
|
||||
/**
|
||||
* @brief [ActiveRecord] ID Database Column
|
||||
*/
|
||||
public static $databaseColumnId = 'id';
|
||||
|
||||
/**
|
||||
* @brief [ActiveRecord] Multiton Map
|
||||
*/
|
||||
protected static $multitonMap = array();
|
||||
|
||||
/**
|
||||
* @brief [Node] Node Title
|
||||
*/
|
||||
public static $nodeTitle = 'reactions';
|
||||
|
||||
/**
|
||||
* @brief [Node] Sortable
|
||||
*/
|
||||
public static $nodeSortable = TRUE;
|
||||
|
||||
/**
|
||||
* @brief [Node] Positon Column
|
||||
*/
|
||||
public static $databaseColumnOrder = 'position';
|
||||
|
||||
/**
|
||||
* @brief [Node] Modal Forms because Charles loves them so
|
||||
*/
|
||||
public static $modalForms = TRUE;
|
||||
|
||||
/**
|
||||
* @brief [Node] Title prefix. If specified, will look for a language key with "{$key}_title" as the key
|
||||
*/
|
||||
public static $titleLangPrefix = 'reaction_title_';
|
||||
|
||||
/**
|
||||
* @brief [Node] Enabled/Disabled Column
|
||||
*/
|
||||
public static $databaseColumnEnabledDisabled = 'enabled';
|
||||
|
||||
/**
|
||||
* Form
|
||||
*
|
||||
* @param \IPS\Helpers\Form The form
|
||||
* @return void
|
||||
*/
|
||||
public function form( &$form )
|
||||
{
|
||||
$form->add( new \IPS\Helpers\Form\Translatable( 'reaction_title', NULL, TRUE, array( 'app' => 'core', 'key' => ( $this->id ? 'reaction_title_' . $this->id : NULL ) ) ) );
|
||||
$form->add( new \IPS\Helpers\Form\Radio( 'reaction_value', $this->id ? $this->value : 1, TRUE, array( 'options' => array( 1 => 'positive', 0 => 'neutral', -1 => 'negative' ) ) ) );
|
||||
$form->add( new \IPS\Helpers\Form\Upload( 'reaction_icon', $this->id ? \IPS\File::get( 'core_Reaction', $this->icon ) : NULL, TRUE, array( 'image' => TRUE, 'storageExtension' => 'core_Reaction', 'storageContainer' => 'reactions', 'obscure' => FALSE ) ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* [Node] Format form values from add/edit form for save
|
||||
*
|
||||
* @param array $values Values from the form
|
||||
* @return array
|
||||
*/
|
||||
public function formatFormValues( $values )
|
||||
{
|
||||
if ( !$this->id )
|
||||
{
|
||||
$this->save();
|
||||
}
|
||||
|
||||
\IPS\Lang::saveCustom( 'core', 'reaction_title_' . $this->id, $values['reaction_title'] );
|
||||
unset( $values['reaction_title'] );
|
||||
|
||||
return parent::formatFormValues( $values );
|
||||
}
|
||||
|
||||
/**
|
||||
* [Node] Does the currently logged in user have permission to delete this node?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function canDelete()
|
||||
{
|
||||
if ( $this->id === 1 )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Icon
|
||||
*
|
||||
* @return \IPS\File
|
||||
*/
|
||||
public function get__icon()
|
||||
{
|
||||
return \IPS\File::get( 'core_Reaction', $this->_data['icon'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Description
|
||||
*
|
||||
* @return strong
|
||||
*/
|
||||
public function get__description()
|
||||
{
|
||||
if ( $this->value == 1 )
|
||||
{
|
||||
return \IPS\Member::loggedIn()->language()->addToStack('positive');
|
||||
}
|
||||
elseif ( $this->value == -1 )
|
||||
{
|
||||
return \IPS\Member::loggedIn()->language()->addToStack('negative');
|
||||
}
|
||||
else
|
||||
{
|
||||
return \IPS\Member::loggedIn()->language()->addToStack('neutral');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch All Root Nodes
|
||||
*
|
||||
* @param string|NULL $permissionCheck The permission key to check for or NULl to not check permissions
|
||||
* @param \IPS\Member|NULL $member The member to check permissions for or NULL for the currently logged in member
|
||||
* @param mixed $where Additional WHERE clause
|
||||
* @return array
|
||||
*/
|
||||
public static function roots( $permissionCheck='view', $member=NULL, $where=array() )
|
||||
{
|
||||
if ( !count( $where ) )
|
||||
{
|
||||
$return = array();
|
||||
foreach( static::reactionStore() AS $reaction )
|
||||
{
|
||||
$return[ $reaction['reaction_id'] ] = static::constructFromData( $reaction );
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
else
|
||||
{
|
||||
return parent::roots( $permissionCheck, $member, $where );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* [Node] Get whether or not this node is enabled
|
||||
*
|
||||
* @note Return value NULL indicates the node cannot be enabled/disabled
|
||||
* @return bool|null
|
||||
*/
|
||||
protected function get__enabled()
|
||||
{
|
||||
if ( $this->id == 1 )
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return parent::get__enabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set Enabled
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function set__enabled( $enabled )
|
||||
{
|
||||
parent::set__enabled( $enabled );
|
||||
|
||||
unset( \IPS\Data\Store::i()->reactions );
|
||||
}
|
||||
|
||||
/**
|
||||
* Reaction Store
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function reactionStore()
|
||||
{
|
||||
if ( !isset( \IPS\Data\Store::i()->reactions ) )
|
||||
{
|
||||
\IPS\Data\Store::i()->reactions = iterator_to_array( \IPS\Db::i()->select( '*', 'core_reactions', NULL, "reaction_position ASC" )->setKeyField( 'reaction_id' ) );
|
||||
}
|
||||
|
||||
return \IPS\Data\Store::i()->reactions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is Like Mode
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function isLikeMode()
|
||||
{
|
||||
$i = 0;
|
||||
foreach( static::roots() AS $row )
|
||||
{
|
||||
if ( $row->_enabled !== FALSE )
|
||||
{
|
||||
$i++;
|
||||
}
|
||||
}
|
||||
|
||||
return ( $i == 1 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Load Record
|
||||
*
|
||||
* @see \IPS\Db::build
|
||||
* @param int|string $id ID
|
||||
* @param string $idField The database column that the $id parameter pertains to (NULL will use static::$databaseColumnId)
|
||||
* @param mixed $extraWhereClause Additional where clause(s) (see \IPS\Db::build for details)
|
||||
* @return static
|
||||
* @throws \InvalidArgumentException
|
||||
* @throws \OutOfRangeException
|
||||
*/
|
||||
public static function load( $id, $idField=NULL, $extraWhereClause=NULL )
|
||||
{
|
||||
if ( ( $idField === NULL or $idField === 'reaction_id' ) and $extraWhereClause === NULL )
|
||||
{
|
||||
$reactions = static::reactionStore();
|
||||
if ( isset( $reactions[ $id ] ) )
|
||||
{
|
||||
return static::constructFromData( $reactions[ $id ] );
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new \OutOfRangeException;
|
||||
}
|
||||
}
|
||||
|
||||
return parent::load( $id, $idField, $extraWhereClause );
|
||||
}
|
||||
|
||||
/**
|
||||
* [ActiveRecord] Delete
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function delete()
|
||||
{
|
||||
parent::delete();
|
||||
|
||||
unset( \IPS\Data\Store::i()->reactions );
|
||||
}
|
||||
|
||||
/**
|
||||
* [ActiveRecord] Save Changed Columns
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function save()
|
||||
{
|
||||
parent::save();
|
||||
|
||||
unset( \IPS\Data\Store::i()->reactions );
|
||||
}
|
||||
|
||||
/**
|
||||
* [Node] Get buttons to display in tree
|
||||
* Example code explains return value
|
||||
*
|
||||
* @code
|
||||
array(
|
||||
array(
|
||||
'icon' => 'plus-circle', // Name of FontAwesome icon to use
|
||||
'title' => 'foo', // Language key to use for button's title parameter
|
||||
'link' => \IPS\Http\Url::internal( 'app=foo...' ) // URI to link to
|
||||
'class' => 'modalLink' // CSS Class to use on link (Optional)
|
||||
),
|
||||
... // Additional buttons
|
||||
);
|
||||
* @endcode
|
||||
* @param string $url Base URL
|
||||
* @param bool $subnode Is this a subnode?
|
||||
* @return array
|
||||
*/
|
||||
public function getButtons( $url, $subnode=FALSE )
|
||||
{
|
||||
$buttons = parent::getButtons( $url, $subnode );
|
||||
|
||||
if ( $this->canDelete() )
|
||||
{
|
||||
$buttons['delete'] = array(
|
||||
'icon' => 'times-circle',
|
||||
'title' => 'delete',
|
||||
'link' => $url->setQueryString( array( 'do' => 'delete', 'id' => $this->_id ) ),
|
||||
'data' => array( 'ipsDialog' => '', 'ipsDialog-title' => \IPS\Member::loggedIn()->language()->addToStack('delete') ),
|
||||
'hotkey'=> 'd'
|
||||
);
|
||||
}
|
||||
return $buttons;
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,11 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Read/Unread Tracking Interface for Content Models
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Community Suite
|
||||
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) Invision Power Services, Inc.
|
||||
* @license https://www.invisioncommunity.com/legal/standards/
|
||||
* @package Invision Community
|
||||
* @since 8 Jul 2013
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS\Content;
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Interface for Reportable Comment Models
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Community Suite
|
||||
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) Invision Power Services, Inc.
|
||||
* @license https://www.invisioncommunity.com/legal/standards/
|
||||
* @package Invision Community
|
||||
* @since 8 Jul 2013
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS\Content;
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Reputation interface for Content Models
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Community Suite
|
||||
* @since 27 Nov 2013
|
||||
* @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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reputation Interface for Content Models
|
||||
*
|
||||
* @note Content classes will gain special functionality by implementing this interface
|
||||
*/
|
||||
interface Reputation
|
||||
{
|
||||
}
|
||||
@@ -1,12 +1,11 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Content Review Model
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Community Suite
|
||||
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) Invision Power Services, Inc.
|
||||
* @license https://www.invisioncommunity.com/legal/standards/
|
||||
* @package Invision Community
|
||||
* @since 4 Nov 2013
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS\Content;
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Content Filter for Search Queries
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Community Suite
|
||||
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) Invision Power Services, Inc.
|
||||
* @license https://www.invisioncommunity.com/legal/standards/
|
||||
* @package Invision Community
|
||||
* @since 6 Jul 2015
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS\Content\Search;
|
||||
@@ -32,6 +31,12 @@ class _ContentFilter
|
||||
* @brief Classes to include
|
||||
*/
|
||||
public $classes = array();
|
||||
|
||||
/**
|
||||
* @brief Container classes to include
|
||||
* @note NULL means not to filter by container classes
|
||||
*/
|
||||
public $containerClasses = NULL;
|
||||
|
||||
/**
|
||||
* @brief Container ID filter
|
||||
@@ -85,7 +90,7 @@ class _ContentFilter
|
||||
* @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
|
||||
* @return \IPS\Content\Search\ContentFilter
|
||||
*/
|
||||
public static function init( $itemClass, $includeItems=TRUE, $includeComments=TRUE, $includeReviews=TRUE )
|
||||
{
|
||||
@@ -114,7 +119,7 @@ class _ContentFilter
|
||||
* Only include results in containers
|
||||
*
|
||||
* @param array $ids Acceptable container IDs
|
||||
* @return \IPS\Content\Search\ContainerFilter (for daisy chaining)
|
||||
* @return \IPS\Content\Search\ContentFilter (for daisy chaining)
|
||||
*/
|
||||
public function onlyInContainers( array $ids )
|
||||
{
|
||||
@@ -128,7 +133,7 @@ class _ContentFilter
|
||||
* Exclude results in containers
|
||||
*
|
||||
* @param array $ids Acceptable container IDs
|
||||
* @return \IPS\Content\Search\ContainerFilter (for daisy chaining)
|
||||
* @return \IPS\Content\Search\ContentFilter (for daisy chaining)
|
||||
*/
|
||||
public function excludeInContainers( array $ids )
|
||||
{
|
||||
@@ -142,7 +147,7 @@ class _ContentFilter
|
||||
* Only include results in items
|
||||
*
|
||||
* @param array $ids Acceptable item IDs
|
||||
* @return \IPS\Content\Search\ContainerFilter (for daisy chaining)
|
||||
* @return \IPS\Content\Search\ContentFilter (for daisy chaining)
|
||||
*/
|
||||
public function onlyInItems( array $ids )
|
||||
{
|
||||
@@ -156,7 +161,7 @@ class _ContentFilter
|
||||
* Exclude results in items
|
||||
*
|
||||
* @param array $ids Acceptable container IDs
|
||||
* @return \IPS\Content\Search\ContainerFilter (for daisy chaining)
|
||||
* @return \IPS\Content\Search\ContentFilter (for daisy chaining)
|
||||
*/
|
||||
public function excludeInItems( array $ids )
|
||||
{
|
||||
@@ -170,7 +175,7 @@ class _ContentFilter
|
||||
* Set minimum number of comments
|
||||
*
|
||||
* @param int $minimumComments The minimum number of comments
|
||||
* @return \IPS\Content\Search\ContainerFilter (for daisy chaining)
|
||||
* @return \IPS\Content\Search\ContentFilter (for daisy chaining)
|
||||
*/
|
||||
public function minimumComments( $minimumComments )
|
||||
{
|
||||
@@ -183,7 +188,7 @@ class _ContentFilter
|
||||
* Set minimum number of reviews
|
||||
*
|
||||
* @param int $minimumReviews The minimum number of reviews
|
||||
* @return \IPS\Content\Search\ContainerFilter (for daisy chaining)
|
||||
* @return \IPS\Content\Search\ContentFilter (for daisy chaining)
|
||||
*/
|
||||
public function minimumReviews( $minimumReviews )
|
||||
{
|
||||
@@ -196,7 +201,7 @@ class _ContentFilter
|
||||
* Set minimum number of views
|
||||
*
|
||||
* @param int $minimumViews The minimum number of views
|
||||
* @return \IPS\Content\Search\ContainerFilter (for daisy chaining)
|
||||
* @return \IPS\Content\Search\ContentFilter (for daisy chaining)
|
||||
*/
|
||||
public function minimumViews( $minimumViews )
|
||||
{
|
||||
@@ -208,7 +213,7 @@ class _ContentFilter
|
||||
/**
|
||||
* Only include results which are the first comment on an item requiring a comment
|
||||
*
|
||||
* @return \IPS\Content\Search\ContainerFilter (for daisy chaining)
|
||||
* @return \IPS\Content\Search\ContentFilter (for daisy chaining)
|
||||
*/
|
||||
public function onlyFirstComment()
|
||||
{
|
||||
@@ -220,7 +225,7 @@ class _ContentFilter
|
||||
/**
|
||||
* Only include results which are the last comment on an item requiring a comment
|
||||
*
|
||||
* @return \IPS\Content\Search\ContainerFilter (for daisy chaining)
|
||||
* @return \IPS\Content\Search\ContentFilter (for daisy chaining)
|
||||
*/
|
||||
public function onlyLastComment()
|
||||
{
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Abstract Search Index
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Community Suite
|
||||
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) Invision Power Services, Inc.
|
||||
* @license https://www.invisioncommunity.com/legal/standards/
|
||||
* @package Invision Community
|
||||
* @since 21 Aug 2014
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS\Content\Search;
|
||||
@@ -250,12 +249,32 @@ abstract class _Index extends \IPS\Patterns\Singleton
|
||||
catch ( \Exception $e ) { }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Club */
|
||||
$container = NULL;
|
||||
$containerId = NULL;
|
||||
$clubId = NULL;
|
||||
if ( $object instanceof \IPS\Content\Item )
|
||||
{
|
||||
$containerId = (int) $object->searchIndexContainer();
|
||||
$container = $object->containerWrapper();
|
||||
}
|
||||
else
|
||||
{
|
||||
$containerId = (int) $object->item()->mapped('container');
|
||||
$container = $object->item()->containerWrapper();
|
||||
}
|
||||
if ( $container and \IPS\IPS::classUsesTrait( $container, 'IPS\Content\ClubContainer' ) )
|
||||
{
|
||||
$clubId = $container->{$container::clubIdColumn()};
|
||||
}
|
||||
|
||||
/* 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_class' => ( $object instanceof \IPS\Content\Item ) ? get_class( $object->searchIndexContainerClass() ) : get_class( $object->item()->searchIndexContainerClass() ),
|
||||
'index_container_id' => ( $object instanceof \IPS\Content\Item ) ? (int) $object->searchIndexContainer() : (int) $object->item()->mapped('container'),
|
||||
'index_title' => $title,
|
||||
'index_content' => $content,
|
||||
@@ -268,7 +287,8 @@ abstract class _Index extends \IPS\Patterns\Singleton
|
||||
'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
|
||||
'index_is_last_comment' => $isLastComment,
|
||||
'index_club_id' => $clubId
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief MySQL Search Index
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Community Suite
|
||||
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) Invision Power Services, Inc.
|
||||
* @license https://www.invisioncommunity.com/legal/standards/
|
||||
* @package Invision Community
|
||||
* @since 21 Aug 2014
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS\Content\Search\Mysql;
|
||||
@@ -331,7 +330,7 @@ class _Index extends \IPS\Content\Search\Index
|
||||
$where[] = array( 'index_container_id=?', $containerId );
|
||||
}
|
||||
|
||||
\IPS\Db::i()->delete( 'core_search_index_item_map', array( 'index_item_id IN( ? )', \IPS\Db::i()->select( 'index_item_id', 'core_search_index', $where ) ) );
|
||||
\IPS\Db::i()->delete( 'core_search_index_item_map', array( 'index_class=? and index_item_id IN( ? )', $class, \IPS\Db::i()->select( 'index_item_id', 'core_search_index', $where ) ) );
|
||||
\IPS\Db::i()->delete( 'core_search_index_tags', array( 'index_id IN( ? )', \IPS\Db::i()->select( 'index_id', 'core_search_index', $where ) ) );
|
||||
\IPS\Db::i()->delete( 'core_search_index', $where );
|
||||
}
|
||||
@@ -375,6 +374,18 @@ class _Index extends \IPS\Content\Search\Index
|
||||
if ( $newContainer )
|
||||
{
|
||||
$update['index_container_id'] = $newContainer;
|
||||
|
||||
if ( $itemClass = ( in_array( 'IPS\Content\Item', class_parents( $class ) ) ? $class : $class::$itemClass ) and $containerClass = $itemClass::$containerNodeClass and \IPS\IPS::classUsesTrait( $containerClass, 'IPS\Content\ClubContainer' ) and $clubIdColumn = $containerClass::clubIdColumn() )
|
||||
{
|
||||
try
|
||||
{
|
||||
$update['index_club_id'] = $containerClass::load( $newContainer )->$clubIdColumn;
|
||||
}
|
||||
catch ( \OutOfRangeException $e )
|
||||
{
|
||||
$update['index_club_id'] = NULL;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ( $newItemId )
|
||||
{
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief MySQL Search Query
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Community Suite
|
||||
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) Invision Power Services, Inc.
|
||||
* @license https://www.invisioncommunity.com/legal/standards/
|
||||
* @package Invision Community
|
||||
* @since 21 Aug 2014
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS\Content\Search\Mysql;
|
||||
@@ -128,6 +127,12 @@ class _Query extends \IPS\Content\Search\Query
|
||||
$clause[] = \IPS\Db::i()->in( 'index_container_id', $unsearchableIds, TRUE );
|
||||
}
|
||||
}
|
||||
|
||||
/* Are we excluding certain container classes? */
|
||||
if( $filter->containerClasses !== NULL )
|
||||
{
|
||||
$clause[] = \IPS\Db::i()->in( 'index_container_class', $filter->containerClasses );
|
||||
}
|
||||
|
||||
/* Set the item IDs */
|
||||
if ( $filter->itemIdFilter !== NULL )
|
||||
@@ -209,7 +214,32 @@ class _Query extends \IPS\Content\Search\Query
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Filter by club
|
||||
*
|
||||
* @param \IPS\Member\Club|int|array|null $club The club, or array of club IDs, or NULL to exclude content from clubs
|
||||
* @return \IPS\Content\Search\Query (for daisy chaining)
|
||||
*/
|
||||
public function filterByClub( $club )
|
||||
{
|
||||
if ( $club === NULL )
|
||||
{
|
||||
$this->where[] = 'index_club_id IS NULL';
|
||||
}
|
||||
if ( is_array( $club ) )
|
||||
{
|
||||
$this->where[] = array( \IPS\Db::i()->in( 'index_club_id', $club ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->where[] = array( 'index_club_id=?', $club instanceof \IPS\Member\Club ? $club->id : $club );
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter for profile
|
||||
@@ -253,6 +283,37 @@ class _Query extends \IPS\Content\Search\Query
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter by container class
|
||||
*
|
||||
* @param array $classes Container classes to exclude from results.
|
||||
* @param array $exclude Content classes to exclude from the filter. For cases where multiple content classes may have the same container class
|
||||
* such as Gallery images, comments and reviews.
|
||||
* @return \IPS\Content\Search\Query (for daisy chaining)
|
||||
*/
|
||||
public function filterByContainerClasses( $classes=array(), $exclude=array() )
|
||||
{
|
||||
if( empty( $exclude ) )
|
||||
{
|
||||
$this->where[] = \IPS\Db::i()->in( 'index_container_class', $classes, TRUE );
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach( $classes as $i => $class )
|
||||
{
|
||||
$classes[$i] = "'" . \IPS\DB::i()->real_escape_string( $class ) . "'";
|
||||
}
|
||||
foreach( $exclude as $i => $class )
|
||||
{
|
||||
$exclude[$i] = "'" . \IPS\DB::i()->real_escape_string( $class ) . "'";
|
||||
}
|
||||
|
||||
$this->where[] = '( index_container_class NOT IN(' . implode( ',', $classes ) . ') OR index_class IN(' . implode( ',', $exclude ) . ') )';
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter by content the user follows
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Abstract Search Query
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Community Suite
|
||||
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) Invision Power Services, Inc.
|
||||
* @license https://www.invisioncommunity.com/legal/standards/
|
||||
* @package Invision Community
|
||||
* @since 21 Aug 2014
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS\Content\Search;
|
||||
@@ -46,7 +45,7 @@ abstract class _Query
|
||||
* Create new query
|
||||
*
|
||||
* @param \IPS\Member $member The member performing the search (NULL for currently logged in member)
|
||||
* @return \IPS\Content\Search
|
||||
* @return \IPS\Content\Search\Query
|
||||
*/
|
||||
public static function init( \IPS\Member $member = NULL )
|
||||
{
|
||||
@@ -108,6 +107,14 @@ abstract class _Query
|
||||
*/
|
||||
abstract public function filterByAuthor( $author );
|
||||
|
||||
/**
|
||||
* Filter by club
|
||||
*
|
||||
* @param \IPS\Member\Club|int|array $club The club, or array of club IDs
|
||||
* @return \IPS\Content\Search\Query (for daisy chaining)
|
||||
*/
|
||||
abstract public function filterByClub( $club );
|
||||
|
||||
/**
|
||||
* Filter for profile
|
||||
*
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Search Result from Index
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Community Suite
|
||||
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) Invision Power Services, Inc.
|
||||
* @license https://www.invisioncommunity.com/legal/standards/
|
||||
* @package Invision Community
|
||||
* @since 15 Sep 2015
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS\Content\Search\Result;
|
||||
@@ -63,9 +62,10 @@ class _Content extends \IPS\Content\Search\Result
|
||||
* @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 array $reactions Reaction Data
|
||||
* @return void
|
||||
*/
|
||||
public function __construct( array $indexData, array $authorData, array $itemData, array $containerData = NULL, array $reputationData, $reviewRating = NULL, $iPostedIn = FALSE )
|
||||
public function __construct( array $indexData, array $authorData, array $itemData, array $containerData = NULL, array $reputationData, $reviewRating = NULL, $iPostedIn = FALSE, $reactions=array() )
|
||||
{
|
||||
$this->createdDate = \IPS\DateTime::ts( $indexData['index_date_created'] );
|
||||
$this->lastUpdatedDate = \IPS\DateTime::ts( $indexData['index_date_updated'] );
|
||||
@@ -76,6 +76,7 @@ class _Content extends \IPS\Content\Search\Result
|
||||
$this->reputationData = $reputationData;
|
||||
$this->reviewRating = $reviewRating;
|
||||
$this->iPostedIn = $iPostedIn;
|
||||
$this->reactions = $reactions;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -89,7 +90,7 @@ class _Content extends \IPS\Content\Search\Result
|
||||
*/
|
||||
public function html( $view = 'expanded', $asItem = FALSE, $canIgnoreComments=FALSE, $template=NULL )
|
||||
{
|
||||
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, $canIgnoreComments, $template );
|
||||
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, $canIgnoreComments, $template, $this->reactions );
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Search Result not from Index
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Community Suite
|
||||
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) Invision Power Services, Inc.
|
||||
* @license https://www.invisioncommunity.com/legal/standards/
|
||||
* @package Invision Community
|
||||
* @since 15 Sep 2015
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS\Content\Search\Result;
|
||||
@@ -32,6 +31,11 @@ class _Custom extends \IPS\Content\Search\Result
|
||||
* @brief Image
|
||||
*/
|
||||
protected $image;
|
||||
|
||||
/**
|
||||
* @brief Data used for merging multiple extra items
|
||||
*/
|
||||
protected $mergeData;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
@@ -39,14 +43,39 @@ class _Custom extends \IPS\Content\Search\Result
|
||||
* @param \IPS\DateTime $time The time for this result
|
||||
* @param string $html HTML to display
|
||||
* @param string|NULL $image HTML for image to display
|
||||
* @param string|NULL $mergeData Data for merging
|
||||
* @return void
|
||||
*/
|
||||
public function __construct( \IPS\DateTime $time, $html, $image = NULL )
|
||||
public function __construct( \IPS\DateTime $time, $html, $image = NULL, $mergeData = NULL )
|
||||
{
|
||||
$this->createdDate = $time;
|
||||
$this->lastUpdatedDate = $time;
|
||||
$this->html = $html;
|
||||
$this->image = $image;
|
||||
$this->mergeData = array( $mergeData );
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge in new data
|
||||
*
|
||||
* @param string $html New HTML to merge in as a list
|
||||
* @param string|NULL $mergeData Data for merging
|
||||
* @return void
|
||||
*/
|
||||
public function mergeInData( $html, $mergeData = NULL )
|
||||
{
|
||||
$this->html = $html;
|
||||
$this->mergeData[] = $mergeData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get data for merging
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getMergeData()
|
||||
{
|
||||
return $this->mergeData ?: array();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Search Result
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Community Suite
|
||||
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) Invision Power Services, Inc.
|
||||
* @license https://www.invisioncommunity.com/legal/standards/
|
||||
* @package Invision Community
|
||||
* @since 15 Sep 2015
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS\Content\Search;
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Search Results
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Community Suite
|
||||
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) Invision Power Services, Inc.
|
||||
* @license https://www.invisioncommunity.com/legal/standards/
|
||||
* @package Invision Community
|
||||
* @since 21 Aug 2014
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS\Content\Search;
|
||||
@@ -53,6 +52,11 @@ class _Results extends \ArrayIterator
|
||||
*/
|
||||
protected $reputationData = array();
|
||||
|
||||
/**
|
||||
* @brief Reaction Data
|
||||
*/
|
||||
protected $reactions = array();
|
||||
|
||||
/**
|
||||
* @brief Review Ratings
|
||||
*/
|
||||
@@ -139,7 +143,7 @@ class _Results extends \ArrayIterator
|
||||
}
|
||||
}
|
||||
|
||||
if ( in_array( 'IPS\Content\Reputation', class_implements( $result['index_class'] ) ) )
|
||||
if ( \IPS\IPS::classUsesTrait( $result['index_class'], 'IPS\Content\Reactable' ) )
|
||||
{
|
||||
$reputationIds[ $result['index_class'] ][ $result['index_object_id'] ] = $result['index_object_id'];
|
||||
}
|
||||
@@ -153,8 +157,13 @@ class _Results extends \ArrayIterator
|
||||
{
|
||||
$itemIndexIds[] = $result['index_item_index_id'];
|
||||
}
|
||||
|
||||
if ( $result['index_club_id'] )
|
||||
{
|
||||
$containersToLoad[ 'IPS\Member\Club' ][ $result['index_club_id'] ] = $result['index_club_id'];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Load item data */
|
||||
foreach ( $itemsToLoad as $itemClass => $itemIds )
|
||||
{
|
||||
@@ -162,7 +171,7 @@ class _Results extends \ArrayIterator
|
||||
{
|
||||
$this->itemData[ $itemClass ][ $itemId ] = $itemData;
|
||||
|
||||
if ( isset( $itemData[ $itemClass::$databasePrefix . $itemClass::$databaseColumnMap['author'] ] ) )
|
||||
if ( isset( $itemClass::$databaseColumnMap ) and isset( $itemData[ $itemClass::$databasePrefix . $itemClass::$databaseColumnMap['author'] ] ) )
|
||||
{
|
||||
$memberId = $itemData[ $itemClass::$databasePrefix . $itemClass::$databaseColumnMap['author'] ];
|
||||
$membersToLoad[ $memberId ] = $memberId;
|
||||
@@ -198,26 +207,23 @@ class _Results extends \ArrayIterator
|
||||
$binds = array();
|
||||
foreach ( $reputationIds as $class => $ids )
|
||||
{
|
||||
$clause[] = "( app=? AND type=? AND " . \IPS\Db::i()->in( 'type_id', $ids ) . " )";
|
||||
$clause[] = "( reaction_enabled=? AND app=? AND type=? AND " . \IPS\Db::i()->in( 'type_id', $ids ) . " )";
|
||||
$binds[] = 1;
|
||||
$binds[] = $class::$application;
|
||||
$binds[] = $class::$reputationType;
|
||||
$binds[] = $class::reactionType();
|
||||
}
|
||||
|
||||
$where = array( array_merge( array( implode( ' OR ', $clause ) ), $binds ) );
|
||||
switch( \IPS\Settings::i()->reputation_point_types )
|
||||
foreach ( \IPS\Db::i()->select( array( 'app', 'type', 'type_id', 'member_id', 'rep_rating', 'reaction' ), 'core_reputation_index', $where )->join( 'core_reactions', 'reaction=reaction_id' ) as $rep )
|
||||
{
|
||||
case 'positive':
|
||||
case 'like':
|
||||
$where[] = array( 'rep_rating=?', "1" );
|
||||
break;
|
||||
case 'negative':
|
||||
$where[] = array( 'rep_rating=?', "-1" );
|
||||
break;
|
||||
}
|
||||
|
||||
foreach ( \IPS\Db::i()->select( array( 'app', 'type', 'type_id', 'member_id', 'rep_rating' ), 'core_reputation_index', $where ) as $rep )
|
||||
{
|
||||
$this->reputationData[ $rep['app'] ][ $rep['type'] ][ $rep['type_id'] ][ $rep['member_id'] ] = $rep['rep_rating'];
|
||||
$this->reputationData[ $rep['app'] ][ $rep['type'] ][ $rep['type_id'] ][ $rep['member_id'] ] = $rep['reaction'];
|
||||
|
||||
if ( !isset( $this->reactions[ $rep['app'] ][ $rep['type'] ][ $rep['type_id' ] ][ $rep['reaction'] ] ) )
|
||||
{
|
||||
$this->reactions[ $rep['app'] ][ $rep['type'] ][ $rep['type_id' ] ][ $rep['reaction'] ] = 0;
|
||||
}
|
||||
|
||||
$this->reactions[ $rep['app'] ][ $rep['type'] ][ $rep['type_id' ] ][ $rep['reaction'] ]++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -258,9 +264,15 @@ class _Results extends \ArrayIterator
|
||||
$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'] ] ) )
|
||||
if ( \IPS\IPS::classUsesTrait( $class, 'IPS\Content\Reactable' ) and isset( $this->reputationData[ $class::$application ][ $class::reactionType() ][ $data['index_object_id'] ] ) )
|
||||
{
|
||||
$reputationData = $this->reputationData[ $class::$application ][ $class::$reputationType ][ $data['index_object_id'] ];
|
||||
$reputationData = $this->reputationData[ $class::$application ][ $class::reactionType() ][ $data['index_object_id'] ];
|
||||
}
|
||||
|
||||
$reactions = array();
|
||||
if ( \IPS\IPS::classUsesTrait( $class, 'IPS\Content\Reactable' ) and isset( $this->reactions[ $class::$application ][ $class::reactionType() ][ $data['index_object_id'] ] ) )
|
||||
{
|
||||
$reactions = $this->reactions[ $class::$application ][ $class::reactionType() ][ $data['index_object_id'] ];
|
||||
}
|
||||
|
||||
/* Ensure we have an item if results are gathered from the map table */
|
||||
@@ -295,14 +307,25 @@ class _Results extends \ArrayIterator
|
||||
$data['index_prefix'] = $this->prefixes[ $data['index_item_index_id'] ]['index_prefix'];
|
||||
}
|
||||
|
||||
$containerData = NULL;
|
||||
if ( $containerClass and isset( $this->containerData[ $containerClass ] ) and isset( $this->containerData[ $containerClass ][ $data['index_container_id'] ] ) )
|
||||
{
|
||||
$containerData = $this->containerData[ $containerClass ][ $data['index_container_id'] ];
|
||||
if ( $data['index_club_id'] and isset( $this->containerData[ 'IPS\Member\Club' ][ $data['index_club_id'] ] ) )
|
||||
{
|
||||
$containerData['_club'] = $this->containerData[ 'IPS\Member\Club' ][ $data['index_club_id'] ];
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
$containerData,
|
||||
$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 )
|
||||
in_array( $data['index_item_index_id'], $this->iPostedIn ),
|
||||
$reactions
|
||||
);
|
||||
}
|
||||
|
||||
@@ -383,8 +406,15 @@ class _Results extends \ArrayIterator
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach ( $results as $result )
|
||||
foreach ( $results as $key => $result )
|
||||
{
|
||||
/* If an item has been removed incorrectly an orphan search index entry can be left. This is already logged in current() so let's not fail catastrophically */
|
||||
if( !$result )
|
||||
{
|
||||
unset( $results[ $key ] );
|
||||
continue;
|
||||
}
|
||||
|
||||
$lastTime = $result->createdDate->getTimestamp();
|
||||
}
|
||||
}
|
||||
@@ -481,6 +511,9 @@ class _Results extends \ArrayIterator
|
||||
/* If an application was not updated or is not installed, do not try to fetch it or we can get a class does not exist fatal error */
|
||||
$where[] = array( "follow_app IN('" . implode( "','", array_keys( \IPS\Application::enabledApplications() ) ) . "')" );
|
||||
|
||||
$lastClass = NULL;
|
||||
$lastMember = NULL;
|
||||
|
||||
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' )
|
||||
@@ -493,66 +526,121 @@ class _Results extends \ArrayIterator
|
||||
else
|
||||
{
|
||||
$class = 'IPS\\' . $follow['follow_app'] . '\\' . ucfirst( $follow['follow_area'] );
|
||||
|
||||
try
|
||||
{
|
||||
if ( class_exists( $class ) )
|
||||
{
|
||||
$thingBeingFollowed = $class::loadAndCheckPerms( $follow['follow_rel_id'] );
|
||||
$thingBeingFollowed = $class::loadAndCheckPerms( $follow['follow_rel_id'] );
|
||||
$urlToThing = \IPS\Theme::i()->getTemplate( 'global', 'core', 'global' )->basicUrl( $thingBeingFollowed->url(), FALSE, $thingBeingFollowed instanceof \IPS\Node\Model ? $thingBeingFollowed->_title : $thingBeingFollowed->mapped('title'), FALSE );
|
||||
|
||||
/* Do we need to merge with the last result? */
|
||||
if( $lastClass !== NULL AND $lastClass === $class AND $lastMember === $follow['follow_member_id'] AND count( $extraItems ) )
|
||||
{
|
||||
$extraItem = array_pop( $extraItems );
|
||||
|
||||
$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 ) ) ) )
|
||||
);
|
||||
$mergeList = array_merge( array( $urlToThing ), $extraItem->getMergeData() );
|
||||
$listToDisplay = $mergeList;
|
||||
|
||||
if( count( $mergeList ) > 3 )
|
||||
{
|
||||
$listToDisplay = array_slice( $mergeList, 0, 3 );
|
||||
$listToDisplay[] = \IPS\Member::loggedIn()->language()->addToStack( 'and_x_others', FALSE, array( 'pluralize' => array( count( $mergeList ) - 3 ) ) );
|
||||
}
|
||||
$extraItem->mergeInData( \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\Member::loggedIn()->language()->formatList( $listToDisplay ) ) ) ), $urlToThing );
|
||||
$extraItems[] = $extraItem;
|
||||
}
|
||||
else
|
||||
{
|
||||
$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'] ) ), $urlToThing ) ) ),
|
||||
NULL,
|
||||
$urlToThing
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
catch ( \OutOfRangeException $e ) { }
|
||||
|
||||
$lastClass = $class;
|
||||
$lastMember = $follow['follow_member_id'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Likes */
|
||||
if ( \IPS\Settings::i()->reputation_enabled and \IPS\Member::loggedIn()->group['gbw_view_reps'] and ( in_array( 'like', $extra ) or in_array( 'rep_neg', $extra ) ) )
|
||||
if ( \IPS\Settings::i()->reputation_enabled and \IPS\Member::loggedIn()->group['gbw_view_reps'] and ( in_array( 'like', $extra ) or in_array( 'react', $extra ) ) )
|
||||
{
|
||||
$where = array( array( 'rep_date>?', $lastTime ) );
|
||||
$where[] = array( 'reaction_enabled=?', 1 );
|
||||
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' );
|
||||
}
|
||||
|
||||
/* get only the reputation data for installed applications */
|
||||
$where[] = \IPS\Db::i()->in( 'app', array_keys( \IPS\Application::applications() ) ) ;
|
||||
|
||||
if ( $author )
|
||||
{
|
||||
foreach ( \IPS\Db::i()->select( '*', 'core_reputation_index', array_merge( $where, array( array( 'member_id=?', $author->member_id ) ) ), 'rep_date DESC', 10 ) as $rep )
|
||||
foreach ( \IPS\Db::i()->select( '*', 'core_reputation_index', array_merge( $where, array( array( 'member_id=?', $author->member_id ) ) ), 'rep_date DESC', 10 )->join( 'core_reactions', 'reaction=reaction_id' ) as $rep )
|
||||
{
|
||||
$this->addReputationExtraItem( $rep, $extraItems );
|
||||
}
|
||||
|
||||
foreach ( \IPS\Db::i()->select( '*', 'core_reputation_index', array_merge( $where, array( array( 'member_received=?', $author->member_id ) ) ), 'rep_date DESC', 10 ) as $rep )
|
||||
foreach ( \IPS\Db::i()->select( '*', 'core_reputation_index', array_merge( $where, array( array( 'member_received=?', $author->member_id ) ) ), 'rep_date DESC', 10 )->join( 'core_reactions', 'reaction=reaction_id' ) as $rep )
|
||||
{
|
||||
$this->addReputationExtraItem( $rep, $extraItems );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach ( \IPS\Db::i()->select( '*', 'core_reputation_index', $where, 'rep_date DESC', 10 ) as $rep )
|
||||
foreach ( \IPS\Db::i()->select( '*', 'core_reputation_index', $where, 'rep_date DESC', 10 )->join( 'core_reactions', 'reaction=reaction_id' ) as $rep )
|
||||
{
|
||||
$this->addReputationExtraItem( $rep, $extraItems );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Clubs */
|
||||
if ( \IPS\Settings::i()->clubs and in_array( 'clubs', $extra ) )
|
||||
{
|
||||
$where = array( array( 'created>?', $lastTime ) );
|
||||
if ( $firstTime )
|
||||
{
|
||||
$where[] = array( 'created<?', $firstTime );
|
||||
}
|
||||
if ( $author )
|
||||
{
|
||||
$where[] = array( 'owner=?', $author->member_id );
|
||||
}
|
||||
|
||||
foreach ( \IPS\Member\Club::clubs( \IPS\Member::loggedIn(), NULL, 'created', FALSE, array(), $where ) as $club )
|
||||
{
|
||||
if ( $club->owner AND $club->owner->member_id )
|
||||
{
|
||||
$extraItems[] = new Result\Custom(
|
||||
$club->created,
|
||||
\IPS\Member::loggedIn()->language()->addToStack( 'activity_club', FALSE, array( 'htmlsprintf' => array( \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' )->userLink( $club->owner ), \IPS\Theme::i()->getTemplate( 'global', 'core', 'global' )->basicUrl( $club->url(), FALSE, $club->name, FALSE ) ) ) )
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Extensions */
|
||||
foreach ( \IPS\Application::allExtensions( 'core', 'StreamItems', TRUE, 'core' ) as $key => $extension )
|
||||
{
|
||||
$settingKey = "all_activity_" . mb_strtolower( $key );
|
||||
|
||||
if( method_exists( $extension, 'extraItems' ) and \IPS\Settings::i()->$settingKey )
|
||||
{
|
||||
$extraItems = array_merge( $extraItems, $extension->extraItems( $author, $lastTime, $firstTime ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Merge them in */
|
||||
if ( !empty( $extraItems ) )
|
||||
{
|
||||
@@ -594,7 +682,7 @@ class _Results extends \ArrayIterator
|
||||
{
|
||||
foreach ( $ext->classes as $class )
|
||||
{
|
||||
if ( in_array( 'IPS\Content\Reputation', class_implements( $class ) ) and $class::$reputationType == $rep['type'] )
|
||||
if ( \IPS\IPS::classUsesTrait( $class, 'IPS\Content\Reactable' ) and $class::reactionType() == $rep['type'] )
|
||||
{
|
||||
$thingBeingRepped = $class::loadAndCheckPerms( $rep['type_id'] );
|
||||
break;
|
||||
@@ -602,7 +690,7 @@ class _Results extends \ArrayIterator
|
||||
if ( isset( $class::$commentClass ) )
|
||||
{
|
||||
$commentClass = $class::$commentClass;
|
||||
if ( in_array( 'IPS\Content\Reputation', class_implements( $commentClass ) ) and $commentClass::$reputationType == $rep['type'] )
|
||||
if ( \IPS\IPS::classUsesTrait( $commentClass, 'IPS\Content\Reactable' ) and $commentClass::reactionType() == $rep['type'] )
|
||||
{
|
||||
$thingBeingRepped = $commentClass::loadAndCheckPerms( $rep['type_id'] );
|
||||
break;
|
||||
@@ -611,7 +699,7 @@ class _Results extends \ArrayIterator
|
||||
if ( isset( $class::$reviewClass ) )
|
||||
{
|
||||
$reviewClass = $class::$reviewClass;
|
||||
if ( in_array( 'IPS\Content\Reputation', class_implements( $reviewClass ) ) and $reviewClass::$reputationType == $rep['type'] )
|
||||
if ( \IPS\IPS::classUsesTrait( $reviewClass, 'IPS\Content\Reactable' ) and $reviewClass::reactionType() == $rep['type'] )
|
||||
{
|
||||
$thingBeingRepped = $reviewClass::loadAndCheckPerms( $rep['type_id'] );
|
||||
break;
|
||||
@@ -624,27 +712,31 @@ class _Results extends \ArrayIterator
|
||||
throw new \OutOfRangeException;
|
||||
}
|
||||
|
||||
if ( \IPS\Settings::i()->reputation_point_types == 'like' )
|
||||
if ( \IPS\Content\Reaction::isLikeMode() )
|
||||
{
|
||||
$lang = 'activity_member_liked';
|
||||
}
|
||||
elseif ( $rep['rep_rating'] > 0 )
|
||||
{
|
||||
$lang = 'activity_rep_pos';
|
||||
$extraItems[] = new Result\Custom(
|
||||
\IPS\DateTime::ts( $rep['rep_date'] ),
|
||||
\IPS\Member::loggedIn()->language()->addToStack( 'activity_member_liked', 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 )
|
||||
) ) )
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
$lang = 'activity_rep_neg';
|
||||
$reaction = \IPS\Content\Reaction::load( $rep['reaction'] );
|
||||
$extraItems[] = new Result\Custom(
|
||||
\IPS\DateTime::ts( $rep['rep_date'] ),
|
||||
\IPS\Member::loggedIn()->language()->addToStack( 'activity_member_reacted', FALSE, array( 'htmlsprintf' => array(
|
||||
(string) $reaction->_icon,
|
||||
$reaction->_title,
|
||||
\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 )
|
||||
) ) )
|
||||
);
|
||||
}
|
||||
|
||||
$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 ) { }
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Interface for Searchable Content/Comment Models
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Community Suite
|
||||
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) Invision Power Services, Inc.
|
||||
* @license https://www.invisioncommunity.com/legal/standards/
|
||||
* @package Invision Community
|
||||
* @since 8 Jul 2013
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS\Content;
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Delicious share link
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Community Suite
|
||||
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) Invision Power Services, Inc.
|
||||
* @license https://www.invisioncommunity.com/legal/standards/
|
||||
* @package Invision Community
|
||||
* @since 11 Sept 2013
|
||||
* @version SVN_VERSION_NUMBER
|
||||
* @see <a href='https://delicious.com/tools'>Delicious button documentation</a>
|
||||
*/
|
||||
|
||||
|
||||
Loaded 100 of 2074 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user