Merge remote-tracking branch 'upstream/nightly' into nightly

This commit is contained in:
www-data 2016-05-01 21:44:34 +02:00
commit 6a95a0cecb
18 changed files with 187 additions and 93 deletions

13
INSTALL
View File

@ -49,6 +49,19 @@ functional setup of GNU Social:
- php5-mysqlnd The native driver for PHP5 MariaDB connections. If you - php5-mysqlnd The native driver for PHP5 MariaDB connections. If you
use MySQL, 'php5-mysql' or 'php5-mysqli' may be enough. use MySQL, 'php5-mysql' or 'php5-mysqli' may be enough.
Or, for PHP7, some or all of these will be necessary. PHP7 support is still
experimental and not necessarily working:
php7.0-bcmath
php7.0-curl
php7.0-exif
php7.0-gd
php7.0-intl
php7.0-mbstring
php7.0-mysqlnd
php7.0-opcache
php7.0-readline
php7.0-xmlwriter
The above package names are for Debian based systems. In the case of The above package names are for Debian based systems. In the case of
Arch Linux, PHP is compiled with support for most extensions but they Arch Linux, PHP is compiled with support for most extensions but they
require manual enabling in the relevant php.ini file (mostly php5-gmp). require manual enabling in the relevant php.ini file (mostly php5-gmp).

View File

@ -28,9 +28,7 @@
* @link http://status.net/ * @link http://status.net/
*/ */
if (!defined('STATUSNET')) { if (!defined('GNUSOCIAL')) { exit(1); }
exit(1);
}
/** /**
* Returns the string "ok" in the requested format with a 200 OK HTTP status code. * Returns the string "ok" in the requested format with a 200 OK HTTP status code.
@ -44,29 +42,9 @@ if (!defined('STATUSNET')) {
*/ */
class ApiHelpTestAction extends ApiPrivateAuthAction class ApiHelpTestAction extends ApiPrivateAuthAction
{ {
/** protected function handle()
* Take arguments for running
*
* @param array $args $_REQUEST args
*
* @return boolean success flag
*/
function prepare($args)
{ {
parent::prepare($args); parent::handle();
return true;
}
/**
* Handle the request
*
* @param array $args $_REQUEST data (unused)
*
* @return void
*/
function handle($args)
{
parent::handle($args);
if ($this->format == 'xml') { if ($this->format == 'xml') {
$this->initDocument('xml'); $this->initDocument('xml');
@ -77,12 +55,8 @@ class ApiHelpTestAction extends ApiPrivateAuthAction
print '"ok"'; print '"ok"';
$this->endDocument('json'); $this->endDocument('json');
} else { } else {
$this->clientError( // TRANS: Client error displayed when coming across a non-supported API method.
// TRANS: Client error displayed when coming across a non-supported API method. throw new ClientException(_('API method not found.'), 404);
_('API method not found.'),
404,
$this->format
);
} }
} }

View File

@ -516,6 +516,11 @@ class File extends Managed_DataObject
return $filepath; return $filepath;
} }
public function getAttachmentUrl()
{
return common_local_url('attachment', array('attachment'=>$this->getID()));
}
public function getUrl($prefer_local=true) public function getUrl($prefer_local=true)
{ {
if ($prefer_local && !empty($this->filename)) { if ($prefer_local && !empty($this->filename)) {

View File

@ -173,12 +173,11 @@ class File_redirection extends Managed_DataObject
try { try {
$r = File_redirection::getByUrl($in_url); $r = File_redirection::getByUrl($in_url);
$f = File::getKV('id',$r->file_id); try {
$f = File::getByID($r->file_id);
if($file instanceof File) {
$r->file = $f; $r->file = $f;
$r->redir_url = $f->url; $r->redir_url = $f->url;
} else { } catch (NoResultException $e) {
// Invalid entry, delete and run again // Invalid entry, delete and run again
common_log(LOG_ERR, "Could not find File with id=".$r->file_id." referenced in File_redirection, deleting File redirection entry and and trying again..."); common_log(LOG_ERR, "Could not find File with id=".$r->file_id." referenced in File_redirection, deleting File redirection entry and and trying again...");
$r->delete(); $r->delete();

View File

@ -955,10 +955,10 @@ class Notice extends Managed_DataObject
$act->context = new ActivityContext(); $act->context = new ActivityContext();
} }
if (array_key_exists('http://activityschema.org/collection/public', $act->context->attention)) { if (array_key_exists(ActivityContext::ATTN_PUBLIC, $act->context->attention)) {
$stored->scope = Notice::PUBLIC_SCOPE; $stored->scope = Notice::PUBLIC_SCOPE;
// TODO: maybe we should actually keep this? if the saveAttentions thing wants to use it... // TODO: maybe we should actually keep this? if the saveAttentions thing wants to use it...
unset($act->context->attention['http://activityschema.org/collection/public']); unset($act->context->attention[ActivityContext::ATTN_PUBLIC]);
} else { } else {
$stored->scope = self::figureOutScope($actor, $groups, $scope); $stored->scope = self::figureOutScope($actor, $groups, $scope);
} }
@ -1537,12 +1537,16 @@ class Notice extends Managed_DataObject
function getProfileTags() function getProfileTags()
{ {
$profile = $this->getProfile();
$list = $profile->getOtherTags($profile);
$ptags = array(); $ptags = array();
try {
$profile = $this->getProfile();
$list = $profile->getOtherTags($profile);
while($list->fetch()) { while($list->fetch()) {
$ptags[] = clone($list); $ptags[] = clone($list);
}
} catch (Exception $e) {
common_log(LOG_ERR, "Error during Notice->getProfileTags() for id=={$this->getID()}: {$e->getMessage()}");
} }
return $ptags; return $ptags;
@ -3087,6 +3091,79 @@ class Notice extends Managed_DataObject
$schema = Schema::get(); $schema = Schema::get();
$schemadef = $schema->getTableDef($table); $schemadef = $schema->getTableDef($table);
/**
* Make sure constraints are met before upgrading, if foreign keys
* are not already in use.
* 2016-03-31
*/
if (!isset($schemadef['foreign keys'])) {
$newschemadef = self::schemaDef();
printfnq("\nConstraint checking Notice table...\n");
/**
* Improve typing and make sure no NULL values in any id-related columns are 0
* 2016-03-31
*/
foreach (['reply_to', 'repeat_of'] as $field) {
$notice = new Notice(); // reset the object
$notice->query(sprintf('UPDATE %1$s SET %2$s=NULL WHERE %2$s=0', $notice->escapedTableName(), $field));
// Now we're sure that no Notice entries have repeat_of=0, only an id > 0 or NULL
unset($notice);
}
/**
* This Will find foreign keys which do not fulfill the constraints and fix
* where appropriate, such as delete when "repeat_of" ID not found in notice.id
* or set to NULL for "reply_to" in the same case.
* 2016-03-31
*
* XXX: How does this work if we would use multicolumn foreign keys?
*/
foreach (['reply_to' => 'reset', 'repeat_of' => 'delete', 'profile_id' => 'delete'] as $field=>$action) {
$notice = new Notice();
$fkeyname = $notice->tableName().'_'.$field.'_fkey';
assert(isset($newschemadef['foreign keys'][$fkeyname]));
assert($newschemadef['foreign keys'][$fkeyname]);
$foreign_key = $newschemadef['foreign keys'][$fkeyname];
$fkeytable = $foreign_key[0];
assert(isset($foreign_key[1][$field]));
$fkeycol = $foreign_key[1][$field];
printfnq("* {$fkeyname} ({$field} => {$fkeytable}.{$fkeycol})\n");
// NOTE: Above we set all repeat_of to NULL if they were 0, so this really gets them all.
$notice->whereAdd(sprintf('%1$s NOT IN (SELECT %2$s FROM %3$s)', $field, $fkeycol, $fkeytable));
if ($notice->find()) {
printfnq("\tFound {$notice->N} notices with {$field} NOT IN notice.id, {$action}ing...");
switch ($action) {
case 'delete': // since it's a directly dependant notice for an unknown ID we don't want it in our DB
while ($notice->fetch()) {
$notice->delete();
}
break;
case 'reset': // just set it to NULL to be compatible with our constraints, if it was related to an unknown ID
$ids = [];
foreach ($notice->fetchAll('id') as $id) {
settype($id, 'int');
$ids[] = $id;
}
unset($notice);
$notice = new Notice();
$notice->query(sprintf('UPDATE %1$s SET %2$s=NULL WHERE id IN (%3$s)',
$notice->escapedTableName(),
$field,
implode(',', $ids)));
break;
default:
throw new ServerException('The programmer sucks, invalid action name when fixing table.');
}
printfnq("DONE.\n");
}
unset($notice);
}
}
// 2015-09-04 We move Notice location data to Notice_location // 2015-09-04 We move Notice location data to Notice_location
// First we see if we have to do this at all // First we see if we have to do this at all
if (!isset($schemadef['fields']['lat']) if (!isset($schemadef['fields']['lat'])

View File

@ -279,6 +279,10 @@ abstract class ActivityHandlerPlugin extends Plugin
if ($this->isMyNotice($notice)) { if ($this->isMyNotice($notice)) {
try { try {
$this->deleteRelated($notice); $this->deleteRelated($notice);
} catch (NoProfileException $e) {
// we failed because of database lookup failure, Notice has no recognized profile as creator
// so we skip this. If we want to remove missing notices we should do a SQL constraints check
// in the affected plugin.
} catch (AlreadyFulfilledException $e) { } catch (AlreadyFulfilledException $e) {
// Nothing to see here, it's obviously already gone... // Nothing to see here, it's obviously already gone...
} }

View File

@ -491,7 +491,7 @@ class ActivityObject
$object->type = self::mimeTypeToObjectType($file->mimetype); $object->type = self::mimeTypeToObjectType($file->mimetype);
$object->id = TagURI::mint(sprintf("file:%d", $file->id)); $object->id = TagURI::mint(sprintf("file:%d", $file->id));
$object->link = common_local_url('attachment', array('attachment' => $file->id)); $object->link = $file->getAttachmentUrl();
if ($file->title) { if ($file->title) {
$object->title = $file->title; $object->title = $file->title;

View File

@ -87,8 +87,8 @@ class AttachmentListItem extends Widget
function linkAttr() { function linkAttr() {
return array('class' => 'attachment', return array('class' => 'attachment',
'href' => $this->attachment->getUrl(false), 'href' => $this->attachment->getAttachmentUrl(),
'id' => 'attachment-' . $this->attachment->id, 'id' => 'attachment-' . $this->attachment->getID(),
'title' => $this->linkTitle()); 'title' => $this->linkTitle());
} }

View File

@ -93,7 +93,7 @@ class ImageFile
$this->type = $info[2]; $this->type = $info[2];
$this->mimetype = $info['mime']; $this->mimetype = $info['mime'];
if ($this->type == IMAGETYPE_JPEG && function_exists('exif_read_data')) { if ($this->type === IMAGETYPE_JPEG && function_exists('exif_read_data')) {
// Orientation value to rotate thumbnails properly // Orientation value to rotate thumbnails properly
$exif = @exif_read_data($this->filepath); $exif = @exif_read_data($this->filepath);
if (is_array($exif) && isset($exif['Orientation'])) { if (is_array($exif) && isset($exif['Orientation'])) {

View File

@ -189,6 +189,7 @@ class NoticeListItem extends Widget
function showNoticeInfo() function showNoticeInfo()
{ {
if (Event::handle('StartShowNoticeInfo', array($this))) { if (Event::handle('StartShowNoticeInfo', array($this))) {
$this->showContextLink();
$this->showNoticeLink(); $this->showNoticeLink();
$this->showNoticeSource(); $this->showNoticeSource();
$this->showNoticeLocation(); $this->showNoticeLocation();
@ -375,14 +376,10 @@ class NoticeListItem extends Widget
*/ */
function showNoticeLink() function showNoticeLink()
{ {
$this->out->elementStart('a', array('rel' => 'bookmark',
'class' => 'timestamp',
'href' => Conversation::getUrlFromNotice($this->notice)));
$this->out->element('time', array('class' => 'dt-published', $this->out->element('time', array('class' => 'dt-published',
'datetime' => common_date_iso8601($this->notice->created), 'datetime' => common_date_iso8601($this->notice->created),
'title' => common_exact_date($this->notice->created)), 'title' => common_exact_date($this->notice->created)),
common_date_string($this->notice->created)); common_date_string($this->notice->created));
$this->out->elementEnd('a');
} }
/** /**
@ -568,6 +565,18 @@ class NoticeListItem extends Widget
} }
} }
/**
* Show link to conversation view.
*/
function showContextLink()
{
$this->out->element('a', array('rel' => 'bookmark',
'class' => 'timestamp',
'href' => Conversation::getUrlFromNotice($this->notice)),
// TRANS: A link to the conversation view of a notice, on the local server.
_('In conversation'));
}
/** /**
* show a link to reply to the current notice * show a link to reply to the current notice
* *

View File

@ -266,7 +266,8 @@ function common_logged_in()
function common_local_referer() function common_local_referer()
{ {
return parse_url($_SERVER['HTTP_REFERER'], PHP_URL_HOST) === common_config('site', 'server'); return isset($_SERVER['HTTP_REFERER'])
&& parse_url($_SERVER['HTTP_REFERER'], PHP_URL_HOST) === common_config('site', 'server');
} }
function common_have_session() function common_have_session()

View File

@ -37,7 +37,7 @@ function getRegisteredDomain($signingDomain) {
global $tldTree; global $tldTree;
$signingDomainParts = split('\.', $signingDomain); $signingDomainParts = explode('.', $signingDomain);
$result = findRegisteredDomain($signingDomainParts, $tldTree); $result = findRegisteredDomain($signingDomainParts, $tldTree);
@ -80,4 +80,4 @@ function findRegisteredDomain($remainingSigningDomainParts, &$treeNode) {
return NULL; return NULL;
} }
?> ?>

View File

@ -193,6 +193,13 @@ class Fave extends Managed_DataObject
$act->content = sprintf(_('%1$s favorited something by %2$s: %3$s'), $act->content = sprintf(_('%1$s favorited something by %2$s: %3$s'),
$actor->getNickname(), $target->getProfile()->getNickname(), $actor->getNickname(), $target->getProfile()->getNickname(),
$target->getRendered()); $target->getRendered());
$act->context = new ActivityContext();
$act->context->replyToID = $target->getUri();
try {
$act->context->replyToURL = $target->getUrl();
} catch (InvalidUrlException $e) {
// ok, no replyToURL, i.e. the href="" in <thr:in-reply-to/>
}
$act->actor = $actor->asActivityObject(); $act->actor = $actor->asActivityObject();
$act->target = $target->asActivityObject(); $act->target = $target->asActivityObject();

View File

@ -201,13 +201,13 @@ function linkback_hcard($mf2, $url) {
} }
function linkback_notice($source, $notice_or_user, $entry, $author, $mf2) { function linkback_notice($source, $notice_or_user, $entry, $author, $mf2) {
$content = $entry['content'] ? $entry['content'][0]['html'] : $content = isset($entry['content']) ? $entry['content'][0]['html'] :
($entry['summary'] ? $entry['sumary'][0] : $entry['name'][0]); (isset($entry['summary']) ? $entry['summary'][0] : $entry['name'][0]);
$rendered = common_purify($content); $rendered = common_purify($content);
if($notice_or_user instanceof Notice && $entry['type'] == 'mention') { if($notice_or_user instanceof Notice && $entry['type'] == 'mention') {
$name = $entry['name'] ? $entry['name'][0] : substr(common_strip_html($content), 0, 20).'…'; $name = isset($entry['name']) ? $entry['name'][0] : substr(common_strip_html($content), 0, 20).'…';
$rendered = _m('linked to this from <a href="'.htmlspecialchars($source).'">'.htmlspecialchars($name).'</a>'); $rendered = _m('linked to this from <a href="'.htmlspecialchars($source).'">'.htmlspecialchars($name).'</a>');
} }
@ -241,12 +241,16 @@ function linkback_notice($source, $notice_or_user, $entry, $author, $mf2) {
} }
} }
if($entry['published'] || $entry['updated']) { if (isset($entry['published']) || isset($entry['updated'])) {
$options['created'] = $entry['published'] ? common_sql_date($entry['published'][0]) : common_sql_date($entry['updated'][0]); $options['created'] = isset($entry['published'])
? common_sql_date($entry['published'][0])
: common_sql_date($entry['updated'][0]);
} }
if($entry['photo']) { if (isset($entry['photo']) && common_valid_http_url($entry['photo'])) {
$options['urls'][] = $entry['photo'][0]; $options['urls'][] = $entry['photo'][0];
} elseif (isset($entry['photo'])) {
common_debug('Linkback got invalid HTTP URL for photo: '._ve($entry['photo']));
} }
foreach((array)$entry['category'] as $tag) { foreach((array)$entry['category'] as $tag) {
@ -287,7 +291,7 @@ function linkback_profile($entry, $mf2, $response, $target) {
$author = array('name' => $entry['name']); $author = array('name' => $entry['name']);
} }
if(!$author['url']) { if (!isset($author['url']) || empty($author['url'])) {
$author['url'] = array($response->getEffectiveUrl()); $author['url'] = array($response->getEffectiveUrl());
} }
@ -299,17 +303,16 @@ function linkback_profile($entry, $mf2, $response, $target) {
try { try {
$profile = Profile::fromUri($author['url'][0]); $profile = Profile::fromUri($author['url'][0]);
} catch(UnknownUriException $ex) {} } catch(UnknownUriException $ex) {
if(!($profile instanceof Profile)) {
$profile = Profile::getKV('profileurl', $author['url'][0]); $profile = Profile::getKV('profileurl', $author['url'][0]);
} }
if(!($profile instanceof Profile)) { // XXX: Is this a good way to create the profile?
if (!$profile instanceof Profile) {
$profile = new Profile(); $profile = new Profile();
$profile->profileurl = $author['url'][0]; $profile->profileurl = $author['url'][0];
$profile->fullname = $author['name'][0]; $profile->fullname = $author['name'][0];
$profile->nickname = $author['nickname'] ? $author['nickname'][0] : str_replace(' ', '', $author['name'][0]); $profile->nickname = isset($author['nickname']) ? $author['nickname'][0] : str_replace(' ', '', $author['name'][0]);
$profile->created = common_sql_now(); $profile->created = common_sql_now();
$profile->insert(); $profile->insert();
} }

View File

@ -43,7 +43,9 @@ class UsersalmonAction extends SalmonAction
if (!empty($this->activity->context->replyToID)) { if (!empty($this->activity->context->replyToID)) {
try { try {
$notice = Notice::getByUri($this->activity->context->replyToID); $notice = Notice::getByUri($this->activity->context->replyToID);
common_debug('Referenced Notice object found with URI: '.$notice->getUri());
} catch (NoResultException $e) { } catch (NoResultException $e) {
common_debug('Referenced Notice object NOT found with URI: '.$this->activity->context->replyToID);
$notice = false; $notice = false;
} }
} }

View File

@ -84,26 +84,25 @@ class OembedAction extends Action
$oembed['html']=$notice->getRendered(); $oembed['html']=$notice->getRendered();
// maybe add thumbnail // maybe add thumbnail
$attachments = $notice->attachments(); foreach ($notice->attachments() as $attachment) {
if (!empty($attachments)) { if (!$attachment instanceof File) {
foreach ($attachments as $attachment) { common_debug('ATTACHMENTS array entry from notice id=='._ve($notice->getID()).' is something else than a File dataobject: '._ve($attachment));
if(is_object($attachment)) { continue;
try { }
$thumb = $attachment->getThumbnail(); try {
} catch (ServerException $e) { $thumb = $attachment->getThumbnail();
// $thumb_url = File_thumbnail::url($thumb->filename);
} $oembed['thumbnail_url'] = $thumb_url;
try { break; // only first one
$thumb_url = File_thumbnail::url($thumb->filename); } catch (UseFileAsThumbnailException $e) {
$oembed['thumbnail_url'] = $thumb_url; $oembed['thumbnail_url'] = $attachment->getUrl();
break; // only first one break; // we're happy with that
} catch (ClientException $e) { } catch (ServerException $e) {
// //
} } catch (ClientException $e) {
} //
} }
} }
break; break;
case 'attachment': case 'attachment':

View File

@ -118,12 +118,13 @@ class Roster {
* @param string $status * @param string $status
*/ */
public function setPresence($presence, $priority, $show, $status) { public function setPresence($presence, $priority, $show, $status) {
list($jid, $resource) = split("/", $presence); $parts = explode('/', $presence);
$jid = $parts[0];
$resource = isset($parts[1]) ? $parts[1] : ''; // apparently we can do '' as an associative array index
if ($show != 'unavailable') { if ($show != 'unavailable') {
if (!$this->isContact($jid)) { if (!$this->isContact($jid)) {
$this->addContact($jid, 'not-in-roster'); $this->addContact($jid, 'not-in-roster');
} }
$resource = $resource ? $resource : '';
$this->roster_array[$jid]['presence'][$resource] = array('priority' => $priority, 'show' => $show, 'status' => $status); $this->roster_array[$jid]['presence'][$resource] = array('priority' => $priority, 'show' => $show, 'status' => $status);
} else { //Nuke unavailable resources to save memory } else { //Nuke unavailable resources to save memory
unset($this->roster_array[$jid]['resource'][$resource]); unset($this->roster_array[$jid]['resource'][$resource]);
@ -137,7 +138,7 @@ class Roster {
* @param string $jid * @param string $jid
*/ */
public function getPresence($jid) { public function getPresence($jid) {
$split = split("/", $jid); $split = explode("/", $jid);
$jid = $split[0]; $jid = $split[0];
if($this->isContact($jid)) { if($this->isContact($jid)) {
$current = array('resource' => '', 'active' => '', 'priority' => -129, 'show' => '', 'status' => ''); //Priorities can only be -128 = 127 $current = array('resource' => '', 'active' => '', 'priority' => -129, 'show' => '', 'status' => ''); //Priorities can only be -128 = 127

View File

@ -263,7 +263,7 @@ class XMPPHP_XMLStream {
$ns_tags = array($xpath); $ns_tags = array($xpath);
} }
foreach($ns_tags as $ns_tag) { foreach($ns_tags as $ns_tag) {
list($l, $r) = split("}", $ns_tag); list($l, $r) = explode("}", $ns_tag);
if ($r != null) { if ($r != null) {
$xpart = array(substr($l, 1), $r); $xpart = array(substr($l, 1), $r);
} else { } else {
@ -564,7 +564,7 @@ class XMPPHP_XMLStream {
if ($searchxml !== null) { if ($searchxml !== null) {
if($handler[2] === null) $handler[2] = $this; if($handler[2] === null) $handler[2] = $this;
$this->log->log("Calling {$handler[1]}", XMPPHP_Log::LEVEL_DEBUG); $this->log->log("Calling {$handler[1]}", XMPPHP_Log::LEVEL_DEBUG);
$handler[2]->$handler[1]($this->xmlobj[2]); $handler[2]->{$handler[1]}($this->xmlobj[2]);
} }
} }
} }
@ -578,13 +578,13 @@ class XMPPHP_XMLStream {
if($searchxml !== null and $searchxml->name == $handler[0] and ($searchxml->ns == $handler[1] or (!$handler[1] and $searchxml->ns == $this->default_ns))) { if($searchxml !== null and $searchxml->name == $handler[0] and ($searchxml->ns == $handler[1] or (!$handler[1] and $searchxml->ns == $this->default_ns))) {
if($handler[3] === null) $handler[3] = $this; if($handler[3] === null) $handler[3] = $this;
$this->log->log("Calling {$handler[2]}", XMPPHP_Log::LEVEL_DEBUG); $this->log->log("Calling {$handler[2]}", XMPPHP_Log::LEVEL_DEBUG);
$handler[3]->$handler[2]($this->xmlobj[2]); $handler[3]->{$handler[2]}($this->xmlobj[2]);
} }
} }
foreach($this->idhandlers as $id => $handler) { foreach($this->idhandlers as $id => $handler) {
if(array_key_exists('id', $this->xmlobj[2]->attrs) and $this->xmlobj[2]->attrs['id'] == $id) { if(array_key_exists('id', $this->xmlobj[2]->attrs) and $this->xmlobj[2]->attrs['id'] == $id) {
if($handler[1] === null) $handler[1] = $this; if($handler[1] === null) $handler[1] = $this;
$handler[1]->$handler[0]($this->xmlobj[2]); $handler[1]->{$handler[0]}($this->xmlobj[2]);
#id handlers are only used once #id handlers are only used once
unset($this->idhandlers[$id]); unset($this->idhandlers[$id]);
break; break;
@ -640,7 +640,7 @@ class XMPPHP_XMLStream {
if($handler[2] === null) { if($handler[2] === null) {
$handler[2] = $this; $handler[2] = $this;
} }
$handler[2]->$handler[1]($payload); $handler[2]->{$handler[1]}($payload);
} }
} }
foreach($this->until as $key => $until) { foreach($this->until as $key => $until) {